From 4a6786267edb9ce7a328c4d68796753dc4c6884e Mon Sep 17 00:00:00 2001 From: Maja Komel Date: Tue, 18 Oct 2022 11:36:27 +0200 Subject: [PATCH 01/41] Add links to search in MAT tooltip for all counts --- components/aggregation/mat/CustomTooltip.js | 40 ++++++++++++++++++--- 1 file changed, 35 insertions(+), 5 deletions(-) diff --git a/components/aggregation/mat/CustomTooltip.js b/components/aggregation/mat/CustomTooltip.js index b282f4a74..8d0f10093 100644 --- a/components/aggregation/mat/CustomTooltip.js +++ b/components/aggregation/mat/CustomTooltip.js @@ -46,7 +46,15 @@ export const generateSearchQuery = (data, query) => { untilFilter = untilPlus1.toISOString().split('T')[0] } - const queryObj = ['probe_cc', 'test_name', 'category_code', 'probe_asn', 'input', 'domain'].reduce((q, k) => { + const queryObj = [ + 'probe_cc', + 'test_name', + 'category_code', + 'probe_asn', + 'input', + 'domain', + 'only', + 'failure'].reduce((q, k) => { if (k in data) q[k] = data[k] else if (query[k]) @@ -68,12 +76,30 @@ export const generateSearchQuery = (data, query) => { } } +const getDataKeyLink = (data, matQuery, keyQuery) => { + const mergeQuery = {...matQuery, ...keyQuery} + const query = generateSearchQuery(data, mergeQuery) + + return { + pathname: '/search', + query + } +} + const CustomToolTip = React.memo(({ data, onClose, title, link = true }) => { const theme = useTheme() const intl = useIntl() const [query] = useMATContext() - const dataKeysToShow = ['anomaly_count', 'confirmed_count', 'failure_count', 'ok_count'] + const dataKeysToShow = useMemo(() => { + return { + 'anomaly_count': getDataKeyLink(data, query, {only: 'anomalies'}), + 'confirmed_count': getDataKeyLink(data, query, {only: 'confirmed'}), + 'failure_count': getDataKeyLink(data, query, {failure: true}), + 'ok_count': getDataKeyLink(data, query) + } + }, [data, query]) + const [linkToMeasurements, derivedTitle] = useMemo(() => { const searchQuery = generateSearchQuery(data, query) const linkObj = { @@ -97,12 +123,16 @@ const CustomToolTip = React.memo(({ data, onClose, title, link = true }) => { - {dataKeysToShow.map(k => ( + {Object.entries(dataKeysToShow).map(([k, v]) => ( - {k} - {intl.formatNumber(Number(data[k] ?? 0))} + + + {k} + + + {intl.formatNumber(Number(data[k] ?? 0))} ))} From 17d901a14ed194eec605e5c7fe9776c1668a912b Mon Sep 17 00:00:00 2001 From: Maja Komel Date: Fri, 21 Oct 2022 12:13:08 +0200 Subject: [PATCH 02/41] Build improvements (#808) * Remove about page * Remove git env vars for production --- .dockerignore | 10 ++ .env.production | 5 - Dockerfile | 29 +++-- next.config.js | 9 +- package.json | 2 +- pages/404.js | 4 +- pages/about.js | 32 ------ pages/experimental/index.js | 21 ---- yarn.lock | 204 ++++++++++++++++++------------------ 9 files changed, 134 insertions(+), 182 deletions(-) delete mode 100644 pages/about.js delete mode 100644 pages/experimental/index.js diff --git a/.dockerignore b/.dockerignore index e1a054c0f..323bce3b4 100644 --- a/.dockerignore +++ b/.dockerignore @@ -5,3 +5,13 @@ npm-debug.log package-lock.json yarn-error.log ignore +cypress +.github +.editorconfig +.dockerignore +CHANGELOG.md +CODE_OF_CONDUCT.md +cypress.config.js +LICENSE.md +Readme.md +Todo.md diff --git a/.env.production b/.env.production index 9e3ff732c..aa64090ca 100644 --- a/.env.production +++ b/.env.production @@ -5,8 +5,3 @@ NEXT_PUBLIC_OONI_API=https://api.ooni.io NEXT_PUBLIC_SENTRY_DSN=https://49af7fff247c445b9a7c98ee21ddfd2f@o155150.ingest.sentry.io/1427510 NEXT_PUBLIC_EXPLORER_URL=https://explorer.ooni.org - -RUN_GIT_COMMIT_SHA_SHORT=yarn --silent git:getCommitSHA:short -RUN_GIT_COMMIT_SHA=yarn --silent git:getCommitSHA -RUN_GIT_COMMIT_REF=yarn --silent git:getCommitRef -RUN_GIT_COMMIT_TAGS=yarn --silent git:getReleasesAndTags diff --git a/Dockerfile b/Dockerfile index dda5429a1..d3e40d1d1 100644 --- a/Dockerfile +++ b/Dockerfile @@ -16,33 +16,30 @@ RUN yarn install --frozen-lockfile # Rebuild the source code only when needed FROM node:16.3-alpine3.12 AS builder WORKDIR /app -COPY . . COPY --from=deps /app/node_modules ./node_modules -ARG NODE_ENV ${NODE_ENV:-production} -ENV NODE_ENV ${NODE_ENV} -RUN yarn build && yarn install --production --ignore-scripts --prefer-offline +COPY . . +RUN yarn build # Production image, copy all the files and run next FROM node:16.3-alpine3.12 AS runner WORKDIR /app -ARG NODE_ENV ${NODE_ENV:-production} -ENV NODE_ENV ${NODE_ENV} -RUN addgroup -g 1001 -S nodejs -RUN adduser -S nextjs -u 1001 +ENV NODE_ENV production +ENV NEXT_TELEMETRY_DISABLED 1 +RUN addgroup --system --gid 1001 nodejs +RUN adduser --system --uid 1001 nextjs # You only need to copy next.config.js if you are NOT using the default configuration COPY --from=builder /app/next.config.js ./ COPY --from=builder /app/public ./public -COPY --from=builder --chown=nextjs:nodejs /app/.next ./.next -COPY --from=builder /app/node_modules ./node_modules -COPY --from=builder /app/package.json ./package.json + +# Automatically leverage output traces to reduce image size +# https://nextjs.org/docs/advanced-features/output-file-tracing +COPY --from=builder --chown=nextjs:nodejs /app/.next/standalone ./ +COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static USER nextjs EXPOSE 3100 +ENV PORT 3100 -ARG NODE_ENV ${NODE_ENV:-production} -ENV NODE_ENV ${NODE_ENV} -ENV NEXT_TELEMETRY_DISABLED 1 - -CMD ["yarn", "start"] +CMD ["node", "server.js"] diff --git a/next.config.js b/next.config.js index 9336cea79..fc3dbea9b 100644 --- a/next.config.js +++ b/next.config.js @@ -14,6 +14,7 @@ const SentryWebpackPluginOptions = { } module.exports = withSentryConfig({ + output: 'standalone', async redirects() { return [ { @@ -25,10 +26,10 @@ module.exports = withSentryConfig({ }, webpack: (config, options) => { - const gitCommitSHAShort = execSync(process.env.RUN_GIT_COMMIT_SHA_SHORT) - const gitCommitSHA = execSync(process.env.RUN_GIT_COMMIT_SHA) - const gitCommitRef = execSync(process.env.RUN_GIT_COMMIT_REF) - const gitCommitTags = execSync(process.env.RUN_GIT_COMMIT_TAGS) + const gitCommitSHAShort = process.env.RUN_GIT_COMMIT_SHA_SHORT ? execSync(process.env.RUN_GIT_COMMIT_SHA_SHORT) : '' + const gitCommitSHA = process.env.RUN_GIT_COMMIT_SHA ? execSync(process.env.RUN_GIT_COMMIT_SHA) : '' + const gitCommitRef = process.env.RUN_GIT_COMMIT_REF ? execSync(process.env.RUN_GIT_COMMIT_REF) : '' + const gitCommitTags = process.env.RUN_GIT_COMMIT_TAGS ? execSync(process.env.RUN_GIT_COMMIT_TAGS) : '' config.plugins.push( new options.webpack.DefinePlugin({ diff --git a/package.json b/package.json index 700679673..59021ce63 100644 --- a/package.json +++ b/package.json @@ -27,7 +27,7 @@ "fontsource-fira-sans": "^4.0.0", "lodash.debounce": "^4.0.8", "markdown-to-jsx": "^7.1.7", - "next": "12.2.5", + "next": "^12.3.1", "nprogress": "^0.2.0", "ooni-components": "^0.4.7", "pretty-ms": "^8.0.0", diff --git a/pages/404.js b/pages/404.js index 8613fcb9a..4c6d69e66 100644 --- a/pages/404.js +++ b/pages/404.js @@ -15,7 +15,7 @@ import Layout from '../components/Layout' import NavBar from '../components/NavBar' import OONI404 from '../public/static/images/OONI_404.svg' -export default function Custom404() { +const Custom404 = () => { const router = useRouter() return ( @@ -61,3 +61,5 @@ export default function Custom404() { ) } + +export default Custom404 diff --git a/pages/about.js b/pages/about.js deleted file mode 100644 index c8f741a2b..000000000 --- a/pages/about.js +++ /dev/null @@ -1,32 +0,0 @@ -import React from 'react' -import Head from 'next/head' - -import { - Text, - Heading, - Container -} from 'ooni-components' - -import Layout from '../components/Layout' -import NavBar from '../components/NavBar' - -export default class About extends React.Component { - render () { - - return ( - - - About OONI Explorer - - - - - - XXX Implement Me - Do we even need an about page? - - - ) - } - -} diff --git a/pages/experimental/index.js b/pages/experimental/index.js deleted file mode 100644 index 786cd0cac..000000000 --- a/pages/experimental/index.js +++ /dev/null @@ -1,21 +0,0 @@ -import React from 'react' -import Link from 'next/link' -import { Container, Flex, Text } from 'ooni-components' - -import Layout from '../../components/Layout' -import NavBar from '../../components/NavBar' - -const Experimental = () => { - return ( - - - - - Measurement Aggregation Toolkit - - - - ) -} - -export default Experimental diff --git a/yarn.lock b/yarn.lock index 77f2695cf..900f7b827 100644 --- a/yarn.lock +++ b/yarn.lock @@ -890,10 +890,10 @@ "@jridgewell/resolve-uri" "^3.0.3" "@jridgewell/sourcemap-codec" "^1.4.10" -"@next/env@12.2.5": - version "12.2.5" - resolved "https://registry.yarnpkg.com/@next/env/-/env-12.2.5.tgz#d908c57b35262b94db3e431e869b72ac3e1ad3e3" - integrity sha512-vLPLV3cpPGjUPT3PjgRj7e3nio9t6USkuew3JE/jMeon/9Mvp1WyR18v3iwnCuX7eUAm1HmAbJHHLAbcu/EJcw== +"@next/env@12.3.1": + version "12.3.1" + resolved "https://registry.yarnpkg.com/@next/env/-/env-12.3.1.tgz#18266bd92de3b4aa4037b1927aa59e6f11879260" + integrity sha512-9P9THmRFVKGKt9DYqeC2aKIxm8rlvkK38V1P1sRE7qyoPBIs8l9oo79QoSdPtOWfzkbDAVUqvbQGgTMsb8BtJg== "@next/eslint-plugin-next@12.2.5": version "12.2.5" @@ -902,70 +902,70 @@ dependencies: glob "7.1.7" -"@next/swc-android-arm-eabi@12.2.5": - version "12.2.5" - resolved "https://registry.yarnpkg.com/@next/swc-android-arm-eabi/-/swc-android-arm-eabi-12.2.5.tgz#903a5479ab4c2705d9c08d080907475f7bacf94d" - integrity sha512-cPWClKxGhgn2dLWnspW+7psl3MoLQUcNqJqOHk2BhNcou9ARDtC0IjQkKe5qcn9qg7I7U83Gp1yh2aesZfZJMA== - -"@next/swc-android-arm64@12.2.5": - version "12.2.5" - resolved "https://registry.yarnpkg.com/@next/swc-android-arm64/-/swc-android-arm64-12.2.5.tgz#2f9a98ec4166c7860510963b31bda1f57a77c792" - integrity sha512-vMj0efliXmC5b7p+wfcQCX0AfU8IypjkzT64GiKJD9PgiA3IILNiGJr1fw2lyUDHkjeWx/5HMlMEpLnTsQslwg== - -"@next/swc-darwin-arm64@12.2.5": - version "12.2.5" - resolved "https://registry.yarnpkg.com/@next/swc-darwin-arm64/-/swc-darwin-arm64-12.2.5.tgz#31b1c3c659d54be546120c488a1e1bad21c24a1d" - integrity sha512-VOPWbO5EFr6snla/WcxUKtvzGVShfs302TEMOtzYyWni6f9zuOetijJvVh9CCTzInnXAZMtHyNhefijA4HMYLg== - -"@next/swc-darwin-x64@12.2.5": - version "12.2.5" - resolved "https://registry.yarnpkg.com/@next/swc-darwin-x64/-/swc-darwin-x64-12.2.5.tgz#2e44dd82b2b7fef88238d1bc4d3bead5884cedfd" - integrity sha512-5o8bTCgAmtYOgauO/Xd27vW52G2/m3i5PX7MUYePquxXAnX73AAtqA3WgPXBRitEB60plSKZgOTkcpqrsh546A== - -"@next/swc-freebsd-x64@12.2.5": - version "12.2.5" - resolved "https://registry.yarnpkg.com/@next/swc-freebsd-x64/-/swc-freebsd-x64-12.2.5.tgz#e24e75d8c2581bfebc75e4f08f6ddbd116ce9dbd" - integrity sha512-yYUbyup1JnznMtEBRkK4LT56N0lfK5qNTzr6/DEyDw5TbFVwnuy2hhLBzwCBkScFVjpFdfiC6SQAX3FrAZzuuw== - -"@next/swc-linux-arm-gnueabihf@12.2.5": - version "12.2.5" - resolved "https://registry.yarnpkg.com/@next/swc-linux-arm-gnueabihf/-/swc-linux-arm-gnueabihf-12.2.5.tgz#46d8c514d834d2b5f67086013f0bd5e3081e10b9" - integrity sha512-2ZE2/G921Acks7UopJZVMgKLdm4vN4U0yuzvAMJ6KBavPzqESA2yHJlm85TV/K9gIjKhSk5BVtauIUntFRP8cg== - -"@next/swc-linux-arm64-gnu@12.2.5": - version "12.2.5" - resolved "https://registry.yarnpkg.com/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-12.2.5.tgz#91f725ac217d3a1f4f9f53b553615ba582fd3d9f" - integrity sha512-/I6+PWVlz2wkTdWqhlSYYJ1pWWgUVva6SgX353oqTh8njNQp1SdFQuWDqk8LnM6ulheVfSsgkDzxrDaAQZnzjQ== - -"@next/swc-linux-arm64-musl@12.2.5": - version "12.2.5" - resolved "https://registry.yarnpkg.com/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-12.2.5.tgz#e627e8c867920995810250303cd9b8e963598383" - integrity sha512-LPQRelfX6asXyVr59p5sTpx5l+0yh2Vjp/R8Wi4X9pnqcayqT4CUJLiHqCvZuLin3IsFdisJL0rKHMoaZLRfmg== - -"@next/swc-linux-x64-gnu@12.2.5": - version "12.2.5" - resolved "https://registry.yarnpkg.com/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-12.2.5.tgz#83a5e224fbc4d119ef2e0f29d0d79c40cc43887e" - integrity sha512-0szyAo8jMCClkjNK0hknjhmAngUppoRekW6OAezbEYwHXN/VNtsXbfzgYOqjKWxEx3OoAzrT3jLwAF0HdX2MEw== - -"@next/swc-linux-x64-musl@12.2.5": - version "12.2.5" - resolved "https://registry.yarnpkg.com/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-12.2.5.tgz#be700d48471baac1ec2e9539396625584a317e95" - integrity sha512-zg/Y6oBar1yVnW6Il1I/08/2ukWtOG6s3acdJdEyIdsCzyQi4RLxbbhkD/EGQyhqBvd3QrC6ZXQEXighQUAZ0g== - -"@next/swc-win32-arm64-msvc@12.2.5": - version "12.2.5" - resolved "https://registry.yarnpkg.com/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-12.2.5.tgz#a93e958133ad3310373fda33a79aa10af2a0aa97" - integrity sha512-3/90DRNSqeeSRMMEhj4gHHQlLhhKg5SCCoYfE3kBjGpE63EfnblYUqsszGGZ9ekpKL/R4/SGB40iCQr8tR5Jiw== - -"@next/swc-win32-ia32-msvc@12.2.5": - version "12.2.5" - resolved "https://registry.yarnpkg.com/@next/swc-win32-ia32-msvc/-/swc-win32-ia32-msvc-12.2.5.tgz#4f5f7ba0a98ff89a883625d4af0125baed8b2e19" - integrity sha512-hGLc0ZRAwnaPL4ulwpp4D2RxmkHQLuI8CFOEEHdzZpS63/hMVzv81g8jzYA0UXbb9pus/iTc3VRbVbAM03SRrw== - -"@next/swc-win32-x64-msvc@12.2.5": - version "12.2.5" - resolved "https://registry.yarnpkg.com/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-12.2.5.tgz#20fed129b04a0d3f632c6d0de135345bb623b1e4" - integrity sha512-7h5/ahY7NeaO2xygqVrSG/Y8Vs4cdjxIjowTZ5W6CKoTKn7tmnuxlUc2h74x06FKmbhAd9agOjr/AOKyxYYm9Q== +"@next/swc-android-arm-eabi@12.3.1": + version "12.3.1" + resolved "https://registry.yarnpkg.com/@next/swc-android-arm-eabi/-/swc-android-arm-eabi-12.3.1.tgz#b15ce8ad376102a3b8c0f3c017dde050a22bb1a3" + integrity sha512-i+BvKA8tB//srVPPQxIQN5lvfROcfv4OB23/L1nXznP+N/TyKL8lql3l7oo2LNhnH66zWhfoemg3Q4VJZSruzQ== + +"@next/swc-android-arm64@12.3.1": + version "12.3.1" + resolved "https://registry.yarnpkg.com/@next/swc-android-arm64/-/swc-android-arm64-12.3.1.tgz#85d205f568a790a137cb3c3f720d961a2436ac9c" + integrity sha512-CmgU2ZNyBP0rkugOOqLnjl3+eRpXBzB/I2sjwcGZ7/Z6RcUJXK5Evz+N0ucOxqE4cZ3gkTeXtSzRrMK2mGYV8Q== + +"@next/swc-darwin-arm64@12.3.1": + version "12.3.1" + resolved "https://registry.yarnpkg.com/@next/swc-darwin-arm64/-/swc-darwin-arm64-12.3.1.tgz#b105457d6760a7916b27e46c97cb1a40547114ae" + integrity sha512-hT/EBGNcu0ITiuWDYU9ur57Oa4LybD5DOQp4f22T6zLfpoBMfBibPtR8XktXmOyFHrL/6FC2p9ojdLZhWhvBHg== + +"@next/swc-darwin-x64@12.3.1": + version "12.3.1" + resolved "https://registry.yarnpkg.com/@next/swc-darwin-x64/-/swc-darwin-x64-12.3.1.tgz#6947b39082271378896b095b6696a7791c6e32b1" + integrity sha512-9S6EVueCVCyGf2vuiLiGEHZCJcPAxglyckTZcEwLdJwozLqN0gtS0Eq0bQlGS3dH49Py/rQYpZ3KVWZ9BUf/WA== + +"@next/swc-freebsd-x64@12.3.1": + version "12.3.1" + resolved "https://registry.yarnpkg.com/@next/swc-freebsd-x64/-/swc-freebsd-x64-12.3.1.tgz#2b6c36a4d84aae8b0ea0e0da9bafc696ae27085a" + integrity sha512-qcuUQkaBZWqzM0F1N4AkAh88lLzzpfE6ImOcI1P6YeyJSsBmpBIV8o70zV+Wxpc26yV9vpzb+e5gCyxNjKJg5Q== + +"@next/swc-linux-arm-gnueabihf@12.3.1": + version "12.3.1" + resolved "https://registry.yarnpkg.com/@next/swc-linux-arm-gnueabihf/-/swc-linux-arm-gnueabihf-12.3.1.tgz#6e421c44285cfedac1f4631d5de330dd60b86298" + integrity sha512-diL9MSYrEI5nY2wc/h/DBewEDUzr/DqBjIgHJ3RUNtETAOB3spMNHvJk2XKUDjnQuluLmFMloet9tpEqU2TT9w== + +"@next/swc-linux-arm64-gnu@12.3.1": + version "12.3.1" + resolved "https://registry.yarnpkg.com/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-12.3.1.tgz#8863f08a81f422f910af126159d2cbb9552ef717" + integrity sha512-o/xB2nztoaC7jnXU3Q36vGgOolJpsGG8ETNjxM1VAPxRwM7FyGCPHOMk1XavG88QZSQf+1r+POBW0tLxQOJ9DQ== + +"@next/swc-linux-arm64-musl@12.3.1": + version "12.3.1" + resolved "https://registry.yarnpkg.com/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-12.3.1.tgz#0038f07cf0b259d70ae0c80890d826dfc775d9f3" + integrity sha512-2WEasRxJzgAmP43glFNhADpe8zB7kJofhEAVNbDJZANp+H4+wq+/cW1CdDi8DqjkShPEA6/ejJw+xnEyDID2jg== + +"@next/swc-linux-x64-gnu@12.3.1": + version "12.3.1" + resolved "https://registry.yarnpkg.com/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-12.3.1.tgz#c66468f5e8181ffb096c537f0dbfb589baa6a9c1" + integrity sha512-JWEaMyvNrXuM3dyy9Pp5cFPuSSvG82+yABqsWugjWlvfmnlnx9HOQZY23bFq3cNghy5V/t0iPb6cffzRWylgsA== + +"@next/swc-linux-x64-musl@12.3.1": + version "12.3.1" + resolved "https://registry.yarnpkg.com/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-12.3.1.tgz#c6269f3e96ac0395bc722ad97ce410ea5101d305" + integrity sha512-xoEWQQ71waWc4BZcOjmatuvPUXKTv6MbIFzpm4LFeCHsg2iwai0ILmNXf81rJR+L1Wb9ifEke2sQpZSPNz1Iyg== + +"@next/swc-win32-arm64-msvc@12.3.1": + version "12.3.1" + resolved "https://registry.yarnpkg.com/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-12.3.1.tgz#83c639ee969cee36ce247c3abd1d9df97b5ecade" + integrity sha512-hswVFYQYIeGHE2JYaBVtvqmBQ1CppplQbZJS/JgrVI3x2CurNhEkmds/yqvDONfwfbttTtH4+q9Dzf/WVl3Opw== + +"@next/swc-win32-ia32-msvc@12.3.1": + version "12.3.1" + resolved "https://registry.yarnpkg.com/@next/swc-win32-ia32-msvc/-/swc-win32-ia32-msvc-12.3.1.tgz#52995748b92aa8ad053440301bc2c0d9fbcf27c2" + integrity sha512-Kny5JBehkTbKPmqulr5i+iKntO5YMP+bVM8Hf8UAmjSMVo3wehyLVc9IZkNmcbxi+vwETnQvJaT5ynYBkJ9dWA== + +"@next/swc-win32-x64-msvc@12.3.1": + version "12.3.1" + resolved "https://registry.yarnpkg.com/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-12.3.1.tgz#27d71a95247a9eaee03d47adee7e3bd594514136" + integrity sha512-W1ijvzzg+kPEX6LAc+50EYYSEo0FVu7dmTE+t+DM4iOLqgGHoW9uYSz9wCVdkXOEEMP9xhXfGpcSxsfDucyPkA== "@nivo/annotations@0.79.1": version "0.79.1" @@ -1423,10 +1423,10 @@ "@styled-system/core" "^5.1.2" "@styled-system/css" "^5.1.5" -"@swc/helpers@0.4.3": - version "0.4.3" - resolved "https://registry.yarnpkg.com/@swc/helpers/-/helpers-0.4.3.tgz#16593dfc248c53b699d4b5026040f88ddb497012" - integrity sha512-6JrF+fdUK2zbGpJIlN7G3v966PQjyx/dPt1T9km2wj+EUBqgrxCk3uX4Kct16MIm9gGxfKRcfax2hVf5jvlTzA== +"@swc/helpers@0.4.11": + version "0.4.11" + resolved "https://registry.yarnpkg.com/@swc/helpers/-/helpers-0.4.11.tgz#db23a376761b3d31c26502122f349a21b592c8de" + integrity sha512-rEUrBSGIoSFuYxwBYtlUFMlE2CwGhmW+w9355/5oduSw8e5h2+Tj4UrAGNNgP9915++wj5vkQo0UuOBqOAq4nw== dependencies: tslib "^2.4.0" @@ -2185,16 +2185,16 @@ camelize@^1.0.0: resolved "https://registry.yarnpkg.com/camelize/-/camelize-1.0.0.tgz#164a5483e630fa4321e5af07020e531831b2609b" integrity sha1-FkpUg+Yw+kMh5a8HAg5TGDGyYJs= -caniuse-lite@^1.0.30001332: - version "1.0.30001365" - resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001365.tgz#72c2c3863b1a545cfd3d9953535bd2ee17568158" - integrity sha512-VDQZ8OtpuIPMBA4YYvZXECtXbddMCUFJk1qu8Mqxfm/SZJNSr1cy4IuLCOL7RJ/YASrvJcYg1Zh+UEUQ5m6z8Q== - caniuse-lite@^1.0.30001370: version "1.0.30001378" resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001378.tgz#3d2159bf5a8f9ca093275b0d3ecc717b00f27b67" integrity sha512-JVQnfoO7FK7WvU4ZkBRbPjaot4+YqxogSDosHv0Hv5mWpUESmN+UubMU6L/hGz8QlQ2aY5U0vR6MOs6j/CXpNA== +caniuse-lite@^1.0.30001406: + version "1.0.30001418" + resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001418.tgz#5f459215192a024c99e3e3a53aac310fc7cf24e6" + integrity sha512-oIs7+JL3K9JRQ3jPZjlH6qyYDp+nBTCais7hjh0s+fuBwufc7uZ7hPYMXrDOJhV360KGMTcczMRObk0/iMqZRg== + caseless@~0.12.0: version "0.12.0" resolved "https://registry.yarnpkg.com/caseless/-/caseless-0.12.0.tgz#1b681c21ff84033c826543090689420d187151dc" @@ -5083,31 +5083,31 @@ neo-async@^2.5.0: resolved "https://registry.yarnpkg.com/neo-async/-/neo-async-2.6.2.tgz#b4aafb93e3aeb2d8174ca53cf163ab7d7308305f" integrity sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw== -next@12.2.5: - version "12.2.5" - resolved "https://registry.yarnpkg.com/next/-/next-12.2.5.tgz#14fb5975e8841fad09553b8ef41fe1393602b717" - integrity sha512-tBdjqX5XC/oFs/6gxrZhjmiq90YWizUYU6qOWAfat7zJwrwapJ+BYgX2PmiacunXMaRpeVT4vz5MSPSLgNkrpA== +next@^12.3.1: + version "12.3.1" + resolved "https://registry.yarnpkg.com/next/-/next-12.3.1.tgz#127b825ad2207faf869b33393ec8c75fe61e50f1" + integrity sha512-l7bvmSeIwX5lp07WtIiP9u2ytZMv7jIeB8iacR28PuUEFG5j0HGAPnMqyG5kbZNBG2H7tRsrQ4HCjuMOPnANZw== dependencies: - "@next/env" "12.2.5" - "@swc/helpers" "0.4.3" - caniuse-lite "^1.0.30001332" + "@next/env" "12.3.1" + "@swc/helpers" "0.4.11" + caniuse-lite "^1.0.30001406" postcss "8.4.14" - styled-jsx "5.0.4" + styled-jsx "5.0.7" use-sync-external-store "1.2.0" optionalDependencies: - "@next/swc-android-arm-eabi" "12.2.5" - "@next/swc-android-arm64" "12.2.5" - "@next/swc-darwin-arm64" "12.2.5" - "@next/swc-darwin-x64" "12.2.5" - "@next/swc-freebsd-x64" "12.2.5" - "@next/swc-linux-arm-gnueabihf" "12.2.5" - "@next/swc-linux-arm64-gnu" "12.2.5" - "@next/swc-linux-arm64-musl" "12.2.5" - "@next/swc-linux-x64-gnu" "12.2.5" - "@next/swc-linux-x64-musl" "12.2.5" - "@next/swc-win32-arm64-msvc" "12.2.5" - "@next/swc-win32-ia32-msvc" "12.2.5" - "@next/swc-win32-x64-msvc" "12.2.5" + "@next/swc-android-arm-eabi" "12.3.1" + "@next/swc-android-arm64" "12.3.1" + "@next/swc-darwin-arm64" "12.3.1" + "@next/swc-darwin-x64" "12.3.1" + "@next/swc-freebsd-x64" "12.3.1" + "@next/swc-linux-arm-gnueabihf" "12.3.1" + "@next/swc-linux-arm64-gnu" "12.3.1" + "@next/swc-linux-arm64-musl" "12.3.1" + "@next/swc-linux-x64-gnu" "12.3.1" + "@next/swc-linux-x64-musl" "12.3.1" + "@next/swc-win32-arm64-msvc" "12.3.1" + "@next/swc-win32-ia32-msvc" "12.3.1" + "@next/swc-win32-x64-msvc" "12.3.1" node-dir@^0.1.17: version "0.1.17" @@ -6535,10 +6535,10 @@ styled-components@5.1.1: shallowequal "^1.1.0" supports-color "^5.5.0" -styled-jsx@5.0.4: - version "5.0.4" - resolved "https://registry.yarnpkg.com/styled-jsx/-/styled-jsx-5.0.4.tgz#5b1bd0b9ab44caae3dd1361295559706e044aa53" - integrity sha512-sDFWLbg4zR+UkNzfk5lPilyIgtpddfxXEULxhujorr5jtePTUqiPDc5BC0v1NRqTr/WaFBGQQUoYToGlF4B2KQ== +styled-jsx@5.0.7: + version "5.0.7" + resolved "https://registry.yarnpkg.com/styled-jsx/-/styled-jsx-5.0.7.tgz#be44afc53771b983769ac654d355ca8d019dff48" + integrity sha512-b3sUzamS086YLRuvnaDigdAewz1/EFYlHpYBP5mZovKEdQQOIIYq8lApylub3HHZ6xFjV051kkGU7cudJmrXEA== styled-system@5.1.5, styled-system@^5.0.0, styled-system@^5.1.5: version "5.1.5" From 7141f1630a162b6764e1de021dff8cb0d6cbcedf Mon Sep 17 00:00:00 2001 From: Maja Komel Date: Fri, 21 Oct 2022 13:26:20 +0200 Subject: [PATCH 03/41] Refactor MAT filters table to virtualize rows (#809) --- components/aggregation/mat/Filters.js | 429 ++++++++++++++++++++++++ components/aggregation/mat/TableView.js | 382 +-------------------- package.json | 1 - yarn.lock | 5 - 4 files changed, 435 insertions(+), 382 deletions(-) create mode 100644 components/aggregation/mat/Filters.js diff --git a/components/aggregation/mat/Filters.js b/components/aggregation/mat/Filters.js new file mode 100644 index 000000000..388852ec5 --- /dev/null +++ b/components/aggregation/mat/Filters.js @@ -0,0 +1,429 @@ +import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react' +import { useTable, useFlexLayout, useRowSelect, useSortBy, useGlobalFilter, useAsyncDebounce } from 'react-table' +import { FormattedMessage, useIntl } from 'react-intl' +import { defaultRangeExtractor, useVirtual } from 'react-virtual' +import styled from 'styled-components' +import { Flex, Box, Button, Text } from 'ooni-components' + +import GridChart, { prepareDataForGridChart } from './GridChart' +import { ResizableBox } from './Resizable' +import { DetailsBox } from '../../measurement/DetailsBox' +import { sortRows } from './computations' + +const TableContainer = styled.div` + ${'' /* These styles are suggested for the table fill all available space in its containing element */} + flex: 1; +` + +const Table = styled.div` + border-spacing: 0; + border: 1px solid black; +` + +const Cell = styled.div` + padding: 8px; +` + +const TableRow = styled(Flex)` + height: 35px; + border-bottom: 1px solid black; + &:last-child { + border-bottom: 0; + } +` + +const TableHeader = styled.div` + ${TableRow} { + height: auto; + margin-bottom: 8px; + border-bottom: 1px solid black; + } + &:last-child { + border-bottom: 2px solid black; + } + & ${Cell} { + border-right: 1px solid black; + font-weight: bold; + &:last-child { + border-right: 0; + } + } +` + +const TableBody = styled.div` + height: 250px; + overflow: auto; +` + +const IndeterminateCheckbox = React.forwardRef( + ({ indeterminate, ...rest }, ref) => { + const defaultRef = React.useRef() + const resolvedRef = ref || defaultRef + + React.useEffect(() => { + resolvedRef.current.indeterminate = indeterminate + }, [resolvedRef, indeterminate]) + + return ( + <> + + + ) + } +) +IndeterminateCheckbox.displayName = 'IndeterminateCheckbox' + +const SearchFilter = ({ + column: { filterValue, preFilteredRows, setFilter }, + groupedRows, +}) => { + const count = groupedRows.length + + return ( + { + setFilter(e.target.value || undefined) // Set undefined to remove the filter entirely + }} + placeholder={`Search ${count} records...`} + /> + ) +} + +const StyledGlobalFilter = styled(Box)` + margin: 16px; + margin-top: 10px; + input { + border: 0; + outline: 0; + } +` + +function GlobalFilter({ + preGlobalFilteredRows, + globalFilter, + setGlobalFilter, +}) { + const count = preGlobalFilteredRows.length + const [value, setValue] = React.useState(globalFilter) + const onChange = useAsyncDebounce(value => { + setGlobalFilter(value || '') + }, 200) + + useEffect(() => { + if (!globalFilter || globalFilter === '') { + setValue('') + } + }, [globalFilter]) + + return ( + + Search:{' '} + { + setValue(e.target.value) + onChange(e.target.value) + }} + placeholder={`Search ${count} records...`} + /> + + ) +} + +const SortHandle = ({ isSorted, isSortedDesc }) => { + return ( + + {isSorted ? ( + isSortedDesc ? '▼' : '▲' + ) : ( +   + )} + ) +} + +// This same reference is passed to GridChart when there are no rows to filter out +// Maybe this can also be `[]` +const noRowsSelected = null + +const Filters = ({ data = [], tableData, setDataForCharts, query }) => { + const intl = useIntl() + const resetTableRef = useRef(false) + const yAxis = query.axis_y + + const defaultColumn = React.useMemo( + () => ({ + // When using the useFlexLayout: + width: 70, // width is used for both the flex-basis and flex-grow + Filter: SearchFilter, + Cell: ({ value }) => { + const intl = useIntl() + return typeof value === 'number' ? intl.formatNumber(value, {}) : String(value) + } + }), + [] + ) + + // Aggregate by the first column + const initialState = React.useMemo(() => ({ + hiddenColumns: ['yAxisCode'], + sortBy: [{ id: 'yAxisLabel', desc: false }] + }),[]) + + const getRowId = React.useCallback(row => row[query.axis_y], []) + + const columns = useMemo(() => [ + { + Header: intl.formatMessage({ id: `MAT.Table.Header.${yAxis}`}), + Cell: ({ value, row }) => ( + + {value} + + ), + id: 'yAxisLabel', + accessor: 'rowLabel', + filter: 'text', + style: { + width: '35%' + } + }, + { + id: 'yAxisCode', + accessor: yAxis, + disableFilters: true, + }, + { + Header: , + accessor: 'anomaly_count', + width: 150, + sortDescFirst: true, + disableFilters: true, + style: { + textAlign: 'end' + } + }, + { + Header: , + accessor: 'confirmed_count', + width: 150, + sortDescFirst: true, + disableFilters: true, + style: { + textAlign: 'end' + } + }, + { + Header: , + accessor: 'failure_count', + width: 150, + sortDescFirst: true, + disableFilters: true, + style: { + textAlign: 'end' + } + }, + { + Header: , + accessor: 'measurement_count', + width: 150, + sortDescFirst: true, + disableFilters: true, + style: { + textAlign: 'end' + } + } + ], [intl, yAxis]) + + const { + getTableProps, + getTableBodyProps, + headerGroups, + rows, // contains filtered rows + toggleAllRowsSelected, + selectedFlatRows, + prepareRow, + state, + setGlobalFilter, + preGlobalFilteredRows, + globalFilteredRows, + } = useTable( + { + columns, + data: tableData, + initialState, + defaultColumn, + getRowId, + }, + useFlexLayout, + useGlobalFilter, + useSortBy, + useRowSelect, + (hooks) => { + hooks.visibleColumns.push((columns) => [ + // Pseudo column for selection checkboxes + { + id: 'selection', + width: 30, + // The header can use the table's getToggleAllRowsSelectedProps method + // to render a checkbox + // eslint-disable-next-line react/display-name + Header: ({ getToggleAllRowsSelectedProps }) => ( +
+ +
+ ), + // The cell can use the individual row's getToggleRowSelectedProps method + // to the render a checkbox + // eslint-disable-next-line react/display-name + Cell: ({ row }) => ( +
+ +
+ ) + }, + ...columns + ]) + } + ) + + const updateCharts = useCallback(() => { + const selectedRows = Object.keys(state.selectedRowIds).sort((a,b) => sortRows(a, b, query.axis_y)) + + if (selectedRows.length > 0 && selectedRows.length !== preGlobalFilteredRows.length) { + setDataForCharts(selectedRows) + } else { + setDataForCharts(noRowsSelected) + } + }, [preGlobalFilteredRows.length, query.axis_y, state.selectedRowIds, setDataForCharts]) + + /** + * Reset the table filter + * Note: doesn't reset the sort state + */ + const resetFilter = useCallback(() => { + // toggleAllRowsSelected() doesn't work after calling setGlobalFilter('') + // so if globalFilter is set, then use resetTableRef to make it a two-step + // reset (step 2 in the below useEffect) + // otherwise, just toggle the selected rows and the reset is done + if (!state.globalFilter) { + toggleAllRowsSelected(false) + } else { + resetTableRef.current = true + setGlobalFilter('') + } + setDataForCharts(noRowsSelected) + }, [setGlobalFilter, state.globalFilter, toggleAllRowsSelected, setDataForCharts]) + + useEffect(() => { + if (state.globalFilter == undefined && resetTableRef.current === true) { + resetTableRef.current = false + toggleAllRowsSelected(false) + } + }, [state.globalFilter, toggleAllRowsSelected]) + + const parentRef = React.useRef() + + const { virtualItems: virtualRows, totalSize } = useVirtual({ + size: rows.length, + parentRef, + overscan: 10, + estimateSize: React.useCallback(() => 35, []), + }) + + return ( + + + + + + + + + + {headerGroups.map((headerGroup, i) => ( + + {headerGroup.headers.map((column, i) => { + return ( + + + {column.render('Header')} + {column.canSort && + + } + + + )} + )} + + ))} + + + + +
+ + {virtualRows.map(virtualRow => { + const row = rows[virtualRow.index] + prepareRow(row) + return ( + + {row.cells.map((cell, i) => { + return ( + + {cell.render('Cell')} + + ) + })} + + ) + })} + +
+
+
+
+
+ ) +} + +export default Filters \ No newline at end of file diff --git a/components/aggregation/mat/TableView.js b/components/aggregation/mat/TableView.js index eb80bd540..66632a9a3 100644 --- a/components/aggregation/mat/TableView.js +++ b/components/aggregation/mat/TableView.js @@ -8,141 +8,7 @@ import GridChart, { prepareDataForGridChart } from './GridChart' import { ResizableBox } from './Resizable' import { DetailsBox } from '../../measurement/DetailsBox' import { sortRows } from './computations' - -const TableContainer = styled.div` - ${'' /* These styles are suggested for the table fill all available space in its containing element */} - flex: 1; - ${'' /* These styles are required for a horizontaly scrollable table overflow */} - overflow: auto; -` - -const Table = styled.div` - border-spacing: 0; - border: 1px solid black; -` - -const Cell = styled.div` - padding: 8px; -` - -const TableRow = styled(Flex)` - border-bottom: 1px solid black; - &:last-child { - border-bottom: 0; - } -` - -const TableHeader = styled.div` - ${TableRow} { - margin-bottom: 8px; - border-bottom: 1px solid black; - } - &:last-child { - border-bottom: 2px solid black; - } - & ${Cell} { - border-right: 1px solid black; - font-weight: bold; - &:last-child { - border-right: 0; - } - } - -` - -const TableBody = styled.div` - ${'' /* These styles are required for a scrollable table body */} - overflow-y: scroll; - overflow-x: hidden; - height: 250px; -` - -const IndeterminateCheckbox = React.forwardRef( - ({ indeterminate, ...rest }, ref) => { - const defaultRef = React.useRef() - const resolvedRef = ref || defaultRef - - React.useEffect(() => { - resolvedRef.current.indeterminate = indeterminate - }, [resolvedRef, indeterminate]) - - return ( - <> - - - ) - } -) -IndeterminateCheckbox.displayName = 'IndeterminateCheckbox' - -const SearchFilter = ({ - column: { filterValue, preFilteredRows, setFilter }, - groupedRows, -}) => { - const count = groupedRows.length - - return ( - { - setFilter(e.target.value || undefined) // Set undefined to remove the filter entirely - }} - placeholder={`Search ${count} records...`} - /> - ) -} - -const StyledGlobalFilter = styled(Box)` - margin: 16px; - margin-top: 10px; - input { - border: 0; - outline: 0; - } -` - -function GlobalFilter({ - preGlobalFilteredRows, - globalFilter, - setGlobalFilter, -}) { - const count = preGlobalFilteredRows.length - const [value, setValue] = React.useState(globalFilter) - const onChange = useAsyncDebounce(value => { - setGlobalFilter(value || undefined) - }, 200) - - useEffect(() => { - if (!globalFilter || globalFilter === '') { - setValue('') - } - }, [globalFilter]) - - return ( - - Search:{' '} - { - setValue(e.target.value) - onChange(e.target.value) - }} - placeholder={`Search ${count} records...`} - /> - - ) -} - -const SortHandle = ({ isSorted, isSortedDesc }) => { - return ( - - {isSorted ? ( - isSortedDesc ? '▼' : '▲' - ) : ( -   - )} - ) -} +import Filters from './Filters' const prepareDataforTable = (data, query) => { const table = [] @@ -182,89 +48,6 @@ const TableView = ({ data, query }) => { const resetTableRef = useRef(false) const yAxis = query.axis_y - const defaultColumn = React.useMemo( - () => ({ - // When using the useFlexLayout: - width: 70, // width is used for both the flex-basis and flex-grow - Filter: SearchFilter, - Cell: ({ value }) => { - const intl = useIntl() - return typeof value === 'number' ? intl.formatNumber(value, {}) : String(value) - } - }), - [] - ) - - // Aggregate by the first column - const initialState = React.useMemo(() => ({ - hiddenColumns: ['yAxisCode'], - sortBy: [{ id: 'yAxisLabel', desc: false }] - }),[]) - - const getRowId = React.useCallback(row => row[query.axis_y], []) - - const columns = useMemo(() => [ - { - Header: intl.formatMessage({ id: `MAT.Table.Header.${yAxis}`}), - Cell: ({ value, row }) => ( - - {value} - - ), - id: 'yAxisLabel', - accessor: 'rowLabel', - filter: 'text', - style: { - width: '35%' - } - }, - { - id: 'yAxisCode', - accessor: yAxis, - disableFilters: true, - }, - { - Header: , - accessor: 'anomaly_count', - width: 150, - sortDescFirst: true, - disableFilters: true, - style: { - textAlign: 'end' - } - }, - { - Header: , - accessor: 'confirmed_count', - width: 150, - sortDescFirst: true, - disableFilters: true, - style: { - textAlign: 'end' - } - }, - { - Header: , - accessor: 'failure_count', - width: 150, - sortDescFirst: true, - disableFilters: true, - style: { - textAlign: 'end' - } - }, - { - Header: , - accessor: 'measurement_count', - width: 150, - sortDescFirst: true, - disableFilters: true, - style: { - textAlign: 'end' - } - } - ], [intl, yAxis]) - // The incoming data is reshaped to generate: // - reshapedData: holds the full set that will be used by GridChart // to then filter out rows based on `selectedRows` generated by the table @@ -279,168 +62,15 @@ const TableView = ({ data, query }) => { } }, [query, data]) - const { - getTableProps, - getTableBodyProps, - headerGroups, - rows, // contains filtered rows - toggleAllRowsSelected, - selectedFlatRows, - prepareRow, - state, - setGlobalFilter, - preGlobalFilteredRows, - globalFilteredRows, - } = useTable( - { - columns, - data: tableData, - initialState, - defaultColumn, - getRowId, - }, - useFlexLayout, - useGlobalFilter, - useSortBy, - useRowSelect, - (hooks) => { - hooks.visibleColumns.push((columns) => [ - // Pseudo column for selection checkboxes - { - id: 'selection', - width: 30, - // The header can use the table's getToggleAllRowsSelectedProps method - // to render a checkbox - // eslint-disable-next-line react/display-name - Header: ({ getToggleAllRowsSelectedProps }) => ( -
- -
- ), - // The cell can use the individual row's getToggleRowSelectedProps method - // to the render a checkbox - // eslint-disable-next-line react/display-name - Cell: ({ row }) => ( -
- -
- ) - }, - ...columns - ]) - } - ) - - // const [chartPanelHeight, setChartPanelHeight] = useState(800) - - // const onPanelResize = useCallback((width, height) => { - // // Panel height - (height of ChartHeader + XAxis) = Height of RowCharts - // setChartPanelHeight(height - (90 + 62)) - // }, []) - const [dataForCharts, setDataForCharts] = useState(noRowsSelected) - - const updateCharts = useCallback(() => { - const selectedRows = Object.keys(state.selectedRowIds).sort((a,b) => sortRows(a, b, query.axis_y)) - - if (selectedRows.length > 0 && selectedRows.length !== preGlobalFilteredRows.length) { - setDataForCharts(selectedRows) - } else { - setDataForCharts(noRowsSelected) - } - }, [preGlobalFilteredRows.length, query.axis_y, state.selectedRowIds]) - - /** - * Reset the table filter - * Note: doesn't reset the sort state - */ - const resetFilter = useCallback(() => { - // toggleAllRowsSelected() doesn't work after calling setGlobalFilter('') - // so if globalFilter is set, then use resetTableRef to make it a two-step - // reset (step 2 in the below useEffect) - // otherwise, just toggle the selected rows and the reset is done - if (!state.globalFilter) { - toggleAllRowsSelected(false) - } else { - resetTableRef.current = true - setGlobalFilter('') - } - setDataForCharts(noRowsSelected) - }, [setGlobalFilter, state.globalFilter, toggleAllRowsSelected]) - - useEffect(() => { - if (state.globalFilter == undefined && resetTableRef.current === true) { - resetTableRef.current = false - toggleAllRowsSelected(false) - } - }, [state.globalFilter, toggleAllRowsSelected]) return ( - - - - {/* {chartsButton} */} - - - - - {/* eslint-disable react/jsx-key */} - - - {headerGroups.map(headerGroup => ( - - {headerGroup.headers.map(column => { - return ( - - - {column.render('Header')} - {column.canSort && - - } - - - )} - )} - - ))} - - - - - - {rows.map(row => { - prepareRow(row) - return ( - - {row.cells.map(cell => { - return ( - - {cell.render('Cell')} - - ) - })} - - ) - })} - -
- {/* eslint-enable react/jsx-key */} -
-
-
+ Date: Fri, 21 Oct 2022 13:27:23 +0200 Subject: [PATCH 04/41] Search page refactoring (#799) * Remove AS0 filter --- pages/search.js | 398 +++++++++++++++++++----------------------------- 1 file changed, 155 insertions(+), 243 deletions(-) diff --git a/pages/search.js b/pages/search.js index 763875fca..1ef1da629 100644 --- a/pages/search.js +++ b/pages/search.js @@ -1,7 +1,7 @@ -import React from 'react' +import React, { useState, useEffect } from 'react' import PropTypes from 'prop-types' import Head from 'next/head' -import { withRouter } from 'next/router' +import { useRouter } from 'next/router' import axios from 'axios' import styled from 'styled-components' import { @@ -24,11 +24,53 @@ import FormattedMarkdown from '../components/FormattedMarkdown' import { sortByKey } from '../utils' +export const getServerSideProps = async ({query}) => { + // By default, on '/search' show measurements published until today + // including the measurements of today (so the date of tomorrow). + // This prevents the search page from showing time-travelling future + // measurements from showing up + query.since = query.since || dayjs(query.until).utc().subtract(30, 'day').format('YYYY-MM-DD') + query.until = query.until || dayjs.utc().add(1, 'day').format('YYYY-MM-DD') + + // If there is no 'failure' in query, default to a false + if ('failure' in query === false) { + query.failure = false + } else { + // Convert the string param into boolean + query.failure = !(query.failure === 'false') + } + + const client = axios.create({baseURL: process.env.NEXT_PUBLIC_OONI_API}) + const [testNamesR, countriesR] = await Promise.all([ + client.get('/api/_/test_names'), + client.get('/api/_/countries') + ]) + + const testNames = testNamesR.data.test_names + testNames.sort(sortByKey('name')) + + const testNamesKeyed = {} + testNames.forEach(v => testNamesKeyed[v.id] = v.name) + + const countries = countriesR.data.countries + countries.sort(sortByKey('name')) + + return { + props: { + testNamesKeyed, + testNames, + countries, + query, + } + } +} + + const queryToParams = ({ query }) => { - // XXX do better validation let params = {}, show = 50 const supportedParams = ['probe_cc', 'domain', 'input','category_code', 'probe_asn', 'test_name', 'since', 'until', 'failure'] + if (query.show) { show = parseInt(query.show) } @@ -143,292 +185,162 @@ const NoResults = () => (
) -class Search extends React.Component { - static async getInitialProps ({ query }) { - let msmtR, testNamesR, countriesR - let client = axios.create({baseURL: process.env.NEXT_PUBLIC_OONI_API}) // eslint-disable-line - - // By default, on '/search' show measurements published until today - // including the measurements of today (so the date of tomorrow). - // This prevents the search page from showing time-travelling future - // measurements from showing up - const since = dayjs(query.until).utc().subtract(30, 'day').format('YYYY-MM-DD') - if (!query.since) { - query.since = since - } +const Search = ({testNames, testNamesKeyed, countries, query: queryProp }) => { + const router = useRouter() + const { query, replace, isReady } = router - const until = dayjs.utc().add(1, 'day').format('YYYY-MM-DD') - if ('until' in query === false) { - query.until = until - } + const [nextURL, setNextURL] = useState(null) + const [loading, setLoading] = useState(false) + const [results, setResults] = useState([]) + const [filters, setFilters] = useState({}) + const [error, setError] = useState(null) - // If there is no 'failure' in query, default to a false - if ('failure' in query === false) { - query.failure = false - } else { - // Convert the string param into boolean - query.failure = !(query.failure === 'false') - } - - [testNamesR, countriesR] = await Promise.all([ - client.get('/api/_/test_names'), - client.get('/api/_/countries') - ]) - - let testNames = testNamesR.data.test_names - testNames.sort(sortByKey('name')) - - let testNamesKeyed = {} - testNames.forEach(v => testNamesKeyed[v.id] = v.name) - - let countries = countriesR.data.countries - countries.sort(sortByKey('name')) - - - try { - msmtR = await getMeasurements(query) - } catch (err) { - let error - if (err.response) { - error = err.response.data - } else { - error = err.message - } - return { - error, - results: [], - nextURL: null, - testNamesKeyed, - testNames, - countries - } - } - - const measurements = msmtR.data - // drop results with probe_asn === 'AS0' - const results = measurements.results.filter(item => item.probe_asn !== 'AS0') - - return { - error: null, - results, - nextURL: measurements.metadata.next_url, - testNamesKeyed, - testNames, - countries, - } - } - - constructor(props) { - super(props) - this.state = { - testNameFilter: props.router.query.test_name, - domainFilter: props.router.query.domain, - inputFilter: props.router.query.input, - categoryFilter: props.router.query.category_code, - countryFilter: props.router.query.probe_cc, - asnFilter: props.router.query.probe_asn, - sinceFilter: props.router.query.since, - untilFilter: props.router.query.until, - onlyFilter: props.router.query.only || 'all', - hideFailed: !props.router.query.failure, - results: props.results, - nextURL: props.nextURL, - error: props.error, - - loading: false - } - this.getFilterQuery = this.getFilterQuery.bind(this) - this.onApplyFilter = this.onApplyFilter.bind(this) - this.loadMore = this.loadMore.bind(this) - } - - componentDidMount () { - const { query, replace } = this.props.router + useEffect(() => { + const query = query || queryProp const href = { pathname: '/search', - query + query: query } replace(href, href, { shallow: true }) - } - shouldComponentUpdate (nextProps, nextState) { - if (this.props != nextProps) { - return true - } - if (this.state.results != nextState.results) { - return true - } - if (this.state.loading != nextState.loading) { - return true - } - return false - } + setLoading(true) + getMeasurements(query) + .then(({ data: { results, metadata: { next_url } } }) => { + setLoading(false) + setResults(results) + setNextURL(next_url) + }) + .catch((err) => { + console.error(err) + const error = serializeError(err) + setLoading(false) + setError(error) + }) + }, []) - loadMore() { - axios.get(this.state.nextURL) - .then((res) => { - // XXX update the query - const nextPageResults = res.data.results.filter(item => item.probe_asn !== 'AS0') - this.setState({ - results: this.state.results.concat(nextPageResults), - nextURL: res.data.metadata.next_url, - show: this.state.show + 50 - }) + const loadMore = () => { + axios.get(nextURL) + .then(({ data: { results: nextPageResults, metadata: { next_url } } }) => { + setResults(results.concat(nextPageResults)) + setNextURL(next_url) }) .catch((err) => { console.error(err) const error = serializeError(err) - this.setState({ - error, - loading: false - }) + setLoading(false) + setError(error) }) } - onApplyFilter (state) { - this.setState({ - error: null, - loading: true, - ...state - }, () => { - const query = this.getFilterQuery() - const href = { - pathname: '/search', - query - } - this.props.router.push(href, href, { shallow: true }).then(() => { - // XXX do error handling - getMeasurements(query) - .then((res) => { - const results = res.data.results.filter(item => item.probe_asn !== 'AS0') - this.setState({ - loading: false, - results, - nextURL: res.data.metadata.next_url - }) - }) - .catch((err) => { - console.error(err) - const error = serializeError(err) - this.setState({ - error, - loading: false - }) - }) + const onApplyFilter = (state) => { + setLoading(true) + setResults([]) + setError(null) + + const query = getFilterQuery(state) + const href = { + pathname: '/search', + query + } + router.push(href, href, { shallow: true }).then(() => { + getMeasurements(query) + .then(({ data: { results, metadata: { next_url } } }) => { + setLoading(false) + setResults(results) + setNextURL(next_url) + }) + .catch((err) => { + console.error(err) + const error = serializeError(err) + setError(error) + setLoading(false) + }) }) - }) } - getFilterQuery () { - let query = {...this.props.router.query} + const getFilterQuery = (state) => { + let query = {...router.query} const resetValues = [undefined, 'XX', ''] for (const [queryParam, [key]] of Object.entries(queryToFilterMap)) { // If it's unset or marked as XX, let's be sure the path is clean - if (resetValues.includes(this.state[key])) { + if (resetValues.includes(state[key])) { if (queryParam in query) { delete query[queryParam] } - } else if (key === 'onlyFilter' && this.state[key] == 'all') { + } else if (key === 'onlyFilter' && state[key] == 'all') { // If the onlyFilter is not set to 'confirmed' or 'anomalies' // remove it from the path if (queryParam in query) { delete query[queryParam] } } else if (key === 'hideFailed') { - if (this.state[key] === true) { + if (state[key] === true) { // When `hideFailure` is true, add `failure=false` in the query query[queryParam] = false } else { query[queryParam] = true } } else { - query[queryParam] = this.state[key] + query[queryParam] = state[key] } } return query } - render () { - const { - testNames, - testNamesKeyed, - countries - } = this.props - - const { - loading, - error, - results, - onlyFilter, - domainFilter, - inputFilter, - categoryFilter, - testNameFilter, - countryFilter, - asnFilter, - sinceFilter, - untilFilter, - hideFailed - } = this.state - - return ( - - - Search through millions of Internet censorship measurements | OONI Explorer - - - - - - - - - - - {error && } - {loading && } - - {!error && !loading && results.length === 0 && } - {!error && !loading && results.length > 0 && - - {this.state.nextURL && - - - - } - } - - - - - ) - } + return ( + + + Search through millions of Internet censorship measurements | OONI Explorer + + + + + + + + + + + {error && } + {loading && } + + {!error && !loading && results.length === 0 && } + {!error && !loading && results.length > 0 && + + {nextURL && + + + + } + } + + + + + ) } Search.propTypes = { - router: PropTypes.object, - results: PropTypes.array, testNamesKeyed: PropTypes.object, testNames: PropTypes.array, countries: PropTypes.array, - nextURL: PropTypes.string, - error: PropTypes.object + query: PropTypes.object, } -export default withRouter(Search) +export default Search \ No newline at end of file From 0914dcf6ea7a2bb3bf3bf4d6e47f5c65bb2141ef Mon Sep 17 00:00:00 2001 From: Maja Komel Date: Tue, 25 Oct 2022 14:21:36 +0200 Subject: [PATCH 05/41] Use SWC instead of Babel (#804) --- babel.config.js | 25 -- next.config.js | 37 +- package.json | 1 + yarn.lock | 1105 ++++++++++++++++++++++++++++++++++++++++++++++- 4 files changed, 1126 insertions(+), 42 deletions(-) delete mode 100644 babel.config.js diff --git a/babel.config.js b/babel.config.js deleted file mode 100644 index f2d146da3..000000000 --- a/babel.config.js +++ /dev/null @@ -1,25 +0,0 @@ -module.exports = function (api) { - const isServer = api.caller((caller) => caller?.isServer) - const isCallerDevelopment = api.caller((caller) => caller?.isDev) - - const plugins = [ - 'inline-react-svg', - ['styled-components', {'ssr': true, 'displayName': true, 'preprocess': false}], - ] - - const presets = [ - [ - 'next/babel', - { - 'preset-react': { - importSource: - !isServer && isCallerDevelopment - ? '@welldone-software/why-did-you-render' - : 'react', - }, - }, - ], - ] - - return { presets, plugins } -} diff --git a/next.config.js b/next.config.js index fc3dbea9b..d0f3a28e5 100644 --- a/next.config.js +++ b/next.config.js @@ -3,6 +3,8 @@ // https://nextjs.org/docs/api-reference/next.config.js/introduction // https://docs.sentry.io/platforms/javascript/guides/nextjs/ const { withSentryConfig } = require('@sentry/nextjs') +const glob = require('glob') +const { dirname, basename, resolve } = require('path') const { execSync } = require('child_process') const SentryWebpackPluginOptions = { @@ -24,7 +26,12 @@ module.exports = withSentryConfig({ }, ] }, - + compiler: { + // see https://styled-components.com/docs/tooling#babel-plugin for more info on the options. + styledComponents: { + ssr: true, + }, + }, webpack: (config, options) => { const gitCommitSHAShort = process.env.RUN_GIT_COMMIT_SHA_SHORT ? execSync(process.env.RUN_GIT_COMMIT_SHA_SHORT) : '' const gitCommitSHA = process.env.RUN_GIT_COMMIT_SHA ? execSync(process.env.RUN_GIT_COMMIT_SHA) : '' @@ -44,6 +51,34 @@ module.exports = withSentryConfig({ 'process.env.WDYR': JSON.stringify(process.env.WDYR), }) ) + + // SVG + config.module.rules.push({ + test: /\.svg$/, + issuer: /\.js?$/, + include: [options.dir], + use: [ + 'next-swc-loader', + { + loader: '@svgr/webpack', + options: { babel: false } + } + ], + }) + + // whyDidYouRender + if (options.dev && !options.isServer) { + const originalEntry = config.entry + config.entry = async () => { + const wdrPath = resolve(__dirname, './scripts/wdyr.js') + const entries = await originalEntry() + if (entries['main.js'] && !entries['main.js'].includes(wdrPath)) { + entries['main.js'].unshift(wdrPath) + } + return entries + } + } + return config }, productionBrowserSourceMaps: true, diff --git a/package.json b/package.json index 6388214a8..1b9ac5629 100644 --- a/package.json +++ b/package.json @@ -66,6 +66,7 @@ "lodash": "^4.17.21" }, "devDependencies": { + "@svgr/webpack": "^6.5.0", "@welldone-software/why-did-you-render": "^7.0.1", "babel-plugin-formatjs": "^10.3.25", "cypress": "^10.6.0", diff --git a/yarn.lock b/yarn.lock index edd589253..3798ab31f 100644 --- a/yarn.lock +++ b/yarn.lock @@ -31,6 +31,11 @@ dependencies: "@babel/highlight" "^7.18.6" +"@babel/compat-data@^7.17.7", "@babel/compat-data@^7.19.3", "@babel/compat-data@^7.19.4": + version "7.19.4" + resolved "https://registry.yarnpkg.com/@babel/compat-data/-/compat-data-7.19.4.tgz#95c86de137bf0317f3a570e1b6e996b427299747" + integrity sha512-CHIGpJcUQ5lU9KrPHTjBMhVwQG6CQjxfg36fGXl3qk/Gik1WwWachaXFuo0uCWJT/mStOKtcbFJCaVLihC1CMw== + "@babel/compat-data@^7.18.8": version "7.18.8" resolved "https://registry.yarnpkg.com/@babel/compat-data/-/compat-data-7.18.8.tgz#2483f565faca607b8535590e84e7de323f27764d" @@ -57,6 +62,27 @@ json5 "^2.2.1" semver "^6.3.0" +"@babel/core@^7.18.5": + version "7.19.6" + resolved "https://registry.yarnpkg.com/@babel/core/-/core-7.19.6.tgz#7122ae4f5c5a37c0946c066149abd8e75f81540f" + integrity sha512-D2Ue4KHpc6Ys2+AxpIx1BZ8+UegLLLE2p3KJEuJRKmokHOtl49jQ5ny1773KsGLZs8MQvBidAF6yWUJxRqtKtg== + dependencies: + "@ampproject/remapping" "^2.1.0" + "@babel/code-frame" "^7.18.6" + "@babel/generator" "^7.19.6" + "@babel/helper-compilation-targets" "^7.19.3" + "@babel/helper-module-transforms" "^7.19.6" + "@babel/helpers" "^7.19.4" + "@babel/parser" "^7.19.6" + "@babel/template" "^7.18.10" + "@babel/traverse" "^7.19.6" + "@babel/types" "^7.19.4" + convert-source-map "^1.7.0" + debug "^4.1.0" + gensync "^1.0.0-beta.2" + json5 "^2.2.1" + semver "^6.3.0" + "@babel/generator@^7.13.0": version "7.13.9" resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.13.9.tgz#3a7aa96f9efb8e2be42d38d80e2ceb4c64d8de39" @@ -75,6 +101,15 @@ "@jridgewell/gen-mapping" "^0.3.2" jsesc "^2.5.1" +"@babel/generator@^7.19.6": + version "7.19.6" + resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.19.6.tgz#9e481a3fe9ca6261c972645ae3904ec0f9b34a1d" + integrity sha512-oHGRUQeoX1QrKeJIKVe0hwjGqNnVYsM5Nep5zo0uE0m42sLH+Fsd2pStJ5sRM1bNyTUUoz0pe2lTeMJrb/taTA== + dependencies: + "@babel/types" "^7.19.4" + "@jridgewell/gen-mapping" "^0.3.2" + jsesc "^2.5.1" + "@babel/helper-annotate-as-pure@^7.0.0": version "7.0.0" resolved "https://registry.yarnpkg.com/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.0.0.tgz#323d39dd0b50e10c7c06ca7d7638e6864d8c5c32" @@ -96,6 +131,24 @@ dependencies: "@babel/types" "^7.18.6" +"@babel/helper-builder-binary-assignment-operator-visitor@^7.18.6": + version "7.18.9" + resolved "https://registry.yarnpkg.com/@babel/helper-builder-binary-assignment-operator-visitor/-/helper-builder-binary-assignment-operator-visitor-7.18.9.tgz#acd4edfd7a566d1d51ea975dff38fd52906981bb" + integrity sha512-yFQ0YCHoIqarl8BCRwBL8ulYUaZpz3bNsA7oFepAzee+8/+ImtADXNOmO5vJvsPff3qi+hvpkY/NYBTrBQgdNw== + dependencies: + "@babel/helper-explode-assignable-expression" "^7.18.6" + "@babel/types" "^7.18.9" + +"@babel/helper-compilation-targets@^7.17.7", "@babel/helper-compilation-targets@^7.19.0", "@babel/helper-compilation-targets@^7.19.3": + version "7.19.3" + resolved "https://registry.yarnpkg.com/@babel/helper-compilation-targets/-/helper-compilation-targets-7.19.3.tgz#a10a04588125675d7c7ae299af86fa1b2ee038ca" + integrity sha512-65ESqLGyGmLvgR0mst5AdW1FkNlj9rQsCKduzEoEPhBCDFGXvz2jW6bXFG6i0/MrV2s7hhXjjb2yAzcPuQlLwg== + dependencies: + "@babel/compat-data" "^7.19.3" + "@babel/helper-validator-option" "^7.18.6" + browserslist "^4.21.3" + semver "^6.3.0" + "@babel/helper-compilation-targets@^7.18.9": version "7.18.9" resolved "https://registry.yarnpkg.com/@babel/helper-compilation-targets/-/helper-compilation-targets-7.18.9.tgz#69e64f57b524cde3e5ff6cc5a9f4a387ee5563bf" @@ -119,11 +172,38 @@ "@babel/helper-replace-supers" "^7.18.9" "@babel/helper-split-export-declaration" "^7.18.6" +"@babel/helper-create-regexp-features-plugin@^7.18.6", "@babel/helper-create-regexp-features-plugin@^7.19.0": + version "7.19.0" + resolved "https://registry.yarnpkg.com/@babel/helper-create-regexp-features-plugin/-/helper-create-regexp-features-plugin-7.19.0.tgz#7976aca61c0984202baca73d84e2337a5424a41b" + integrity sha512-htnV+mHX32DF81amCDrwIDr8nrp1PTm+3wfBN9/v8QJOLEioOCOG7qNyq0nHeFiWbT3Eb7gsPwEmV64UCQ1jzw== + dependencies: + "@babel/helper-annotate-as-pure" "^7.18.6" + regexpu-core "^5.1.0" + +"@babel/helper-define-polyfill-provider@^0.3.3": + version "0.3.3" + resolved "https://registry.yarnpkg.com/@babel/helper-define-polyfill-provider/-/helper-define-polyfill-provider-0.3.3.tgz#8612e55be5d51f0cd1f36b4a5a83924e89884b7a" + integrity sha512-z5aQKU4IzbqCC1XH0nAqfsFLMVSo22SBKUc0BxGrLkolTdPTructy0ToNnlO2zA4j9Q/7pjMZf0DSY+DSTYzww== + dependencies: + "@babel/helper-compilation-targets" "^7.17.7" + "@babel/helper-plugin-utils" "^7.16.7" + debug "^4.1.1" + lodash.debounce "^4.0.8" + resolve "^1.14.2" + semver "^6.1.2" + "@babel/helper-environment-visitor@^7.18.9": version "7.18.9" resolved "https://registry.yarnpkg.com/@babel/helper-environment-visitor/-/helper-environment-visitor-7.18.9.tgz#0c0cee9b35d2ca190478756865bb3528422f51be" integrity sha512-3r/aACDJ3fhQ/EVgFy0hpj8oHyHpQc+LPtJoY9SzTThAsStm4Ptegq92vqKoE3vD706ZVFWITnMnxucw+S9Ipg== +"@babel/helper-explode-assignable-expression@^7.18.6": + version "7.18.6" + resolved "https://registry.yarnpkg.com/@babel/helper-explode-assignable-expression/-/helper-explode-assignable-expression-7.18.6.tgz#41f8228ef0a6f1a036b8dfdfec7ce94f9a6bc096" + integrity sha512-eyAYAsQmB80jNfg4baAtLeWAQHfHFiR483rzFK+BhETlGZaQC9bsfrugfXDCbRHLQbIA7U5NxhhOxN7p/dWIcg== + dependencies: + "@babel/types" "^7.18.6" + "@babel/helper-function-name@^7.12.13": version "7.12.13" resolved "https://registry.yarnpkg.com/@babel/helper-function-name/-/helper-function-name-7.12.13.tgz#93ad656db3c3c2232559fd7b2c3dbdcbe0eb377a" @@ -141,6 +221,14 @@ "@babel/template" "^7.18.6" "@babel/types" "^7.18.9" +"@babel/helper-function-name@^7.19.0": + version "7.19.0" + resolved "https://registry.yarnpkg.com/@babel/helper-function-name/-/helper-function-name-7.19.0.tgz#941574ed5390682e872e52d3f38ce9d1bef4648c" + integrity sha512-WAwHBINyrpqywkUH0nTnNgI5ina5TFn85HKS0pbPDfxFfhyR/aNQEn4hGi1P1JyT//I0t4OgXUlofzWILRvS5w== + dependencies: + "@babel/template" "^7.18.10" + "@babel/types" "^7.19.0" + "@babel/helper-get-function-arity@^7.12.13": version "7.12.13" resolved "https://registry.yarnpkg.com/@babel/helper-get-function-arity/-/helper-get-function-arity-7.12.13.tgz#bc63451d403a3b3082b97e1d8b3fe5bd4091e583" @@ -197,6 +285,20 @@ "@babel/traverse" "^7.18.9" "@babel/types" "^7.18.9" +"@babel/helper-module-transforms@^7.19.6": + version "7.19.6" + resolved "https://registry.yarnpkg.com/@babel/helper-module-transforms/-/helper-module-transforms-7.19.6.tgz#6c52cc3ac63b70952d33ee987cbee1c9368b533f" + integrity sha512-fCmcfQo/KYr/VXXDIyd3CBGZ6AFhPFy1TfSEJ+PilGVlQT6jcbqtHAM4C1EciRqMza7/TpOUZliuSH+U6HAhJw== + dependencies: + "@babel/helper-environment-visitor" "^7.18.9" + "@babel/helper-module-imports" "^7.18.6" + "@babel/helper-simple-access" "^7.19.4" + "@babel/helper-split-export-declaration" "^7.18.6" + "@babel/helper-validator-identifier" "^7.19.1" + "@babel/template" "^7.18.10" + "@babel/traverse" "^7.19.6" + "@babel/types" "^7.19.4" + "@babel/helper-optimise-call-expression@^7.18.6": version "7.18.6" resolved "https://registry.yarnpkg.com/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.18.6.tgz#9369aa943ee7da47edab2cb4e838acf09d290ffe" @@ -214,6 +316,32 @@ resolved "https://registry.yarnpkg.com/@babel/helper-plugin-utils/-/helper-plugin-utils-7.18.9.tgz#4b8aea3b069d8cb8a72cdfe28ddf5ceca695ef2f" integrity sha512-aBXPT3bmtLryXaoJLyYPXPlSD4p1ld9aYeR+sJNOZjJJGiOpb+fKfh3NkcCu7J54nUJwCERPBExCCpyCOHnu/w== +"@babel/helper-plugin-utils@^7.12.13", "@babel/helper-plugin-utils@^7.14.5", "@babel/helper-plugin-utils@^7.16.7", "@babel/helper-plugin-utils@^7.19.0", "@babel/helper-plugin-utils@^7.8.3": + version "7.19.0" + resolved "https://registry.yarnpkg.com/@babel/helper-plugin-utils/-/helper-plugin-utils-7.19.0.tgz#4796bb14961521f0f8715990bee2fb6e51ce21bf" + integrity sha512-40Ryx7I8mT+0gaNxm8JGTZFUITNqdLAgdg0hXzeVZxVD6nFsdhQvip6v8dqkRHzsz1VFpFAaOCHNn0vKBL7Czw== + +"@babel/helper-remap-async-to-generator@^7.18.6", "@babel/helper-remap-async-to-generator@^7.18.9": + version "7.18.9" + resolved "https://registry.yarnpkg.com/@babel/helper-remap-async-to-generator/-/helper-remap-async-to-generator-7.18.9.tgz#997458a0e3357080e54e1d79ec347f8a8cd28519" + integrity sha512-dI7q50YKd8BAv3VEfgg7PS7yD3Rtbi2J1XMXaalXO0W0164hYLnh8zpjRS0mte9MfVp/tltvr/cfdXPvJr1opA== + dependencies: + "@babel/helper-annotate-as-pure" "^7.18.6" + "@babel/helper-environment-visitor" "^7.18.9" + "@babel/helper-wrap-function" "^7.18.9" + "@babel/types" "^7.18.9" + +"@babel/helper-replace-supers@^7.18.6": + version "7.19.1" + resolved "https://registry.yarnpkg.com/@babel/helper-replace-supers/-/helper-replace-supers-7.19.1.tgz#e1592a9b4b368aa6bdb8784a711e0bcbf0612b78" + integrity sha512-T7ahH7wV0Hfs46SFh5Jz3s0B6+o8g3c+7TMxu7xKfmHikg7EAZ3I2Qk9LFhjxXq8sL7UkP5JflezNwoZa8WvWw== + dependencies: + "@babel/helper-environment-visitor" "^7.18.9" + "@babel/helper-member-expression-to-functions" "^7.18.9" + "@babel/helper-optimise-call-expression" "^7.18.6" + "@babel/traverse" "^7.19.1" + "@babel/types" "^7.19.0" + "@babel/helper-replace-supers@^7.18.9": version "7.18.9" resolved "https://registry.yarnpkg.com/@babel/helper-replace-supers/-/helper-replace-supers-7.18.9.tgz#1092e002feca980fbbb0bd4d51b74a65c6a500e6" @@ -232,6 +360,13 @@ dependencies: "@babel/types" "^7.18.6" +"@babel/helper-simple-access@^7.19.4": + version "7.19.4" + resolved "https://registry.yarnpkg.com/@babel/helper-simple-access/-/helper-simple-access-7.19.4.tgz#be553f4951ac6352df2567f7daa19a0ee15668e7" + integrity sha512-f9Xq6WqBFqaDfbCzn2w85hwklswz5qsKlh7f08w4Y9yhJHpnNC0QemtSkK5YyOY8kPGvyiwdzZksGUhnGdaUIg== + dependencies: + "@babel/types" "^7.19.4" + "@babel/helper-skip-transparent-expression-wrappers@^7.18.9": version "7.18.9" resolved "https://registry.yarnpkg.com/@babel/helper-skip-transparent-expression-wrappers/-/helper-skip-transparent-expression-wrappers-7.18.9.tgz#778d87b3a758d90b471e7b9918f34a9a02eb5818" @@ -258,6 +393,11 @@ resolved "https://registry.yarnpkg.com/@babel/helper-string-parser/-/helper-string-parser-7.18.10.tgz#181f22d28ebe1b3857fa575f5c290b1aaf659b56" integrity sha512-XtIfWmeNY3i4t7t4D2t02q50HvqHybPqW2ki1kosnvWCwuCMeo81Jf0gwr85jy/neUdg5XDdeFE/80DXiO+njw== +"@babel/helper-string-parser@^7.19.4": + version "7.19.4" + resolved "https://registry.yarnpkg.com/@babel/helper-string-parser/-/helper-string-parser-7.19.4.tgz#38d3acb654b4701a9b77fb0615a96f775c3a9e63" + integrity sha512-nHtDoQcuqFmwYNYPz3Rah5ph2p8PFeFCsZk9A/48dPc/rGocJ5J3hAAZ7pb76VWX3fZKu+uEr/FhH5jLx7umrw== + "@babel/helper-validator-identifier@^7.12.11": version "7.12.11" resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.12.11.tgz#c9a1f021917dcb5ccf0d4e453e399022981fc9ed" @@ -273,6 +413,11 @@ resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.18.6.tgz#9c97e30d31b2b8c72a1d08984f2ca9b574d7a076" integrity sha512-MmetCkz9ej86nJQV+sFCxoGGrUbU3q02kgLciwkrt9QqEB7cP39oKEY0PakknEO0Gu20SskMRi+AYZ3b1TpN9g== +"@babel/helper-validator-identifier@^7.19.1": + version "7.19.1" + resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.19.1.tgz#7eea834cf32901ffdc1a7ee555e2f9c27e249ca2" + integrity sha512-awrNfaMtnHUr653GgGEs++LlAvW6w+DcPrOliSMXWCKo597CwL5Acf/wWdNkf/tfEQE3mjkeD1YOVZOUV/od1w== + "@babel/helper-validator-identifier@^7.9.0": version "7.9.0" resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.9.0.tgz#ad53562a7fc29b3b9a91bbf7d10397fd146346ed" @@ -283,6 +428,16 @@ resolved "https://registry.yarnpkg.com/@babel/helper-validator-option/-/helper-validator-option-7.18.6.tgz#bf0d2b5a509b1f336099e4ff36e1a63aa5db4db8" integrity sha512-XO7gESt5ouv/LRJdrVjkShckw6STTaB7l9BrpBaAHDeF5YZT+01PCwmR0SJHnkW6i8OwW/EVWRShfi4j2x+KQw== +"@babel/helper-wrap-function@^7.18.9": + version "7.19.0" + resolved "https://registry.yarnpkg.com/@babel/helper-wrap-function/-/helper-wrap-function-7.19.0.tgz#89f18335cff1152373222f76a4b37799636ae8b1" + integrity sha512-txX8aN8CZyYGTwcLhlk87KRqncAzhh5TpQamZUa0/u3an36NtDpUP6bQgBCBcLeBs09R/OwQu3OjK0k/HwfNDg== + dependencies: + "@babel/helper-function-name" "^7.19.0" + "@babel/template" "^7.18.10" + "@babel/traverse" "^7.19.0" + "@babel/types" "^7.19.0" + "@babel/helpers@^7.18.9": version "7.18.9" resolved "https://registry.yarnpkg.com/@babel/helpers/-/helpers-7.18.9.tgz#4bef3b893f253a1eced04516824ede94dcfe7ff9" @@ -292,6 +447,15 @@ "@babel/traverse" "^7.18.9" "@babel/types" "^7.18.9" +"@babel/helpers@^7.19.4": + version "7.19.4" + resolved "https://registry.yarnpkg.com/@babel/helpers/-/helpers-7.19.4.tgz#42154945f87b8148df7203a25c31ba9a73be46c5" + integrity sha512-G+z3aOx2nfDHwX/kyVii5fJq+bgscg89/dJNWpYeKeBv3v9xX8EIabmx1k6u9LS04H7nROFVRVK+e3k0VHp+sw== + dependencies: + "@babel/template" "^7.18.10" + "@babel/traverse" "^7.19.4" + "@babel/types" "^7.19.4" + "@babel/highlight@^7.0.0": version "7.0.0" resolved "https://registry.yarnpkg.com/@babel/highlight/-/highlight-7.0.0.tgz#f710c38c8d458e6dd9a201afb637fcb781ce99e4" @@ -334,7 +498,38 @@ resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.13.12.tgz#ba320059420774394d3b0c0233ba40e4250b81d1" integrity sha512-4T7Pb244rxH24yR116LAuJ+adxXXnHhZaLJjegJVKSdoNCe4x1eDBaud5YIcQFcqzsaD5BHvJw5BQ0AZapdCRw== -"@babel/plugin-proposal-class-properties@^7.13.0": +"@babel/parser@^7.19.6": + version "7.19.6" + resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.19.6.tgz#b923430cb94f58a7eae8facbffa9efd19130e7f8" + integrity sha512-h1IUp81s2JYJ3mRkdxJgs4UvmSsRvDrx5ICSJbPvtWYv5i1nTBGcBpnog+89rAFMwvvru6E5NUHdBe01UeSzYA== + +"@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression@^7.18.6": + version "7.18.6" + resolved "https://registry.yarnpkg.com/@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression/-/plugin-bugfix-safari-id-destructuring-collision-in-function-expression-7.18.6.tgz#da5b8f9a580acdfbe53494dba45ea389fb09a4d2" + integrity sha512-Dgxsyg54Fx1d4Nge8UnvTrED63vrwOdPmyvPzlNN/boaliRP54pm3pGzZD1SJUwrBA+Cs/xdG8kXX6Mn/RfISQ== + dependencies: + "@babel/helper-plugin-utils" "^7.18.6" + +"@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining@^7.18.9": + version "7.18.9" + resolved "https://registry.yarnpkg.com/@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining/-/plugin-bugfix-v8-spread-parameters-in-optional-chaining-7.18.9.tgz#a11af19aa373d68d561f08e0a57242350ed0ec50" + integrity sha512-AHrP9jadvH7qlOj6PINbgSuphjQUAK7AOT7DPjBo9EHoLhQTnnK5u45e1Hd4DbSQEO9nqPWtQ89r+XEOWFScKg== + dependencies: + "@babel/helper-plugin-utils" "^7.18.9" + "@babel/helper-skip-transparent-expression-wrappers" "^7.18.9" + "@babel/plugin-proposal-optional-chaining" "^7.18.9" + +"@babel/plugin-proposal-async-generator-functions@^7.19.1": + version "7.19.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-async-generator-functions/-/plugin-proposal-async-generator-functions-7.19.1.tgz#34f6f5174b688529342288cd264f80c9ea9fb4a7" + integrity sha512-0yu8vNATgLy4ivqMNBIwb1HebCelqN7YX8SL3FDXORv/RqT0zEEWUCH4GH44JsSrvCu6GqnAdR5EBFAPeNBB4Q== + dependencies: + "@babel/helper-environment-visitor" "^7.18.9" + "@babel/helper-plugin-utils" "^7.19.0" + "@babel/helper-remap-async-to-generator" "^7.18.9" + "@babel/plugin-syntax-async-generators" "^7.8.4" + +"@babel/plugin-proposal-class-properties@^7.13.0", "@babel/plugin-proposal-class-properties@^7.18.6": version "7.18.6" resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-class-properties/-/plugin-proposal-class-properties-7.18.6.tgz#b110f59741895f7ec21a6fff696ec46265c446a3" integrity sha512-cumfXOF0+nzZrrN8Rf0t7M+tF6sZc7vhQwYQck9q1/5w2OExlD+b4v4RpMJFaV1Z7WcDRgO6FqvxqxGlwo+RHQ== @@ -342,7 +537,48 @@ "@babel/helper-create-class-features-plugin" "^7.18.6" "@babel/helper-plugin-utils" "^7.18.6" -"@babel/plugin-proposal-nullish-coalescing-operator@^7.13.8": +"@babel/plugin-proposal-class-static-block@^7.18.6": + version "7.18.6" + resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-class-static-block/-/plugin-proposal-class-static-block-7.18.6.tgz#8aa81d403ab72d3962fc06c26e222dacfc9b9020" + integrity sha512-+I3oIiNxrCpup3Gi8n5IGMwj0gOCAjcJUSQEcotNnCCPMEnixawOQ+KeJPlgfjzx+FKQ1QSyZOWe7wmoJp7vhw== + dependencies: + "@babel/helper-create-class-features-plugin" "^7.18.6" + "@babel/helper-plugin-utils" "^7.18.6" + "@babel/plugin-syntax-class-static-block" "^7.14.5" + +"@babel/plugin-proposal-dynamic-import@^7.18.6": + version "7.18.6" + resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-dynamic-import/-/plugin-proposal-dynamic-import-7.18.6.tgz#72bcf8d408799f547d759298c3c27c7e7faa4d94" + integrity sha512-1auuwmK+Rz13SJj36R+jqFPMJWyKEDd7lLSdOj4oJK0UTgGueSAtkrCvz9ewmgyU/P941Rv2fQwZJN8s6QruXw== + dependencies: + "@babel/helper-plugin-utils" "^7.18.6" + "@babel/plugin-syntax-dynamic-import" "^7.8.3" + +"@babel/plugin-proposal-export-namespace-from@^7.18.9": + version "7.18.9" + resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-export-namespace-from/-/plugin-proposal-export-namespace-from-7.18.9.tgz#5f7313ab348cdb19d590145f9247540e94761203" + integrity sha512-k1NtHyOMvlDDFeb9G5PhUXuGj8m/wiwojgQVEhJ/fsVsMCpLyOP4h0uGEjYJKrRI+EVPlb5Jk+Gt9P97lOGwtA== + dependencies: + "@babel/helper-plugin-utils" "^7.18.9" + "@babel/plugin-syntax-export-namespace-from" "^7.8.3" + +"@babel/plugin-proposal-json-strings@^7.18.6": + version "7.18.6" + resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-json-strings/-/plugin-proposal-json-strings-7.18.6.tgz#7e8788c1811c393aff762817e7dbf1ebd0c05f0b" + integrity sha512-lr1peyn9kOdbYc0xr0OdHTZ5FMqS6Di+H0Fz2I/JwMzGmzJETNeOFq2pBySw6X/KFL5EWDjlJuMsUGRFb8fQgQ== + dependencies: + "@babel/helper-plugin-utils" "^7.18.6" + "@babel/plugin-syntax-json-strings" "^7.8.3" + +"@babel/plugin-proposal-logical-assignment-operators@^7.18.9": + version "7.18.9" + resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-logical-assignment-operators/-/plugin-proposal-logical-assignment-operators-7.18.9.tgz#8148cbb350483bf6220af06fa6db3690e14b2e23" + integrity sha512-128YbMpjCrP35IOExw2Fq+x55LMP42DzhOhX2aNNIdI9avSWl2PI0yuBWarr3RYpZBSPtabfadkH2yeRiMD61Q== + dependencies: + "@babel/helper-plugin-utils" "^7.18.9" + "@babel/plugin-syntax-logical-assignment-operators" "^7.10.4" + +"@babel/plugin-proposal-nullish-coalescing-operator@^7.13.8", "@babel/plugin-proposal-nullish-coalescing-operator@^7.18.6": version "7.18.6" resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-nullish-coalescing-operator/-/plugin-proposal-nullish-coalescing-operator-7.18.6.tgz#fdd940a99a740e577d6c753ab6fbb43fdb9467e1" integrity sha512-wQxQzxYeJqHcfppzBDnm1yAY0jSRkUXR2z8RePZYrKwMKgMlE8+Z6LUno+bd6LvbGh8Gltvy74+9pIYkr+XkKA== @@ -350,7 +586,34 @@ "@babel/helper-plugin-utils" "^7.18.6" "@babel/plugin-syntax-nullish-coalescing-operator" "^7.8.3" -"@babel/plugin-proposal-optional-chaining@^7.13.12": +"@babel/plugin-proposal-numeric-separator@^7.18.6": + version "7.18.6" + resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-numeric-separator/-/plugin-proposal-numeric-separator-7.18.6.tgz#899b14fbafe87f053d2c5ff05b36029c62e13c75" + integrity sha512-ozlZFogPqoLm8WBr5Z8UckIoE4YQ5KESVcNudyXOR8uqIkliTEgJ3RoketfG6pmzLdeZF0H/wjE9/cCEitBl7Q== + dependencies: + "@babel/helper-plugin-utils" "^7.18.6" + "@babel/plugin-syntax-numeric-separator" "^7.10.4" + +"@babel/plugin-proposal-object-rest-spread@^7.19.4": + version "7.19.4" + resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-object-rest-spread/-/plugin-proposal-object-rest-spread-7.19.4.tgz#a8fc86e8180ff57290c91a75d83fe658189b642d" + integrity sha512-wHmj6LDxVDnL+3WhXteUBaoM1aVILZODAUjg11kHqG4cOlfgMQGxw6aCgvrXrmaJR3Bn14oZhImyCPZzRpC93Q== + dependencies: + "@babel/compat-data" "^7.19.4" + "@babel/helper-compilation-targets" "^7.19.3" + "@babel/helper-plugin-utils" "^7.19.0" + "@babel/plugin-syntax-object-rest-spread" "^7.8.3" + "@babel/plugin-transform-parameters" "^7.18.8" + +"@babel/plugin-proposal-optional-catch-binding@^7.18.6": + version "7.18.6" + resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-optional-catch-binding/-/plugin-proposal-optional-catch-binding-7.18.6.tgz#f9400d0e6a3ea93ba9ef70b09e72dd6da638a2cb" + integrity sha512-Q40HEhs9DJQyaZfUjjn6vE8Cv4GmMHCYuMGIWUnlxH6400VGxOuwWsPt4FxXxJkC/5eOzgn0z21M9gMT4MOhbw== + dependencies: + "@babel/helper-plugin-utils" "^7.18.6" + "@babel/plugin-syntax-optional-catch-binding" "^7.8.3" + +"@babel/plugin-proposal-optional-chaining@^7.13.12", "@babel/plugin-proposal-optional-chaining@^7.18.9": version "7.18.9" resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-optional-chaining/-/plugin-proposal-optional-chaining-7.18.9.tgz#e8e8fe0723f2563960e4bf5e9690933691915993" integrity sha512-v5nwt4IqBXihxGsW2QmCWMDS3B3bzGIk/EQVZz2ei7f3NJl8NzAJVvUmpDW5q1CRNY+Beb/k58UAH1Km1N411w== @@ -359,6 +622,67 @@ "@babel/helper-skip-transparent-expression-wrappers" "^7.18.9" "@babel/plugin-syntax-optional-chaining" "^7.8.3" +"@babel/plugin-proposal-private-methods@^7.18.6": + version "7.18.6" + resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-private-methods/-/plugin-proposal-private-methods-7.18.6.tgz#5209de7d213457548a98436fa2882f52f4be6bea" + integrity sha512-nutsvktDItsNn4rpGItSNV2sz1XwS+nfU0Rg8aCx3W3NOKVzdMjJRu0O5OkgDp3ZGICSTbgRpxZoWsxoKRvbeA== + dependencies: + "@babel/helper-create-class-features-plugin" "^7.18.6" + "@babel/helper-plugin-utils" "^7.18.6" + +"@babel/plugin-proposal-private-property-in-object@^7.18.6": + version "7.18.6" + resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-private-property-in-object/-/plugin-proposal-private-property-in-object-7.18.6.tgz#a64137b232f0aca3733a67eb1a144c192389c503" + integrity sha512-9Rysx7FOctvT5ouj5JODjAFAkgGoudQuLPamZb0v1TGLpapdNaftzifU8NTWQm0IRjqoYypdrSmyWgkocDQ8Dw== + dependencies: + "@babel/helper-annotate-as-pure" "^7.18.6" + "@babel/helper-create-class-features-plugin" "^7.18.6" + "@babel/helper-plugin-utils" "^7.18.6" + "@babel/plugin-syntax-private-property-in-object" "^7.14.5" + +"@babel/plugin-proposal-unicode-property-regex@^7.18.6", "@babel/plugin-proposal-unicode-property-regex@^7.4.4": + version "7.18.6" + resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-unicode-property-regex/-/plugin-proposal-unicode-property-regex-7.18.6.tgz#af613d2cd5e643643b65cded64207b15c85cb78e" + integrity sha512-2BShG/d5yoZyXZfVePH91urL5wTG6ASZU9M4o03lKK8u8UW1y08OMttBSOADTcJrnPMpvDXRG3G8fyLh4ovs8w== + dependencies: + "@babel/helper-create-regexp-features-plugin" "^7.18.6" + "@babel/helper-plugin-utils" "^7.18.6" + +"@babel/plugin-syntax-async-generators@^7.8.4": + version "7.8.4" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-async-generators/-/plugin-syntax-async-generators-7.8.4.tgz#a983fb1aeb2ec3f6ed042a210f640e90e786fe0d" + integrity sha512-tycmZxkGfZaxhMRbXlPXuVFpdWlXpir2W4AMhSJgRKzk/eDlIXOhb2LHWoLpDF7TEHylV5zNhykX6KAgHJmTNw== + dependencies: + "@babel/helper-plugin-utils" "^7.8.0" + +"@babel/plugin-syntax-class-properties@^7.12.13": + version "7.12.13" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-class-properties/-/plugin-syntax-class-properties-7.12.13.tgz#b5c987274c4a3a82b89714796931a6b53544ae10" + integrity sha512-fm4idjKla0YahUNgFNLCB0qySdsoPiZP3iQE3rky0mBUtMZ23yDJ9SJdg6dXTSDnulOVqiF3Hgr9nbXvXTQZYA== + dependencies: + "@babel/helper-plugin-utils" "^7.12.13" + +"@babel/plugin-syntax-class-static-block@^7.14.5": + version "7.14.5" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-class-static-block/-/plugin-syntax-class-static-block-7.14.5.tgz#195df89b146b4b78b3bf897fd7a257c84659d406" + integrity sha512-b+YyPmr6ldyNnM6sqYeMWE+bgJcJpO6yS4QD7ymxgH34GBPNDM/THBh8iunyvKIZztiwLH4CJZ0RxTk9emgpjw== + dependencies: + "@babel/helper-plugin-utils" "^7.14.5" + +"@babel/plugin-syntax-dynamic-import@^7.8.3": + version "7.8.3" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-dynamic-import/-/plugin-syntax-dynamic-import-7.8.3.tgz#62bf98b2da3cd21d626154fc96ee5b3cb68eacb3" + integrity sha512-5gdGbFon+PszYzqs83S3E5mpi7/y/8M9eC90MRTZfduQOYW76ig6SOSPNe41IG5LoP3FGBn2N0RjVDSQiS94kQ== + dependencies: + "@babel/helper-plugin-utils" "^7.8.0" + +"@babel/plugin-syntax-export-namespace-from@^7.8.3": + version "7.8.3" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-export-namespace-from/-/plugin-syntax-export-namespace-from-7.8.3.tgz#028964a9ba80dbc094c915c487ad7c4e7a66465a" + integrity sha512-MXf5laXo6c1IbEbegDmzGPwGNTsHZmEy6QGznu5Sh2UCWvueywb2ee+CCE4zQiZstxU9BMoQO9i6zUFSY0Kj0Q== + dependencies: + "@babel/helper-plugin-utils" "^7.8.3" + "@babel/plugin-syntax-flow@^7.18.6": version "7.18.6" resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-flow/-/plugin-syntax-flow-7.18.6.tgz#774d825256f2379d06139be0c723c4dd444f3ca1" @@ -366,13 +690,34 @@ dependencies: "@babel/helper-plugin-utils" "^7.18.6" -"@babel/plugin-syntax-jsx@7": +"@babel/plugin-syntax-import-assertions@^7.18.6": + version "7.18.6" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-import-assertions/-/plugin-syntax-import-assertions-7.18.6.tgz#cd6190500a4fa2fe31990a963ffab4b63e4505e4" + integrity sha512-/DU3RXad9+bZwrgWJQKbr39gYbJpLJHezqEzRzi/BHRlJ9zsQb4CK2CA/5apllXNomwA1qHwzvHl+AdEmC5krQ== + dependencies: + "@babel/helper-plugin-utils" "^7.18.6" + +"@babel/plugin-syntax-json-strings@^7.8.3": + version "7.8.3" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-json-strings/-/plugin-syntax-json-strings-7.8.3.tgz#01ca21b668cd8218c9e640cb6dd88c5412b2c96a" + integrity sha512-lY6kdGpWHvjoe2vk4WrAapEuBR69EMxZl+RoGRhrFGNYVK8mOPAW8VfbT/ZgrFbXlDNiiaxQnAtgVCZ6jv30EA== + dependencies: + "@babel/helper-plugin-utils" "^7.8.0" + +"@babel/plugin-syntax-jsx@7", "@babel/plugin-syntax-jsx@^7.18.6": version "7.18.6" resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.18.6.tgz#a8feef63b010150abd97f1649ec296e849943ca0" integrity sha512-6mmljtAedFGTWu2p/8WIORGwy+61PLgOMPOdazc7YoJ9ZCWUyFy3A6CpPkRKLKD1ToAesxX8KGEViAiLo9N+7Q== dependencies: "@babel/helper-plugin-utils" "^7.18.6" +"@babel/plugin-syntax-logical-assignment-operators@^7.10.4": + version "7.10.4" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-logical-assignment-operators/-/plugin-syntax-logical-assignment-operators-7.10.4.tgz#ca91ef46303530448b906652bac2e9fe9941f699" + integrity sha512-d8waShlpFDinQ5MtvGU9xDAOzKH47+FFoney2baFIoMr952hKOLp1HR7VszoZvOsV/4+RRszNY7D17ba0te0ig== + dependencies: + "@babel/helper-plugin-utils" "^7.10.4" + "@babel/plugin-syntax-nullish-coalescing-operator@^7.8.3": version "7.8.3" resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-nullish-coalescing-operator/-/plugin-syntax-nullish-coalescing-operator-7.8.3.tgz#167ed70368886081f74b5c36c65a88c03b66d1a9" @@ -380,6 +725,27 @@ dependencies: "@babel/helper-plugin-utils" "^7.8.0" +"@babel/plugin-syntax-numeric-separator@^7.10.4": + version "7.10.4" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-numeric-separator/-/plugin-syntax-numeric-separator-7.10.4.tgz#b9b070b3e33570cd9fd07ba7fa91c0dd37b9af97" + integrity sha512-9H6YdfkcK/uOnY/K7/aA2xpzaAgkQn37yzWUMRK7OaPOqOpGS1+n0H5hxT9AUw9EsSjPW8SVyMJwYRtWs3X3ug== + dependencies: + "@babel/helper-plugin-utils" "^7.10.4" + +"@babel/plugin-syntax-object-rest-spread@^7.8.3": + version "7.8.3" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-object-rest-spread/-/plugin-syntax-object-rest-spread-7.8.3.tgz#60e225edcbd98a640332a2e72dd3e66f1af55871" + integrity sha512-XoqMijGZb9y3y2XskN+P1wUGiVwWZ5JmoDRwx5+3GmEplNyVM2s2Dg8ILFQm8rWM48orGy5YpI5Bl8U1y7ydlA== + dependencies: + "@babel/helper-plugin-utils" "^7.8.0" + +"@babel/plugin-syntax-optional-catch-binding@^7.8.3": + version "7.8.3" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-optional-catch-binding/-/plugin-syntax-optional-catch-binding-7.8.3.tgz#6111a265bcfb020eb9efd0fdfd7d26402b9ed6c1" + integrity sha512-6VPD0Pc1lpTqw0aKoeRTMiB+kWhAoT24PA+ksWSBrFtl5SIRVpZlwN3NNPQjehA2E/91FV3RjLWoVTglWcSV3Q== + dependencies: + "@babel/helper-plugin-utils" "^7.8.0" + "@babel/plugin-syntax-optional-chaining@^7.8.3": version "7.8.3" resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-optional-chaining/-/plugin-syntax-optional-chaining-7.8.3.tgz#4f69c2ab95167e0180cd5336613f8c5788f7d48a" @@ -387,6 +753,20 @@ dependencies: "@babel/helper-plugin-utils" "^7.8.0" +"@babel/plugin-syntax-private-property-in-object@^7.14.5": + version "7.14.5" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-private-property-in-object/-/plugin-syntax-private-property-in-object-7.14.5.tgz#0dc6671ec0ea22b6e94a1114f857970cd39de1ad" + integrity sha512-0wVnp9dxJ72ZUJDV27ZfbSj6iHLoytYZmh3rFcxNnvsJF3ktkzLDZPy/mA17HGsaQT3/DQsWYX1f1QGWkCoVUg== + dependencies: + "@babel/helper-plugin-utils" "^7.14.5" + +"@babel/plugin-syntax-top-level-await@^7.14.5": + version "7.14.5" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-top-level-await/-/plugin-syntax-top-level-await-7.14.5.tgz#c1cfdadc35a646240001f06138247b741c34d94c" + integrity sha512-hx++upLv5U1rgYfwe1xBQUhRmU41NEvpUvrp8jkrSCdvGSnM5/qdRMtylJ6PG5OFkBaHkbTAKTnd3/YyESRHFw== + dependencies: + "@babel/helper-plugin-utils" "^7.14.5" + "@babel/plugin-syntax-typescript@^7.18.6": version "7.18.6" resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.18.6.tgz#1c09cd25795c7c2b8a4ba9ae49394576d4133285" @@ -394,6 +774,88 @@ dependencies: "@babel/helper-plugin-utils" "^7.18.6" +"@babel/plugin-transform-arrow-functions@^7.18.6": + version "7.18.6" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-arrow-functions/-/plugin-transform-arrow-functions-7.18.6.tgz#19063fcf8771ec7b31d742339dac62433d0611fe" + integrity sha512-9S9X9RUefzrsHZmKMbDXxweEH+YlE8JJEuat9FdvW9Qh1cw7W64jELCtWNkPBPX5En45uy28KGvA/AySqUh8CQ== + dependencies: + "@babel/helper-plugin-utils" "^7.18.6" + +"@babel/plugin-transform-async-to-generator@^7.18.6": + version "7.18.6" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-async-to-generator/-/plugin-transform-async-to-generator-7.18.6.tgz#ccda3d1ab9d5ced5265fdb13f1882d5476c71615" + integrity sha512-ARE5wZLKnTgPW7/1ftQmSi1CmkqqHo2DNmtztFhvgtOWSDfq0Cq9/9L+KnZNYSNrydBekhW3rwShduf59RoXag== + dependencies: + "@babel/helper-module-imports" "^7.18.6" + "@babel/helper-plugin-utils" "^7.18.6" + "@babel/helper-remap-async-to-generator" "^7.18.6" + +"@babel/plugin-transform-block-scoped-functions@^7.18.6": + version "7.18.6" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-block-scoped-functions/-/plugin-transform-block-scoped-functions-7.18.6.tgz#9187bf4ba302635b9d70d986ad70f038726216a8" + integrity sha512-ExUcOqpPWnliRcPqves5HJcJOvHvIIWfuS4sroBUenPuMdmW+SMHDakmtS7qOo13sVppmUijqeTv7qqGsvURpQ== + dependencies: + "@babel/helper-plugin-utils" "^7.18.6" + +"@babel/plugin-transform-block-scoping@^7.19.4": + version "7.19.4" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-block-scoping/-/plugin-transform-block-scoping-7.19.4.tgz#315d70f68ce64426db379a3d830e7ac30be02e9b" + integrity sha512-934S2VLLlt2hRJwPf4MczaOr4hYF0z+VKPwqTNxyKX7NthTiPfhuKFWQZHXRM0vh/wo/VyXB3s4bZUNA08l+tQ== + dependencies: + "@babel/helper-plugin-utils" "^7.19.0" + +"@babel/plugin-transform-classes@^7.19.0": + version "7.19.0" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-classes/-/plugin-transform-classes-7.19.0.tgz#0e61ec257fba409c41372175e7c1e606dc79bb20" + integrity sha512-YfeEE9kCjqTS9IitkgfJuxjcEtLUHMqa8yUJ6zdz8vR7hKuo6mOy2C05P0F1tdMmDCeuyidKnlrw/iTppHcr2A== + dependencies: + "@babel/helper-annotate-as-pure" "^7.18.6" + "@babel/helper-compilation-targets" "^7.19.0" + "@babel/helper-environment-visitor" "^7.18.9" + "@babel/helper-function-name" "^7.19.0" + "@babel/helper-optimise-call-expression" "^7.18.6" + "@babel/helper-plugin-utils" "^7.19.0" + "@babel/helper-replace-supers" "^7.18.9" + "@babel/helper-split-export-declaration" "^7.18.6" + globals "^11.1.0" + +"@babel/plugin-transform-computed-properties@^7.18.9": + version "7.18.9" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-computed-properties/-/plugin-transform-computed-properties-7.18.9.tgz#2357a8224d402dad623caf6259b611e56aec746e" + integrity sha512-+i0ZU1bCDymKakLxn5srGHrsAPRELC2WIbzwjLhHW9SIE1cPYkLCL0NlnXMZaM1vhfgA2+M7hySk42VBvrkBRw== + dependencies: + "@babel/helper-plugin-utils" "^7.18.9" + +"@babel/plugin-transform-destructuring@^7.19.4": + version "7.19.4" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-destructuring/-/plugin-transform-destructuring-7.19.4.tgz#46890722687b9b89e1369ad0bd8dc6c5a3b4319d" + integrity sha512-t0j0Hgidqf0aM86dF8U+vXYReUgJnlv4bZLsyoPnwZNrGY+7/38o8YjaELrvHeVfTZao15kjR0PVv0nju2iduA== + dependencies: + "@babel/helper-plugin-utils" "^7.19.0" + +"@babel/plugin-transform-dotall-regex@^7.18.6", "@babel/plugin-transform-dotall-regex@^7.4.4": + version "7.18.6" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-dotall-regex/-/plugin-transform-dotall-regex-7.18.6.tgz#b286b3e7aae6c7b861e45bed0a2fafd6b1a4fef8" + integrity sha512-6S3jpun1eEbAxq7TdjLotAsl4WpQI9DxfkycRcKrjhQYzU87qpXdknpBg/e+TdcMehqGnLFi7tnFUBR02Vq6wg== + dependencies: + "@babel/helper-create-regexp-features-plugin" "^7.18.6" + "@babel/helper-plugin-utils" "^7.18.6" + +"@babel/plugin-transform-duplicate-keys@^7.18.9": + version "7.18.9" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-duplicate-keys/-/plugin-transform-duplicate-keys-7.18.9.tgz#687f15ee3cdad6d85191eb2a372c4528eaa0ae0e" + integrity sha512-d2bmXCtZXYc59/0SanQKbiWINadaJXqtvIQIzd4+hNwkWBgyCd5F/2t1kXoUdvPMrxzPvhK6EMQRROxsue+mfw== + dependencies: + "@babel/helper-plugin-utils" "^7.18.9" + +"@babel/plugin-transform-exponentiation-operator@^7.18.6": + version "7.18.6" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-exponentiation-operator/-/plugin-transform-exponentiation-operator-7.18.6.tgz#421c705f4521888c65e91fdd1af951bfefd4dacd" + integrity sha512-wzEtc0+2c88FVR34aQmiz56dxEkxr2g8DQb/KfaFa1JYXOFVsbhvAonFN6PwVWj++fKmku8NP80plJ5Et4wqHw== + dependencies: + "@babel/helper-builder-binary-assignment-operator-visitor" "^7.18.6" + "@babel/helper-plugin-utils" "^7.18.6" + "@babel/plugin-transform-flow-strip-types@^7.18.6": version "7.18.9" resolved "https://registry.yarnpkg.com/@babel/plugin-transform-flow-strip-types/-/plugin-transform-flow-strip-types-7.18.9.tgz#5b4cc521426263b5ce08893a2db41097ceba35bf" @@ -402,6 +864,44 @@ "@babel/helper-plugin-utils" "^7.18.9" "@babel/plugin-syntax-flow" "^7.18.6" +"@babel/plugin-transform-for-of@^7.18.8": + version "7.18.8" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-for-of/-/plugin-transform-for-of-7.18.8.tgz#6ef8a50b244eb6a0bdbad0c7c61877e4e30097c1" + integrity sha512-yEfTRnjuskWYo0k1mHUqrVWaZwrdq8AYbfrpqULOJOaucGSp4mNMVps+YtA8byoevxS/urwU75vyhQIxcCgiBQ== + dependencies: + "@babel/helper-plugin-utils" "^7.18.6" + +"@babel/plugin-transform-function-name@^7.18.9": + version "7.18.9" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-function-name/-/plugin-transform-function-name-7.18.9.tgz#cc354f8234e62968946c61a46d6365440fc764e0" + integrity sha512-WvIBoRPaJQ5yVHzcnJFor7oS5Ls0PYixlTYE63lCj2RtdQEl15M68FXQlxnG6wdraJIXRdR7KI+hQ7q/9QjrCQ== + dependencies: + "@babel/helper-compilation-targets" "^7.18.9" + "@babel/helper-function-name" "^7.18.9" + "@babel/helper-plugin-utils" "^7.18.9" + +"@babel/plugin-transform-literals@^7.18.9": + version "7.18.9" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-literals/-/plugin-transform-literals-7.18.9.tgz#72796fdbef80e56fba3c6a699d54f0de557444bc" + integrity sha512-IFQDSRoTPnrAIrI5zoZv73IFeZu2dhu6irxQjY9rNjTT53VmKg9fenjvoiOWOkJ6mm4jKVPtdMzBY98Fp4Z4cg== + dependencies: + "@babel/helper-plugin-utils" "^7.18.9" + +"@babel/plugin-transform-member-expression-literals@^7.18.6": + version "7.18.6" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-member-expression-literals/-/plugin-transform-member-expression-literals-7.18.6.tgz#ac9fdc1a118620ac49b7e7a5d2dc177a1bfee88e" + integrity sha512-qSF1ihLGO3q+/g48k85tUjD033C29TNTVB2paCwZPVmOsjn9pClvYYrM2VeJpBY2bcNkuny0YUyTNRyRxJ54KA== + dependencies: + "@babel/helper-plugin-utils" "^7.18.6" + +"@babel/plugin-transform-modules-amd@^7.18.6": + version "7.19.6" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-amd/-/plugin-transform-modules-amd-7.19.6.tgz#aca391801ae55d19c4d8d2ebfeaa33df5f2a2cbd" + integrity sha512-uG3od2mXvAtIFQIh0xrpLH6r5fpSQN04gIVovl+ODLdUMANokxQLZnPBHcjmv3GxRjnqwLuHvppjjcelqUFZvg== + dependencies: + "@babel/helper-module-transforms" "^7.19.6" + "@babel/helper-plugin-utils" "^7.19.0" + "@babel/plugin-transform-modules-commonjs@^7.13.8": version "7.18.6" resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.18.6.tgz#afd243afba166cca69892e24a8fd8c9f2ca87883" @@ -412,6 +912,161 @@ "@babel/helper-simple-access" "^7.18.6" babel-plugin-dynamic-import-node "^2.3.3" +"@babel/plugin-transform-modules-commonjs@^7.18.6": + version "7.19.6" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.19.6.tgz#25b32feef24df8038fc1ec56038917eacb0b730c" + integrity sha512-8PIa1ym4XRTKuSsOUXqDG0YaOlEuTVvHMe5JCfgBMOtHvJKw/4NGovEGN33viISshG/rZNVrACiBmPQLvWN8xQ== + dependencies: + "@babel/helper-module-transforms" "^7.19.6" + "@babel/helper-plugin-utils" "^7.19.0" + "@babel/helper-simple-access" "^7.19.4" + +"@babel/plugin-transform-modules-systemjs@^7.19.0": + version "7.19.6" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-systemjs/-/plugin-transform-modules-systemjs-7.19.6.tgz#59e2a84064b5736a4471b1aa7b13d4431d327e0d" + integrity sha512-fqGLBepcc3kErfR9R3DnVpURmckXP7gj7bAlrTQyBxrigFqszZCkFkcoxzCp2v32XmwXLvbw+8Yq9/b+QqksjQ== + dependencies: + "@babel/helper-hoist-variables" "^7.18.6" + "@babel/helper-module-transforms" "^7.19.6" + "@babel/helper-plugin-utils" "^7.19.0" + "@babel/helper-validator-identifier" "^7.19.1" + +"@babel/plugin-transform-modules-umd@^7.18.6": + version "7.18.6" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-umd/-/plugin-transform-modules-umd-7.18.6.tgz#81d3832d6034b75b54e62821ba58f28ed0aab4b9" + integrity sha512-dcegErExVeXcRqNtkRU/z8WlBLnvD4MRnHgNs3MytRO1Mn1sHRyhbcpYbVMGclAqOjdW+9cfkdZno9dFdfKLfQ== + dependencies: + "@babel/helper-module-transforms" "^7.18.6" + "@babel/helper-plugin-utils" "^7.18.6" + +"@babel/plugin-transform-named-capturing-groups-regex@^7.19.1": + version "7.19.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-named-capturing-groups-regex/-/plugin-transform-named-capturing-groups-regex-7.19.1.tgz#ec7455bab6cd8fb05c525a94876f435a48128888" + integrity sha512-oWk9l9WItWBQYS4FgXD4Uyy5kq898lvkXpXQxoJEY1RnvPk4R/Dvu2ebXU9q8lP+rlMwUQTFf2Ok6d78ODa0kw== + dependencies: + "@babel/helper-create-regexp-features-plugin" "^7.19.0" + "@babel/helper-plugin-utils" "^7.19.0" + +"@babel/plugin-transform-new-target@^7.18.6": + version "7.18.6" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-new-target/-/plugin-transform-new-target-7.18.6.tgz#d128f376ae200477f37c4ddfcc722a8a1b3246a8" + integrity sha512-DjwFA/9Iu3Z+vrAn+8pBUGcjhxKguSMlsFqeCKbhb9BAV756v0krzVK04CRDi/4aqmk8BsHb4a/gFcaA5joXRw== + dependencies: + "@babel/helper-plugin-utils" "^7.18.6" + +"@babel/plugin-transform-object-super@^7.18.6": + version "7.18.6" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-object-super/-/plugin-transform-object-super-7.18.6.tgz#fb3c6ccdd15939b6ff7939944b51971ddc35912c" + integrity sha512-uvGz6zk+pZoS1aTZrOvrbj6Pp/kK2mp45t2B+bTDre2UgsZZ8EZLSJtUg7m/no0zOJUWgFONpB7Zv9W2tSaFlA== + dependencies: + "@babel/helper-plugin-utils" "^7.18.6" + "@babel/helper-replace-supers" "^7.18.6" + +"@babel/plugin-transform-parameters@^7.18.8": + version "7.18.8" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-parameters/-/plugin-transform-parameters-7.18.8.tgz#ee9f1a0ce6d78af58d0956a9378ea3427cccb48a" + integrity sha512-ivfbE3X2Ss+Fj8nnXvKJS6sjRG4gzwPMsP+taZC+ZzEGjAYlvENixmt1sZ5Ca6tWls+BlKSGKPJ6OOXvXCbkFg== + dependencies: + "@babel/helper-plugin-utils" "^7.18.6" + +"@babel/plugin-transform-property-literals@^7.18.6": + version "7.18.6" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-property-literals/-/plugin-transform-property-literals-7.18.6.tgz#e22498903a483448e94e032e9bbb9c5ccbfc93a3" + integrity sha512-cYcs6qlgafTud3PAzrrRNbQtfpQ8+y/+M5tKmksS9+M1ckbH6kzY8MrexEM9mcA6JDsukE19iIRvAyYl463sMg== + dependencies: + "@babel/helper-plugin-utils" "^7.18.6" + +"@babel/plugin-transform-react-constant-elements@^7.17.12": + version "7.18.12" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-react-constant-elements/-/plugin-transform-react-constant-elements-7.18.12.tgz#edf3bec47eb98f14e84fa0af137fcc6aad8e0443" + integrity sha512-Q99U9/ttiu+LMnRU8psd23HhvwXmKWDQIpocm0JKaICcZHnw+mdQbHm6xnSy7dOl8I5PELakYtNBubNQlBXbZw== + dependencies: + "@babel/helper-plugin-utils" "^7.18.9" + +"@babel/plugin-transform-react-display-name@^7.18.6": + version "7.18.6" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-react-display-name/-/plugin-transform-react-display-name-7.18.6.tgz#8b1125f919ef36ebdfff061d664e266c666b9415" + integrity sha512-TV4sQ+T013n61uMoygyMRm+xf04Bd5oqFpv2jAEQwSZ8NwQA7zeRPg1LMVg2PWi3zWBz+CLKD+v5bcpZ/BS0aA== + dependencies: + "@babel/helper-plugin-utils" "^7.18.6" + +"@babel/plugin-transform-react-jsx-development@^7.18.6": + version "7.18.6" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-react-jsx-development/-/plugin-transform-react-jsx-development-7.18.6.tgz#dbe5c972811e49c7405b630e4d0d2e1380c0ddc5" + integrity sha512-SA6HEjwYFKF7WDjWcMcMGUimmw/nhNRDWxr+KaLSCrkD/LMDBvWRmHAYgE1HDeF8KUuI8OAu+RT6EOtKxSW2qA== + dependencies: + "@babel/plugin-transform-react-jsx" "^7.18.6" + +"@babel/plugin-transform-react-jsx@^7.18.6": + version "7.19.0" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-react-jsx/-/plugin-transform-react-jsx-7.19.0.tgz#b3cbb7c3a00b92ec8ae1027910e331ba5c500eb9" + integrity sha512-UVEvX3tXie3Szm3emi1+G63jyw1w5IcMY0FSKM+CRnKRI5Mr1YbCNgsSTwoTwKphQEG9P+QqmuRFneJPZuHNhg== + dependencies: + "@babel/helper-annotate-as-pure" "^7.18.6" + "@babel/helper-module-imports" "^7.18.6" + "@babel/helper-plugin-utils" "^7.19.0" + "@babel/plugin-syntax-jsx" "^7.18.6" + "@babel/types" "^7.19.0" + +"@babel/plugin-transform-react-pure-annotations@^7.18.6": + version "7.18.6" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-react-pure-annotations/-/plugin-transform-react-pure-annotations-7.18.6.tgz#561af267f19f3e5d59291f9950fd7b9663d0d844" + integrity sha512-I8VfEPg9r2TRDdvnHgPepTKvuRomzA8+u+nhY7qSI1fR2hRNebasZEETLyM5mAUr0Ku56OkXJ0I7NHJnO6cJiQ== + dependencies: + "@babel/helper-annotate-as-pure" "^7.18.6" + "@babel/helper-plugin-utils" "^7.18.6" + +"@babel/plugin-transform-regenerator@^7.18.6": + version "7.18.6" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-regenerator/-/plugin-transform-regenerator-7.18.6.tgz#585c66cb84d4b4bf72519a34cfce761b8676ca73" + integrity sha512-poqRI2+qiSdeldcz4wTSTXBRryoq3Gc70ye7m7UD5Ww0nE29IXqMl6r7Nd15WBgRd74vloEMlShtH6CKxVzfmQ== + dependencies: + "@babel/helper-plugin-utils" "^7.18.6" + regenerator-transform "^0.15.0" + +"@babel/plugin-transform-reserved-words@^7.18.6": + version "7.18.6" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-reserved-words/-/plugin-transform-reserved-words-7.18.6.tgz#b1abd8ebf8edaa5f7fe6bbb8d2133d23b6a6f76a" + integrity sha512-oX/4MyMoypzHjFrT1CdivfKZ+XvIPMFXwwxHp/r0Ddy2Vuomt4HDFGmft1TAY2yiTKiNSsh3kjBAzcM8kSdsjA== + dependencies: + "@babel/helper-plugin-utils" "^7.18.6" + +"@babel/plugin-transform-shorthand-properties@^7.18.6": + version "7.18.6" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-shorthand-properties/-/plugin-transform-shorthand-properties-7.18.6.tgz#6d6df7983d67b195289be24909e3f12a8f664dc9" + integrity sha512-eCLXXJqv8okzg86ywZJbRn19YJHU4XUa55oz2wbHhaQVn/MM+XhukiT7SYqp/7o00dg52Rj51Ny+Ecw4oyoygw== + dependencies: + "@babel/helper-plugin-utils" "^7.18.6" + +"@babel/plugin-transform-spread@^7.19.0": + version "7.19.0" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-spread/-/plugin-transform-spread-7.19.0.tgz#dd60b4620c2fec806d60cfaae364ec2188d593b6" + integrity sha512-RsuMk7j6n+r752EtzyScnWkQyuJdli6LdO5Klv8Yx0OfPVTcQkIUfS8clx5e9yHXzlnhOZF3CbQ8C2uP5j074w== + dependencies: + "@babel/helper-plugin-utils" "^7.19.0" + "@babel/helper-skip-transparent-expression-wrappers" "^7.18.9" + +"@babel/plugin-transform-sticky-regex@^7.18.6": + version "7.18.6" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-sticky-regex/-/plugin-transform-sticky-regex-7.18.6.tgz#c6706eb2b1524028e317720339583ad0f444adcc" + integrity sha512-kfiDrDQ+PBsQDO85yj1icueWMfGfJFKN1KCkndygtu/C9+XUfydLC8Iv5UYJqRwy4zk8EcplRxEOeLyjq1gm6Q== + dependencies: + "@babel/helper-plugin-utils" "^7.18.6" + +"@babel/plugin-transform-template-literals@^7.18.9": + version "7.18.9" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-template-literals/-/plugin-transform-template-literals-7.18.9.tgz#04ec6f10acdaa81846689d63fae117dd9c243a5e" + integrity sha512-S8cOWfT82gTezpYOiVaGHrCbhlHgKhQt8XH5ES46P2XWmX92yisoZywf5km75wv5sYcXDUCLMmMxOLCtthDgMA== + dependencies: + "@babel/helper-plugin-utils" "^7.18.9" + +"@babel/plugin-transform-typeof-symbol@^7.18.9": + version "7.18.9" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-typeof-symbol/-/plugin-transform-typeof-symbol-7.18.9.tgz#c8cea68263e45addcd6afc9091429f80925762c0" + integrity sha512-SRfwTtF11G2aemAZWivL7PD+C9z52v9EvMqH9BuYbabyPuKUvSWks3oCg6041pT925L4zVFqaVBeECwsmlguEw== + dependencies: + "@babel/helper-plugin-utils" "^7.18.9" + "@babel/plugin-transform-typescript@^7.18.6": version "7.18.12" resolved "https://registry.yarnpkg.com/@babel/plugin-transform-typescript/-/plugin-transform-typescript-7.18.12.tgz#712e9a71b9e00fde9f8c0238e0cceee86ab2f8fd" @@ -421,6 +1076,102 @@ "@babel/helper-plugin-utils" "^7.18.9" "@babel/plugin-syntax-typescript" "^7.18.6" +"@babel/plugin-transform-unicode-escapes@^7.18.10": + version "7.18.10" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-unicode-escapes/-/plugin-transform-unicode-escapes-7.18.10.tgz#1ecfb0eda83d09bbcb77c09970c2dd55832aa246" + integrity sha512-kKAdAI+YzPgGY/ftStBFXTI1LZFju38rYThnfMykS+IXy8BVx+res7s2fxf1l8I35DV2T97ezo6+SGrXz6B3iQ== + dependencies: + "@babel/helper-plugin-utils" "^7.18.9" + +"@babel/plugin-transform-unicode-regex@^7.18.6": + version "7.18.6" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-unicode-regex/-/plugin-transform-unicode-regex-7.18.6.tgz#194317225d8c201bbae103364ffe9e2cea36cdca" + integrity sha512-gE7A6Lt7YLnNOL3Pb9BNeZvi+d8l7tcRrG4+pwJjK9hD2xX4mEvjlQW60G9EEmfXVYRPv9VRQcyegIVHCql/AA== + dependencies: + "@babel/helper-create-regexp-features-plugin" "^7.18.6" + "@babel/helper-plugin-utils" "^7.18.6" + +"@babel/preset-env@^7.18.2": + version "7.19.4" + resolved "https://registry.yarnpkg.com/@babel/preset-env/-/preset-env-7.19.4.tgz#4c91ce2e1f994f717efb4237891c3ad2d808c94b" + integrity sha512-5QVOTXUdqTCjQuh2GGtdd7YEhoRXBMVGROAtsBeLGIbIz3obCBIfRMT1I3ZKkMgNzwkyCkftDXSSkHxnfVf4qg== + dependencies: + "@babel/compat-data" "^7.19.4" + "@babel/helper-compilation-targets" "^7.19.3" + "@babel/helper-plugin-utils" "^7.19.0" + "@babel/helper-validator-option" "^7.18.6" + "@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression" "^7.18.6" + "@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining" "^7.18.9" + "@babel/plugin-proposal-async-generator-functions" "^7.19.1" + "@babel/plugin-proposal-class-properties" "^7.18.6" + "@babel/plugin-proposal-class-static-block" "^7.18.6" + "@babel/plugin-proposal-dynamic-import" "^7.18.6" + "@babel/plugin-proposal-export-namespace-from" "^7.18.9" + "@babel/plugin-proposal-json-strings" "^7.18.6" + "@babel/plugin-proposal-logical-assignment-operators" "^7.18.9" + "@babel/plugin-proposal-nullish-coalescing-operator" "^7.18.6" + "@babel/plugin-proposal-numeric-separator" "^7.18.6" + "@babel/plugin-proposal-object-rest-spread" "^7.19.4" + "@babel/plugin-proposal-optional-catch-binding" "^7.18.6" + "@babel/plugin-proposal-optional-chaining" "^7.18.9" + "@babel/plugin-proposal-private-methods" "^7.18.6" + "@babel/plugin-proposal-private-property-in-object" "^7.18.6" + "@babel/plugin-proposal-unicode-property-regex" "^7.18.6" + "@babel/plugin-syntax-async-generators" "^7.8.4" + "@babel/plugin-syntax-class-properties" "^7.12.13" + "@babel/plugin-syntax-class-static-block" "^7.14.5" + "@babel/plugin-syntax-dynamic-import" "^7.8.3" + "@babel/plugin-syntax-export-namespace-from" "^7.8.3" + "@babel/plugin-syntax-import-assertions" "^7.18.6" + "@babel/plugin-syntax-json-strings" "^7.8.3" + "@babel/plugin-syntax-logical-assignment-operators" "^7.10.4" + "@babel/plugin-syntax-nullish-coalescing-operator" "^7.8.3" + "@babel/plugin-syntax-numeric-separator" "^7.10.4" + "@babel/plugin-syntax-object-rest-spread" "^7.8.3" + "@babel/plugin-syntax-optional-catch-binding" "^7.8.3" + "@babel/plugin-syntax-optional-chaining" "^7.8.3" + "@babel/plugin-syntax-private-property-in-object" "^7.14.5" + "@babel/plugin-syntax-top-level-await" "^7.14.5" + "@babel/plugin-transform-arrow-functions" "^7.18.6" + "@babel/plugin-transform-async-to-generator" "^7.18.6" + "@babel/plugin-transform-block-scoped-functions" "^7.18.6" + "@babel/plugin-transform-block-scoping" "^7.19.4" + "@babel/plugin-transform-classes" "^7.19.0" + "@babel/plugin-transform-computed-properties" "^7.18.9" + "@babel/plugin-transform-destructuring" "^7.19.4" + "@babel/plugin-transform-dotall-regex" "^7.18.6" + "@babel/plugin-transform-duplicate-keys" "^7.18.9" + "@babel/plugin-transform-exponentiation-operator" "^7.18.6" + "@babel/plugin-transform-for-of" "^7.18.8" + "@babel/plugin-transform-function-name" "^7.18.9" + "@babel/plugin-transform-literals" "^7.18.9" + "@babel/plugin-transform-member-expression-literals" "^7.18.6" + "@babel/plugin-transform-modules-amd" "^7.18.6" + "@babel/plugin-transform-modules-commonjs" "^7.18.6" + "@babel/plugin-transform-modules-systemjs" "^7.19.0" + "@babel/plugin-transform-modules-umd" "^7.18.6" + "@babel/plugin-transform-named-capturing-groups-regex" "^7.19.1" + "@babel/plugin-transform-new-target" "^7.18.6" + "@babel/plugin-transform-object-super" "^7.18.6" + "@babel/plugin-transform-parameters" "^7.18.8" + "@babel/plugin-transform-property-literals" "^7.18.6" + "@babel/plugin-transform-regenerator" "^7.18.6" + "@babel/plugin-transform-reserved-words" "^7.18.6" + "@babel/plugin-transform-shorthand-properties" "^7.18.6" + "@babel/plugin-transform-spread" "^7.19.0" + "@babel/plugin-transform-sticky-regex" "^7.18.6" + "@babel/plugin-transform-template-literals" "^7.18.9" + "@babel/plugin-transform-typeof-symbol" "^7.18.9" + "@babel/plugin-transform-unicode-escapes" "^7.18.10" + "@babel/plugin-transform-unicode-regex" "^7.18.6" + "@babel/preset-modules" "^0.1.5" + "@babel/types" "^7.19.4" + babel-plugin-polyfill-corejs2 "^0.3.3" + babel-plugin-polyfill-corejs3 "^0.6.0" + babel-plugin-polyfill-regenerator "^0.4.1" + core-js-compat "^3.25.1" + semver "^6.3.0" + "@babel/preset-flow@^7.13.13": version "7.18.6" resolved "https://registry.yarnpkg.com/@babel/preset-flow/-/preset-flow-7.18.6.tgz#83f7602ba566e72a9918beefafef8ef16d2810cb" @@ -430,7 +1181,30 @@ "@babel/helper-validator-option" "^7.18.6" "@babel/plugin-transform-flow-strip-types" "^7.18.6" -"@babel/preset-typescript@^7.13.0": +"@babel/preset-modules@^0.1.5": + version "0.1.5" + resolved "https://registry.yarnpkg.com/@babel/preset-modules/-/preset-modules-0.1.5.tgz#ef939d6e7f268827e1841638dc6ff95515e115d9" + integrity sha512-A57th6YRG7oR3cq/yt/Y84MvGgE0eJG2F1JLhKuyG+jFxEgrd/HAMJatiFtmOiZurz+0DkrvbheCLaV5f2JfjA== + dependencies: + "@babel/helper-plugin-utils" "^7.0.0" + "@babel/plugin-proposal-unicode-property-regex" "^7.4.4" + "@babel/plugin-transform-dotall-regex" "^7.4.4" + "@babel/types" "^7.4.4" + esutils "^2.0.2" + +"@babel/preset-react@^7.17.12": + version "7.18.6" + resolved "https://registry.yarnpkg.com/@babel/preset-react/-/preset-react-7.18.6.tgz#979f76d6277048dc19094c217b507f3ad517dd2d" + integrity sha512-zXr6atUmyYdiWRVLOZahakYmOBHtWc2WGCkP8PYTgZi0iJXDY2CN180TdrIW4OGOAdLc7TifzDIvtx6izaRIzg== + dependencies: + "@babel/helper-plugin-utils" "^7.18.6" + "@babel/helper-validator-option" "^7.18.6" + "@babel/plugin-transform-react-display-name" "^7.18.6" + "@babel/plugin-transform-react-jsx" "^7.18.6" + "@babel/plugin-transform-react-jsx-development" "^7.18.6" + "@babel/plugin-transform-react-pure-annotations" "^7.18.6" + +"@babel/preset-typescript@^7.13.0", "@babel/preset-typescript@^7.17.12": version "7.18.6" resolved "https://registry.yarnpkg.com/@babel/preset-typescript/-/preset-typescript-7.18.6.tgz#ce64be3e63eddc44240c6358daefac17b3186399" integrity sha512-s9ik86kXBAnD760aybBucdpnLsAt0jK1xqJn2juOn9lkOvSHV60os5hxoVJsPzMQxvnUJFAlkont2DvvaYEBtQ== @@ -479,6 +1253,13 @@ dependencies: regenerator-runtime "^0.13.4" +"@babel/runtime@^7.8.4": + version "7.19.4" + resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.19.4.tgz#a42f814502ee467d55b38dd1c256f53a7b885c78" + integrity sha512-EXpLCrk55f+cYqmHsSR+yD/0gAIMxxA9QK9lnQWzhMCvt+YmoBN7Zx94s++Kv0+unHk39vxNO8t+CMA2WSS3wA== + dependencies: + regenerator-runtime "^0.13.4" + "@babel/template@^7.12.13": version "7.12.13" resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.12.13.tgz#530265be8a2589dbb37523844c5bcb55947fb327" @@ -513,6 +1294,22 @@ debug "^4.1.0" globals "^11.1.0" +"@babel/traverse@^7.19.0", "@babel/traverse@^7.19.1", "@babel/traverse@^7.19.4", "@babel/traverse@^7.19.6": + version "7.19.6" + resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.19.6.tgz#7b4c865611df6d99cb131eec2e8ac71656a490dc" + integrity sha512-6l5HrUCzFM04mfbG09AagtYyR2P0B71B1wN7PfSPiksDPz2k5H9CBC1tcZpz2M8OxbKTPccByoOJ22rUKbpmQQ== + dependencies: + "@babel/code-frame" "^7.18.6" + "@babel/generator" "^7.19.6" + "@babel/helper-environment-visitor" "^7.18.9" + "@babel/helper-function-name" "^7.19.0" + "@babel/helper-hoist-variables" "^7.18.6" + "@babel/helper-split-export-declaration" "^7.18.6" + "@babel/parser" "^7.19.6" + "@babel/types" "^7.19.4" + debug "^4.1.0" + globals "^11.1.0" + "@babel/traverse@^7.4.5": version "7.13.0" resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.13.0.tgz#6d95752475f86ee7ded06536de309a65fc8966cc" @@ -563,6 +1360,15 @@ "@babel/helper-validator-identifier" "^7.16.7" to-fast-properties "^2.0.0" +"@babel/types@^7.18.4", "@babel/types@^7.19.0", "@babel/types@^7.19.4", "@babel/types@^7.4.4": + version "7.19.4" + resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.19.4.tgz#0dd5c91c573a202d600490a35b33246fed8a41c7" + integrity sha512-M5LK7nAeS6+9j7hAq+b3fQs+pNfUtTGq+yFFfHnauFA8zQtLRfmuipmsKDKKLuyG+wC8ABW43A153YNawNTEtw== + dependencies: + "@babel/helper-string-parser" "^7.19.4" + "@babel/helper-validator-identifier" "^7.19.1" + to-fast-properties "^2.0.0" + "@cypress/request@^2.88.10": version "2.88.10" resolved "https://registry.yarnpkg.com/@cypress/request/-/request-2.88.10.tgz#b66d76b07f860d3a4b8d7a0604d020c662752cce" @@ -1423,6 +2229,112 @@ "@styled-system/core" "^5.1.2" "@styled-system/css" "^5.1.5" +"@svgr/babel-plugin-add-jsx-attribute@^6.5.0": + version "6.5.0" + resolved "https://registry.yarnpkg.com/@svgr/babel-plugin-add-jsx-attribute/-/babel-plugin-add-jsx-attribute-6.5.0.tgz#21788f7e982aacafc805ed30a14048a1406fbabc" + integrity sha512-Cp1JR1IPrQNvPRbkfcPmax52iunBC+eQDyBce8feOIIbVH6ZpVhErYoJtPWRBj2rKi4Wi9HvCm1+L1UD6QlBmg== + +"@svgr/babel-plugin-remove-jsx-attribute@^6.5.0": + version "6.5.0" + resolved "https://registry.yarnpkg.com/@svgr/babel-plugin-remove-jsx-attribute/-/babel-plugin-remove-jsx-attribute-6.5.0.tgz#652bfd4ed0a0699843585cda96faeb09d6e1306e" + integrity sha512-8zYdkym7qNyfXpWvu4yq46k41pyNM9SOstoWhKlm+IfdCE1DdnRKeMUPsWIEO/DEkaWxJ8T9esNdG3QwQ93jBA== + +"@svgr/babel-plugin-remove-jsx-empty-expression@^6.5.0": + version "6.5.0" + resolved "https://registry.yarnpkg.com/@svgr/babel-plugin-remove-jsx-empty-expression/-/babel-plugin-remove-jsx-empty-expression-6.5.0.tgz#4b78994ab7d39032c729903fc2dd5c0fa4565cb8" + integrity sha512-NFdxMq3xA42Kb1UbzCVxplUc0iqSyM9X8kopImvFnB+uSDdzIHOdbs1op8ofAvVRtbg4oZiyRl3fTYeKcOe9Iw== + +"@svgr/babel-plugin-replace-jsx-attribute-value@^6.5.0": + version "6.5.0" + resolved "https://registry.yarnpkg.com/@svgr/babel-plugin-replace-jsx-attribute-value/-/babel-plugin-replace-jsx-attribute-value-6.5.0.tgz#ca57c0a62a9c22ff11cdb475dc9a2b35586335d1" + integrity sha512-XWm64/rSPUCQ+MFyA9lhMO+w8bOZvkTvovRIU1lpIy63ysPaVAFtxjQiZj+S7QaLaLGUXkSkf8WZsaN+QPo/gA== + +"@svgr/babel-plugin-svg-dynamic-title@^6.5.0": + version "6.5.0" + resolved "https://registry.yarnpkg.com/@svgr/babel-plugin-svg-dynamic-title/-/babel-plugin-svg-dynamic-title-6.5.0.tgz#57c0e0409757373d641f115d33cf2559b47bff77" + integrity sha512-JIF2D2ltiWFGlTw2fJ9jJg1fNT9rWjOD2Cf0/xzeW6Z2LIRQTHcRHxpZq359+SRWtEPsCXEWV2Xmd+DMBj6dBw== + +"@svgr/babel-plugin-svg-em-dimensions@^6.5.0": + version "6.5.0" + resolved "https://registry.yarnpkg.com/@svgr/babel-plugin-svg-em-dimensions/-/babel-plugin-svg-em-dimensions-6.5.0.tgz#dbca40a18c308f135b4b672ea8e410855e8e3352" + integrity sha512-uuo0FfLP4Nu2zncOcoUFDzZdXWma2bxkTGk0etRThs4/PghvPIGaW8cPhCg6yJ8zpaauWcKV0wZtzKlJRCtVzg== + +"@svgr/babel-plugin-transform-react-native-svg@^6.5.0": + version "6.5.0" + resolved "https://registry.yarnpkg.com/@svgr/babel-plugin-transform-react-native-svg/-/babel-plugin-transform-react-native-svg-6.5.0.tgz#a5453127365b925a7f766615ef6f5cfd01018f98" + integrity sha512-VMRWyOmrV+DaEFPgP3hZMsFgs2g87ojs3txw0Rx8iz6Nf/E3UoHUwTqpkSCWd3Hsnc9gMOY9+wl6+/Ycleh1sw== + +"@svgr/babel-plugin-transform-svg-component@^6.5.0": + version "6.5.0" + resolved "https://registry.yarnpkg.com/@svgr/babel-plugin-transform-svg-component/-/babel-plugin-transform-svg-component-6.5.0.tgz#ec116e9223a02c6dcd9f8cb2bdbf174a3c2ef2f5" + integrity sha512-b67Ul3SelaqvGEEG/1B3VJ03KUtGFgRQjRLCCjdttMQLcYa9l/izQFEclNFx53pNqhijUMNKHPhGMY/CWGVKig== + +"@svgr/babel-preset@^6.5.0": + version "6.5.0" + resolved "https://registry.yarnpkg.com/@svgr/babel-preset/-/babel-preset-6.5.0.tgz#dc14bbe1c74e0c8c4ab77221064645b3399836db" + integrity sha512-UWM98PKVuMqw2UZo8YO3erI6nF1n7/XBYTXBqR0QhZP7HTjYK6QxFNvPfIshddy1hBdzhVpkf148Vg8xiVOtyg== + dependencies: + "@svgr/babel-plugin-add-jsx-attribute" "^6.5.0" + "@svgr/babel-plugin-remove-jsx-attribute" "^6.5.0" + "@svgr/babel-plugin-remove-jsx-empty-expression" "^6.5.0" + "@svgr/babel-plugin-replace-jsx-attribute-value" "^6.5.0" + "@svgr/babel-plugin-svg-dynamic-title" "^6.5.0" + "@svgr/babel-plugin-svg-em-dimensions" "^6.5.0" + "@svgr/babel-plugin-transform-react-native-svg" "^6.5.0" + "@svgr/babel-plugin-transform-svg-component" "^6.5.0" + +"@svgr/core@^6.5.0": + version "6.5.0" + resolved "https://registry.yarnpkg.com/@svgr/core/-/core-6.5.0.tgz#13af3337b7b66a2b6ebe197b67b62badbe490bf1" + integrity sha512-jIbu36GMjfK8HCCQitkfVVeQ2vSXGfq0ef0GO9HUxZGjal6Kvpkk4PwpkFP+OyCzF+skQFT9aWrUqekT3pKF8w== + dependencies: + "@babel/core" "^7.18.5" + "@svgr/babel-preset" "^6.5.0" + "@svgr/plugin-jsx" "^6.5.0" + camelcase "^6.2.0" + cosmiconfig "^7.0.1" + +"@svgr/hast-util-to-babel-ast@^6.5.0": + version "6.5.0" + resolved "https://registry.yarnpkg.com/@svgr/hast-util-to-babel-ast/-/hast-util-to-babel-ast-6.5.0.tgz#0e4aebea26328e22a6fff940711472a47ec24e5c" + integrity sha512-PPy94U/EiPQ2dY0b4jEqj4QOdDRq6DG7aTHjpGaL8HlKSHkpU1DpjfywCXTJqtOdCo2FywjWvg0U2FhqMeUJaA== + dependencies: + "@babel/types" "^7.18.4" + entities "^4.3.0" + +"@svgr/plugin-jsx@^6.5.0": + version "6.5.0" + resolved "https://registry.yarnpkg.com/@svgr/plugin-jsx/-/plugin-jsx-6.5.0.tgz#c23ba0048007f1591fe7a9b060be373e4771487b" + integrity sha512-1CHMqOBKoNk/ZPU+iGXKcQPC6q9zaD7UOI99J+BaGY5bdCztcf5bZyi0QZSDRJtCQpdofeVv7XfBYov2mtl0Pw== + dependencies: + "@babel/core" "^7.18.5" + "@svgr/babel-preset" "^6.5.0" + "@svgr/hast-util-to-babel-ast" "^6.5.0" + svg-parser "^2.0.4" + +"@svgr/plugin-svgo@^6.5.0": + version "6.5.0" + resolved "https://registry.yarnpkg.com/@svgr/plugin-svgo/-/plugin-svgo-6.5.0.tgz#1d9b7d0909bde9fe7d724569c7f7833f3a7bacd7" + integrity sha512-8Zv1Yyv6I7HlIqrqGFM0sDKQrhjbfNZJawR8UjIaVWSb0tKZP1Ra6ymhqIFu6FT6kDRD0Ct5NlQZ10VUujSspw== + dependencies: + cosmiconfig "^7.0.1" + deepmerge "^4.2.2" + svgo "^2.8.0" + +"@svgr/webpack@^6.5.0": + version "6.5.0" + resolved "https://registry.yarnpkg.com/@svgr/webpack/-/webpack-6.5.0.tgz#663407b826cb96a3c3394cfe1f9bd107e693770a" + integrity sha512-rM/Z4pwMhqvAXEHoHIlE4SeTb0ToQNmJuBdiHwhP2ZtywyX6XqrgCv2WX7K/UCgNYJgYbekuylgyjnuLUHTcZQ== + dependencies: + "@babel/core" "^7.18.5" + "@babel/plugin-transform-react-constant-elements" "^7.17.12" + "@babel/preset-env" "^7.18.2" + "@babel/preset-react" "^7.17.12" + "@babel/preset-typescript" "^7.17.12" + "@svgr/core" "^6.5.0" + "@svgr/plugin-jsx" "^6.5.0" + "@svgr/plugin-svgo" "^6.5.0" + "@swc/helpers@0.4.11": version "0.4.11" resolved "https://registry.yarnpkg.com/@swc/helpers/-/helpers-0.4.11.tgz#db23a376761b3d31c26502122f349a21b592c8de" @@ -1995,6 +2907,30 @@ babel-plugin-macros@^2.0.0: cosmiconfig "^6.0.0" resolve "^1.12.0" +babel-plugin-polyfill-corejs2@^0.3.3: + version "0.3.3" + resolved "https://registry.yarnpkg.com/babel-plugin-polyfill-corejs2/-/babel-plugin-polyfill-corejs2-0.3.3.tgz#5d1bd3836d0a19e1b84bbf2d9640ccb6f951c122" + integrity sha512-8hOdmFYFSZhqg2C/JgLUQ+t52o5nirNwaWM2B9LWteozwIvM14VSwdsCAUET10qT+kmySAlseadmfeeSWFCy+Q== + dependencies: + "@babel/compat-data" "^7.17.7" + "@babel/helper-define-polyfill-provider" "^0.3.3" + semver "^6.1.1" + +babel-plugin-polyfill-corejs3@^0.6.0: + version "0.6.0" + resolved "https://registry.yarnpkg.com/babel-plugin-polyfill-corejs3/-/babel-plugin-polyfill-corejs3-0.6.0.tgz#56ad88237137eade485a71b52f72dbed57c6230a" + integrity sha512-+eHqR6OPcBhJOGgsIar7xoAB1GcSwVUA3XjAd7HJNzOXT4wv6/H7KIdA/Nc60cvUlDbKApmqNvD1B1bzOt4nyA== + dependencies: + "@babel/helper-define-polyfill-provider" "^0.3.3" + core-js-compat "^3.25.1" + +babel-plugin-polyfill-regenerator@^0.4.1: + version "0.4.1" + resolved "https://registry.yarnpkg.com/babel-plugin-polyfill-regenerator/-/babel-plugin-polyfill-regenerator-0.4.1.tgz#390f91c38d90473592ed43351e801a9d3e0fd747" + integrity sha512-NtQGmyQDXjQqQ+IzRkBVwEOz9lQ4zxAQZgoAYEtU9dJjnl1Oc98qnN7jcp+bE7O7aYzVpavXE3/VKXNzUbh7aw== + dependencies: + "@babel/helper-define-polyfill-provider" "^0.3.3" + "babel-plugin-styled-components@>= 1": version "2.0.6" resolved "https://registry.yarnpkg.com/babel-plugin-styled-components/-/babel-plugin-styled-components-2.0.6.tgz#6f76c7f7224b7af7edc24a4910351948c691fc90" @@ -2124,6 +3060,16 @@ browserslist@^4.20.2: node-releases "^2.0.6" update-browserslist-db "^1.0.5" +browserslist@^4.21.3, browserslist@^4.21.4: + version "4.21.4" + resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-4.21.4.tgz#e7496bbc67b9e39dd0f98565feccdcb0d4ff6987" + integrity sha512-CBHJJdDmgjl3daYjN5Cp5kbTf1mUhZoS+beLklHIvkOWscs83YAhLlF3Wsh/lciQYAcbBJgTOD44VtG31ZM4Hw== + dependencies: + caniuse-lite "^1.0.30001400" + electron-to-chromium "^1.4.251" + node-releases "^2.0.6" + update-browserslist-db "^1.0.9" + buffer-crc32@~0.2.3: version "0.2.13" resolved "https://registry.yarnpkg.com/buffer-crc32/-/buffer-crc32-0.2.13.tgz#0d333e3f00eac50aa1454abd30ef8c2a5d9a7242" @@ -2180,6 +3126,11 @@ callsites@^3.0.0: resolved "https://registry.yarnpkg.com/callsites/-/callsites-3.1.0.tgz#b3630abd8943432f54b3f0519238e33cd7df2f73" integrity sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ== +camelcase@^6.2.0: + version "6.3.0" + resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-6.3.0.tgz#5685b95eb209ac9c0c177467778c9c84df58ba9a" + integrity sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA== + camelize@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/camelize/-/camelize-1.0.0.tgz#164a5483e630fa4321e5af07020e531831b2609b" @@ -2190,6 +3141,11 @@ caniuse-lite@^1.0.30001370: resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001378.tgz#3d2159bf5a8f9ca093275b0d3ecc717b00f27b67" integrity sha512-JVQnfoO7FK7WvU4ZkBRbPjaot4+YqxogSDosHv0Hv5mWpUESmN+UubMU6L/hGz8QlQ2aY5U0vR6MOs6j/CXpNA== +caniuse-lite@^1.0.30001400: + version "1.0.30001425" + resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001425.tgz#52917791a453eb3265143d2cd08d80629e82c735" + integrity sha512-/pzFv0OmNG6W0ym80P3NtapU0QEiDS3VuYAZMGoLLqiC7f6FJFe1MjpQDREGApeenD9wloeytmVDj+JLXPC6qw== + caniuse-lite@^1.0.30001406: version "1.0.30001418" resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001418.tgz#5f459215192a024c99e3e3a53aac310fc7cf24e6" @@ -2416,6 +3372,13 @@ copy-descriptor@^0.1.0: resolved "https://registry.yarnpkg.com/copy-descriptor/-/copy-descriptor-0.1.1.tgz#676f6eb3c39997c2ee1ac3a924fd6124748f578d" integrity sha512-XgZ0pFcakEUlbwQEVNg3+QAis1FyTL3Qel9FYy8pSkQqoG3PNoT0bOCQtOXcOkur21r2Eq2kI+IE+gsmAEVlYw== +core-js-compat@^3.25.1: + version "3.26.0" + resolved "https://registry.yarnpkg.com/core-js-compat/-/core-js-compat-3.26.0.tgz#94e2cf8ba3e63800c4956ea298a6473bc9d62b44" + integrity sha512-piOX9Go+Z4f9ZiBFLnZ5VrOpBl0h7IGCkiFUN11QTe6LjAvOT3ifL/5TdoizMh99hcGy5SoLyWbapIY/PIb/3A== + dependencies: + browserslist "^4.21.4" + core-js-pure@^3.16.0: version "3.18.3" resolved "https://registry.yarnpkg.com/core-js-pure/-/core-js-pure-3.18.3.tgz#7eed77dcce1445ab68fd68715856633e2fb3b90c" @@ -2442,6 +3405,17 @@ cosmiconfig@^6.0.0: path-type "^4.0.0" yaml "^1.7.2" +cosmiconfig@^7.0.1: + version "7.0.1" + resolved "https://registry.yarnpkg.com/cosmiconfig/-/cosmiconfig-7.0.1.tgz#714d756522cace867867ccb4474c5d01bbae5d6d" + integrity sha512-a1YWNUV2HwGimB7dU2s1wUMurNKjpx60HxBB6xUM8Re+2s1g1IIfJvFR0/iCF+XHdE0GMTKTuLR32UQff4TEyQ== + dependencies: + "@types/parse-json" "^4.0.0" + import-fresh "^3.2.1" + parse-json "^5.0.0" + path-type "^4.0.0" + yaml "^1.10.0" + country-util@^0.2.0: version "0.2.0" resolved "https://registry.yarnpkg.com/country-util/-/country-util-0.2.0.tgz#28570f922c0ace7bcc59d11073f996fab79c5cfb" @@ -2974,6 +3948,11 @@ electron-to-chromium@^1.4.202: resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.4.222.tgz#2ba24bef613fc1985dbffea85df8f62f2dec6448" integrity sha512-gEM2awN5HZknWdLbngk4uQCVfhucFAfFzuchP3wM3NN6eow1eDU0dFy2kts43FB20ZfhVFF0jmFSTb1h5OhyIg== +electron-to-chromium@^1.4.251: + version "1.4.284" + resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.4.284.tgz#61046d1e4cab3a25238f6bf7413795270f125592" + integrity sha512-M8WEXFuKXMYMVr45fo8mq0wUrrJHheiKZf6BArTKk9ZBYCKJEOU5H8cdWgDT+qCVZf7Na4lVUaZsA+h6uA9+PA== + emoji-regex@^8.0.0: version "8.0.0" resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-8.0.0.tgz#e818fd69ce5ccfcb404594f842963bf53164cc37" @@ -3012,6 +3991,11 @@ entities@^2.0.0: resolved "https://registry.yarnpkg.com/entities/-/entities-2.2.0.tgz#098dc90ebb83d8dffa089d55256b351d34c4da55" integrity sha512-p92if5Nz619I0w+akJrLZH0MX0Pb5DX39XOwQTtXSdQQOaYH03S1uIQp4mhOZtAXrxq4ViO67YTiLBo2638o9A== +entities@^4.3.0: + version "4.4.0" + resolved "https://registry.yarnpkg.com/entities/-/entities-4.4.0.tgz#97bdaba170339446495e653cfd2db78962900174" + integrity sha512-oYp7156SP8LkeGD0GF85ad1X9Ai79WtRsZ2gxJqtBuzH+98YUV6jkHEKlZkMbcrjJjIVJNIDP/3WL9wQkoPbWA== + error-ex@^1.3.1: version "1.3.2" resolved "https://registry.yarnpkg.com/error-ex/-/error-ex-1.3.2.tgz#b4ac40648107fdcdcfae242f428bea8a14d4f1bf" @@ -4583,6 +5567,11 @@ jsesc@^2.5.1: resolved "https://registry.yarnpkg.com/jsesc/-/jsesc-2.5.1.tgz#e421a2a8e20d6b0819df28908f782526b96dd1fe" integrity sha1-5CGiqOINawgZ3yiQj3glJrlt0f4= +jsesc@~0.5.0: + version "0.5.0" + resolved "https://registry.yarnpkg.com/jsesc/-/jsesc-0.5.0.tgz#e7dee66e35d6fc16f710fe91d5cf69f70f08911d" + integrity sha512-uZz5UnB7u4T9LvwmFqXii7pZSouaRPorGs5who1Ip7VO0wxanFvBL7GkM6dTHlgX+jhBApRetaWpnDabOeTcnA== + json-parse-even-better-errors@^2.3.0: version "2.3.1" resolved "https://registry.yarnpkg.com/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz#7c47805a94319928e05777405dc12e1f7a4ee02d" @@ -5939,11 +6928,30 @@ reflexbox@^4.0.6: "@styled-system/should-forward-prop" "^5.0.0" styled-system "^5.0.0" +regenerate-unicode-properties@^10.1.0: + version "10.1.0" + resolved "https://registry.yarnpkg.com/regenerate-unicode-properties/-/regenerate-unicode-properties-10.1.0.tgz#7c3192cab6dd24e21cb4461e5ddd7dd24fa8374c" + integrity sha512-d1VudCLoIGitcU/hEg2QqvyGZQmdC0Lf8BqdOMXGFSvJP4bNV1+XqbPQeHHLD51Jh4QJJ225dlIFvY4Ly6MXmQ== + dependencies: + regenerate "^1.4.2" + +regenerate@^1.4.2: + version "1.4.2" + resolved "https://registry.yarnpkg.com/regenerate/-/regenerate-1.4.2.tgz#b9346d8827e8f5a32f7ba29637d398b69014848a" + integrity sha512-zrceR/XhGYU/d/opr2EKO7aRHUeiBI8qjtfHqADTwZd6Szfy16la6kqD0MIUs5z5hx6AaKa+PixpPrR289+I0A== + regenerator-runtime@^0.13.4: version "0.13.9" resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.13.9.tgz#8925742a98ffd90814988d7566ad30ca3b263b52" integrity sha512-p3VT+cOEgxFsRRA9X4lkI1E+k2/CtnKtU4gcxyaCUreilL/vqI6CdZ3wxVUx3UOUg+gnUOQQcRI7BmSI656MYA== +regenerator-transform@^0.15.0: + version "0.15.0" + resolved "https://registry.yarnpkg.com/regenerator-transform/-/regenerator-transform-0.15.0.tgz#cbd9ead5d77fae1a48d957cf889ad0586adb6537" + integrity sha512-LsrGtPmbYg19bcPHwdtmXwbW+TqNvtY4riE3P83foeHRroMbH6/2ddFBfab3t7kbzc7v7p4wbkIecHImqt0QNg== + dependencies: + "@babel/runtime" "^7.8.4" + regex-not@^1.0.0, regex-not@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/regex-not/-/regex-not-1.0.2.tgz#1f4ece27e00b0b65e0247a6810e6a85d83a5752c" @@ -5966,6 +6974,30 @@ regexpp@^3.2.0: resolved "https://registry.yarnpkg.com/regexpp/-/regexpp-3.2.0.tgz#0425a2768d8f23bad70ca4b90461fa2f1213e1b2" integrity sha512-pq2bWo9mVD43nbts2wGv17XLiNLya+GklZ8kaDLV2Z08gDCsGpnKn9BFMepvWuHCbyVvY7J5o5+BVvoQbmlJLg== +regexpu-core@^5.1.0: + version "5.2.1" + resolved "https://registry.yarnpkg.com/regexpu-core/-/regexpu-core-5.2.1.tgz#a69c26f324c1e962e9ffd0b88b055caba8089139" + integrity sha512-HrnlNtpvqP1Xkb28tMhBUO2EbyUHdQlsnlAhzWcwHy8WJR53UWr7/MAvqrsQKMbV4qdpv03oTMG8iIhfsPFktQ== + dependencies: + regenerate "^1.4.2" + regenerate-unicode-properties "^10.1.0" + regjsgen "^0.7.1" + regjsparser "^0.9.1" + unicode-match-property-ecmascript "^2.0.0" + unicode-match-property-value-ecmascript "^2.0.0" + +regjsgen@^0.7.1: + version "0.7.1" + resolved "https://registry.yarnpkg.com/regjsgen/-/regjsgen-0.7.1.tgz#ee5ef30e18d3f09b7c369b76e7c2373ed25546f6" + integrity sha512-RAt+8H2ZEzHeYWxZ3H2z6tF18zyyOnlcdaafLrm21Bguj7uZy6ULibiAFdXEtKQY4Sy7wDTwDiOazasMLc4KPA== + +regjsparser@^0.9.1: + version "0.9.1" + resolved "https://registry.yarnpkg.com/regjsparser/-/regjsparser-0.9.1.tgz#272d05aa10c7c1f67095b1ff0addae8442fc5709" + integrity sha512-dQUtn90WanSNl+7mQKcXAgZxvUe7Z0SqXlgzv0za4LwiUhyzBC58yQO3liFoUgu8GiJVInAhJjkj1N0EtQ5nkQ== + dependencies: + jsesc "~0.5.0" + repeat-element@^1.1.2: version "1.1.4" resolved "https://registry.yarnpkg.com/repeat-element/-/repeat-element-1.1.4.tgz#be681520847ab58c7568ac75fbfad28ed42d39e9" @@ -6006,15 +7038,7 @@ resolve@^1.12.0: is-core-module "^2.1.0" path-parse "^1.0.6" -resolve@^1.20.0: - version "1.20.0" - resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.20.0.tgz#629a013fb3f70755d6f0b7935cc1c2c5378b1975" - integrity sha512-wENBPt4ySzg4ybFQW2TT1zMQucPK95HSh/nq2CFTZVOGut2+pQvSsgtda4d26YrYcr067wjbmzOG8byDPBX63A== - dependencies: - is-core-module "^2.2.0" - path-parse "^1.0.6" - -resolve@^1.22.0: +resolve@^1.14.2, resolve@^1.22.0: version "1.22.1" resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.22.1.tgz#27cb2ebb53f91abb49470a928bba7558066ac177" integrity sha512-nBpuuYuY5jFsli/JIs1oldw6fOQCBioohqWZg/2hiaOybXOft4lonv85uDOKXdf8rhyK159cxU5cDcK/NKk8zw== @@ -6023,6 +7047,14 @@ resolve@^1.22.0: path-parse "^1.0.7" supports-preserve-symlinks-flag "^1.0.0" +resolve@^1.20.0: + version "1.20.0" + resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.20.0.tgz#629a013fb3f70755d6f0b7935cc1c2c5378b1975" + integrity sha512-wENBPt4ySzg4ybFQW2TT1zMQucPK95HSh/nq2CFTZVOGut2+pQvSsgtda4d26YrYcr067wjbmzOG8byDPBX63A== + dependencies: + is-core-module "^2.2.0" + path-parse "^1.0.6" + resolve@^2.0.0-next.3: version "2.0.0-next.3" resolved "https://registry.yarnpkg.com/resolve/-/resolve-2.0.0-next.3.tgz#d41016293d4a8586a39ca5d9b5f15cbea1f55e46" @@ -6142,7 +7174,7 @@ semver@^5.6.0: resolved "https://registry.yarnpkg.com/semver/-/semver-5.7.1.tgz#a954f931aeba508d307bbf069eff0c01c96116f7" integrity sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ== -semver@^6.3.0: +semver@^6.1.1, semver@^6.1.2, semver@^6.3.0: version "6.3.0" resolved "https://registry.yarnpkg.com/semver/-/semver-6.3.0.tgz#ee0a64c8af5e8ceea67687b133761e1becbd1d3d" integrity sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw== @@ -6580,7 +7612,12 @@ supports-preserve-symlinks-flag@^1.0.0: resolved "https://registry.yarnpkg.com/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz#6eda4bd344a3c94aea376d4cc31bc77311039e09" integrity sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w== -svgo@^2.0.3: +svg-parser@^2.0.4: + version "2.0.4" + resolved "https://registry.yarnpkg.com/svg-parser/-/svg-parser-2.0.4.tgz#fdc2e29e13951736140b76cb122c8ee6630eb6b5" + integrity sha512-e4hG1hRwoOdRb37cIMSgzNsxyzKfayW6VOflrwvR+/bzrkyxY/31WkbgnQpgtrNp1SdpJvpUAGTa/ZoiPNDuRQ== + +svgo@^2.0.3, svgo@^2.8.0: version "2.8.0" resolved "https://registry.yarnpkg.com/svgo/-/svgo-2.8.0.tgz#4ff80cce6710dc2795f0c7c74101e6764cfccd24" integrity sha512-+N/Q9kV1+F+UeWYoSiULYo4xYSDQlTgb+ayMobAXPwMnLvop7oxKMo9OzIrX5x3eS4L4f2UHhc9axXwY8DpChg== @@ -6781,6 +7818,29 @@ unbox-primitive@^1.0.2: has-symbols "^1.0.3" which-boxed-primitive "^1.0.2" +unicode-canonical-property-names-ecmascript@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/unicode-canonical-property-names-ecmascript/-/unicode-canonical-property-names-ecmascript-2.0.0.tgz#301acdc525631670d39f6146e0e77ff6bbdebddc" + integrity sha512-yY5PpDlfVIU5+y/BSCxAJRBIS1Zc2dDG3Ujq+sR0U+JjUevW2JhocOF+soROYDSaAezOzOKuyyixhD6mBknSmQ== + +unicode-match-property-ecmascript@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/unicode-match-property-ecmascript/-/unicode-match-property-ecmascript-2.0.0.tgz#54fd16e0ecb167cf04cf1f756bdcc92eba7976c3" + integrity sha512-5kaZCrbp5mmbz5ulBkDkbY0SsPOjKqVS35VpL9ulMPfSl0J0Xsm+9Evphv9CoIZFwre7aJoa94AY6seMKGVN5Q== + dependencies: + unicode-canonical-property-names-ecmascript "^2.0.0" + unicode-property-aliases-ecmascript "^2.0.0" + +unicode-match-property-value-ecmascript@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/unicode-match-property-value-ecmascript/-/unicode-match-property-value-ecmascript-2.0.0.tgz#1a01aa57247c14c568b89775a54938788189a714" + integrity sha512-7Yhkc0Ye+t4PNYzOGKedDhXbYIBe1XEQYQxOPyhcXNMJ0WCABqqj6ckydd6pWRZTHV4GuCPKdBAUiMc60tsKVw== + +unicode-property-aliases-ecmascript@^2.0.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/unicode-property-aliases-ecmascript/-/unicode-property-aliases-ecmascript-2.1.0.tgz#43d41e3be698bd493ef911077c9b131f827e8ccd" + integrity sha512-6t3foTQI9qne+OZoVQB/8x8rk2k1eVy1gRXhV3oFQ5T6R1dqQ1xtin3XqSlx3+ATBkliTaR/hHyJBm+LVPNM8w== + union-value@^1.0.0: version "1.0.1" resolved "https://registry.yarnpkg.com/union-value/-/union-value-1.0.1.tgz#0b6fe7b835aecda61c6ea4d4f02c14221e109847" @@ -6822,6 +7882,14 @@ update-browserslist-db@^1.0.5: escalade "^3.1.1" picocolors "^1.0.0" +update-browserslist-db@^1.0.9: + version "1.0.10" + resolved "https://registry.yarnpkg.com/update-browserslist-db/-/update-browserslist-db-1.0.10.tgz#0f54b876545726f17d00cd9a2561e6dade943ff3" + integrity sha512-OztqDenkfFkbSG+tRxBeAnCVPckDBcvibKd35yDONx6OU8N7sqgwc7rCbkJ/WcYtVRZ4ba68d6byhC21GFh7sQ== + dependencies: + escalade "^3.1.1" + picocolors "^1.0.0" + uri-js@^4.2.2: version "4.2.2" resolved "https://registry.yarnpkg.com/uri-js/-/uri-js-4.2.2.tgz#94c540e1ff772956e2299507c010aea6c8838eb0" @@ -7312,6 +8380,11 @@ yallist@^4.0.0: resolved "https://registry.yarnpkg.com/yallist/-/yallist-4.0.0.tgz#9bb92790d9c0effec63be73519e11a35019a3a72" integrity sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A== +yaml@^1.10.0: + version "1.10.2" + resolved "https://registry.yarnpkg.com/yaml/-/yaml-1.10.2.tgz#2301c5ffbf12b467de8da2333a459e29e7920e4b" + integrity sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg== + yaml@^1.7.2: version "1.10.0" resolved "https://registry.yarnpkg.com/yaml/-/yaml-1.10.0.tgz#3b593add944876077d4d683fee01081bd9fff31e" From 0234a1d37ea5223809eeacc4cebd84f91bcf5507 Mon Sep 17 00:00:00 2001 From: Maja Komel Date: Tue, 25 Oct 2022 14:33:11 +0200 Subject: [PATCH 06/41] Stringify MAT errors --- pages/chart/mat.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pages/chart/mat.js b/pages/chart/mat.js index baf56e79e..ff342d54d 100644 --- a/pages/chart/mat.js +++ b/pages/chart/mat.js @@ -145,7 +145,7 @@ const MeasurementAggregationToolkit = ({ testNames }) => {
{error && - + } {showLoadingIndicator && From 951d58a38c3d2444fb0106b17e5ae93dcdcfaf8d Mon Sep 17 00:00:00 2001 From: Maja Komel Date: Thu, 10 Nov 2022 12:38:33 +0100 Subject: [PATCH 07/41] Fix measurement tests --- cypress/e2e/legacy.e2e.cy.js | 10 ++--- cypress/e2e/measurement.e2e.cy.js | 70 +++++++++++++++---------------- 2 files changed, 38 insertions(+), 42 deletions(-) diff --git a/cypress/e2e/legacy.e2e.cy.js b/cypress/e2e/legacy.e2e.cy.js index 704872d10..d1bbc379f 100644 --- a/cypress/e2e/legacy.e2e.cy.js +++ b/cypress/e2e/legacy.e2e.cy.js @@ -5,10 +5,10 @@ describe('Seearch Page Tests', () => { }) it('shows page full of results', () => { - cy.get('[data-test-id="results-list"]').children('a').should('have.length.of.at.least', 49) + cy.get('[data-test-id="results-list"]').children('a').should('have.length.of.at.least', 48) }) - it('can access "HTTP Hosts" measurements', () => { + it.skip('can access "HTTP Hosts" measurements', () => { cy.get('select[name="testNameFilter"]').select('http_host') cy.get('button').contains('Filter Results').should('not.be.disabled').click() cy.get('[data-test-id="results-list"]', { timeout: 10000 }) @@ -24,15 +24,11 @@ describe('Seearch Page Tests', () => { }) }) - it.only('legacy measurement page shows enough information', () => { + it.skip('legacy measurement page shows enough information', () => { cy.visit('/measurement/20150330T231214Z_bLcnlMHNRrNezvvmUePFtkbJNDglhMvTNoivcUMZqpUjXhkHlR?input=https%3A%2F%2Fcryptbin.com') cy.contains('Country').siblings().contains('Canada') cy.contains('Network').siblings().contains('AS812') cy.contains('Date & Time').siblings().contains('March 30, 2015, 11:12 PM UTC') // Raw measurement rendered? }) - - - - }) \ No newline at end of file diff --git a/cypress/e2e/measurement.e2e.cy.js b/cypress/e2e/measurement.e2e.cy.js index f818429ba..dfa4135d8 100644 --- a/cypress/e2e/measurement.e2e.cy.js +++ b/cypress/e2e/measurement.e2e.cy.js @@ -8,21 +8,21 @@ describe('Measurement Page Tests', () => { describe('Web Connectivity tests', () => { it('renders a valid accessible og:description', () => { - cy.visit('/measurement/20200807T220702Z_AS9009_VDIirQFXzvVZXGTDXrRKAd7oQB3CpnKGISOZLs7kQFV6RJNR7n?input=https%3A%2F%2Fwww.theguardian.com%2F') + cy.visit('/measurement/20221110T100756Z_webconnectivity_US_13335_n1_KWJqHUAPqMdtf2Up?input=https%3A%2F%2Fwww.theguardian.com%2F') cy.get('head meta[property="og:description"]') - .should('have.attr', 'content', 'OONI data suggests www.theguardian.com was accessible in United States on August 7, 2020, 10:44:09 PM UTC, find more open data on internet censorship on OONI Explorer.') + .should('have.attr', 'content', 'OONI data suggests www.theguardian.com was accessible in United States on November 10, 2022, 10:09:16 AM UTC, find more open data on internet censorship on OONI Explorer.') }) it('renders a valid blocked og:description', () => { - cy.visit('/measurement/20200303T085244Z_AS42668_UThI3Fdoo0IZ6610604dd0CGkhd7oQV6QLWWzZDVLJ35oGxBO4?input=http%3A%2F%2Frutor.org%2F') + cy.visit('/measurement/20211215T052819Z_webconnectivity_RU_8369_n1_PkPgEYV2DrBAfPxu?input=http%3A%2F%2Frutor.org') cy.get('head meta[property="og:description"]') - .should('have.attr', 'content', 'OONI data suggests rutor.org was blocked in Russia on March 3, 2020, 9:15:05 AM UTC, find more open data on internet censorship on OONI Explorer.') + .should('have.attr', 'content', 'OONI data suggests rutor.org was blocked in Russia on December 15, 2021, 5:44:55 AM UTC, find more open data on internet censorship on OONI Explorer.') }) it('renders a valid anomaly og:description', () => { - cy.visit('/measurement/20200807T223513Z_AS27364_QJdsvFRG6B98MqqWR3QTey9WheksI7757sefWWMwAe0OHU8hWT?input=http%3A%2F%2Fwww.google.com%2Fsearch%3Fq%3Dlesbian') + cy.visit('/measurement/20221110T082316Z_webconnectivity_MY_4788_n1_Ue0y9OwyBLvoIfgm?input=http%3A%2F%2Fwww.google.com%2Fsearch%3Fq%3Dlesbian') cy.get('head meta[property="og:description"]') - .should('have.attr', 'content', 'OONI data suggests www.google.com was accessible in United States on August 7, 2020, 10:36:29 PM UTC, find more open data on internet censorship on OONI Explorer.') + .should('have.attr', 'content', 'OONI data suggests www.google.com was accessible in Malaysia on November 10, 2022, 10:25:51 AM UTC, find more open data on internet censorship on OONI Explorer.') }) // it('renders a valid website down og:description', () => { @@ -68,15 +68,15 @@ describe('Measurement Page Tests', () => { describe('Telegram Tests', () => { it('renders a reachable og:description', () => { - cy.visit('/measurement/20200807T220134Z_AS9009_SOmXTa7NLwrzRniaVS0yxlJ3TbKTDJJxrfaIJkpURTn3GBHiA2') + cy.visit('/measurement/20221110T102855Z_telegram_US_7018_n1_HKJ2sF9m0lP7JMsW') cy.get('head meta[property="og:description"]') - .should('have.attr', 'content', 'OONI data suggests Telegram was reachable in United States on August 7, 2020, 10:37:28 PM UTC, find more open data on internet censorship on OONI Explorer.') + .should('have.attr', 'content', 'OONI data suggests Telegram was reachable in United States on November 10, 2022, 10:28:56 AM UTC, find more open data on internet censorship on OONI Explorer.') }) it('renders a unreachable og:description', () => { - cy.visit('/measurement/20200304T183801Z_AS42610_uehNyFJgkAJBCaq5thzAovFEODIQ1u5vlTTk3D6GDvbaYeoJY8') + cy.visit('measurement/20221109T225726Z_telegram_RU_8402_n1_qnYloXASGMUg2G9O') cy.get('head meta[property="og:description"]') - .should('have.attr', 'content', 'OONI data suggests Telegram was NOT reachable in Russia on March 4, 2020, 6:57:54 PM UTC, find more open data on internet censorship on OONI Explorer.') + .should('have.attr', 'content', 'OONI data suggests Telegram was NOT reachable in Russia on November 9, 2022, 10:57:59 PM UTC, find more open data on internet censorship on OONI Explorer.') }) it('renders an accessible measurement', () => { @@ -86,7 +86,7 @@ describe('Measurement Page Tests', () => { }) it('renders an anomaly measurement', () => { - cy.visit('/measurement/20200304T183801Z_AS42610_uehNyFJgkAJBCaq5thzAovFEODIQ1u5vlTTk3D6GDvbaYeoJY8') + cy.visit('measurement/20221109T225726Z_telegram_RU_8402_n1_qnYloXASGMUg2G9O') cy.heroHasColor(anomalyColor) .contains('Anomaly') }) @@ -94,15 +94,15 @@ describe('Measurement Page Tests', () => { describe('WhatsApp Tests', () => { it('renders a reachable og:description', () => { - cy.visit('/measurement/20200807T220116Z_AS9009_JaRyaMzjdCq5JA89mE1nl8VCZltDv4vAaNhUqi193SFCcwR8av') + cy.visit('/measurement/20221110T103853Z_whatsapp_US_7922_n1_JPLapx8JfJ0J4nf4') cy.get('head meta[property="og:description"]') - .should('have.attr', 'content', 'OONI data suggests WhatsApp was reachable in United States on August 7, 2020, 10:37:10 PM UTC, find more open data on internet censorship on OONI Explorer.') + .should('have.attr', 'content', 'OONI data suggests WhatsApp was reachable in United States on November 10, 2022, 10:38:53 AM UTC, find more open data on internet censorship on OONI Explorer.') }) it('renders an unreachable og:description', () => { - cy.visit('/measurement/20200407T024309Z_AS4713_xA9Wh81DQrIFqRe46zwKeyJw4DJQwjyTLBIi2zSQqWUBsfQMJS') + cy.visit('/measurement/20221105T223928Z_whatsapp_JP_55392_n1_aL6HH9GHYc1YbILm') cy.get('head meta[property="og:description"]') - .should('have.attr', 'content', 'OONI data suggests WhatsApp was likely blocked in Japan on April 7, 2020, 2:43:10 AM UTC, find more open data on internet censorship on OONI Explorer.') + .should('have.attr', 'content', 'OONI data suggests WhatsApp was likely blocked in Japan on November 5, 2022, 10:39:29 PM UTC, find more open data on internet censorship on OONI Explorer.') }) it('renders an accessible measurement', () => { @@ -157,15 +157,15 @@ describe('Measurement Page Tests', () => { }) it('renders a reachable og:description', () => { - cy.visit('/measurement/20200807T220152Z_AS9009_K9rqQAfaNssMkVbX1uhfdORk0w49f1sk14vCMs0ZuzUrBgg0Te') + cy.visit('/measurement/20221110T104252Z_facebookmessenger_US_20115_n1_o61hepYQFOp1mtT9') cy.get('head meta[property="og:description"]') - .should('have.attr', 'content', 'OONI data suggests Facebook Messenger was reachable in United States on August 7, 2020, 10:37:46 PM UTC, find more open data on internet censorship on OONI Explorer.') + .should('have.attr', 'content', 'OONI data suggests Facebook Messenger was reachable in United States on November 10, 2022, 10:42:52 AM UTC, find more open data on internet censorship on OONI Explorer.') }) it('renders a unreachable og:description', () => { - cy.visit('/measurement/20200304T191012Z_AS42610_fqDY31xiRoWEdKd4GWtV84UYpXG2RlpjBK7kd8rTLHIItqMnej') + cy.visit('/measurement/20221110T103257Z_facebookmessenger_RU_12389_n1_I1KmLISJCV1o4EoV') cy.get('head meta[property="og:description"]') - .should('have.attr', 'content', 'OONI data suggests Facebook Messenger was NOT reachable in Russia on March 4, 2020, 6:37:43 PM UTC, find more open data on internet censorship on OONI Explorer.') + .should('have.attr', 'content', 'OONI data suggests Facebook Messenger was NOT reachable in Russia on November 10, 2022, 10:32:58 AM UTC, find more open data on internet censorship on OONI Explorer.') }) }) @@ -181,14 +181,14 @@ describe('Measurement Page Tests', () => { .contains('Network tampering') }) it('renders a valid og:description', () => { - cy.visit('/measurement/20200807T222222Z_AS37168_CdrDnfRHdhGBU3n5sjhbTJtR3B3IlOejPlsq3WBf1si6bhRlo0') + cy.visit('/measurement/20221110T105736Z_httpheaderfieldmanipulation_ES_57269_n1_8SnGox89HKlVQoDJ') cy.get('head meta[property="og:description"]') - .should('have.attr', 'content', 'OONI data suggests HTTP header manipulation was not detected in South Africa on August 7, 2020, 10:22:22 PM UTC, find more open data on internet censorship on OONI Explorer.') + .should('have.attr', 'content', 'OONI data suggests HTTP header manipulation was not detected in Spain on November 10, 2022, 10:57:36 AM UTC, find more open data on internet censorship on OONI Explorer.') }) it('renders an anomaly og:description', () => { - cy.visit('/measurement/20190530T141520Z_AS4788_DNqCUqL7CAfijExowyaymigb2sITdpS47gjrieDJCx8kDc1TfO') + cy.visit('/measurement/20221110T104927Z_httpheaderfieldmanipulation_IR_58224_n1_voFn4ODgxZHJCpoy') cy.get('head meta[property="og:description"]') - .should('have.attr', 'content', 'OONI data suggests HTTP header manipulation was detected in Malaysia on May 30, 2019, 2:15:19 PM UTC, find more open data on internet censorship on OONI Explorer.') + .should('have.attr', 'content', 'OONI data suggests HTTP header manipulation was detected in Iran on November 10, 2022, 10:49:28 AM UTC, find more open data on internet censorship on OONI Explorer.') }) }) @@ -204,14 +204,14 @@ describe('Measurement Page Tests', () => { .contains('Network tampering') }) it('renders a valid og:description', () => { - cy.visit('/measurement/20200807T222629Z_AS31334_YNx9EqZSwRVEzVLN3TowlkW0a8pw1PYzJSI8yxHKDhVa7a2han') + cy.visit('/measurement/20221110T105936Z_httpinvalidrequestline_TR_47331_n1_fB1HONVuo6bJAcbz') cy.get('head meta[property="og:description"]') - .should('have.attr', 'content', 'OONI data suggests Network traffic manipulation was not detected in Germany on August 7, 2020, 10:26:29 PM UTC, find more open data on internet censorship on OONI Explorer.') + .should('have.attr', 'content', 'OONI data suggests Network traffic manipulation was not detected in Turkey on November 10, 2022, 10:59:36 AM UTC, find more open data on internet censorship on OONI Explorer.') }) it('render an anomaly og:description', () => { - cy.visit('/measurement/20170213T160709Z_AS8452_M5qSjOZgYwFrkQYVfdrYmYw2tLc3dzJB7mVbtjVoR1qCdbcEOA') + cy.visit('/measurement/20221110T105942Z_httpinvalidrequestline_US_13335_n1_cwbvshRglfEgMGAF') cy.get('head meta[property="og:description"]') - .should('have.attr', 'content', 'OONI data suggests Network traffic manipulation was detected in Egypt on February 13, 2017, 4:07:00 PM UTC, find more open data on internet censorship on OONI Explorer.') + .should('have.attr', 'content', 'OONI data suggests Network traffic manipulation was detected in United States on November 10, 2022, 10:59:45 AM UTC, find more open data on internet censorship on OONI Explorer.') }) }) @@ -229,9 +229,9 @@ describe('Measurement Page Tests', () => { }) it('renders a valid og:description', () => { - cy.visit('/measurement/20200807T222629Z_AS31334_YNx9EqZSwRVEzVLN3TowlkW0a8pw1PYzJSI8yxHKDhVa7a2han') + cy.visit('/measurement/20221110T105028Z_ndt_DE_3209_n1_9eyIJwUdcD6u3WgC') cy.get('head meta[property="og:description"]') - .should('have.attr', 'content', 'OONI data suggests Network traffic manipulation was not detected in Germany on August 7, 2020, 10:26:29 PM UTC, find more open data on internet censorship on OONI Explorer.') + .should('have.attr', 'content', 'OONI data suggests Speed test result (NDT Test) in Germany on November 10, 2022, 10:50:28 AM UTC, find more open data on internet censorship on OONI Explorer.') }) }) @@ -247,9 +247,9 @@ describe('Measurement Page Tests', () => { .contains('Error') }) it('renders a valid og:description', () => { - cy.visit('/measurement/20200807T222018Z_AS33363_FTKkox83LnAxEXkK8hJQuuqzBjZu2nyTl87aLXQ7MbCPvZIEgW') + cy.visit('/measurement/20221110T111104Z_dash_US_19969_n1_kyclVb6Fj9VuW3A9') cy.get('head meta[property="og:description"]') - .should('have.attr', 'content', 'OONI data suggests 1440p (2k) quality video streaming at 25.43 Mbit/s speed in United States on August 7, 2020, 10:20:16 PM UTC, find more open data on internet censorship on OONI Explorer.') + .should('have.attr', 'content', 'OONI data suggests 2160p (4k) quality video streaming at 539.09 Mbit/s speed in United States on November 10, 2022, 11:11:04 AM UTC, find more open data on internet censorship on OONI Explorer.') }) }) @@ -265,9 +265,9 @@ describe('Measurement Page Tests', () => { .contains('Psiphon is likely blocked') }) it('renders a reachable og:description', () => { - cy.visit('/measurement/20200807T220939Z_AS9009_kBQ0oHp4qmji2gRJdiirDac5lYoSXxMDg5qe72r0NP4rW2q7ee') + cy.visit('/measurement/20221110T112242Z_psiphon_US_7018_n1_clM85Z0Pof3RqXdp') cy.get('head meta[property="og:description"]') - .should('have.attr', 'content', 'OONI data suggests Psiphon was reachable in United States on August 7, 2020, 10:45:32 PM UTC, find more open data on internet censorship on OONI Explorer.') + .should('have.attr', 'content', 'OONI data suggests Psiphon was reachable in United States on November 10, 2022, 11:22:43 AM UTC, find more open data on internet censorship on OONI Explorer.') }) }) @@ -283,9 +283,9 @@ describe('Measurement Page Tests', () => { .contains('Tor is likely blocked') }) it('renders a valid og:description', () => { - cy.visit('/measurement/20200807T220959Z_AS9009_BKXGtNm91coGhmT6iRzvveeGGKvvebMpRONk1qv3DAMvxDPQwV') + cy.visit('/measurement/20221110T112301Z_tor_US_7018_n1_uszAjDiPyoWLMyuV') cy.get('head meta[property="og:description"]') - .should('have.attr', 'content', 'OONI data suggests Tor censorship test result in United States on August 7, 2020, 10:45:52 PM UTC, find more open data on internet censorship on OONI Explorer.') + .should('have.attr', 'content', 'OONI data suggests Tor censorship test result in United States on November 10, 2022, 11:23:02 AM UTC, find more open data on internet censorship on OONI Explorer.') }) }) From 75d127b2a81da69fd5f4eb0deacb2d04f4fefa85 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Arturo=20Filast=C3=B2?= Date: Thu, 10 Nov 2022 14:24:45 +0100 Subject: [PATCH 08/41] Update docs on managing translations (#748) --- Readme.md | 24 ++++++++++++++++++++++++ scripts/build-translations.js | 9 +++------ 2 files changed, 27 insertions(+), 6 deletions(-) diff --git a/Readme.md b/Readme.md index 3ffe2cef1..d4986dc52 100644 --- a/Readme.md +++ b/Readme.md @@ -33,3 +33,27 @@ yarn run start ``` We also provide a `Dockerfile` for easy deployment. + +## Managing translations + +You should have checked out the https://github.com/ooni/translations +repository. + +From inside of `ooni/translations` to update the transifex master copy (this is +done when edits to the master spreadsheet are done), you should run: +``` +./update-explorer-source.sh +``` + +Then when the translations have been done and you want to pull in the +translated versions, run: +``` +./update-explorer-translations.sh +``` + +From inside of the ooni/explorer repo you should then run: +``` +yarn run script:build-translations +``` + +(this assumes you have `ooni/translations` checked out in the parent directory) diff --git a/scripts/build-translations.js b/scripts/build-translations.js index 4766c55d9..fbf2864c2 100644 --- a/scripts/build-translations.js +++ b/scripts/build-translations.js @@ -1,19 +1,16 @@ /* eslint-disable no-console */ -/* global require */ const glob = require('glob') -const { basename, resolve } = require('path') -// const csvParse = require('csv-parse/lib/sync') +const { basename } = require('path') const { readFileSync, writeFileSync } = require('fs') const LANG_DIR = './public/static/lang/' -const DEFAULT_LOCALE = 'en' const TRANSLATED_STRINGS_DIR = '../translations/explorer' -const supportedLanguages = glob.sync(`${LANG_DIR}/*.json`).map((f) => basename(f, '.json')) +const supportedLanguages = glob.sync(`${TRANSLATED_STRINGS_DIR}/*`).map((f) => basename(f, '.json')) // Copy latest files from `translations` supportedLanguages.forEach((lang) => { - console.log('> Getting latest translations for langugae ✨', lang) + console.log('> Getting latest translations for:', lang) writeFileSync(`${LANG_DIR}/${lang}.json`, readFileSync(`${TRANSLATED_STRINGS_DIR}/${lang}/strings.json`)) }) From ddad91022384747805cc2d63f0285f5f1239ef3c Mon Sep 17 00:00:00 2001 From: Maja Komel Date: Thu, 24 Nov 2022 16:45:29 +0100 Subject: [PATCH 09/41] Import regenerator-runtime --- pages/_app.js | 1 + 1 file changed, 1 insertion(+) diff --git a/pages/_app.js b/pages/_app.js index 97664ba35..e90e07d07 100644 --- a/pages/_app.js +++ b/pages/_app.js @@ -2,6 +2,7 @@ // https://github.com/zeit/next.js/blob/master/examples/with-sentry // https://github.com/vercel/next.js/blob/canary/examples/with-loading/pages/_app.js import 'scripts/wdyr' +import 'regenerator-runtime/runtime' import { useEffect } from 'react' import NProgress from 'nprogress' import { useRouter } from 'next/router' From 5dac6ab610f42908f965debe8f2d7213343eda3f Mon Sep 17 00:00:00 2001 From: Maja Komel Date: Thu, 24 Nov 2022 17:41:56 +0100 Subject: [PATCH 10/41] Fix server errors due to missing vars --- components/aggregation/mat/ChartHeader.js | 8 ++++---- components/aggregation/mat/RowChart.js | 4 ++-- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/components/aggregation/mat/ChartHeader.js b/components/aggregation/mat/ChartHeader.js index cbb180797..95d4ffe03 100644 --- a/components/aggregation/mat/ChartHeader.js +++ b/components/aggregation/mat/ChartHeader.js @@ -33,10 +33,10 @@ export const SubtitleStr = ({ query }) => { const intl = useIntl() const params = new Set() - const testName = intl.formatMessage({id: getRowLabel(query.test_name, 'test_name')}) - params.add(testName) - - + if (query.test_name) { + const testName = intl.formatMessage({id: getRowLabel(query.test_name, 'test_name')}) + params.add(testName) + } if (query.domain) { params.add(query.domain) } diff --git a/components/aggregation/mat/RowChart.js b/components/aggregation/mat/RowChart.js index 23d7602b0..4b3dca59e 100644 --- a/components/aggregation/mat/RowChart.js +++ b/components/aggregation/mat/RowChart.js @@ -153,8 +153,8 @@ const RowChart = ({ data, indexBy, label, height, rowIndex /* width, first, last const chartProps = useMemo(() => { const xAxisTicks = getXAxisTicks(query) chartProps1D.axisBottom.tickValues = xAxisTicks - chartProps1D.axisBottom.legend = intl.formatMessage(messages[`x_axis.${query.axis_x}`]) - return label === undefined ? chartProps1D : chartProps2D + chartProps1D.axisBottom.legend = query.axis_x ? intl.formatMessage(messages[`x_axis.${query.axis_x}`]) : '' + return label === undefined ? chartProps1D : chartProps2D }, [intl, label, query]) return ( From 0f9218008573a7013d25606e30994ca38af54f6b Mon Sep 17 00:00:00 2001 From: Maja Komel Date: Tue, 6 Dec 2022 12:21:13 +0100 Subject: [PATCH 11/41] Force time_grain to day --- pages/chart/mat.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/pages/chart/mat.js b/pages/chart/mat.js index ff342d54d..a680b4e02 100644 --- a/pages/chart/mat.js +++ b/pages/chart/mat.js @@ -113,7 +113,9 @@ const MeasurementAggregationToolkit = ({ testNames }) => { }, []) const shouldFetchData = router.pathname !== router.asPath - const query = router.query + // THIS IS TEMPORARY - in the next iteration users will be + // able to set time_grain themselves + const query = {...router.query, time_grain: 'day'} const { data, error, isValidating } = useSWR( () => shouldFetchData ? [query] : null, From 311341da5dd3a5c01e86ccc744946cd54394a3fc Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 7 Dec 2022 22:15:57 +0100 Subject: [PATCH 12/41] Bump decode-uri-component from 0.2.0 to 0.2.2 (#817) Bumps [decode-uri-component](https://github.com/SamVerschueren/decode-uri-component) from 0.2.0 to 0.2.2. - [Release notes](https://github.com/SamVerschueren/decode-uri-component/releases) - [Commits](https://github.com/SamVerschueren/decode-uri-component/compare/v0.2.0...v0.2.2) --- updated-dependencies: - dependency-name: decode-uri-component dependency-type: indirect ... Signed-off-by: dependabot[bot] Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- yarn.lock | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/yarn.lock b/yarn.lock index 3798ab31f..b1a9306e2 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3801,9 +3801,9 @@ debug@^4.3.4: ms "2.1.2" decode-uri-component@^0.2.0: - version "0.2.0" - resolved "https://registry.yarnpkg.com/decode-uri-component/-/decode-uri-component-0.2.0.tgz#eb3913333458775cb84cd1a1fae062106bb87545" - integrity sha512-hjf+xovcEn31w/EUYdTXQh/8smFL/dzYjohQGEIgjyNavaJfBY2p5F527Bo1VPATxv0VYTUC2bOcXvqFwk78Og== + version "0.2.2" + resolved "https://registry.yarnpkg.com/decode-uri-component/-/decode-uri-component-0.2.2.tgz#e69dbe25d37941171dd540e024c444cd5188e1e9" + integrity sha512-FqUYQ+8o158GyGTrMFJms9qh3CqTKvAqgqsTnkLI8sKu0028orqBhxNMFkFen0zGyg6epACD32pjVk58ngIErQ== deep-extend@^0.6.0: version "0.6.0" From 7510800aae8d661af5669698da6bf2a426e4597e Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 7 Dec 2022 22:18:14 +0100 Subject: [PATCH 13/41] Bump qs from 6.5.2 to 6.5.3 (#819) Bumps [qs](https://github.com/ljharb/qs) from 6.5.2 to 6.5.3. - [Release notes](https://github.com/ljharb/qs/releases) - [Changelog](https://github.com/ljharb/qs/blob/main/CHANGELOG.md) - [Commits](https://github.com/ljharb/qs/compare/v6.5.2...v6.5.3) --- updated-dependencies: - dependency-name: qs dependency-type: indirect ... Signed-off-by: dependabot[bot] Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- yarn.lock | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/yarn.lock b/yarn.lock index b1a9306e2..41bc971c7 100644 --- a/yarn.lock +++ b/yarn.lock @@ -6660,9 +6660,9 @@ pure-color@^1.2.0: integrity sha1-H+Bk+wrIUfDeYTIKi/eWg2Qi8z4= qs@~6.5.2: - version "6.5.2" - resolved "https://registry.yarnpkg.com/qs/-/qs-6.5.2.tgz#cb3ae806e8740444584ef154ce8ee98d403f3e36" - integrity sha512-N5ZAX4/LxJmF+7wN74pUD6qAh9/wnvdQcjq9TZjevvXzSUo7bfmw91saqMjzGS2xq91/odN2dW/WOl7qQHNDGA== + version "6.5.3" + resolved "https://registry.yarnpkg.com/qs/-/qs-6.5.3.tgz#3aeeffc91967ef6e35c0e488ef46fb296ab76aad" + integrity sha512-qxXIEh4pCGfHICj1mAJQ2/2XVZkjCDTcEgfoSQxc/fYivUZxTkk7L3bDBJSoNrEzXI17oUO5Dp07ktqE5KzczA== queue-microtask@^1.2.2: version "1.2.3" From 463793338be58102e273079518daffdfae5ff1d2 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 7 Dec 2022 22:18:40 +0100 Subject: [PATCH 14/41] Bump minimatch from 3.0.4 to 3.1.2 (#820) Bumps [minimatch](https://github.com/isaacs/minimatch) from 3.0.4 to 3.1.2. - [Release notes](https://github.com/isaacs/minimatch/releases) - [Changelog](https://github.com/isaacs/minimatch/blob/main/changelog.md) - [Commits](https://github.com/isaacs/minimatch/compare/v3.0.4...v3.1.2) --- updated-dependencies: - dependency-name: minimatch dependency-type: indirect ... Signed-off-by: dependabot[bot] Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- yarn.lock | 17 +++++------------ 1 file changed, 5 insertions(+), 12 deletions(-) diff --git a/yarn.lock b/yarn.lock index 41bc971c7..0caa83de0 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2963,9 +2963,9 @@ babylon@^6.14.0: integrity sha512-q/UEjfGJ2Cm3oKV71DJz9d25TPnq5rhBVL2Q4fA5wcC3jcrdn7+SssEybFIxwAvvP+YCsCYNKughoF33GxgycQ== balanced-match@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.0.tgz#89b4d199ab2bee49de164ea02b89ce462d71b767" - integrity sha1-ibTRmasr7kneFk6gK4nORi1xt2c= + version "1.0.2" + resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.2.tgz#e83e3a7e3f300b34cb9d87f615fa0cbf357690ee" + integrity sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw== base16@^1.0.0: version "1.0.0" @@ -3336,7 +3336,7 @@ component-emitter@^1.2.1: concat-map@0.0.1: version "0.0.1" resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b" - integrity sha1-2Klr13/Wjfd5OnMDajug1UBdR3s= + integrity sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg== console-control-strings@^1.0.0, console-control-strings@~1.1.0: version "1.1.0" @@ -5950,20 +5950,13 @@ mimic-fn@^2.1.0: resolved "https://registry.yarnpkg.com/mimic-fn/-/mimic-fn-2.1.0.tgz#7ed2c2ccccaf84d3ffcb7a69b57711fc2083401b" integrity sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg== -minimatch@^3.0.2, minimatch@^3.1.1, minimatch@^3.1.2: +minimatch@^3.0.2, minimatch@^3.0.4, minimatch@^3.1.1, minimatch@^3.1.2: version "3.1.2" resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.1.2.tgz#19cd194bfd3e428f049a70817c038d89ab4be35b" integrity sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw== dependencies: brace-expansion "^1.1.7" -minimatch@^3.0.4: - version "3.0.4" - resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.0.4.tgz#5166e286457f03306064be5497e8dbb0c3d32083" - integrity sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA== - dependencies: - brace-expansion "^1.1.7" - minimatch@^5.0.1: version "5.1.0" resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-5.1.0.tgz#1717b464f4971b144f6aabe8f2d0b8e4511e09c7" From acc70fe8d095b64bbfc7d0470aa684adfe02906e Mon Sep 17 00:00:00 2001 From: Maja Komel Date: Wed, 21 Dec 2022 17:20:38 +0100 Subject: [PATCH 15/41] Upgrade nivo libraries --- package.json | 10 +-- yarn.lock | 232 +++++++++++++++++++++++++-------------------------- 2 files changed, 119 insertions(+), 123 deletions(-) diff --git a/package.json b/package.json index 1b9ac5629..bd242dabf 100644 --- a/package.json +++ b/package.json @@ -8,11 +8,11 @@ "dependencies": { "@babel/core": "7.18.10", "@datapunt/matomo-tracker-react": "^0.5.1", - "@nivo/bar": "^0.79.1", - "@nivo/calendar": "^0.79.1", - "@nivo/core": "0.78.0", - "@nivo/funnel": "^0.79.1", - "@nivo/heatmap": "^0.79.1", + "@nivo/bar": "^0.80.0", + "@nivo/calendar": "^0.80.0", + "@nivo/core": "^0.80.0", + "@nivo/funnel": "^0.80.0", + "@nivo/heatmap": "^0.80.0", "@rebass/forms": "^4.0.6", "@sentry/nextjs": "^7.11.1", "axios": "^0.27.2", diff --git a/yarn.lock b/yarn.lock index 0caa83de0..00f47bde2 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1773,70 +1773,70 @@ resolved "https://registry.yarnpkg.com/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-12.3.1.tgz#27d71a95247a9eaee03d47adee7e3bd594514136" integrity sha512-W1ijvzzg+kPEX6LAc+50EYYSEo0FVu7dmTE+t+DM4iOLqgGHoW9uYSz9wCVdkXOEEMP9xhXfGpcSxsfDucyPkA== -"@nivo/annotations@0.79.1": - version "0.79.1" - resolved "https://registry.yarnpkg.com/@nivo/annotations/-/annotations-0.79.1.tgz#c1b93a1facf55e3f32e2af1b8fb0ba1bebc01910" - integrity sha512-lYso9Luu0maSDtIufwvyVt2+Wue7R9Fh3CIjuRDmNR72UjAgAVEcCar27Fy865UXGsj2hRJZ7KY/1s6kT3gu/w== +"@nivo/annotations@0.80.0": + version "0.80.0" + resolved "https://registry.yarnpkg.com/@nivo/annotations/-/annotations-0.80.0.tgz#127e4801fff7370dcfb9acfe1e335781dd65cfd5" + integrity sha512-bC9z0CLjU07LULTMWsqpjovRtHxP7n8oJjqBQBLmHOGB4IfiLbrryBfu9+aEZH3VN2jXHhdpWUz+HxeZzOzsLg== dependencies: - "@nivo/colors" "0.79.1" - "@react-spring/web" "9.3.1" + "@nivo/colors" "0.80.0" + "@react-spring/web" "9.4.5" lodash "^4.17.21" -"@nivo/axes@0.79.0": - version "0.79.0" - resolved "https://registry.yarnpkg.com/@nivo/axes/-/axes-0.79.0.tgz#6f009819b26f93a4126697152aeab5f979f1ab6c" - integrity sha512-EhSeCPxtWEuxqnifeyF/pIJEzL7pRM3rfygL+MpfT5ypu5NcXYRGQo/Bw0Vh+GF1ML+tNAE0rRvCu2jgLSdVNQ== +"@nivo/axes@0.80.0": + version "0.80.0" + resolved "https://registry.yarnpkg.com/@nivo/axes/-/axes-0.80.0.tgz#22788855ddc45bb6a619dcd03d62d4bd8c0fc35f" + integrity sha512-AsUyaSHGwQVSEK8QXpsn8X+poZxvakLMYW7crKY1xTGPNw+SU4SSBohPVumm2jMH3fTSLNxLhAjWo71GBJXfdA== dependencies: - "@nivo/scales" "0.79.0" - "@react-spring/web" "9.3.1" + "@nivo/scales" "0.80.0" + "@react-spring/web" "9.4.5" d3-format "^1.4.4" d3-time-format "^3.0.0" -"@nivo/bar@^0.79.1": - version "0.79.1" - resolved "https://registry.yarnpkg.com/@nivo/bar/-/bar-0.79.1.tgz#42d28169307e735cb84e57b4b6915195ef1c97fb" - integrity sha512-swJ2FtFeRPWJK9O6aZiqTDi2J6GrU2Z6kIHBBCXBlFmq6+vfd5AqOHytdXPTaN80JsKDBBdtY7tqRjpRPlDZwQ== - dependencies: - "@nivo/annotations" "0.79.1" - "@nivo/axes" "0.79.0" - "@nivo/colors" "0.79.1" - "@nivo/legends" "0.79.1" - "@nivo/scales" "0.79.0" - "@nivo/tooltip" "0.79.0" - "@react-spring/web" "9.3.1" +"@nivo/bar@^0.80.0": + version "0.80.0" + resolved "https://registry.yarnpkg.com/@nivo/bar/-/bar-0.80.0.tgz#6518449aeb068f2ffe263822e44898f3f427d482" + integrity sha512-woE/S12Sp+RKQeOHtp302WXfy5usj73cV/gjP95PzJxMv+Rn01i1Uwys3BILzc9h4+OxYuWTFqLADAySAmi7qQ== + dependencies: + "@nivo/annotations" "0.80.0" + "@nivo/axes" "0.80.0" + "@nivo/colors" "0.80.0" + "@nivo/legends" "0.80.0" + "@nivo/scales" "0.80.0" + "@nivo/tooltip" "0.80.0" + "@react-spring/web" "9.4.5" d3-scale "^3.2.3" - d3-shape "^1.2.2" + d3-shape "^1.3.5" lodash "^4.17.21" -"@nivo/calendar@^0.79.1": - version "0.79.1" - resolved "https://registry.yarnpkg.com/@nivo/calendar/-/calendar-0.79.1.tgz#ee28feeb443d11a09d83576f5ac43dd393c031dc" - integrity sha512-xX83jlyKAPk+ieS03vaNZqhW/atL56lghsT8fPSQzsYJTCNWFjjcLvR2UkG8LgVNUrZEvc9nSbrg5VvctaNCHQ== +"@nivo/calendar@^0.80.0": + version "0.80.0" + resolved "https://registry.yarnpkg.com/@nivo/calendar/-/calendar-0.80.0.tgz#3339ae45297813f4e8bcdc6ad54151a70e824f56" + integrity sha512-UfBnfzdQs9tZz/APH8DqYnGBraqovyUr6S5zxKI0T+LX5RV9p7uEcz1mBIlbvwFudMWuuXBB6Jc7pqmwnn2ZpQ== dependencies: - "@nivo/legends" "0.79.1" - "@nivo/tooltip" "0.79.0" + "@nivo/legends" "0.80.0" + "@nivo/tooltip" "0.80.0" d3-scale "^3.2.3" d3-time "^1.0.10" d3-time-format "^3.0.0" lodash "^4.17.21" -"@nivo/colors@0.79.1": - version "0.79.1" - resolved "https://registry.yarnpkg.com/@nivo/colors/-/colors-0.79.1.tgz#0504c08b6a598bc5cb5a8b823d332a73fdc6ef43" - integrity sha512-45huBmz46OoQtfqzHrnqDJ9msebOBX84fTijyOBi8mn8iTDOK2xWgzT7cCYP3hKE58IclkibkzVyWCeJ+rUlqg== +"@nivo/colors@0.80.0": + version "0.80.0" + resolved "https://registry.yarnpkg.com/@nivo/colors/-/colors-0.80.0.tgz#5b70b4979df246d9d0d69fb638bba9764dd88b52" + integrity sha512-T695Zr411FU4RPo7WDINOAn8f79DPP10SFJmDdEqELE+cbzYVTpXqLGZ7JMv88ko7EOf9qxLQgcBqY69rp9tHQ== dependencies: d3-color "^2.0.0" d3-scale "^3.2.3" d3-scale-chromatic "^2.0.0" lodash "^4.17.21" -"@nivo/core@0.78.0": - version "0.78.0" - resolved "https://registry.yarnpkg.com/@nivo/core/-/core-0.78.0.tgz#9c8497fded24d075facc203d2a75e1571dcc5ef3" - integrity sha512-W7likA+5mB4Xw8eIwzLoocel1yaesRqkljmCNmp7jg0yxuRn3k05HnPF5XFWFh6gskRcOnsGMMjkAEirqzP/9Q== +"@nivo/core@^0.80.0": + version "0.80.0" + resolved "https://registry.yarnpkg.com/@nivo/core/-/core-0.80.0.tgz#d180cb2622158eb7bc5f984131ff07984f12297e" + integrity sha512-6caih0RavXdWWSfde+rC2pk17WrX9YQlqK26BrxIdXzv3Ydzlh5SkrC7dR2TEvMGBhunzVeLOfiC2DWT1S8CFg== dependencies: - "@nivo/recompose" "0.78.0" - "@react-spring/web" "9.3.1" + "@nivo/recompose" "0.80.0" + "@react-spring/web" "9.4.5" d3-color "^2.0.0" d3-format "^1.4.4" d3-interpolate "^2.0.1" @@ -1845,60 +1845,60 @@ d3-shape "^1.3.5" d3-time-format "^3.0.0" lodash "^4.17.21" - resize-observer-polyfill "^1.5.1" -"@nivo/funnel@^0.79.1": - version "0.79.1" - resolved "https://registry.yarnpkg.com/@nivo/funnel/-/funnel-0.79.1.tgz#b875945e2696f34d8b4969fcfc9efff495557944" - integrity sha512-nn0YqDT/U2ORDAbcQoyBH+I5MvkIouSk5imWLB6SsEbvZnzSokvxccDpJJsok1E4JqbjG/dNKeZckE14YNTHwQ== +"@nivo/funnel@^0.80.0": + version "0.80.0" + resolved "https://registry.yarnpkg.com/@nivo/funnel/-/funnel-0.80.0.tgz#d13d6a8ff4b6329c17698ba7a46d8e1fb56fd5ba" + integrity sha512-KkyTGMK2dqMrp51vNIhWuZMzu0t7fBbYlPLBFEfdxC6oAV52uZqEja+EIomkEmHP+7FZXeL1AuRsay4yj/zofw== dependencies: - "@nivo/annotations" "0.79.1" - "@nivo/colors" "0.79.1" - "@nivo/tooltip" "0.79.0" - "@react-spring/web" "9.3.1" + "@nivo/annotations" "0.80.0" + "@nivo/colors" "0.80.0" + "@nivo/tooltip" "0.80.0" + "@react-spring/web" "9.4.5" d3-scale "^3.2.3" d3-shape "^1.3.5" -"@nivo/heatmap@^0.79.1": - version "0.79.1" - resolved "https://registry.yarnpkg.com/@nivo/heatmap/-/heatmap-0.79.1.tgz#e420e1d4a6a6d88126be9b06bd4b5c5f461a7ebc" - integrity sha512-lMYtmGrMCjnV0SJ/FwRFY94Wjw93U7x8Ex2J9hIRoabtEbGcMZBaQMVQwQE2gUDjh4aDj+/ihLXcUxLc+7fz3g== - dependencies: - "@nivo/annotations" "0.79.1" - "@nivo/axes" "0.79.0" - "@nivo/colors" "0.79.1" - "@nivo/tooltip" "0.79.0" - "@react-spring/web" "9.3.1" +"@nivo/heatmap@^0.80.0": + version "0.80.0" + resolved "https://registry.yarnpkg.com/@nivo/heatmap/-/heatmap-0.80.0.tgz#07db42570c8cd5882c907f452ebea6b221256670" + integrity sha512-FZufo3Y79NaO2mTlboGj3kBpNYGEJhgI0WpONkkUwnjqF27FkLZ6geuqBnsRSpK+3vgUQeakd8ZvSBtBHhHUvQ== + dependencies: + "@nivo/annotations" "0.80.0" + "@nivo/axes" "0.80.0" + "@nivo/colors" "0.80.0" + "@nivo/legends" "0.80.0" + "@nivo/tooltip" "0.80.0" + "@react-spring/web" "9.4.5" d3-scale "^3.2.3" -"@nivo/legends@0.79.1": - version "0.79.1" - resolved "https://registry.yarnpkg.com/@nivo/legends/-/legends-0.79.1.tgz#60b1806bba547f796e6e5b66943d65153de60c79" - integrity sha512-AoabiLherOAk3/HR/N791fONxNdwNk/gCTJC/6BKUo2nX+JngEYm3nVFmTC1R6RdjwJTeCb9Vtuc4MHA+mcgig== +"@nivo/legends@0.80.0": + version "0.80.0" + resolved "https://registry.yarnpkg.com/@nivo/legends/-/legends-0.80.0.tgz#49edc54000075b4df055f86794a8c32810269d06" + integrity sha512-h0IUIPGygpbKIZZZWIxkkxOw4SO0rqPrqDrykjaoQz4CvL4HtLIUS3YRA4akKOVNZfS5agmImjzvIe0s3RvqlQ== -"@nivo/recompose@0.78.0": - version "0.78.0" - resolved "https://registry.yarnpkg.com/@nivo/recompose/-/recompose-0.78.0.tgz#79ba274077725d32e0107ca6989f8555a2c69442" - integrity sha512-iYrScMqmw2RUy6erhS/9w3wan5oWkdEYCLbB7NF9RwHXuS9TxqdWGw5BTCgibGnjZGHHvtZGr+J1TTX5M6WmEA== +"@nivo/recompose@0.80.0": + version "0.80.0" + resolved "https://registry.yarnpkg.com/@nivo/recompose/-/recompose-0.80.0.tgz#572048aed793321a0bada1fd176b72df5a25282e" + integrity sha512-iL3g7j3nJGD9+mRDbwNwt/IXDXH6E29mhShY1I7SP91xrfusZV9pSFf4EzyYgruNJk/2iqMuaqn+e+TVFra44A== dependencies: react-lifecycles-compat "^3.0.4" -"@nivo/scales@0.79.0": - version "0.79.0" - resolved "https://registry.yarnpkg.com/@nivo/scales/-/scales-0.79.0.tgz#553b6910288080fbfbbe4d2aab1dd80e2d172e6e" - integrity sha512-5fAt5Wejp8yzAk6qmA3KU+celCxNYrrBhfvOi2ECDG8KQi+orbDnrO6qjVF6+ebfOn9az8ZVukcSeGA5HceiMg== +"@nivo/scales@0.80.0": + version "0.80.0" + resolved "https://registry.yarnpkg.com/@nivo/scales/-/scales-0.80.0.tgz#39313fb97c8ae9633c2aa1e17adb57cb851e8a50" + integrity sha512-4y2pQdCg+f3n4TKXC2tYuq71veZM+xPRQbOTgGYJpuBvMc7pQsXF9T5z7ryeIG9hkpXkrlyjecU6XcAG7tLSNg== dependencies: d3-scale "^3.2.3" d3-time "^1.0.11" d3-time-format "^3.0.0" lodash "^4.17.21" -"@nivo/tooltip@0.79.0": - version "0.79.0" - resolved "https://registry.yarnpkg.com/@nivo/tooltip/-/tooltip-0.79.0.tgz#3d46be8734e5d30e5387515db0c83bd1c795f442" - integrity sha512-hsJsvhDVR9P/QqIEDIttaA6aslR3tU9So1s/k2jMdppL7J9ZH/IrVx9TbIP7jDKmnU5AMIP5uSstXj9JiKLhQA== +"@nivo/tooltip@0.80.0": + version "0.80.0" + resolved "https://registry.yarnpkg.com/@nivo/tooltip/-/tooltip-0.80.0.tgz#07ebef47eb708a0612bd6297d5ad156bbec19d34" + integrity sha512-qGmrreRwnCsYjn/LAuwBtxBn/tvG8y+rwgd4gkANLBAoXd3bzJyvmkSe+QJPhUG64bq57ibDK+lO2pC48a3/fw== dependencies: - "@react-spring/web" "9.3.1" + "@react-spring/web" "9.4.5" "@nodelib/fs.scandir@2.1.5": version "2.1.5" @@ -1926,50 +1926,51 @@ resolved "https://registry.yarnpkg.com/@reach/observe-rect/-/observe-rect-1.2.0.tgz#d7a6013b8aafcc64c778a0ccb83355a11204d3b2" integrity sha512-Ba7HmkFgfQxZqqaeIWWkNK0rEhpxVQHIoVyW1YDSkGsGIXzcaW4deC8B0pZrNSSyLTdIk7y+5olKt5+g0GmFIQ== -"@react-spring/animated@~9.3.0": - version "9.3.2" - resolved "https://registry.yarnpkg.com/@react-spring/animated/-/animated-9.3.2.tgz#bda85e92e9e9b6861c259f2dacb54270a37b0f39" - integrity sha512-pBvKydRHbTzuyaeHtxGIOvnskZxGo/S5/YK1rtYm88b9NQZuZa95Rgd3O0muFL+99nvBMBL8cvQGD0UJmsqQsg== +"@react-spring/animated@~9.4.5": + version "9.4.5" + resolved "https://registry.yarnpkg.com/@react-spring/animated/-/animated-9.4.5.tgz#dd9921c716a4f4a3ed29491e0c0c9f8ca0eb1a54" + integrity sha512-KWqrtvJSMx6Fj9nMJkhTwM9r6LIriExDRV6YHZV9HKQsaolUFppgkOXpC+rsL1JEtEvKv6EkLLmSqHTnuYjiIA== dependencies: - "@react-spring/shared" "~9.3.0" - "@react-spring/types" "~9.3.0" + "@react-spring/shared" "~9.4.5" + "@react-spring/types" "~9.4.5" -"@react-spring/core@~9.3.0": - version "9.3.2" - resolved "https://registry.yarnpkg.com/@react-spring/core/-/core-9.3.2.tgz#d1dc5810666ac18550db89c58567f28fbe04fb07" - integrity sha512-kMRjkgdQ6LJ0lmb/wQlONpghaMT83UxglXHJC6m9kZS/GKVmN//TYMEK85xN1rC5Gg+BmjG61DtLCSkkLDTfNw== +"@react-spring/core@~9.4.5": + version "9.4.5" + resolved "https://registry.yarnpkg.com/@react-spring/core/-/core-9.4.5.tgz#4616e1adc18dd10f5731f100ebdbe9518b89ba3c" + integrity sha512-83u3FzfQmGMJFwZLAJSwF24/ZJctwUkWtyPD7KYtNagrFeQKUH1I05ZuhmCmqW+2w1KDW1SFWQ43RawqfXKiiQ== dependencies: - "@react-spring/animated" "~9.3.0" - "@react-spring/shared" "~9.3.0" - "@react-spring/types" "~9.3.0" + "@react-spring/animated" "~9.4.5" + "@react-spring/rafz" "~9.4.5" + "@react-spring/shared" "~9.4.5" + "@react-spring/types" "~9.4.5" -"@react-spring/rafz@~9.3.0": - version "9.3.2" - resolved "https://registry.yarnpkg.com/@react-spring/rafz/-/rafz-9.3.2.tgz#0cbd296cd17bbf1e7e49d3b3616884e026d5fb67" - integrity sha512-YtqNnAYp5bl6NdnDOD5TcYS40VJmB+Civ4LPtcWuRPKDAOa/XAf3nep48r0wPTmkK936mpX8aIm7h+luW59u5A== +"@react-spring/rafz@~9.4.5": + version "9.4.5" + resolved "https://registry.yarnpkg.com/@react-spring/rafz/-/rafz-9.4.5.tgz#84f809f287f2a66bbfbc66195db340482f886bd7" + integrity sha512-swGsutMwvnoyTRxvqhfJBtGM8Ipx6ks0RkIpNX9F/U7XmyPvBMGd3GgX/mqxZUpdlsuI1zr/jiYw+GXZxAlLcQ== -"@react-spring/shared@~9.3.0": - version "9.3.2" - resolved "https://registry.yarnpkg.com/@react-spring/shared/-/shared-9.3.2.tgz#967ce1d8a16d820a99e6eeb2a8f7ca9311d9dfa0" - integrity sha512-ypGQQ8w7mWnrELLon4h6mBCBxdd8j1pgLzmHXLpTC/f4ya2wdP+0WIKBWXJymIf+5NiTsXgSJra5SnHP5FBY+A== +"@react-spring/shared@~9.4.5": + version "9.4.5" + resolved "https://registry.yarnpkg.com/@react-spring/shared/-/shared-9.4.5.tgz#4c3ad817bca547984fb1539204d752a412a6d829" + integrity sha512-JhMh3nFKsqyag0KM5IIM8BQANGscTdd0mMv3BXsUiMZrcjQTskyfnv5qxEeGWbJGGar52qr5kHuBHtCjQOzniA== dependencies: - "@react-spring/rafz" "~9.3.0" - "@react-spring/types" "~9.3.0" + "@react-spring/rafz" "~9.4.5" + "@react-spring/types" "~9.4.5" -"@react-spring/types@~9.3.0": - version "9.3.2" - resolved "https://registry.yarnpkg.com/@react-spring/types/-/types-9.3.2.tgz#0277d436e50d7a824897dd7bb880f4842fbcd0fe" - integrity sha512-u+IK9z9Re4hjNkBYKebZr7xVDYTai2RNBsI4UPL/k0B6lCNSwuqWIXfKZUDVlMOeZHtDqayJn4xz6HcSkTj3FQ== +"@react-spring/types@~9.4.5": + version "9.4.5" + resolved "https://registry.yarnpkg.com/@react-spring/types/-/types-9.4.5.tgz#9c71e5ff866b5484a7ef3db822bf6c10e77bdd8c" + integrity sha512-mpRIamoHwql0ogxEUh9yr4TP0xU5CWyZxVQeccGkHHF8kPMErtDXJlxyo0lj+telRF35XNihtPTWoflqtyARmg== -"@react-spring/web@9.3.1": - version "9.3.1" - resolved "https://registry.yarnpkg.com/@react-spring/web/-/web-9.3.1.tgz#5b377ba7ad52e746c2b59e2738c021de3f219d0b" - integrity sha512-sisZIgFGva/Z+xKWPSfXpukF0AP3kR9ALTxlHL87fVotMUCJX5vtH/YlVcywToEFwTHKt3MpI5Wy2M+vgVEeaw== +"@react-spring/web@9.4.5": + version "9.4.5" + resolved "https://registry.yarnpkg.com/@react-spring/web/-/web-9.4.5.tgz#b92f05b87cdc0963a59ee149e677dcaff09f680e" + integrity sha512-NGAkOtKmOzDEctL7MzRlQGv24sRce++0xAY7KlcxmeVkR7LRSGkoXHaIfm9ObzxPMcPHQYQhf3+X9jepIFNHQA== dependencies: - "@react-spring/animated" "~9.3.0" - "@react-spring/core" "~9.3.0" - "@react-spring/shared" "~9.3.0" - "@react-spring/types" "~9.3.0" + "@react-spring/animated" "~9.4.5" + "@react-spring/core" "~9.4.5" + "@react-spring/shared" "~9.4.5" + "@react-spring/types" "~9.4.5" "@rebass/forms@^4.0.6": version "4.0.6" @@ -3664,7 +3665,7 @@ d3-shape@^1.0.0, d3-shape@^1.2.0: dependencies: d3-path "1" -d3-shape@^1.2.2, d3-shape@^1.3.5: +d3-shape@^1.3.5: version "1.3.7" resolved "https://registry.yarnpkg.com/d3-shape/-/d3-shape-1.3.7.tgz#df63801be07bc986bc54f63789b4fe502992b5d7" integrity sha512-EUkvKjqPFUAZyOlhY5gzCxCeI0Aep04LwIRpsZ/mLFelJiUfnK56jo5JMDSE7yyP2kLSb6LtF+S5chMk7uqPqw== @@ -7008,11 +7009,6 @@ request-progress@^3.0.0: dependencies: throttleit "^1.0.0" -resize-observer-polyfill@^1.5.1: - version "1.5.1" - resolved "https://registry.yarnpkg.com/resize-observer-polyfill/-/resize-observer-polyfill-1.5.1.tgz#0e9020dd3d21024458d4ebd27e23e40269810464" - integrity sha512-LwZrotdHOo12nQuZlHEmtuXdqGoOD0OhaxopaNFxWzInpEgaLWoVuAMbTzixuosCx2nEG58ngzW3vxdWoxIgdg== - resolve-from@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/resolve-from/-/resolve-from-4.0.0.tgz#4abcd852ad32dd7baabfe9b40e00a36db5f392e6" From 2eb37112d826903a05869e4a38f948f348094fc2 Mon Sep 17 00:00:00 2001 From: Maja Komel Date: Thu, 22 Dec 2022 16:19:27 +0100 Subject: [PATCH 16/41] Move strings to localization file, localized date formatting, RTL support (#824) --- .env.test | 5 + components/DateRangePicker.js | 38 ++- components/Footer.js | 6 +- components/Header.js | 2 +- components/Layout.js | 9 +- components/NavBar.js | 89 +++-- components/SocialButtons.js | 17 +- components/TestNameOptions.js | 7 +- components/aggregation/mat/CalendarChart.js | 71 ---- components/aggregation/mat/ChartHeader.js | 12 +- .../aggregation/mat/CountryNameLabel.js | 8 +- components/aggregation/mat/CustomTooltip.js | 4 +- components/aggregation/mat/Filters.js | 19 +- components/aggregation/mat/Form.js | 17 +- components/aggregation/mat/GridChart.js | 6 +- components/aggregation/mat/Help.js | 9 +- components/aggregation/mat/NoCharts.js | 2 +- components/aggregation/mat/RowChart.js | 26 +- components/aggregation/mat/StackedBarChart.js | 7 +- components/aggregation/mat/TableView.js | 8 +- components/aggregation/mat/XAxis.js | 8 +- components/aggregation/mat/computations.js | 24 +- components/aggregation/mat/labels.js | 43 ++- components/country/Apps.js | 6 +- components/country/AppsStatsChart.js | 4 +- .../country/AppsStatsCircumventionRow.js | 8 +- components/country/AppsStatsRow.js | 4 +- components/country/CountryHead.js | 2 +- components/country/NetworkProperties.js | 8 +- components/country/NetworkStats.js | 4 +- components/country/Overview.js | 4 +- components/country/OverviewCharts.js | 4 +- components/country/PageNavMenu.js | 4 +- components/country/URLChart.js | 4 +- components/country/WebsiteChartLoader.js | 4 +- components/country/Websites.js | 6 +- components/country/WebsitesCharts.js | 8 +- components/dashboard/Charts.js | 15 +- components/dashboard/Form.js | 8 +- components/form/Select.js | 31 ++ components/landing/HighlightBox.js | 105 +++--- components/landing/HighlightsSection.js | 9 +- components/landing/Stats.js | 4 +- components/landing/highlights.json | 86 ++--- components/measurement/AccessPointStatus.js | 4 +- components/measurement/CommonDetails.js | 8 +- components/measurement/CommonSummary.js | 10 +- components/measurement/DetailsBox.js | 3 +- components/measurement/DetailsHeader.js | 4 +- components/measurement/HeadMetadata.js | 10 +- components/measurement/Hero.js | 6 +- .../measurement/MeasurementContainer.js | 4 +- components/measurement/MeasurementNotFound.js | 9 +- components/measurement/SummaryText.js | 4 +- .../measurement/nettests/FacebookMessenger.js | 12 +- components/measurement/nettests/Ndt.js | 4 +- components/measurement/nettests/Psiphon.js | 8 +- components/measurement/nettests/Telegram.js | 4 +- components/measurement/nettests/Tor.js | 18 +- .../measurement/nettests/TorSnowflake.js | 8 +- components/measurement/nettests/VanillaTor.js | 4 +- .../measurement/nettests/WebConnectivity.js | 26 +- components/measurement/nettests/WhatsApp.js | 12 +- components/network/Chart.js | 12 +- components/network/Form.js | 4 +- components/search/FilterSidebar.js | 6 +- components/search/Loader.js | 15 +- components/search/ResultsList.js | 165 ++++++++- components/utils/categoryCodes.js | 2 +- components/withIntl.js | 69 ++-- cypress/e2e/measurement.e2e.cy.js | 42 +-- cypress/e2e/search.e2e.cy.js | 4 +- next.config.js | 34 +- package.json | 2 +- pages/404.js | 10 +- pages/_app.js | 15 +- pages/_document.js | 2 +- pages/chart/circumvention.js | 9 +- pages/chart/mat.js | 98 +++--- pages/countries.js | 180 +++++----- pages/country/[countryCode].js | 14 +- pages/index.js | 319 +++++++++--------- pages/measurement/[[...report_id]].js | 25 +- pages/network/[asn].js | 10 +- pages/search.js | 13 +- public/static/lang/en.json | 210 ++++++------ public/static/lang/translations.js | 2 +- public/static/locale-data.js | 1 - scripts/build-translations.js | 4 +- services/dayjs.js | 8 + utils/i18nCountries.js | 34 ++ yarn.lock | 10 +- 92 files changed, 1252 insertions(+), 990 deletions(-) delete mode 100644 components/aggregation/mat/CalendarChart.js create mode 100644 components/form/Select.js delete mode 100644 public/static/locale-data.js create mode 100644 utils/i18nCountries.js diff --git a/.env.test b/.env.test index c0ad9ff34..8a566942a 100644 --- a/.env.test +++ b/.env.test @@ -4,3 +4,8 @@ NEXT_PUBLIC_OONI_API=https://api.ooni.io NEXT_PUBLIC_EXPLORER_URL=https://explorer-test.ooni.io + +RUN_GIT_COMMIT_SHA_SHORT=yarn --silent git:getCommitSHA:short +RUN_GIT_COMMIT_SHA=yarn --silent git:getCommitSHA +RUN_GIT_COMMIT_REF=yarn --silent git:getCommitRef +RUN_GIT_COMMIT_TAGS=yarn --silent git:getReleasesAndTags \ No newline at end of file diff --git a/components/DateRangePicker.js b/components/DateRangePicker.js index 0c9816ac7..bd0af286a 100644 --- a/components/DateRangePicker.js +++ b/components/DateRangePicker.js @@ -6,6 +6,17 @@ import OutsideClickHandler from 'react-outside-click-handler' import { useIntl } from 'react-intl' import styled from 'styled-components' import { Button } from 'ooni-components' +import { getDirection } from 'components/withIntl' + +import de from 'date-fns/locale/de' +import en from 'date-fns/locale/en-US' +import es from 'date-fns/locale/es' +import fa from 'date-fns/locale/fa-IR' +import fr from 'date-fns/locale/fr' +import is from 'date-fns/locale/is' +import ru from 'date-fns/locale/ru' +import tr from 'date-fns/locale/tr' +import zh from 'date-fns/locale/zh-CN' const StyledDatetime = styled.div` z-index: 99999; @@ -42,10 +53,33 @@ justify-content: right; gap: 6px; ` +const getDateFnsLocale = locale => { + switch (locale) { + case 'de': + return de + case 'es': + return es + case 'fa': + return fa + case 'fr': + return fr + case 'is': + return is + case 'ru': + return ru + case 'tr': + return tr + case 'zh': + return zh + default: + return en + } +} + const DateRangePicker = ({handleRangeSelect, initialRange, close, ...props}) => { const intl = useIntl() - const ranges = ['Today', 'LastWeek', 'LastMonth', 'LastYear'] + const selectRange = (range) => { switch (range) { case 'Today': @@ -99,6 +133,8 @@ const DateRangePicker = ({handleRangeSelect, initialRange, close, ...props}) => {rangesList} { const intl = useIntl() - const currentYear = dayjs().get('year') + const currentYear = new Intl.DateTimeFormat(intl.locale, { year: 'numeric' }).format(new Date()) return ( @@ -65,7 +65,7 @@ const Footer = () => { - {intl.formatMessage({ id: 'Footer.Text.Slogan' })} + {intl.formatMessage({ id: 'Footer.Text.Slogan' })} @@ -73,7 +73,7 @@ const Footer = () => { - + diff --git a/components/Header.js b/components/Header.js index f9e247c71..b46782eba 100644 --- a/components/Header.js +++ b/components/Header.js @@ -4,7 +4,7 @@ import { useRouter } from 'next/router' import { useIntl } from 'react-intl' const Header = () => { - const canonical = 'https://explorer.ooni.org' + useRouter().pathname + const canonical = 'https://explorer.ooni.org' + useRouter().asPath.split('?')[0] const intl = useIntl() const description = intl.formatMessage({ id: 'Home.Meta.Description' }) diff --git a/components/Layout.js b/components/Layout.js index 8c6436c8b..f0c07fa1a 100644 --- a/components/Layout.js +++ b/components/Layout.js @@ -6,7 +6,8 @@ import { theme } from 'ooni-components' import Header from './Header' import Footer from './Footer' -import withIntl from './withIntl' +import { useIntl } from 'react-intl' +import { getDirection } from 'components/withIntl' // import FeedbackButton from '../components/FeedbackFloat' theme.maxWidth = 1024 @@ -17,6 +18,7 @@ const GlobalStyle = createGlobalStyle` box-sizing: border-box; } body, html { + direction: ${props => props.direction}; margin: 0; padding: 0; font-family: "Fira Sans"; @@ -51,6 +53,7 @@ const matomoInstance = createInstance({ }) const Layout = ({ children, disableFooter = false }) => { + const { locale } = useIntl() useEffect(() => { matomoInstance.trackPageView() }, []) @@ -58,7 +61,7 @@ const Layout = ({ children, disableFooter = false }) => { return ( - +
@@ -77,4 +80,4 @@ Layout.propTypes = { disableFooter: PropTypes.bool } -export default withIntl(Layout) +export default Layout diff --git a/components/NavBar.js b/components/NavBar.js index 3f1e229c1..6198d7a0d 100644 --- a/components/NavBar.js +++ b/components/NavBar.js @@ -1,9 +1,10 @@ import React from 'react' - -import { withRouter } from 'next/router' +import { useRouter, withRouter } from 'next/router' import NLink from 'next/link' import styled from 'styled-components' -import { FormattedMessage } from 'react-intl' +import { FormattedMessage, useIntl } from 'react-intl' + +import { getLocalisedLanguageName } from 'utils/i18nCountries' import ExplorerLogo from 'ooni-components/components/svgs/logos/Explorer-HorizontalMonochromeInverted.svg' @@ -11,7 +12,8 @@ import { Link, Flex, Box, - Container + Container, + Select, } from 'ooni-components' const StyledNavItem = styled.a` @@ -45,6 +47,15 @@ const Underline = styled.span` } ` +const LanguageSelect = styled.select` + color: ${props => props.theme.colors.white}; + background: none; + opacity: 0.6; + border: none; + text-transform: capitalize; + cursor: pointer; +` + const NavItemComponent = ({router, label, href}) => { const active = router.pathname === href return ( @@ -66,31 +77,51 @@ const StyledNavBar = styled.div` padding-bottom: 20px; z-index: 999; ` +const languages = process.env.LOCALES -export const NavBar = ({color}) => ( - - - - - - - - - - - } href='/search' /> - } href='/chart/mat' /> - } href='/chart/circumvention' /> - } href='/countries' /> - - - - - -) +export const NavBar = ({color}) => { + const { locale } = useIntl() + const router = useRouter() + const { pathname, asPath, query } = router + + const handleLocaleChange = (event) => { + router.push({ pathname, query }, asPath, { locale: event.target.value }) + } + + return ( + + + + + + + + + + + } href='/search' /> + } href='/chart/mat' /> + } href='/chart/circumvention' /> + } href='/countries' /> + {/* + + {languages.map((c) => ( + + ))} + + */} + + + + + + ) +} export default NavBar diff --git a/components/SocialButtons.js b/components/SocialButtons.js index 50f525dd7..359434c71 100644 --- a/components/SocialButtons.js +++ b/components/SocialButtons.js @@ -2,17 +2,22 @@ import React from 'react' import PropTypes from 'prop-types' import { Link, Flex, Text } from 'ooni-components' import { MdShare } from 'react-icons/md' +import { useIntl } from 'react-intl' + +const SocialButtons = ({ url }) => { + const intl = useIntl() -function SocialButtons({ url }){ - const text = 'Data from OONI Explorer' return( - Share on - Facebook - or - Twitter + {intl.formatMessage( + {id: 'SocialButtons.CTA'}, + { + 'facebook-link': (string) => ({string}), + 'twitter-link': (string) => ({string}) + } + )} ) diff --git a/components/TestNameOptions.js b/components/TestNameOptions.js index 116c7ef0d..24fb6f105 100644 --- a/components/TestNameOptions.js +++ b/components/TestNameOptions.js @@ -9,6 +9,7 @@ export const TestNameOptions = ({ testNames, includeAllOption = true}) => { const option = { id: test.id, name: test.name, + intlKey: testNamesIntl[test.id]?.id, group } if (group in grouped) { @@ -32,9 +33,9 @@ export const TestNameOptions = ({ testNames, includeAllOption = true}) => { includeAllOption && , [...sortedGroupedTestNameOptions].map(([group, tests]) => { const groupName = group in testGroups ? intl.formatMessage({id: testGroups[group].id}) : group - const testOptions = tests.map(({id, name}) => ( - - )) + const testOptions = tests.map(({id, name, intlKey}) => { + return + }) return [, ...testOptions] }) ]) diff --git a/components/aggregation/mat/CalendarChart.js b/components/aggregation/mat/CalendarChart.js deleted file mode 100644 index f2533773b..000000000 --- a/components/aggregation/mat/CalendarChart.js +++ /dev/null @@ -1,71 +0,0 @@ -/* global process */ -import React, { useEffect, useState } from 'react' -import { ResponsiveCalendar } from '@nivo/calendar' -import { Select } from 'ooni-components' -import useSWR from 'swr' - - -const AGGREGATION_API = `${process.env.NEXT_PUBLIC_OONI_API}/api/v1/aggregation?` - -// TODO adapt to axios -const fetcher = url => fetch(AGGREGATION_API + url).then(r => r.json()) - -const fromDate = '2019-06-01' -const toDate = '2020-05-31' -const URL = `probe_cc=BR&since=${fromDate}&until=${toDate}&axis_x=measurement_start_day` - -export const Calendar = () => { - const { data } = useSWR(URL, fetcher) - const [dataX, setDataX ] = useState([]) - - useEffect(() => { - if (data) { - setDataX(data.result.map(item => ({ - day: item.measurement_start_day, - value: item.measurement_count - }))) - } - }, [data]) - - if (!data) { - return ( -
Loading...
- ) - } - - return ( -
- - {dataX && - - } -
- ) -} diff --git a/components/aggregation/mat/ChartHeader.js b/components/aggregation/mat/ChartHeader.js index 95d4ffe03..138bbfa76 100644 --- a/components/aggregation/mat/ChartHeader.js +++ b/components/aggregation/mat/ChartHeader.js @@ -19,7 +19,7 @@ const ChartHeaderContainer = styled(Flex)` const Legend = ({label, color}) => { return ( - +
@@ -34,7 +34,7 @@ export const SubtitleStr = ({ query }) => { const params = new Set() if (query.test_name) { - const testName = intl.formatMessage({id: getRowLabel(query.test_name, 'test_name')}) + const testName = intl.formatMessage({id: getRowLabel(query.test_name, 'test_name'), defaultMessage: ''}) params.add(testName) } if (query.domain) { @@ -76,10 +76,10 @@ export const ChartHeader = ({ options = {}}) => { {subTitle} } {options.legend !== false && - - - - + + + + } diff --git a/components/aggregation/mat/CountryNameLabel.js b/components/aggregation/mat/CountryNameLabel.js index 947e15313..9f0f7a30c 100644 --- a/components/aggregation/mat/CountryNameLabel.js +++ b/components/aggregation/mat/CountryNameLabel.js @@ -1,9 +1,11 @@ import { Box } from 'ooni-components' -import { countryList } from 'country-util' +import { localisedCountries } from 'utils/i18nCountries' +import { useIntl } from 'react-intl' const CountryNameLabel = ({ countryCode, ...props }) => { - const country = countryList.find(o => o.iso3166_alpha2 === countryCode) - const name = country ? country.name : countryCode + const intl = useIntl() + const country = localisedCountries(intl.locale).find(o => o.iso3166_alpha2 === countryCode) + const name = country ? country.localisedCountryName : countryCode return ( {name} ) diff --git a/components/aggregation/mat/CustomTooltip.js b/components/aggregation/mat/CustomTooltip.js index b282f4a74..2c27050be 100644 --- a/components/aggregation/mat/CustomTooltip.js +++ b/components/aggregation/mat/CustomTooltip.js @@ -101,7 +101,7 @@ const CustomToolTip = React.memo(({ data, onClose, title, link = true }) => { - {k} + {intl.formatMessage({id: `MAT.Table.Header.${k}`})} {intl.formatNumber(Number(data[k] ?? 0))} @@ -109,7 +109,7 @@ const CustomToolTip = React.memo(({ data, onClose, title, link = true }) => { {link && - view measurements > + {intl.formatMessage({id: 'MAT.CustomTooltip.ViewMeasurements'})} > } diff --git a/components/aggregation/mat/Filters.js b/components/aggregation/mat/Filters.js index 388852ec5..d6218f7a6 100644 --- a/components/aggregation/mat/Filters.js +++ b/components/aggregation/mat/Filters.js @@ -85,7 +85,7 @@ const SearchFilter = ({ onChange={e => { setFilter(e.target.value || undefined) // Set undefined to remove the filter entirely }} - placeholder={`Search ${count} records...`} + placeholder={intl.formatMessage({id: 'MAT.Table.FilterPlaceholder'}, {count})} /> ) } @@ -104,6 +104,7 @@ function GlobalFilter({ globalFilter, setGlobalFilter, }) { + const intl = useIntl() const count = preGlobalFilteredRows.length const [value, setValue] = React.useState(globalFilter) const onChange = useAsyncDebounce(value => { @@ -118,14 +119,14 @@ function GlobalFilter({ return ( - Search:{' '} + {intl.formatMessage({id: 'MAT.Table.Search'})}{' '} { setValue(e.target.value) onChange(e.target.value) }} - placeholder={`Search ${count} records...`} + placeholder={intl.formatMessage({id: 'MAT.Table.FilterPlaceholder'}, {count})} /> ) @@ -170,7 +171,7 @@ const Filters = ({ data = [], tableData, setDataForCharts, query }) => { sortBy: [{ id: 'yAxisLabel', desc: false }] }),[]) - const getRowId = React.useCallback(row => row[query.axis_y], []) + const getRowId = React.useCallback(row => row[query.axis_y], [query.axis_y]) const columns = useMemo(() => [ { @@ -287,14 +288,14 @@ const Filters = ({ data = [], tableData, setDataForCharts, query }) => { ) const updateCharts = useCallback(() => { - const selectedRows = Object.keys(state.selectedRowIds).sort((a,b) => sortRows(a, b, query.axis_y)) + const selectedRows = Object.keys(state.selectedRowIds).sort((a,b) => sortRows(a, b, query.axis_y, intl.locale)) if (selectedRows.length > 0 && selectedRows.length !== preGlobalFilteredRows.length) { setDataForCharts(selectedRows) } else { setDataForCharts(noRowsSelected) } - }, [preGlobalFilteredRows.length, query.axis_y, state.selectedRowIds, setDataForCharts]) + }, [preGlobalFilteredRows.length, query.axis_y, state.selectedRowIds, setDataForCharts, intl.locale]) /** * Reset the table filter @@ -331,11 +332,11 @@ const Filters = ({ data = [], tableData, setDataForCharts, query }) => { }) return ( - + - - + + diff --git a/components/aggregation/mat/Form.js b/components/aggregation/mat/Form.js index f515c2e2c..d9863b560 100644 --- a/components/aggregation/mat/Form.js +++ b/components/aggregation/mat/Form.js @@ -4,13 +4,14 @@ import { useForm, Controller } from 'react-hook-form' import styled from 'styled-components' import { Flex, Box, - Label, Input, Select, Button + Label, Input, Button } from 'ooni-components' -import { countryList } from 'country-util' import dayjs from 'services/dayjs' import { format } from 'date-fns' import { defineMessages, useIntl, FormattedMessage } from 'react-intl' +import { localisedCountries } from 'utils/i18nCountries' +import Select from 'components/form/Select' import { categoryCodes } from '../../utils/categoryCodes' import DateRangePicker from '../../DateRangePicker' import { ConfirmationModal } from './ConfirmationModal' @@ -119,8 +120,8 @@ export const Form = ({ onSubmit, testNames, query }) => { } }, [reset, query]) - const sortedCountries = countryList - .sort((a,b) => (a.iso3166_name < b.iso3166_name) ? -1 : (a.iso3166_name > b.iso3166_name) ? 1 : 0) + const sortedCountries = localisedCountries(intl.locale) + .sort((a,b) => new Intl.Collator(intl.locale).compare(a.localisedCountryName, b.localisedCountryName)) const testNameValue = watch('test_name') const showWebConnectivityFilters = isValidFilterForTestname(testNameValue, testsWithValidDomainFilter) @@ -180,9 +181,9 @@ export const Form = ({ onSubmit, testNames, query }) => { ( )} @@ -338,11 +339,11 @@ export const Form = ({ onSubmit, testNames, query }) => { control={control} render={({field}) => ( )} diff --git a/components/aggregation/mat/GridChart.js b/components/aggregation/mat/GridChart.js index 209bbf0f2..23b5c6f1e 100644 --- a/components/aggregation/mat/GridChart.js +++ b/components/aggregation/mat/GridChart.js @@ -40,7 +40,7 @@ const GRID_MAX_HEIGHT = 600 * */ -export const prepareDataForGridChart = (data, query) => { +export const prepareDataForGridChart = (data, query, locale) => { const rows = [] const rowLabels = {} let reshapedData = {} @@ -54,13 +54,13 @@ export const prepareDataForGridChart = (data, query) => { } else { rows.push(key) reshapedData[key] = [item] - rowLabels[key] = getRowLabel(key, query.axis_y) + rowLabels[key] = getRowLabel(key, query.axis_y, locale) } }) const reshapedDataWithoutHoles = fillDataHoles(reshapedData, query) - rows.sort((a,b) => sortRows(a, b, query.axis_y)) + rows.sort((a,b) => sortRows(a, b, query.axis_y, locale)) return [reshapedDataWithoutHoles, rows, rowLabels] } diff --git a/components/aggregation/mat/Help.js b/components/aggregation/mat/Help.js index 4d3a9c4a6..3067d1d81 100644 --- a/components/aggregation/mat/Help.js +++ b/components/aggregation/mat/Help.js @@ -3,7 +3,7 @@ import { Flex, Box, Text, Heading } from 'ooni-components' import { MdHelp } from 'react-icons/md' import styled from 'styled-components' -import { categoryCodes } from 'components/utils/categoryCodes' +import { getCategoryCodesMap } from 'components/utils/categoryCodes' import FormattedMarkdown from 'components/FormattedMarkdown' import { FormattedMessage } from 'react-intl' @@ -28,13 +28,12 @@ const boxTitle = ( const Help = () => { return ( - {/* */} - {categoryCodes.map(([code, name, description], i) => ( + {[...getCategoryCodesMap().values()].map(({ code, name, description }, i) => ( - {name} - {description} + + ))} diff --git a/components/aggregation/mat/NoCharts.js b/components/aggregation/mat/NoCharts.js index 156e55194..1f4f2523d 100644 --- a/components/aggregation/mat/NoCharts.js +++ b/components/aggregation/mat/NoCharts.js @@ -12,7 +12,7 @@ export const NoCharts = ({ message }) => { {message && - Details: + {message} diff --git a/components/aggregation/mat/RowChart.js b/components/aggregation/mat/RowChart.js index 4b3dca59e..4a808fff8 100644 --- a/components/aggregation/mat/RowChart.js +++ b/components/aggregation/mat/RowChart.js @@ -10,21 +10,7 @@ import { colorMap } from './colorMap' import { useMATContext } from './MATContext' import { getXAxisTicks } from './timeScaleXAxis' import { defineMessages, useIntl } from 'react-intl' - -const messages = defineMessages({ - 'x_axis.measurement_start_day': { - id: 'MAT.Form.Label.AxisOption.measurement_start_day', - defaultMessage: '' - }, - 'x_axis.category_code': { - id: 'MAT.Form.Label.AxisOption.category_code', - defaultMessage: '' - }, - 'x_axis.probe_cc': { - id: 'MAT.Form.Label.AxisOption.probe_cc', - defaultMessage: '' - } -}) +import styled from 'styled-components' const keys = [ 'anomaly_count', @@ -33,6 +19,10 @@ const keys = [ 'ok_count', ] +const StyledFlex = styled(Flex)` + direction: ltr; +` + const colorFunc = (d) => colorMap[d.id] || '#ccc' const barLayers = ['grid', 'axes', 'bars'] @@ -153,12 +143,12 @@ const RowChart = ({ data, indexBy, label, height, rowIndex /* width, first, last const chartProps = useMemo(() => { const xAxisTicks = getXAxisTicks(query) chartProps1D.axisBottom.tickValues = xAxisTicks - chartProps1D.axisBottom.legend = query.axis_x ? intl.formatMessage(messages[`x_axis.${query.axis_x}`]) : '' + chartProps1D.axisBottom.legend = query.axis_x ? intl.formatMessage({id: `MAT.Form.Label.AxisOption.${query.axis_x}`, defaultMessage: ''}) : '' return label === undefined ? chartProps1D : chartProps2D }, [intl, label, query]) return ( - + {label && {label} } @@ -178,7 +168,7 @@ const RowChart = ({ data, indexBy, label, height, rowIndex /* width, first, last {...chartProps} /> - + ) } diff --git a/components/aggregation/mat/StackedBarChart.js b/components/aggregation/mat/StackedBarChart.js index 9dd8a8835..e046f8346 100644 --- a/components/aggregation/mat/StackedBarChart.js +++ b/components/aggregation/mat/StackedBarChart.js @@ -2,6 +2,7 @@ import React from 'react' import PropTypes from 'prop-types' import { Flex } from 'ooni-components' import styled from 'styled-components' +import { useIntl } from 'react-intl' import GridChart, { prepareDataForGridChart } from './GridChart' import { NoCharts } from './NoCharts' @@ -13,10 +14,10 @@ const ChartContainer = styled(Flex)` ` export const StackedBarChart = ({ data, query }) => { + const intl = useIntl() - try { - - const [gridData, rows ] = prepareDataForGridChart(data.data.result, query) + try { + const [gridData, rows ] = prepareDataForGridChart(data.data.result, query, intl.locale) return ( diff --git a/components/aggregation/mat/TableView.js b/components/aggregation/mat/TableView.js index 66632a9a3..e3e2a5c62 100644 --- a/components/aggregation/mat/TableView.js +++ b/components/aggregation/mat/TableView.js @@ -10,10 +10,10 @@ import { DetailsBox } from '../../measurement/DetailsBox' import { sortRows } from './computations' import Filters from './Filters' -const prepareDataforTable = (data, query) => { +const prepareDataforTable = (data, query, locale) => { const table = [] - const [reshapedData, rows, rowLabels] = prepareDataForGridChart(data, query) + const [reshapedData, rows, rowLabels] = prepareDataForGridChart(data, query, locale) for (const [key, rowData] of reshapedData) { @@ -56,11 +56,11 @@ const TableView = ({ data, query }) => { // - indexes - const [reshapedData, tableData, rowKeys, rowLabels] = useMemo(() => { try { - return prepareDataforTable(data, query) + return prepareDataforTable(data, query, intl.locale) } catch (e) { return [null, [], [], {}] } - }, [query, data]) + }, [query, data, intl.locale]) const [dataForCharts, setDataForCharts] = useState(noRowsSelected) diff --git a/components/aggregation/mat/XAxis.js b/components/aggregation/mat/XAxis.js index 38516a208..36b6ac095 100644 --- a/components/aggregation/mat/XAxis.js +++ b/components/aggregation/mat/XAxis.js @@ -1,9 +1,13 @@ import { ResponsiveBar } from '@nivo/bar' import { Box, Flex } from 'ooni-components' +import styled from 'styled-components' import { useMATContext } from './MATContext' import { getXAxisTicks } from './timeScaleXAxis' +const StyledFlex = styled(Flex)` + direction: ltr; +` export const XAxis = ({ data }) => { const [ query ] = useMATContext() @@ -18,7 +22,7 @@ export const XAxis = ({ data }) => { } return ( - + @@ -39,6 +43,6 @@ export const XAxis = ({ data }) => { animate={false} /> - + ) } \ No newline at end of file diff --git a/components/aggregation/mat/computations.js b/components/aggregation/mat/computations.js index cc936a127..0e65736e4 100644 --- a/components/aggregation/mat/computations.js +++ b/components/aggregation/mat/computations.js @@ -1,5 +1,5 @@ -import { countryList, territoryNames } from 'country-util' import { getCategoryCodesMap } from '../../utils/categoryCodes' +import { getLocalisedRegionName } from 'utils/i18nCountries' const categoryCodesMap = getCategoryCodesMap() @@ -13,15 +13,7 @@ export function getDatesBetween(startDate, endDate) { return dateSet } -/* dateSet is an optional precomputed set from `getDatesBetween` */ -export function fillDataInMissingDates (data, startDate, endDate) { - - const dateRange = getDatesBetween(new Date(startDate), new Date(endDate)) - - return fillRowHoles(data, 'measurement_start_day', dateRange) -} - -export function fillRowHoles (data, query) { +export function fillRowHoles (data, query, locale) { const newData = [...data] let domain = null @@ -34,7 +26,7 @@ export function fillRowHoles (data, query) { domain = [...getCategoryCodesMap().keys()] break case 'probe_cc': - domain = countryList.map(cc => cc.iso3166_alpha2) + domain = localisedCountries(locale).map(cc => cc.iso3166_alpha2) break default: throw new Error(`x-axis: ${query.axis_x}. Please select a valid value for X-Axis.`) @@ -74,15 +66,11 @@ export function fillDataHoles (data, query) { return newData } -export const sortRows = (a, b, type) => { +export const sortRows = (a, b, type, locale = 'en') => { switch(type) { case 'probe_cc': - return territoryNames[a] < territoryNames[b] ? -1 : territoryNames[a] > territoryNames[b] ? 1 : 0 - case 'category_code': - const A = categoryCodesMap.get(a).name - const B = categoryCodesMap.get(b).name - return A < B ? -1 : A > B ? 1 : 0 + return new Intl.Collator(locale).compare(getLocalisedRegionName(a, locale), getLocalisedRegionName(b, locale)) default: - return a < b ? -1 : a > b ? 1 : 0 + return new Intl.Collator(locale).compare(a, b) } } diff --git a/components/aggregation/mat/labels.js b/components/aggregation/mat/labels.js index e508293f7..48f480188 100644 --- a/components/aggregation/mat/labels.js +++ b/components/aggregation/mat/labels.js @@ -1,10 +1,10 @@ import PropTypes from 'prop-types' -import { useIntl } from 'react-intl' -import countryUtil from 'country-util' import { Box } from 'ooni-components' import { testNames } from '../../test-info' import { getCategoryCodesMap } from '../../utils/categoryCodes' +import { getLocalisedRegionName } from 'utils/i18nCountries' +import { FormattedMessage, useIntl } from 'react-intl' const InputRowLabel = ({ input }) => { const truncatedInput = input @@ -32,22 +32,29 @@ const blockingTypeLabels = { 'tcp_ip': 'TCP/IP Blocking' } -export const getRowLabel = (key, yAxis) => { +const CategoryLabel = ({ code }) => { + const intl = useIntl() + return ( + + ) +} + +export const getRowLabel = (key, yAxis, locale = 'en') => { switch (yAxis) { - case 'probe_cc': - return countryUtil.territoryNames[key] ?? key - case 'category_code': - return categoryCodesMap.get(key)?.name ?? key - case 'input': - case 'domain': - return () - case 'blocking_type': - return blockingTypeLabels[key] ?? key - case 'probe_asn': - return `AS${key}` - case 'test_name': - return Object.keys(testNames).includes(key) ? testNames[key].id : key - default: - return key + case 'probe_cc': + return getLocalisedRegionName(key, locale) + case 'category_code': + return () + case 'input': + case 'domain': + return () + case 'blocking_type': + return blockingTypeLabels[key] ?? key + case 'probe_asn': + return `AS${key}` + case 'test_name': + return Object.keys(testNames).includes(key) ? testNames[key].id : key + default: + return key } } \ No newline at end of file diff --git a/components/country/Apps.js b/components/country/Apps.js index 252fda6ee..16c446692 100644 --- a/components/country/Apps.js +++ b/components/country/Apps.js @@ -4,13 +4,13 @@ import { Text } from 'ooni-components' import SectionHeader from './SectionHeader' import { SimpleBox } from './boxes' -import PeriodFilter from './PeriodFilter' +// import PeriodFilter from './PeriodFilter' import AppsStatsGroup from './AppsStats' import AppsStatsCircumvention from './AppsStatsCircumvention' import FormattedMarkdown from '../FormattedMarkdown' const AppsSection = () => ( - + <> @@ -33,7 +33,7 @@ const AppsSection = () => ( title={} testGroup='circumvention' />} - + ) export default AppsSection diff --git a/components/country/AppsStatsChart.js b/components/country/AppsStatsChart.js index 905d09c22..a846d6a83 100644 --- a/components/country/AppsStatsChart.js +++ b/components/country/AppsStatsChart.js @@ -55,7 +55,7 @@ class AppsStatChart extends React.Component { ), 0) return ( - + <> } /> - + ) } } diff --git a/components/country/AppsStatsCircumventionRow.js b/components/country/AppsStatsCircumventionRow.js index 0011e5abe..0181dd7c6 100644 --- a/components/country/AppsStatsCircumventionRow.js +++ b/components/country/AppsStatsCircumventionRow.js @@ -108,7 +108,7 @@ class AppsStatsCircumventionRow extends React.Component { } return ( - + <> {content} @@ -122,7 +122,7 @@ class AppsStatsCircumventionRow extends React.Component { } - + ) } @@ -148,7 +148,7 @@ class AppsStatsCircumventionRow extends React.Component { {data.networks.length > 0 && `${data.networks.length} Networks Tested`} {totalNetworks > 0 && - + <> {' '} @@ -162,7 +162,7 @@ class AppsStatsCircumventionRow extends React.Component { onClick={this.toggleMinimize} /> - + } {!minimized && this.renderCharts()} diff --git a/components/country/AppsStatsRow.js b/components/country/AppsStatsRow.js index a49650917..6acc04626 100644 --- a/components/country/AppsStatsRow.js +++ b/components/country/AppsStatsRow.js @@ -126,7 +126,7 @@ class AppsStatRow extends React.Component { } return ( - + <> {content} @@ -137,7 +137,7 @@ class AppsStatRow extends React.Component { } - + ) } diff --git a/components/country/CountryHead.js b/components/country/CountryHead.js index df8f70e22..f78e20d2e 100644 --- a/components/country/CountryHead.js +++ b/components/country/CountryHead.js @@ -10,7 +10,7 @@ const CountryHead = ({ const intl = useIntl() return ( - Internet Censorship in {countryName} | OONI Explorer + {intl.formatMessage({ id: 'Country.Meta.Title'}, { countryName })} + <> {content} {(visibleNetworks < totalNetworks) && @@ -116,13 +116,13 @@ class NetworkPropertiesSection extends React.Component { } - + ) } render() { return ( - + <> @@ -154,7 +154,7 @@ class NetworkPropertiesSection extends React.Component { {this.renderStats()} - + ) } } diff --git a/components/country/NetworkStats.js b/components/country/NetworkStats.js index 0067dad4f..8aec06c84 100644 --- a/components/country/NetworkStats.js +++ b/components/country/NetworkStats.js @@ -45,7 +45,7 @@ const NetworkStats = ({ avgPing, middleboxes }) => ( - + <> AS{asn} {asnName} @@ -85,7 +85,7 @@ const NetworkStats = ({ value={} /> - + ) export default NetworkStats diff --git a/components/country/Overview.js b/components/country/Overview.js index 7f8d0d21a..da2f6728f 100644 --- a/components/country/Overview.js +++ b/components/country/Overview.js @@ -112,7 +112,7 @@ const Overview = ({ const intl = useIntl() const { countryCode } = useCountry() return ( - + <> @@ -178,7 +178,7 @@ const Overview = ({ } {/* Highlight Box */} - + ) } export default Overview diff --git a/components/country/OverviewCharts.js b/components/country/OverviewCharts.js index edf27a6f5..97a18f4ff 100644 --- a/components/country/OverviewCharts.js +++ b/components/country/OverviewCharts.js @@ -252,7 +252,7 @@ class TestsByGroup extends React.Component { } return ( - + <> {notEnoughData && } { @@ -272,7 +272,7 @@ class TestsByGroup extends React.Component { {notEnoughData ? renderEmptyChart() : renderCharts()} - + ) } } diff --git a/components/country/PageNavMenu.js b/components/country/PageNavMenu.js index e4204fab1..c9a91de64 100644 --- a/components/country/PageNavMenu.js +++ b/components/country/PageNavMenu.js @@ -39,7 +39,7 @@ const PageNavMenu = ({ countryCode }) => { const [isOpen, setOpen] = useState(true) return ( - + <> {/* Show a trigger to open and close the nav menu, but hide it on desktops */} setOpen(!isOpen)} /> @@ -63,7 +63,7 @@ const PageNavMenu = ({ countryCode }) => { - + ) } diff --git a/components/country/URLChart.js b/components/country/URLChart.js index b1b877f32..fca084f7a 100644 --- a/components/country/URLChart.js +++ b/components/country/URLChart.js @@ -186,8 +186,8 @@ class URLChart extends React.Component { {/* TODO: Show percentages - - + + */} diff --git a/components/country/WebsiteChartLoader.js b/components/country/WebsiteChartLoader.js index 5d4d4a95c..6022d15fd 100644 --- a/components/country/WebsiteChartLoader.js +++ b/components/country/WebsiteChartLoader.js @@ -24,14 +24,14 @@ export const WebsiteChartLoader = (props) => { } export const WebsiteSectionLoader = ({ rows = 5 }) => ( - + <> {Array(rows) .fill('') .map((e, i) => ( )) } - + ) WebsiteSectionLoader.propTypes = { diff --git a/components/country/Websites.js b/components/country/Websites.js index b6baf2e76..51e65351e 100644 --- a/components/country/Websites.js +++ b/components/country/Websites.js @@ -6,7 +6,7 @@ import { Flex, Box, Heading, Text, Input } from 'ooni-components' import SectionHeader from './SectionHeader' import { SimpleBox } from './boxes' -import PeriodFilter from './PeriodFilter' +// import PeriodFilter from './PeriodFilter' import TestsByCategoryInNetwork from './WebsitesCharts' import FormattedMarkdown from '../FormattedMarkdown' @@ -52,7 +52,7 @@ class WebsitesSection extends React.Component { const { onPeriodChange, countryCode } = this.props const { noData, selectedNetwork } = this.state return ( - + <> @@ -80,7 +80,7 @@ class WebsitesSection extends React.Component { networks={this.state.networks} /> - + ) } } diff --git a/components/country/WebsitesCharts.js b/components/country/WebsitesCharts.js index 7e5dd78f0..005da24e0 100644 --- a/components/country/WebsitesCharts.js +++ b/components/country/WebsitesCharts.js @@ -100,7 +100,7 @@ class TestsByCategoryInNetwork extends React.Component { ) return ( - + <> {/* */} {/* {(network !== null && networks !== null) ? - + <> {testedUrlsCount} - + : } @@ -157,7 +157,7 @@ class TestsByCategoryInNetwork extends React.Component { {e.preventDefault(); this.nextPage()}}>{' >'} } {/* URL-wise barcharts End */} - + ) } } diff --git a/components/dashboard/Charts.js b/components/dashboard/Charts.js index ece80d2b0..3ba6faa9b 100644 --- a/components/dashboard/Charts.js +++ b/components/dashboard/Charts.js @@ -3,7 +3,7 @@ import { Flex, Box, Heading } from 'ooni-components' import { useRouter } from 'next/router' import useSWR from 'swr' import axios from 'axios' -import { territoryNames } from 'country-util' +import { useIntl } from 'react-intl' import GridChart, { prepareDataForGridChart } from '../aggregation/mat/GridChart' import { MATContextProvider } from '../aggregation/mat/MATContext' @@ -45,6 +45,7 @@ const fixedQuery = { } const Chart = React.memo(function Chart({ testName }) { + const intl = useIntl() const { query: {probe_cc, since, until} } = useRouter() // Construct a `query` object that matches the router.query @@ -73,18 +74,18 @@ const Chart = React.memo(function Chart({ testName }) { return [null, 0] } - let chartData = data.data.sort((a, b) => (territoryNames[a.probe_cc] < territoryNames[b.probe_cc]) ? -1 : (territoryNames[a.probe_cc] > territoryNames[b.probe_cc]) ? 1 : 0) + let chartData = data.data.sort((a, b) => (new Intl.Collator(intl.locale).compare(a.probe_cc, b.probe_cc))) const selectedCountries = probe_cc?.length > 1 ? probe_cc.split(',') : [] if (selectedCountries.length > 0) { chartData = chartData.filter(d => selectedCountries.includes(d.probe_cc)) } - const [reshapedData, rowKeys, rowLabels] = prepareDataForGridChart(chartData, query) + const [reshapedData, rowKeys, rowLabels] = prepareDataForGridChart(chartData, query, intl.locale) return [reshapedData, rowKeys, rowLabels] - }, [data, probe_cc, query]) + }, [data, probe_cc, query, intl]) const headerOptions = { probe_cc: false, subtitle: false } @@ -94,10 +95,10 @@ const Chart = React.memo(function Chart({ testName }) { {testNames[testName].name} {(!chartData && !error) ? ( -
Loading ...
+
{intl.formatMessage({id: 'General.Loading'})}
) : ( chartData === null || chartData.length === 0 ? ( - No Data + {intl.formatMessage({id: 'General.NoData'})} ) : (
- Error: {error.message} + {intl.formatMessage({id: 'General.Error'})}: {error.message} {JSON.stringify(error, null, 2)} diff --git a/components/dashboard/Form.js b/components/dashboard/Form.js index 5e7dff54e..71aae8c26 100644 --- a/components/dashboard/Form.js +++ b/components/dashboard/Form.js @@ -1,11 +1,11 @@ import { useEffect, useMemo, useState } from 'react' -import { territoryNames } from 'country-util' import { useForm, Controller } from 'react-hook-form' import { Box, Flex, Input } from 'ooni-components' import { MultiSelect } from 'react-multi-select-component' import { useIntl } from 'react-intl' import dayjs from 'services/dayjs' import { format } from 'date-fns' +import { getLocalisedRegionName } from '../../utils/i18nCountries' import { StyledLabel } from '../aggregation/mat/Form' import DateRangePicker from '../DateRangePicker' @@ -23,12 +23,12 @@ export const Form = ({ onChange, query, availableCountries }) => { const intl = useIntl() const countryOptions = useMemo(() => availableCountries - .sort((a,b) => (territoryNames[a] < territoryNames[b]) ? -1 : (territoryNames[a] > territoryNames[b]) ? 1 : 0) .map(cc => ({ - label: territoryNames[cc], + label: getLocalisedRegionName(cc, intl.locale), value: cc })) - , [availableCountries]) + .sort((a, b) => (new Intl.Collator(intl.locale).compare(a.label, b.label))) + , [availableCountries, intl]) const query2formValues = (query) => { const countriesInQuery = query.probe_cc?.split(',') ?? defaultDefaultValues.probe_cc diff --git a/components/form/Select.js b/components/form/Select.js new file mode 100644 index 000000000..95ae14dee --- /dev/null +++ b/components/form/Select.js @@ -0,0 +1,31 @@ +// this is a workaround to be able to style rebass Select for RTL languages + +import styled from 'styled-components' +import { Select as DSSelect } from 'ooni-components' +import { useIntl } from 'react-intl' +import { getDirection } from 'components/withIntl' +import { useMemo } from 'react' + +const StyledSelect = styled.div` +${props => props.direction === 'rtl' ? ` +svg { + margin-inline-start: -28px; + margin-left: 0; +} +` : ''} +` + +const Select = (props) => { + const { locale } = useIntl() + const direction = useMemo(() => (getDirection(locale)), [locale]) + + return ( + + + {props.children} + + + ) +} + +export default Select \ No newline at end of file diff --git a/components/landing/HighlightBox.js b/components/landing/HighlightBox.js index 31a2dbed8..4276bd149 100644 --- a/components/landing/HighlightBox.js +++ b/components/landing/HighlightBox.js @@ -1,9 +1,11 @@ import React from 'react' +import { useIntl } from 'react-intl' import PropTypes from 'prop-types' import NLink from 'next/link' import { Flex, Box, Link, theme, Text } from 'ooni-components' import styled from 'styled-components' import Markdown from 'markdown-to-jsx' +import { getLocalisedRegionName } from 'utils/i18nCountries' import Flag from '../Flag' @@ -17,57 +19,68 @@ const FlexGrowBox = styled(Box)` const HighlightBox = ({ countryCode, - countryName, title, text, report, explore, tileColor = 'black' -}) => ( - - - - - {countryName} - - - {title && - {title} - } - - - - {text} - - - - - - {explore && - - Explore - - } - {report && - - Read Report - - } - - - -) +}) => { + const intl = useIntl() + + return ( + + + + + {getLocalisedRegionName(countryCode, intl.locale)} + + + {title && + + {intl.formatMessage({id: title})} + + } + + + + {intl.formatMessage({id: text})} + + + + + + {explore && + + + {intl.formatMessage({id: 'Home.Highlights.Explore'})} + + + } + {report && + + + {intl.formatMessage({id: 'Home.Highlights.ReadReport'})} + + + } + + + + ) +} + + HighlightBox.propTypes = { countryCode: PropTypes.string.isRequired, diff --git a/components/landing/HighlightsSection.js b/components/landing/HighlightsSection.js index 756dbf11c..7af266ea3 100644 --- a/components/landing/HighlightsSection.js +++ b/components/landing/HighlightsSection.js @@ -12,9 +12,11 @@ const HighlightSection = ({ return (
- - {title} - + + + {title} + + {/* Optional Description */} {description && @@ -46,7 +48,6 @@ HighlightSection.propTypes = { ]), highlights: PropTypes.arrayOf(PropTypes.shape({ countryCode: PropTypes.string.isRequired, - countryName: PropTypes.string.isRequired, title: PropTypes.string, text: PropTypes.string, report: PropTypes.string, diff --git a/components/landing/Stats.js b/components/landing/Stats.js index dd7efa8cf..3b2183320 100644 --- a/components/landing/Stats.js +++ b/components/landing/Stats.js @@ -81,7 +81,7 @@ const CoverageChart = () => { const VictoryCursorVoronoiContainer = createContainer('cursor', 'voronoi') return ( - + <> { }} /> - + ) } else { return () diff --git a/components/landing/highlights.json b/components/landing/highlights.json index f98e70739..c12d3350e 100644 --- a/components/landing/highlights.json +++ b/components/landing/highlights.json @@ -2,72 +2,64 @@ "political": [ { "countryCode": "CU", - "countryName": "Cuba", - "title": "2019 constitutional referendum", - "text": "Blocking of independent media", + "title": "Highlights.Political.CubaReferendum2019.Title", + "text": "Highlights.Political.CubaReferendum2019.Text", "report": "https://ooni.org/post/cuba-referendum/", "explore": "/search?probe_cc=CU&test_name=web_connectivity&until=2019-02-26&domain=www.tremendanota.com&since=2019-01-31", "tileColor": "#0050F0" }, { "countryCode": "VE", - "countryName": "Venezuela", - "title": "2019 presidential crisis", - "text": "Blocking of Wikipedia and social media", + "title": "Highlights.Political.VenezuelaCrisis2019.Title", + "text": "Highlights.Political.VenezuelaCrisis2019.Text", "report": "https://ooni.org/post/venezuela-blocking-wikipedia-and-social-media-2019/", "explore": "/search?domain=www.wikipedia.org&probe_cc=VE&test_name=web_connectivity&since=2019-01-13&until=2019-01-17", "tileColor": "#00247D" }, { "countryCode": "ZW", - "countryName": "Zimbabwe", - "title": "2019 fuel protests", - "text": "Social media blocking and internet blackouts", + "title": "Highlights.Political.ZimbabweProtests2019.Title", + "text": "Highlights.Political.ZimbabweProtests2019.Text", "report": "https://ooni.org/post/zimbabwe-protests-social-media-blocking-2019/", "explore": "/search?probe_cc=ZW&test_name=whatsapp&since=2019-01-14&until=2019-01-17", "tileColor": "#000000" }, { "countryCode": "ML", - "countryName": "Mali", - "title": "2018 presidential election", - "text": "Blocking of WhatsApp and Twitter", + "title": "Highlights.Political.MaliElection2018.Title", + "text": "Highlights.Political.MaliElection2018.Text", "report": "https://ooni.org/post/mali-disruptions-amid-2018-election/", "explore": "/search?probe_cc=ML&test_name=whatsapp&since=2018-07-26&until=2018-07-30", "tileColor": "#009A00" }, { "countryCode": "ES", - "countryName": "Spain", - "title": "Catalonia 2017 independence referendum", - "text": "Blocking of sites related to the referendum", + "title": "Highlights.Political.CataloniaReferendum2017.Title", + "text": "Highlights.Political.CataloniaReferendum2017.Text", "report": "https://ooni.org/post/internet-censorship-catalonia-independence-referendum/", "explore": "/search?domain=www.referendum.legal&probe_cc=ES&test_name=web_connectivity&since=2017-09-30&until=2017-10-02", "tileColor": "#c60b1e" }, { "countryCode": "IR", - "countryName": "Iran", - "title": "2018 anti-government protests", - "text": "Blocking of Telegram, Instagram and Tor", + "title": "Highlights.Political.IranProtests2018.Title", + "text": "Highlights.Political.IranProtests2018.Text", "report": "https://ooni.org/post/2018-iran-protests/", "explore": "/search?domain=www.instagram.com&probe_cc=IR&test_name=web_connectivity&since=2017-12-30&until=2018-01-02", "tileColor": "#249F40" }, { "countryCode": "ET", - "countryName": "Ethiopia", - "title": "2016 wave of protests", - "text": "Blocking of news websites and social media", + "title": "Highlights.Political.EthiopiaProtests2016.Title", + "text": "Highlights.Political.EthiopiaProtests2016.Text", "report": "https://ooni.org/post/ethiopia-report/", "explore": "/search?domain=ethsat.com&probe_cc=ET&test_name=web_connectivity&since=2016-06-15&until=2016-10-07", "tileColor": "#ef2118" }, { "countryCode": "PK", - "countryName": "Pakistan", - "title": "2017 protests", - "text": "Blocking of news websites and social media", + "title": "Highlights.Political.PakistanProtests2017.Title", + "text": "Highlights.Political.PakistanProtests2017.Text", "report": "https://ooni.org/post/how-pakistan-blocked-social-media/", "explore": "/search?domain=www.facebook.com&probe_cc=PK&test_name=web_connectivity&since=2017-11-24&until=2017-11-26", "tileColor": "#0c590b" @@ -76,45 +68,40 @@ "media": [ { "countryCode": "EG", - "countryName": "Egypt", - "title": "Pervasive media censorship", - "text": "Blocking of hundreds of media websites", + "title": "Highlights.Media.Egypt.Title", + "text": "Highlights.Media.Egypt.Text", "report": "https://ooni.org/post/egypt-internet-censorship/", "explore": "/search?domain=madamasr.com&probe_cc=EG&test_name=web_connectivity&since=2018-04-01&until=2018-07-02", "tileColor": "#000000" }, { "countryCode": "VE", - "countryName": "Venezuela", - "title": "Blocking of independent media websites", - "text": "Venezuela's economic and political crisis", + "title": "Highlights.Media.Venezuela.Title", + "text": "Highlights.Media.Venezuela.Text", "report": "https://ooni.org/post/venezuela-internet-censorship/#media", "explore": "/search?domain=elpitazo.com&probe_cc=VE&test_name=web_connectivity&since=2018-01-01&until=2018-08-16", "tileColor": "#00247D" }, { "countryCode": "SS", - "countryName": "South Sudan", - "title": "Blocking of foreign-based media", - "text": "Media accused of hostile reporting against the government", + "title": "Highlights.Media.SouthSudan.Title", + "text": "Highlights.Media.SouthSudan.Text", "report": "https://ooni.org/post/south-sudan-censorship/#blocked-websites", "explore": "/search?domain=www.sudantribune.com&probe_cc=SS&test_name=web_connectivity&since=2018-04-01&until=2018-08-01", "tileColor": "#000000" }, { "countryCode": "MY", - "countryName": "Malaysia", - "title": "Blocking of media", - "text": "1MDB scandal", + "title": "Highlights.Media.Malaysia.Title", + "text": "Highlights.Media.Malaysia.Text", "report": "https://ooni.org/post/malaysia-report/#news-media", "explore": "/search?domain=www.sarawakreport.org&probe_cc=MY&test_name=web_connectivity&since=2016-09-30&until=2016-12-14", "tileColor": "#010066" }, { "countryCode": "IR", - "countryName": "Iran", - "title": "Pervasive media censorship", - "text": "Blocking of at least 121 news outlets", + "title": "Highlights.Media.Iran.Title", + "text": "Highlights.Media.Iran.Text", "report": "https://ooni.org/post/iran-internet-censorship/#news-websites", "explore": "/search?domain=iranshahrnewsagency.com&probe_cc=IR&test_name=web_connectivity&since=2017-01-01&until=2017-09-04", "tileColor": "#249F40" @@ -123,27 +110,21 @@ "lgbtqi": [ { "countryCode": "ID", - "countryName": "Indonesia", - "title": "", - "text": "Blocking of LGBTQI sites", + "text": "Highlights.Lgbtqi.Indonesia.Text", "report": "https://ooni.org/post/indonesia-internet-censorship/#lgbt", "explore": "/search?domain=www.queernet.org&probe_cc=ID&test_name=web_connectivity&since=2017-01-01&until=2017-03-01", "tileColor": "#e70011" }, { "countryCode": "IR", - "countryName": "Iran", - "title": "", - "text": "Blocking of Grindr", + "text": "Highlights.Lgbtqi.Iran.Text", "report": "https://ooni.org/post/iran-internet-censorship/#human-rights-issues", "explore": "/search?domain=www.grindr.com&probe_cc=IR&test_name=web_connectivity&since=2017-01-01&until=2017-09-04", "tileColor": "#249F40" }, { "countryCode": "ET", - "countryName": "Ethiopia", - "title": "", - "text": "Blocking of QueerNet", + "text": "Highlights.Lgbtqi.Ethiopia.Text", "report": "https://ooni.org/post/ethiopia-report/#lgbti-websites", "explore": "/search?domain=www.queernet.org&probe_cc=ET&test_name=web_connectivity&since=2016-06-15&until=2016-10-07", "tileColor": "#ef2118" @@ -152,20 +133,17 @@ "changes": [ { "countryCode": "CU", - "countryName": "Cuba", - "text": "Cuba [used to primarily serve blank block pages](https://ooni.torproject.org/post/cuba-internet-censorship-2017/), only blocking the HTTP version of websites. Now they censor access to sites that support HTTPS by means of [IP blocking](https://ooni.org/post/cuba-referendum/).", + "text": "Highlights.Changes.Cuba.Text", "tileColor": "#0050F0" }, { "countryCode": "VE", - "countryName": "Venezuela", - "text": "Venezuelan ISPs used to primarily block sites by means of [DNS tampering](https://ooni.torproject.org/post/venezuela-internet-censorship/). Now state-owned CANTV also implements [SNI-based filtering](https://ooni.torproject.org/post/venezuela-blocking-wikipedia-and-social-media-2019/).", + "text": "Highlights.Changes.Venezuela.Text", "tileColor": "#00247D" }, { "countryCode": "ET", - "countryName": "Ethiopia", - "text": "Ethiopia [used to block](https://ooni.org/post/ethiopia-report/) numerous news websites, LGBTQI, political opposition, and circumvention tool sites. As part of the 2018 political reforms, most of these sites have been [unblocked](https://ooni.org/post/ethiopia-unblocking/).", + "text": "Highlights.Changes.Ethiopia.Text", "tileColor": "#ef2118" } ] diff --git a/components/measurement/AccessPointStatus.js b/components/measurement/AccessPointStatus.js index 12090bd6a..76a6e6099 100644 --- a/components/measurement/AccessPointStatus.js +++ b/components/measurement/AccessPointStatus.js @@ -11,9 +11,9 @@ const StatusText = styled(Text)` const AccessPointStatus = ({ icon, label, ok, content, color, ...props}) => { if (content === undefined) { if (ok === true) { - content = + content = } else if (ok === false){ - content = + content = } else { content = } diff --git a/components/measurement/CommonDetails.js b/components/measurement/CommonDetails.js index e658b15ff..cdb2d49e5 100644 --- a/components/measurement/CommonDetails.js +++ b/components/measurement/CommonDetails.js @@ -17,7 +17,7 @@ import { useRouter } from 'next/router' import { DetailsBoxTable, DetailsBox } from './DetailsBox' const LoadingRawData = (props) => { - return (Loading) + return () } const ReactJson = dynamic( @@ -126,7 +126,7 @@ const CommonDetails = ({ } return ( - + <> {showResolverItems && {/* Resolver data */} @@ -178,7 +178,7 @@ const CommonDetails = ({ } content={ measurement && typeof measurement === 'object' ? ( - + ) : ( @@ -187,7 +187,7 @@ const CommonDetails = ({ } /> - + ) } diff --git a/components/measurement/CommonSummary.js b/components/measurement/CommonSummary.js index 56f07843d..a34df52ef 100644 --- a/components/measurement/CommonSummary.js +++ b/components/measurement/CommonSummary.js @@ -63,11 +63,11 @@ const CommonSummary = ({ {country} - - const formattedDate = dayjs(startTime).utc().format('MMMM DD, YYYY, hh:mm A [UTC]') - + + const formattedDate = new Intl.DateTimeFormat(intl.locale, { dateStyle: 'long', timeStyle: 'long', timeZone: 'UTC' }).format(new Date(startTime)) + return ( - + <> @@ -91,7 +91,7 @@ const CommonSummary = ({ - + ) } diff --git a/components/measurement/DetailsBox.js b/components/measurement/DetailsBox.js index 1cfd2637e..4806071ad 100644 --- a/components/measurement/DetailsBox.js +++ b/components/measurement/DetailsBox.js @@ -50,6 +50,7 @@ const StyledDetailsBox = styled(Box)` const StyledDetailsBoxHeader = styled(Flex)` cursor: pointer; + justify-content: space-between; ` const StyledDetailsBoxContent = styled(Box)` @@ -70,7 +71,7 @@ export const DetailsBox = ({ title, content, collapsed = false, children, ...res {title} - + diff --git a/components/measurement/DetailsHeader.js b/components/measurement/DetailsHeader.js index 7a56c870a..bf8aef3cc 100644 --- a/components/measurement/DetailsHeader.js +++ b/components/measurement/DetailsHeader.js @@ -35,7 +35,7 @@ const DetailsHeader = ({testName, runtime, notice, url}) => { const metadata = getTestMetadata(testName) return ( - + <> @@ -69,7 +69,7 @@ const DetailsHeader = ({testName, runtime, notice, url}) => { - + ) } diff --git a/components/measurement/HeadMetadata.js b/components/measurement/HeadMetadata.js index 94b47ed02..7dedfcf23 100644 --- a/components/measurement/HeadMetadata.js +++ b/components/measurement/HeadMetadata.js @@ -15,7 +15,7 @@ const HeadMetadata = ({ const intl = useIntl() let description = '' - const formattedDate = dayjs(date).utc().format('MMMM D, YYYY, h:mm:ss A [UTC]') + const formattedDate = new Intl.DateTimeFormat(intl.locale, { dateStyle: 'long', timeStyle: 'long', timeZone: 'UTC' }).format(new Date(date)) if (content.formatted) { description = content.message @@ -31,7 +31,13 @@ const HeadMetadata = ({ ) } - const metaDescription = `OONI data suggests ${description} on ${formattedDate}, find more open data on internet censorship on OONI Explorer.` + const metaDescription = intl.formatMessage({ + id: 'Measurement.MetaDescription'}, + { + description, + formattedDate + } + ) return ( diff --git a/components/measurement/Hero.js b/components/measurement/Hero.js index 6b880c364..57ff5d343 100644 --- a/components/measurement/Hero.js +++ b/components/measurement/Hero.js @@ -17,15 +17,15 @@ const Hero = ({ status, color, icon, label, info }) => { if (status) { switch (status) { case 'anomaly': - computedLabel = + computedLabel = icon = break case 'reachable': - computedLabel = + computedLabel = icon = break case 'error': - computedLabel = + computedLabel = icon = break case 'confirmed': diff --git a/components/measurement/MeasurementContainer.js b/components/measurement/MeasurementContainer.js index ae54e1d65..b31395176 100644 --- a/components/measurement/MeasurementContainer.js +++ b/components/measurement/MeasurementContainer.js @@ -39,9 +39,9 @@ const mapTestDetails = { const MeasurementContainer = ({ testName, measurement, ...props }) => { const TestDetails = testName in mapTestDetails ? mapTestDetails[testName] : DefaultTestDetails return ( - + <> - + ) } diff --git a/components/measurement/MeasurementNotFound.js b/components/measurement/MeasurementNotFound.js index 8100c8cb5..4c6879587 100644 --- a/components/measurement/MeasurementNotFound.js +++ b/components/measurement/MeasurementNotFound.js @@ -5,24 +5,27 @@ import { Container, Flex, Box, Heading, Text } from 'ooni-components' import { useRouter } from 'next/router' import OONI404 from '../../public/static/images/OONI_404.svg' +import { useIntl } from 'react-intl' const MeasurementNotFound = () => { const { asPath } = useRouter() + const intl = useIntl() + return ( - + <> - Measurement Not Found + {intl.formatMessage({id: 'Measurement.NotFound' })} {`${process.env.NEXT_PUBLIC_EXPLORER_URL}${asPath}`} - + ) } diff --git a/components/measurement/SummaryText.js b/components/measurement/SummaryText.js index 7587188cc..0df0a303f 100644 --- a/components/measurement/SummaryText.js +++ b/components/measurement/SummaryText.js @@ -5,6 +5,7 @@ import dayjs from 'services/dayjs' import { getTestMetadata } from '../utils' import FormattedMarkdown from '../FormattedMarkdown' +import { useIntl } from 'react-intl' const SummaryText = ({ testName, @@ -13,8 +14,9 @@ const SummaryText = ({ date, content, }) => { + const { locale } = useIntl() const metadata = getTestMetadata(testName) - const formattedDateTime = dayjs(date).utc().format('MMMM DD, YYYY, hh:mm A [UTC]') + const formattedDateTime = dayjs(date).locale(locale).utc().format('MMMM DD, YYYY, hh:mm A [UTC]') let textToRender = null if (typeof content === 'function') { diff --git a/components/measurement/nettests/FacebookMessenger.js b/components/measurement/nettests/FacebookMessenger.js index e155198e3..e33906da2 100644 --- a/components/measurement/nettests/FacebookMessenger.js +++ b/components/measurement/nettests/FacebookMessenger.js @@ -78,7 +78,7 @@ export const FacebookMessengerDetails = ({ measurement, render }) => { formatted: false }, details: ( - + <> { } content={ - + <> {Array.isArray(tcpConnections) && tcpConnections.length > 0 && - + <> {tcpConnections.map((connection, index) => ( @@ -117,12 +117,12 @@ export const FacebookMessengerDetails = ({ measurement, render }) => { ))} - + } - + } /> - + ) }) ) diff --git a/components/measurement/nettests/Ndt.js b/components/measurement/nettests/Ndt.js index f34a5cddb..cfb072406 100644 --- a/components/measurement/nettests/Ndt.js +++ b/components/measurement/nettests/Ndt.js @@ -16,9 +16,9 @@ const ServerLocation = ({ serverAddress = '', isNdt7 }) => { const server = mlabServerDetails(serverAddress, isNdt7) return ( - + <> {server ? `${server.city}, ${server.countryName}` : 'N/A'} - + ) } diff --git a/components/measurement/nettests/Psiphon.js b/components/measurement/nettests/Psiphon.js index d7550e013..640585542 100644 --- a/components/measurement/nettests/Psiphon.js +++ b/components/measurement/nettests/Psiphon.js @@ -53,7 +53,7 @@ const PsiphonDetails = ({ } return ( - + <> {render({ status: status, statusInfo: hint, @@ -63,7 +63,7 @@ const PsiphonDetails = ({ formatted: false }, details: ( - + <> { @@ -77,10 +77,10 @@ const PsiphonDetails = ({ } - + ) })} - + ) } diff --git a/components/measurement/nettests/Telegram.js b/components/measurement/nettests/Telegram.js index 530af1078..59cbd0a99 100644 --- a/components/measurement/nettests/Telegram.js +++ b/components/measurement/nettests/Telegram.js @@ -71,7 +71,7 @@ const TelegramDetails = ({ measurement, render }) => { formatted: false }, details: ( - + <> { ))} } - + ) }) ) diff --git a/components/measurement/nettests/Tor.js b/components/measurement/nettests/Tor.js index 134246a03..5cde51a60 100644 --- a/components/measurement/nettests/Tor.js +++ b/components/measurement/nettests/Tor.js @@ -56,7 +56,7 @@ const NameCell = ({ children }) => { const clipboard = useClipboard({ copiedTimeout: 1500 }) return ( - + <> clipboard.copy(children)} @@ -67,7 +67,7 @@ const NameCell = ({ children }) => { - + ) } @@ -138,9 +138,9 @@ const ConnectionStatusCell = ({ cell: { value} }) => { statusIcon = value === null ? : } return ( - + <> {statusIcon} {value} - + ) } @@ -196,7 +196,7 @@ const TorDetails = ({ accessor: 'type' }, { - Header: , + Header: , accessor: 'failure', collapse: true, Cell: ConnectionStatusCell @@ -222,7 +222,7 @@ const TorDetails = ({ }) return ( - + <> {render({ status: status, statusInfo: hint, @@ -232,7 +232,7 @@ const TorDetails = ({ formatted: false }, details: ( - + <> - + ) })} - + ) } diff --git a/components/measurement/nettests/TorSnowflake.js b/components/measurement/nettests/TorSnowflake.js index b34aefedb..5b02174d5 100644 --- a/components/measurement/nettests/TorSnowflake.js +++ b/components/measurement/nettests/TorSnowflake.js @@ -49,7 +49,7 @@ const TorSnowflakeDetails = ({ isAnomaly, isFailure, measurement, render }) => { } return ( - + <> {render({ status: status, statusInfo: hint, @@ -59,7 +59,7 @@ const TorSnowflakeDetails = ({ isAnomaly, isFailure, measurement, render }) => { formatted: false }, details: ( - + <> { isAnomaly && @@ -80,10 +80,10 @@ const TorSnowflakeDetails = ({ isAnomaly, isFailure, measurement, render }) => { } - + ) })} - + ) } diff --git a/components/measurement/nettests/VanillaTor.js b/components/measurement/nettests/VanillaTor.js index 963028371..f3bc96244 100644 --- a/components/measurement/nettests/VanillaTor.js +++ b/components/measurement/nettests/VanillaTor.js @@ -45,7 +45,7 @@ const VanillaTorDetails = ({ measurement, render }) => { formatted: false }, details: ( - + <> { /> */} - + )} ) ) diff --git a/components/measurement/nettests/WebConnectivity.js b/components/measurement/nettests/WebConnectivity.js index 67a741f13..667002aa2 100644 --- a/components/measurement/nettests/WebConnectivity.js +++ b/components/measurement/nettests/WebConnectivity.js @@ -94,7 +94,7 @@ const RequestResponseContainer = ({request}) => { // e.g ?report_id=20180709T222326Z_AS37594_FFQFSoqLJWYMgU0EnSbIK7PxicwJTFenIz9PupZYZWoXwtpCTy request.failure ? ( - + ) : ( // !request.failure && @@ -146,7 +146,7 @@ RequestResponseContainer.propTypes = { const FailureString = ({failure}) => { if (typeof failure === 'undefined') { - return () + return () } if (!failure) { return ( @@ -319,8 +319,8 @@ const WebConnectivityDetails = ({ } = validateMeasurement(measurement ?? {}) const intl = useIntl() - const date = dayjs(measurement_start_time).utc().format('MMMM DD, YYYY, hh:mm A [UTC]') - + const date = new Intl.DateTimeFormat(intl.locale, { dateStyle: 'long', timeStyle: 'long', timeZone: 'UTC' }).format(new Date(measurement_start_time)) + const p = url.parse(input) const hostname = p.host @@ -471,14 +471,14 @@ const WebConnectivityDetails = ({ }) : [] return ( - + <> {render({ status: status, statusInfo: , summaryText: summaryText, headMetadata: headMetadata, details: ( - + <> {/* Failures */} } content={ Array.isArray(queries) ? ( - + <> : @@ -525,9 +525,9 @@ const WebConnectivityDetails = ({ {queries.map((query, index) => )} - + ) : ( - + ) } /> @@ -554,7 +554,7 @@ const WebConnectivityDetails = ({ )) ) : ( - + ) } /> @@ -571,15 +571,15 @@ const WebConnectivityDetails = ({ {requests.map((request, index) => )} ) : ( - + ) } /> - + ) })} - + ) } diff --git a/components/measurement/nettests/WhatsApp.js b/components/measurement/nettests/WhatsApp.js index bf1e4e692..f9ed14aa6 100644 --- a/components/measurement/nettests/WhatsApp.js +++ b/components/measurement/nettests/WhatsApp.js @@ -72,7 +72,7 @@ const WhatsAppDetails = ({ isAnomaly, scores, measurement, render }) => { formatted: false }, details: ( - + <> @@ -99,11 +99,11 @@ const WhatsAppDetails = ({ isAnomaly, scores, measurement, render }) => { {Array.isArray(tcp_connect) && tcp_connect.length > 0 && - + <> } content={ - + <> {tcp_connect.map((connection, index) => ( @@ -122,12 +122,12 @@ const WhatsAppDetails = ({ isAnomaly, scores, measurement, render }) => { ))} - + } /> - + } - + ) }) } diff --git a/components/network/Chart.js b/components/network/Chart.js index cbff2ad05..b517d15d2 100644 --- a/components/network/Chart.js +++ b/components/network/Chart.js @@ -1,4 +1,5 @@ import React, { useMemo } from 'react' +import { useIntl } from 'react-intl' import { useRouter } from 'next/router' import { Heading, Box, Flex } from 'ooni-components' import useSWR from 'swr' @@ -13,6 +14,7 @@ const swrOptions = { } const Chart = React.memo(function Chart({testName, testGroup = null, title, queryParams = {}}) { + const intl = useIntl() const router = useRouter() const { query: {since, until, asn} } = router @@ -50,9 +52,9 @@ const Chart = React.memo(function Chart({testName, testGroup = null, title, quer } let chartData = testGroup ? data : data.data const graphQuery = testGroup ? {...query, axis_y: name} : query - const [reshapedData, rowKeys, rowLabels] = prepareDataForGridChart(chartData, graphQuery) + const [reshapedData, rowKeys, rowLabels] = prepareDataForGridChart(chartData, graphQuery, intl.locale) return [reshapedData, rowKeys, rowLabels] - }, [data, query, name, testGroup]) + }, [data, query, name, testGroup, intl]) const headerOptions = { probe_cc: false, subtitle: false } @@ -62,10 +64,10 @@ const Chart = React.memo(function Chart({testName, testGroup = null, title, quer {title} {(!chartData && !error) ? ( -
Loading ...
+
{intl.formatMessage({id: 'General.Loading'})}
) : ( chartData === null || chartData.length === 0 ? ( - No Data + {intl.formatMessage({id: 'General.NoData'})} ) : (
- Error: {error.message} + {intl.formatMessage({id: 'General.Error'})}: {error.message} {JSON.stringify(error, null, 2)} diff --git a/components/network/Form.js b/components/network/Form.js index 910803084..a22788843 100644 --- a/components/network/Form.js +++ b/components/network/Form.js @@ -61,7 +61,7 @@ const Form = ({ onChange, query }) => { - Since + {intl.formatMessage({id: 'Search.Sidebar.From'})} { /> - Until + {intl.formatMessage({id: 'Search.Sidebar.Until'})} ({...c, name: getLocalisedRegionName(c.alpha_2, intl.locale)}))] + countryOptions.sort((a,b) => (a.name < b.name) ? -1 : (a.name > b.name) ? 1 : 0) countryOptions.unshift({name: intl.formatMessage({id: 'Search.Sidebar.Country.AllCountries'}), alpha_2: 'XX'}) return ( diff --git a/components/search/Loader.js b/components/search/Loader.js index ea29a08b7..4ae57e862 100644 --- a/components/search/Loader.js +++ b/components/search/Loader.js @@ -25,13 +25,14 @@ export const LoaderRow = (props) => { export const Loader = ({ rows = 10 }) => ( - - {Array(rows) - .fill('') - .map((e, i) => ( - - ))} - + <> + {Array(rows) + .fill('') + .map((e, i) => ( + + )) + } + ) Loader.propTypes = { diff --git a/components/search/ResultsList.js b/components/search/ResultsList.js index f0e26e83e..2c9bda2e9 100644 --- a/components/search/ResultsList.js +++ b/components/search/ResultsList.js @@ -4,7 +4,7 @@ import url from 'url' import dayjs from 'services/dayjs' import NLink from 'next/link' import styled from 'styled-components' -import { useIntl } from 'react-intl' +import { useIntl, defineMessages } from 'react-intl' import { Flex, Box, Link, @@ -56,6 +56,161 @@ const imTests = [ 'facebook_messenger' ] +const messages = defineMessages({ + 'Search.WebConnectivity.Results.Reachable': { + id: 'General.Accessible', + defaultMessage: '' + }, + 'Search.WebConnectivity.Results.Anomaly': { + id: 'General.Anomaly', + defaultMessage: '' + }, + 'Search.WebConnectivity.Results.Blocked': { + id: 'Search.WebConnectivity.Results.Blocked', + defaultMessage: '' + }, + 'Search.WebConnectivity.Results.Error': { + id: 'General.Error', + defaultMessage: '' + }, + 'Search.WhatsApp.Results.Reachable': { + id: 'General.Accessible', + defaultMessage: '' + }, + 'Search.WhatsApp.Results.Anomaly': { + id: 'General.Anomaly', + defaultMessage: '' + }, + 'Search.WhatsApp.Results.Error': { + id: 'General.Error', + defaultMessage: '' + }, + 'Search.FacebookMessenger.Results.Reachable': { + id: 'General.Accessible', + defaultMessage: '' + }, + 'Search.FacebookMessenger.Results.Anomaly': { + id: 'General.Anomaly', + defaultMessage: '' + }, + 'Search.FacebookMessenger.Results.Error': { + id: 'General.Error', + defaultMessage: '' + }, + 'Search.Telegram.Results.Reachable': { + id: 'General.Accessible', + defaultMessage: '' + }, + 'Search.Telegram.Results.Anomaly': { + id: 'General.Anomaly', + defaultMessage: '' + }, + 'Search.Telegram.Results.Error': { + id: 'General.Error', + defaultMessage: '' + }, + 'Search.Signal.Results.Reachable': { + id: 'General.Accessible', + defaultMessage: '' + }, + 'Search.Signal.Results.Anomaly': { + id: 'General.Anomaly', + defaultMessage: '' + }, + 'Search.Signal.Results.Error': { + id: 'General.Error', + defaultMessage: '' + }, + 'Search.HTTPInvalidRequestLine.Results.Anomaly': { + id: 'General.Anomaly', + defaultMessage: '' + }, + 'Search.HTTPInvalidRequestLine.Results.Reachable': { + id: 'General.OK', + defaultMessage: '' + }, + 'Search.HTTPInvalidRequestLine.Results.Error': { + id: 'General.Error', + defaultMessage: '' + }, + 'Search.HTTPHeaderFieldManipulation.Results.Anomaly': { + id: 'General.Anomaly', + defaultMessage: '' + }, + 'Search.HTTPHeaderFieldManipulation.Results.Reachable': { + id: 'General.OK', + defaultMessage: '' + }, + 'Search.HTTPHeaderFieldManipulation.Results.Error': { + id: 'General.Error', + defaultMessage: '' + }, + 'Search.HTTPRequests.Results.Reachable': { + id: 'Search.HTTPRequests.Results.Reachable', + defaultMessage: '' + }, + 'Search.HTTPRequests.Results.Error': { + id: 'Search.HTTPRequests.Results.Error', + defaultMessage: '' + }, + 'Search.HTTPRequests.Results.Blocked': { + id: 'Search.HTTPRequests.Results.Blocked', + defaultMessage: '' + }, + 'Search.HTTPRequests.Results.Anomaly': { + id: 'Search.HTTPRequests.Results.Anomaly', + defaultMessage: '' + }, + 'Search.Tor.Results.Reachable': { + id: 'General.OK', + defaultMessage: '' + }, + 'Search.Tor.Results.Anomaly': { + id: 'General.Anomaly', + defaultMessage: '' + }, + 'Search.Tor.Results.Error': { + id: 'General.Error', + defaultMessage: '' + }, + 'Search.TorSnowflake.Results.Reachable': { + id: 'General.OK', + defaultMessage: 'Reachable' + }, + 'Search.TorSnowflake.Results.Anomaly': { + id: 'General.Anomaly', + defaultMessage: 'Anomaly' + }, + 'Search.TorSnowflake.Results.Error': { + id: 'General.Error', + defaultMessage: 'Anomaly' + }, + 'Search.Psiphon.Results.Reachable': { + id: 'General.OK', + defaultMessage: '' + }, + 'Search.Psiphon.Results.Anomaly': { + id: 'General.Anomaly', + defaultMessage: '' + }, + 'Search.Psiphon.Results.Error': { + id: 'General.Error', + defaultMessage: '' + }, + 'Search.RiseupVPN.Results.Reachable': { + id: 'General.Accessible', + defaultMessage: '' + }, + 'Search.RiseupVPN.Results.Anomaly': { + id: 'General.Anomaly', + defaultMessage: '' + }, + 'Search.RiseupVPN.Results.Error': { + id: 'General.Error', + defaultMessage: '' + }, +}) + const ASNBox = ({asn}) => { const justNumber = asn.split('AS')[1] return AS {justNumber} @@ -133,14 +288,14 @@ const getIndicators = ({ test_name, testDisplayName, scores = {}, confirmed, ano color = colorError tag = ( - {intl.formatMessage({id:`${computedMessageIdPrefix}.Error`, defaultMessage: ''})} + {intl.formatMessage(messages[`${computedMessageIdPrefix}.Error`])} ) } else if (confirmed === true) { color = colorConfirmed tag = ( - {intl.formatMessage({id: `${computedMessageIdPrefix}.Blocked`, defaultMessage: ''})} + {intl.formatMessage(messages[`${computedMessageIdPrefix}.Blocked`])} ) } else if (blockingType !== undefined) { @@ -154,14 +309,14 @@ const getIndicators = ({ test_name, testDisplayName, scores = {}, confirmed, ano color = colorAnomaly tag = ( - {intl.formatMessage({id:`${computedMessageIdPrefix}.Anomaly`, defaultMessage: ''})} + {intl.formatMessage(messages[`${computedMessageIdPrefix}.Anomaly`])} ) } else { color = colorNormal tag = ( - {intl.formatMessage({id: `${computedMessageIdPrefix}.Reachable`, defaultMessage: ''})} + {intl.formatMessage(messages[`${computedMessageIdPrefix}.Reachable`])} ) } diff --git a/components/utils/categoryCodes.js b/components/utils/categoryCodes.js index 1cdbcc8af..ab9093089 100644 --- a/components/utils/categoryCodes.js +++ b/components/utils/categoryCodes.js @@ -158,7 +158,7 @@ export const categoryCodes = [ export const getCategoryCodesMap = () => { const map = categoryCodes.reduce((acc, [code, name, description]) => - acc.set(code, {name, description}) + acc.set(code, {code, name: `CategoryCode.${code}.Name`, description: `CategoryCode.${code}.Description`}) , new Map()) return map } diff --git a/components/withIntl.js b/components/withIntl.js index 80edd1bd1..67ff6bc5e 100644 --- a/components/withIntl.js +++ b/components/withIntl.js @@ -1,41 +1,50 @@ /* global require */ -import React, {Component} from 'react' +import React, { useEffect, useState, useMemo, createContext } from 'react' import { IntlProvider } from 'react-intl' +import { useRouter } from 'next/router' -let messages = { - en: require('../public/static/lang/en.json') -} - -const getLocale = () => { - let navigatorLang = 'en-US' - if (typeof window !== 'undefined') { - navigatorLang = window.navigator.userLanguage || window.navigator.language +export const getDirection = locale => { + switch (locale) { + case 'fa': + return 'rtl' + default: + return 'ltr' } - return navigatorLang.split('-')[0] } -if (typeof window !== 'undefined' && window.OONITranslations) { - messages = window.OONITranslations -} +export const LocaleProvider = ({ children }) => { + const { locale, defaultLocale } = useRouter() -const withIntl = (Page) => { + const messages = useMemo(() => { + try { + const messages = require(`../public/static/lang/${locale}.json`) + const defaultMessages = require(`../public/static/lang/${defaultLocale}.json`) - return class PageWithIntl extends Component { - render () { - const now = Date.now() - let locale = getLocale() - // Use 'en' when locale is unsupported - if (Object.keys(messages).indexOf(locale) < 0) { - locale = 'en' - } - const messagesToLoad = Object.assign({}, messages[locale], messages['en']) - return ( - - - - ) + const mergedMessages = Object.assign({}, defaultMessages, messages) + return mergedMessages + } catch (e) { + console.error(`Failed to load messages for ${locale}: ${e.message}`) + const defaultMessages = require(`../public/static/lang/${defaultLocale}.json`) + return defaultMessages } + }, [locale, defaultLocale]) + + const fixedLocale = (locale) => { + if (locale === 'pt_BR') return 'pt' + if (locale === 'pt_PT') return 'pt-PT' + if (locale === 'zh_CN') return 'zh-Hant' + if (locale === 'zh_HK') return 'zh-Hant-HK' + return locale } -} -export default withIntl + return ( + + {children} + + ) +} diff --git a/cypress/e2e/measurement.e2e.cy.js b/cypress/e2e/measurement.e2e.cy.js index dfa4135d8..71336044b 100644 --- a/cypress/e2e/measurement.e2e.cy.js +++ b/cypress/e2e/measurement.e2e.cy.js @@ -10,19 +10,19 @@ describe('Measurement Page Tests', () => { it('renders a valid accessible og:description', () => { cy.visit('/measurement/20221110T100756Z_webconnectivity_US_13335_n1_KWJqHUAPqMdtf2Up?input=https%3A%2F%2Fwww.theguardian.com%2F') cy.get('head meta[property="og:description"]') - .should('have.attr', 'content', 'OONI data suggests www.theguardian.com was accessible in United States on November 10, 2022, 10:09:16 AM UTC, find more open data on internet censorship on OONI Explorer.') + .should('have.attr', 'content', 'OONI data suggests www.theguardian.com was accessible in United States on November 10, 2022 at 10:09:16 AM UTC, find more open data on internet censorship on OONI Explorer.') }) it('renders a valid blocked og:description', () => { cy.visit('/measurement/20211215T052819Z_webconnectivity_RU_8369_n1_PkPgEYV2DrBAfPxu?input=http%3A%2F%2Frutor.org') cy.get('head meta[property="og:description"]') - .should('have.attr', 'content', 'OONI data suggests rutor.org was blocked in Russia on December 15, 2021, 5:44:55 AM UTC, find more open data on internet censorship on OONI Explorer.') + .should('have.attr', 'content', 'OONI data suggests rutor.org was blocked in Russia on December 15, 2021 at 5:44:55 AM UTC, find more open data on internet censorship on OONI Explorer.') }) it('renders a valid anomaly og:description', () => { cy.visit('/measurement/20221110T082316Z_webconnectivity_MY_4788_n1_Ue0y9OwyBLvoIfgm?input=http%3A%2F%2Fwww.google.com%2Fsearch%3Fq%3Dlesbian') cy.get('head meta[property="og:description"]') - .should('have.attr', 'content', 'OONI data suggests www.google.com was accessible in Malaysia on November 10, 2022, 10:25:51 AM UTC, find more open data on internet censorship on OONI Explorer.') + .should('have.attr', 'content', 'OONI data suggests www.google.com was accessible in Malaysia on November 10, 2022 at 10:25:51 AM UTC, find more open data on internet censorship on OONI Explorer.') }) // it('renders a valid website down og:description', () => { @@ -70,13 +70,13 @@ describe('Measurement Page Tests', () => { it('renders a reachable og:description', () => { cy.visit('/measurement/20221110T102855Z_telegram_US_7018_n1_HKJ2sF9m0lP7JMsW') cy.get('head meta[property="og:description"]') - .should('have.attr', 'content', 'OONI data suggests Telegram was reachable in United States on November 10, 2022, 10:28:56 AM UTC, find more open data on internet censorship on OONI Explorer.') + .should('have.attr', 'content', 'OONI data suggests Telegram was reachable in United States on November 10, 2022 at 10:28:56 AM UTC, find more open data on internet censorship on OONI Explorer.') }) it('renders a unreachable og:description', () => { cy.visit('measurement/20221109T225726Z_telegram_RU_8402_n1_qnYloXASGMUg2G9O') cy.get('head meta[property="og:description"]') - .should('have.attr', 'content', 'OONI data suggests Telegram was NOT reachable in Russia on November 9, 2022, 10:57:59 PM UTC, find more open data on internet censorship on OONI Explorer.') + .should('have.attr', 'content', 'OONI data suggests Telegram was NOT reachable in Russia on November 9, 2022 at 10:57:59 PM UTC, find more open data on internet censorship on OONI Explorer.') }) it('renders an accessible measurement', () => { @@ -96,13 +96,13 @@ describe('Measurement Page Tests', () => { it('renders a reachable og:description', () => { cy.visit('/measurement/20221110T103853Z_whatsapp_US_7922_n1_JPLapx8JfJ0J4nf4') cy.get('head meta[property="og:description"]') - .should('have.attr', 'content', 'OONI data suggests WhatsApp was reachable in United States on November 10, 2022, 10:38:53 AM UTC, find more open data on internet censorship on OONI Explorer.') + .should('have.attr', 'content', 'OONI data suggests WhatsApp was reachable in United States on November 10, 2022 at 10:38:53 AM UTC, find more open data on internet censorship on OONI Explorer.') }) it('renders an unreachable og:description', () => { cy.visit('/measurement/20221105T223928Z_whatsapp_JP_55392_n1_aL6HH9GHYc1YbILm') cy.get('head meta[property="og:description"]') - .should('have.attr', 'content', 'OONI data suggests WhatsApp was likely blocked in Japan on November 5, 2022, 10:39:29 PM UTC, find more open data on internet censorship on OONI Explorer.') + .should('have.attr', 'content', 'OONI data suggests WhatsApp was likely blocked in Japan on November 5, 2022 at 10:39:29 PM UTC, find more open data on internet censorship on OONI Explorer.') }) it('renders an accessible measurement', () => { @@ -131,7 +131,7 @@ describe('Measurement Page Tests', () => { cy.heroHasColor(normalColor) .contains('OK') cy.get('head meta[property="og:description"]') - .should('have.attr', 'content', 'OONI data suggests Signal was reachable in Brazil on April 14, 2021, 11:32:39 PM UTC, find more open data on internet censorship on OONI Explorer.') + .should('have.attr', 'content', 'OONI data suggests Signal was reachable in Brazil on April 14, 2021 at 11:32:39 PM UTC, find more open data on internet censorship on OONI Explorer.') }) it('renders an anomaly measurement', () => { @@ -139,7 +139,7 @@ describe('Measurement Page Tests', () => { cy.heroHasColor(anomalyColor) .contains('Anomaly') cy.get('head meta[property="og:description"]') - .should('have.attr', 'content', 'OONI data suggests Signal was NOT reachable in Iran on April 15, 2021, 8:42:27 AM UTC, find more open data on internet censorship on OONI Explorer.') + .should('have.attr', 'content', 'OONI data suggests Signal was NOT reachable in Iran on April 15, 2021 at 8:42:27 AM UTC, find more open data on internet censorship on OONI Explorer.') }) }) @@ -159,13 +159,13 @@ describe('Measurement Page Tests', () => { it('renders a reachable og:description', () => { cy.visit('/measurement/20221110T104252Z_facebookmessenger_US_20115_n1_o61hepYQFOp1mtT9') cy.get('head meta[property="og:description"]') - .should('have.attr', 'content', 'OONI data suggests Facebook Messenger was reachable in United States on November 10, 2022, 10:42:52 AM UTC, find more open data on internet censorship on OONI Explorer.') + .should('have.attr', 'content', 'OONI data suggests Facebook Messenger was reachable in United States on November 10, 2022 at 10:42:52 AM UTC, find more open data on internet censorship on OONI Explorer.') }) it('renders a unreachable og:description', () => { cy.visit('/measurement/20221110T103257Z_facebookmessenger_RU_12389_n1_I1KmLISJCV1o4EoV') cy.get('head meta[property="og:description"]') - .should('have.attr', 'content', 'OONI data suggests Facebook Messenger was NOT reachable in Russia on November 10, 2022, 10:32:58 AM UTC, find more open data on internet censorship on OONI Explorer.') + .should('have.attr', 'content', 'OONI data suggests Facebook Messenger was NOT reachable in Russia on November 10, 2022 at 10:32:58 AM UTC, find more open data on internet censorship on OONI Explorer.') }) }) @@ -183,12 +183,12 @@ describe('Measurement Page Tests', () => { it('renders a valid og:description', () => { cy.visit('/measurement/20221110T105736Z_httpheaderfieldmanipulation_ES_57269_n1_8SnGox89HKlVQoDJ') cy.get('head meta[property="og:description"]') - .should('have.attr', 'content', 'OONI data suggests HTTP header manipulation was not detected in Spain on November 10, 2022, 10:57:36 AM UTC, find more open data on internet censorship on OONI Explorer.') + .should('have.attr', 'content', 'OONI data suggests HTTP header manipulation was not detected in Spain on November 10, 2022 at 10:57:36 AM UTC, find more open data on internet censorship on OONI Explorer.') }) it('renders an anomaly og:description', () => { cy.visit('/measurement/20221110T104927Z_httpheaderfieldmanipulation_IR_58224_n1_voFn4ODgxZHJCpoy') cy.get('head meta[property="og:description"]') - .should('have.attr', 'content', 'OONI data suggests HTTP header manipulation was detected in Iran on November 10, 2022, 10:49:28 AM UTC, find more open data on internet censorship on OONI Explorer.') + .should('have.attr', 'content', 'OONI data suggests HTTP header manipulation was detected in Iran on November 10, 2022 at 10:49:28 AM UTC, find more open data on internet censorship on OONI Explorer.') }) }) @@ -206,12 +206,12 @@ describe('Measurement Page Tests', () => { it('renders a valid og:description', () => { cy.visit('/measurement/20221110T105936Z_httpinvalidrequestline_TR_47331_n1_fB1HONVuo6bJAcbz') cy.get('head meta[property="og:description"]') - .should('have.attr', 'content', 'OONI data suggests Network traffic manipulation was not detected in Turkey on November 10, 2022, 10:59:36 AM UTC, find more open data on internet censorship on OONI Explorer.') + .should('have.attr', 'content', 'OONI data suggests Network traffic manipulation was not detected in Turkey on November 10, 2022 at 10:59:36 AM UTC, find more open data on internet censorship on OONI Explorer.') }) it('render an anomaly og:description', () => { cy.visit('/measurement/20221110T105942Z_httpinvalidrequestline_US_13335_n1_cwbvshRglfEgMGAF') cy.get('head meta[property="og:description"]') - .should('have.attr', 'content', 'OONI data suggests Network traffic manipulation was detected in United States on November 10, 2022, 10:59:45 AM UTC, find more open data on internet censorship on OONI Explorer.') + .should('have.attr', 'content', 'OONI data suggests Network traffic manipulation was detected in United States on November 10, 2022 at 10:59:45 AM UTC, find more open data on internet censorship on OONI Explorer.') }) }) @@ -231,7 +231,7 @@ describe('Measurement Page Tests', () => { it('renders a valid og:description', () => { cy.visit('/measurement/20221110T105028Z_ndt_DE_3209_n1_9eyIJwUdcD6u3WgC') cy.get('head meta[property="og:description"]') - .should('have.attr', 'content', 'OONI data suggests Speed test result (NDT Test) in Germany on November 10, 2022, 10:50:28 AM UTC, find more open data on internet censorship on OONI Explorer.') + .should('have.attr', 'content', 'OONI data suggests Speed test result (NDT Test) in Germany on November 10, 2022 at 10:50:28 AM UTC, find more open data on internet censorship on OONI Explorer.') }) }) @@ -249,7 +249,7 @@ describe('Measurement Page Tests', () => { it('renders a valid og:description', () => { cy.visit('/measurement/20221110T111104Z_dash_US_19969_n1_kyclVb6Fj9VuW3A9') cy.get('head meta[property="og:description"]') - .should('have.attr', 'content', 'OONI data suggests 2160p (4k) quality video streaming at 539.09 Mbit/s speed in United States on November 10, 2022, 11:11:04 AM UTC, find more open data on internet censorship on OONI Explorer.') + .should('have.attr', 'content', 'OONI data suggests 2160p (4k) quality video streaming at 539.09 Mbit/s speed in United States on November 10, 2022 at 11:11:04 AM UTC, find more open data on internet censorship on OONI Explorer.') }) }) @@ -267,7 +267,7 @@ describe('Measurement Page Tests', () => { it('renders a reachable og:description', () => { cy.visit('/measurement/20221110T112242Z_psiphon_US_7018_n1_clM85Z0Pof3RqXdp') cy.get('head meta[property="og:description"]') - .should('have.attr', 'content', 'OONI data suggests Psiphon was reachable in United States on November 10, 2022, 11:22:43 AM UTC, find more open data on internet censorship on OONI Explorer.') + .should('have.attr', 'content', 'OONI data suggests Psiphon was reachable in United States on November 10, 2022 at 11:22:43 AM UTC, find more open data on internet censorship on OONI Explorer.') }) }) @@ -285,7 +285,7 @@ describe('Measurement Page Tests', () => { it('renders a valid og:description', () => { cy.visit('/measurement/20221110T112301Z_tor_US_7018_n1_uszAjDiPyoWLMyuV') cy.get('head meta[property="og:description"]') - .should('have.attr', 'content', 'OONI data suggests Tor censorship test result in United States on November 10, 2022, 11:23:02 AM UTC, find more open data on internet censorship on OONI Explorer.') + .should('have.attr', 'content', 'OONI data suggests Tor censorship test result in United States on November 10, 2022 at 11:23:02 AM UTC, find more open data on internet censorship on OONI Explorer.') }) }) @@ -293,13 +293,13 @@ describe('Measurement Page Tests', () => { it('URL with invalid report_id says measurement was not found', () => { const reportIdNotInDB = 'this-measurement-does-not-exist' cy.visit(`/measurement/${reportIdNotInDB}`) - cy.get('h4').contains('Measurement Not Found') + cy.get('h4').contains('Measurement not found') .siblings('div').contains(reportIdNotInDB) }) it('Missing report_id in URL says the page cannot be found', () => { cy.visit('/measurement/', {failOnStatusCode: false}) // bypasss 4xx errors - cy.get('h4').contains('Measurement Not Found') + cy.get('h4').contains('Measurement not found') }) }) diff --git a/cypress/e2e/search.e2e.cy.js b/cypress/e2e/search.e2e.cy.js index 0a4874ec5..1826fb551 100644 --- a/cypress/e2e/search.e2e.cy.js +++ b/cypress/e2e/search.e2e.cy.js @@ -68,7 +68,7 @@ describe('Search Page Tests', () => { expect(lastOfPreviousMonth).to.equal(selectedUntilDate) }) - cy.get('[data-test-id="testname-filter"]').select('Telegram') + cy.get('[data-test-id="testname-filter"]').select('Telegram Test') cy.get('label').contains('Anomalies').click() cy.get('label').contains('All Results').click() @@ -76,7 +76,7 @@ describe('Search Page Tests', () => { it('conditional filters are hidden and shown depending on selections', () => { cy.get('[data-test-id="domain-filter"]').should('not.exist') - cy.get('[data-test-id="testname-filter"]').select('Web Connectivity') + cy.get('[data-test-id="testname-filter"]').select('Web Connectivity Test') cy.get('[data-test-id="domain-filter"]').should('be.visible') }) }) diff --git a/next.config.js b/next.config.js index d0f3a28e5..516474b53 100644 --- a/next.config.js +++ b/next.config.js @@ -15,6 +15,18 @@ const SentryWebpackPluginOptions = { silent: false, } +const LANG_DIR = './public/static/lang/' +const DEFAULT_LOCALE = 'en' + +function getSupportedLanguages() { + const supportedLanguages = new Set() + supportedLanguages.add(DEFAULT_LOCALE) // at least 1 supported language + glob.sync(`${LANG_DIR}/**/*.json`).forEach((f) => + supportedLanguages.add(basename(f, '.json')) + ) + return [...supportedLanguages] +} + module.exports = withSentryConfig({ output: 'standalone', async redirects() { @@ -32,6 +44,10 @@ module.exports = withSentryConfig({ ssr: true, }, }, + i18n: { + locales: getSupportedLanguages(), + defaultLocale: DEFAULT_LOCALE, + }, webpack: (config, options) => { const gitCommitSHAShort = process.env.RUN_GIT_COMMIT_SHA_SHORT ? execSync(process.env.RUN_GIT_COMMIT_SHA_SHORT) : '' const gitCommitSHA = process.env.RUN_GIT_COMMIT_SHA ? execSync(process.env.RUN_GIT_COMMIT_SHA) : '' @@ -40,14 +56,16 @@ module.exports = withSentryConfig({ config.plugins.push( new options.webpack.DefinePlugin({ - 'process.env.GIT_COMMIT_SHA_SHORT': JSON.stringify( - gitCommitSHAShort.toString() - ), - 'process.env.GIT_COMMIT_SHA': JSON.stringify(gitCommitSHA.toString()), - 'process.env.GIT_COMMIT_REF': JSON.stringify(gitCommitRef.toString()), - 'process.env.GIT_COMMIT_TAGS': JSON.stringify( - gitCommitTags.toString() - ), + 'process.env.GIT_COMMIT_SHA_SHORT': JSON.stringify( + gitCommitSHAShort.toString() + ), + 'process.env.GIT_COMMIT_SHA': JSON.stringify(gitCommitSHA.toString()), + 'process.env.GIT_COMMIT_REF': JSON.stringify(gitCommitRef.toString()), + 'process.env.GIT_COMMIT_TAGS': JSON.stringify( + gitCommitTags.toString() + ), + 'process.env.DEFAULT_LOCALE': DEFAULT_LOCALE, + 'process.env.LOCALES': JSON.stringify(getSupportedLanguages()), 'process.env.WDYR': JSON.stringify(process.env.WDYR), }) ) diff --git a/package.json b/package.json index bd242dabf..9ec2e6e1e 100644 --- a/package.json +++ b/package.json @@ -8,6 +8,7 @@ "dependencies": { "@babel/core": "7.18.10", "@datapunt/matomo-tracker-react": "^0.5.1", + "@fontsource/fira-sans": "^4.5.9", "@nivo/bar": "^0.80.0", "@nivo/calendar": "^0.80.0", "@nivo/core": "^0.80.0", @@ -24,7 +25,6 @@ "dayjs": "^1.11.5", "deepmerge": "^4.2.2", "flag-icon-css": "^4.1.7", - "fontsource-fira-sans": "^4.0.0", "lodash.debounce": "^4.0.8", "markdown-to-jsx": "^7.1.7", "next": "^12.3.1", diff --git a/pages/404.js b/pages/404.js index 4c6d69e66..3b27f36db 100644 --- a/pages/404.js +++ b/pages/404.js @@ -9,18 +9,18 @@ import { Text, Heading } from 'ooni-components' -import { FormattedMessage } from 'react-intl' +import { FormattedMessage, useIntl } from 'react-intl' -import Layout from '../components/Layout' import NavBar from '../components/NavBar' import OONI404 from '../public/static/images/OONI_404.svg' const Custom404 = () => { const router = useRouter() + const intl = useIntl() return ( - + <> - Page Not Found + {intl.formatMessage({id: 'Error.404.PageNotFound'})} @@ -58,7 +58,7 @@ const Custom404 = () => { - + ) } diff --git a/pages/_app.js b/pages/_app.js index e90e07d07..ef3425645 100644 --- a/pages/_app.js +++ b/pages/_app.js @@ -3,12 +3,15 @@ // https://github.com/vercel/next.js/blob/canary/examples/with-loading/pages/_app.js import 'scripts/wdyr' import 'regenerator-runtime/runtime' -import { useEffect } from 'react' +import { useEffect, useId, useMemo, useState } from 'react' +import 'regenerator-runtime/runtime' import NProgress from 'nprogress' import { useRouter } from 'next/router' -import 'fontsource-fira-sans/latin.css' +import '@fontsource/fira-sans' import '../public/static/nprogress.css' +import Layout from '../components/Layout' +import { LocaleProvider } from 'components/withIntl' export default function App({ Component, pageProps, err }) { const router = useRouter() @@ -34,5 +37,11 @@ export default function App({ Component, pageProps, err }) { }, [router]) // Workaround for https://github.com/vercel/next.js/issues/8592 - return + return ( + + + + + + ) } diff --git a/pages/_document.js b/pages/_document.js index de2511cc6..a3615cc17 100644 --- a/pages/_document.js +++ b/pages/_document.js @@ -13,7 +13,7 @@ export default class MyDocument extends Document { render () { return ( - + {this.props.styleTags} diff --git a/pages/chart/circumvention.js b/pages/chart/circumvention.js index 7c3321030..f39a9c17b 100644 --- a/pages/chart/circumvention.js +++ b/pages/chart/circumvention.js @@ -4,7 +4,6 @@ import { Container, Heading, Box } from 'ooni-components' import { FormattedMessage } from 'react-intl' import axios from 'axios' -import Layout from 'components/Layout' import NavBar from 'components/NavBar' import { MetaTags } from 'components/dashboard/MetaTags' import { Form } from 'components/dashboard/Form' @@ -41,20 +40,20 @@ const DashboardCircumvention = ({ availableCountries }) => { }, [router, query]) return ( - + <> - {router.isReady && + {router.isReady && <> - } + } - + ) } diff --git a/pages/chart/mat.js b/pages/chart/mat.js index a680b4e02..2df1e6497 100644 --- a/pages/chart/mat.js +++ b/pages/chart/mat.js @@ -11,7 +11,7 @@ import { Link } from 'ooni-components' import useSWR from 'swr' -import { FormattedMessage } from 'react-intl' +import { FormattedMessage, useIntl } from 'react-intl' import Layout from 'components/Layout' import NavBar from 'components/NavBar' @@ -71,7 +71,7 @@ const fetcher = (query) => { } const MeasurementAggregationToolkit = ({ testNames }) => { - + const intl = useIntl() const router = useRouter() const onSubmit = useCallback((data) => { @@ -134,59 +134,55 @@ const MeasurementAggregationToolkit = ({ testNames }) => { return ( - - - OONI Measurement Aggregation Toolkit - - - - - - - - - - {error && - + + {intl.formatMessage({id: 'MAT.Title'})} + + + + + + + + + + {error && + + } + + {showLoadingIndicator && + +

{intl.formatMessage({id: 'General.Loading'})}

+
+ } + {data && data.data.dimension_count == 0 && + + } + {data && data.data.dimension_count == 1 && + + } + {data && data.data.dimension_count > 1 && + } - - {showLoadingIndicator && + + {linkToAPIQuery && + + -

Loading ...

+ {intl.formatMessage({id: 'MAT.JSONData'})} +
- } - {data && data.data.dimension_count == 0 && - - } - {data && data.data.dimension_count == 1 && - - } - {data && data.data.dimension_count > 1 && - - } -
- {linkToAPIQuery && - - - - - JSON Data - - - - - CSV Data - - - - - } - - + + {intl.formatMessage({id: 'MAT.CSVData'})} + + +
- - - + } + + + + + ) } diff --git a/pages/countries.js b/pages/countries.js index bd5525f7f..7c21560c8 100644 --- a/pages/countries.js +++ b/pages/countries.js @@ -1,7 +1,8 @@ -import React from 'react' +import React, { useMemo, useState } from 'react' import Head from 'next/head' import NLink from 'next/link' import axios from 'axios' +import { useIntl } from 'react-intl' import styled from 'styled-components' import { FormattedMessage, FormattedNumber } from 'react-intl' import debounce from 'lodash.debounce' @@ -14,10 +15,10 @@ import { import { StickyContainer, Sticky } from 'react-sticky' import Flag from '../components/Flag' -import Layout from '../components/Layout' import NavBar from '../components/NavBar' import countryUtil from 'country-util' +import { getLocalisedRegionName } from 'utils/i18nCountries' const CountryLink = styled(Link)` color: ${props => props.theme.colors.black}; @@ -37,6 +38,7 @@ const Divider = styled.div` ` const CountryBlock = ({countryCode, msmtCount}) => { + const intl = useIntl() const href = `/country/${countryCode}` return ( @@ -45,11 +47,11 @@ const CountryBlock = ({countryCode, msmtCount}) => { - {countryUtil.territoryNames[countryCode]} + {getLocalisedRegionName(countryCode, intl.locale)} - Measurements + {intl.formatMessage({id: 'Home.Banner.Stats.Measurements'})} @@ -82,7 +84,13 @@ const RegionHeaderAnchor = styled.div` ` const RegionBlock = ({regionCode, countries}) => { - const regionName = countryUtil.territoryNames[regionCode] + const intl = useIntl() + + countries = countries + .map((c) => ({...c, localisedName: getLocalisedRegionName(c.alpha_2, intl.locale)})) + .sort((a, b) => (new Intl.Collator(intl.locale).compare(a.localisedName, b.localisedName))) + + const regionName = getLocalisedRegionName(regionCode, intl.locale) // Select countries in the region where we have measuremennts from const measuredCountriesInRegion = countryUtil.regions[regionCode].countries.filter((countryCode) => ( countries.find((item) => item.alpha_2 === countryCode) @@ -145,118 +153,96 @@ const NoCountriesFound = ({ searchTerm }) => ( {/* TODO Add to copy */} ) -class Countries extends React.Component { - constructor (props) { - super(props) - this.state = { - initial: true, - searchTerm: '', - filteredCountries: [] - } - this.onSearchChange = debounce(this.onSearchChange, 200) - } - - static async getInitialProps () { - const client = axios.create({baseURL: process.env.NEXT_PUBLIC_OONI_API}) // eslint-disable-line +export const getServerSideProps = async () => { + const client = axios.create({baseURL: process.env.NEXT_PUBLIC_OONI_API}) // eslint-disable-line const result = await client.get('/api/_/countries') - - // Sort countries by name (instead of by country codes) - result.data.countries.sort((a,b) => a.name < b.name ? -1 : 1) - const responseUrl = result?.request?.res?.responseUrl return { - countries: result.data.countries, - ssrRequests: [{...result.config, responseUrl}] + props: { + countries: result.data.countries, + } } - } +} - componentDidMount () { - console.log(this.props.ssrRequests) - } +const Countries = ({countries}) => { + const intl = useIntl() + const [searchInput, setSearchInput] = useState('') - onSearchChange (searchTerm) { - const filteredCountries = this.props.countries.filter((country) => ( - country.name.toLowerCase().indexOf(searchTerm.toLowerCase()) > -1 + let filteredCountries = countries + + if (searchInput !== '') { + filteredCountries = countries.filter((country) => ( + country.name.toLowerCase().indexOf(searchInput.toLowerCase()) > -1 )) - this.setState({ - filteredCountries, - searchTerm - }) } - static getDerivedStateFromProps (props, state) { - if (state.filteredCountries.length === 0 && state.initial === true) { - return { - filteredCountries: props.countries, - initial: false - } - } - return state + const searchHandler = (searchTerm) => { + setSearchInput(searchTerm) } - render () { - const { filteredCountries, searchTerm } = this.state - // Africa Americas Asia Europe Oceania Antarctica - const regions = ['002', '019', '142', '150', '009', 'AQ'] + const debouncedSearchHandler = useMemo(() => debounce(searchHandler, 200), []) - return ( - - - Internet Censorship around the world | OONI Explorer - + // Africa Americas Asia Europe Oceania Antarctica + const regions = ['002', '019', '142', '150', '009', 'AQ'] - - - {({ style }) => ( - - - - - - - this.onSearchChange(e.target.value)} - placeholder='Search for Countries' - error={filteredCountries.length === 0} - /> - - - - - - - - - - - )} - - - { - // Show a message when there are no countries to show, when search is empty - (filteredCountries.length === 0) - ? - : regions.map((regionCode, index) => ( - - )) - } - - - - ) - } + return ( + <> + + {intl.formatMessage({id: 'Countries.PageTitle'})} + + + + + {({ style }) => ( + + + + + + + debouncedSearchHandler(e.target.value)} + placeholder={intl.formatMessage({id: 'Countries.Search.Placeholder'})} + error={filteredCountries.length === 0} + /> + + + + + + + + + + + + )} + + + { + // Show a message when there are no countries to show, when search is empty + (filteredCountries.length === 0) + ? + : regions.map((regionCode, index) => ( + + )) + } + + + + ) } export default Countries diff --git a/pages/country/[countryCode].js b/pages/country/[countryCode].js index 95b4c4d34..c569c4c04 100644 --- a/pages/country/[countryCode].js +++ b/pages/country/[countryCode].js @@ -6,13 +6,12 @@ import { Heading, Flex, Box } from 'ooni-components' -import countryUtil from 'country-util' import styled from 'styled-components' import { StickyContainer, Sticky } from 'react-sticky' +import { getLocalisedRegionName } from '../../utils/i18nCountries' import NavBar from '../../components/NavBar' import Flag from '../../components/Flag' -import Layout from '../../components/Layout' import PageNavMenu from '../../components/country/PageNavMenu' import Overview from '../../components/country/Overview' import WebsitesSection from '../../components/country/Websites' @@ -20,6 +19,7 @@ import AppsSection from '../../components/country/Apps' // import NetworkPropertiesSection from '../../components/country/NetworkProperties' import { CountryContextProvider } from '../../components/country/CountryContext' import CountryHead from '../../components/country/CountryHead' +import { useIntl } from 'react-intl' const getCountryReports = (countryCode, data) => { const reports = data.filter((article) => ( @@ -73,15 +73,15 @@ export async function getServerSideProps ({ res, query }) { overviewStats, reports, countryCode, - countryName: countryUtil.territoryNames[countryCode] } } } - -const Country = ({ countryCode, countryName, overviewStats, reports, ...coverageDataSSR }) => { +const Country = ({ countryCode, overviewStats, reports, ...coverageDataSSR }) => { + const intl = useIntl() const [newData, setNewData] = useState(false) + const countryName = getLocalisedRegionName(countryCode, intl.locale) const fetchTestCoverageData = useCallback((testGroupList) => { console.log(testGroupList) @@ -106,7 +106,7 @@ const Country = ({ countryCode, countryName, overviewStats, reports, ...coverage const { testCoverage, networkCoverage } = newData !== false ? newData : coverageDataSSR return ( - + <> @@ -160,7 +160,7 @@ const Country = ({ countryCode, countryName, overviewStats, reports, ...coverage - + ) } diff --git a/pages/index.js b/pages/index.js index 6a4aff97d..b4745a85d 100644 --- a/pages/index.js +++ b/pages/index.js @@ -7,7 +7,7 @@ import Router from 'next/router' import FormattedMarkdown from '../components/FormattedMarkdown' import styled from 'styled-components' import axios from 'axios' -import { FormattedMessage } from 'react-intl' +import { FormattedMessage, useIntl } from 'react-intl' import { Link, Flex, @@ -18,7 +18,6 @@ import { Text } from 'ooni-components' -import Layout from '../components/Layout' import NavBar from '../components/NavBar' import { toCompactNumberUnit } from '../utils' import HighlightSection from '../components/landing/HighlightsSection' @@ -113,10 +112,9 @@ FeatureBox.defaultProps = { lineHeight: 1.5, } -const FeatureBoxTitle = styled(Text)` +const FeatureBoxTitle = styled(Flex)` ` FeatureBoxTitle.defaultProps = { - textAlign: ['center', 'left'], color: 'blue9', fontSize: 24, fontWeight: 600, @@ -133,177 +131,178 @@ const StyledContainer = styled(Container)` background-position: center; ` -export default class LandingPage extends React.Component { +export async function getServerSideProps({ query }) { + const client = axios.create({baseURL: process.env.NEXT_PUBLIC_OONI_API}) // eslint-disable-line + const result = await client.get('/api/_/global_overview') - static async getInitialProps () { - const client = axios.create({baseURL: process.env.NEXT_PUBLIC_OONI_API}) // eslint-disable-line - const result = await client.get('/api/_/global_overview') - return { + return { + props: { measurementCount: result.data.measurement_count, asnCount: result.data.network_count, countryCount: result.data.country_count } } +} - constructor(props) { - super(props) - } - - render () { - let { - measurementCount, - asnCount, - countryCount - } = this.props +const LandingPage = ({ measurementCount, asnCount, countryCount}) => { + const intl = useIntl() + measurementCount = toCompactNumberUnit(measurementCount) + asnCount = toCompactNumberUnit(asnCount) - measurementCount = toCompactNumberUnit(measurementCount) - asnCount = toCompactNumberUnit(asnCount) + return ( + <> + + {intl.formatMessage({id: 'General.OoniExplorer'})} + + + + + + + + + + ( + Router.push('/chart/mat') + )}> + + + + + + + + } + unit={measurementCount.unit} + value={measurementCount.value} + /> + } + value={countryCount} + /> + } + unit={asnCount.unit} + value={asnCount.value} + /> + - return ( - - - OONI Explorer - - - - - - - - - - ( - Router.push('/chart/mat') - )}> - - + {/* Intro text about Explorer */} + + + + - - - - - } - unit={measurementCount.unit} - value={measurementCount.value} - /> - } - value={countryCount} - /> - } - unit={asnCount.unit} - value={asnCount.value} - /> - + + - {/* Intro text about Explorer */} - - - - + {/* Websites & Apps */} + + + + + + + + + + + + {/* Search & Filter */} + {/* Arrange in {[img, para], [img, para], [img, para]} pattern on smaller screens */} + + + + + + + + + + + + {/* Network Properties */} + + + + + + + + + + + + {/* Measurement Statistics */} + + + + + + + + + {/* Highlights */} + + + + + + + + + + + + - {/* Websites & Apps */} - - - - - - - - - - - - {/* Search & Filter */} - {/* Arrange in {[img, para], [img, para], [img, para]} pattern on smaller screens */} - - - - - - - - - - - - {/* Network Properties */} - - - - - - - - - - - - {/* Measurement Statistics */} - - - - - - - - - {/* Highlights */} - - - - - - - - - - - - - - - - - {/* Political Events */} - } - description={} - highlights={highlightContent.political} - /> - {/* Media */} - } - description={} - highlights={highlightContent.media} - /> - {/* LGBTQI sites */} - } - description={} - highlights={highlightContent.lgbtqi} - /> - {/* Censorship changes */} - } - description={} - highlights={highlightContent.changes} - /> - - We encourage you to explore OONI measurements to find more highlights! - - + {/* Political Events */} + } + description={} + highlights={highlightContent.political} + /> + {/* Media */} + } + description={} + highlights={highlightContent.media} + /> + {/* LGBTQI sites */} + } + description={} + highlights={highlightContent.lgbtqi} + /> + {/* Censorship changes */} + } + description={} + highlights={highlightContent.changes} + /> + + + ( + + {string} + + ) + }} + /> + + - - ) - } + + + ) } LandingPage.propTypes = { @@ -311,3 +310,5 @@ LandingPage.propTypes = { asnCount: PropTypes.number, measurementCount: PropTypes.number } + +export default LandingPage \ No newline at end of file diff --git a/pages/measurement/[[...report_id]].js b/pages/measurement/[[...report_id]].js index a694bc85b..07ab3c77a 100644 --- a/pages/measurement/[[...report_id]].js +++ b/pages/measurement/[[...report_id]].js @@ -2,9 +2,9 @@ import React from 'react' import PropTypes from 'prop-types' import Head from 'next/head' -import countryUtil from 'country-util' import axios from 'axios' import { Container, theme } from 'ooni-components' +import { getLocalisedRegionName } from '../../utils/i18nCountries' import Hero from '../../components/measurement/Hero' import CommonSummary from '../../components/measurement/CommonSummary' @@ -15,9 +15,9 @@ import MeasurementContainer from '../../components/measurement/MeasurementContai import MeasurementNotFound from '../../components/measurement/MeasurementNotFound' import HeadMetadata from '../../components/measurement/HeadMetadata' -import Layout from '../../components/Layout' import NavBar from '../../components/NavBar' import ErrorPage from '../_error' +import { useIntl } from 'react-intl' const pageColors = { default: theme.colors.base, @@ -83,12 +83,6 @@ export async function getServerSideProps({ query }) { initialProps['raw_measurement'] ? initialProps['raw_measurement'] = JSON.parse(initialProps['raw_measurement']) : initialProps.notFound = true - - const { probe_cc } = response.data - const countryObj = countryUtil.countryList.find(country => ( - country.iso3166_alpha2 === probe_cc - )) - initialProps['country'] = countryObj?.name || 'Unknown' } else { // Measurement not found initialProps.notFound = true @@ -103,7 +97,6 @@ export async function getServerSideProps({ query }) { const Measurement = ({ error, - country, confirmed, anomaly, failure, @@ -118,7 +111,8 @@ const Measurement = ({ scores, ...rest }) => { - + const intl = useIntl() + const country = getLocalisedRegionName(probe_cc, intl.locale) // Add the 'AS' prefix to probe_asn when API chooses to send just the number probe_asn = typeof probe_asn === 'number' ? `AS${probe_asn}` : probe_asn if (error) { @@ -128,9 +122,9 @@ const Measurement = ({ } return ( - + <> - OONI Explorer + {intl.formatMessage({id: 'General.OoniExplorer'})} {notFound ? ( @@ -161,7 +155,7 @@ const Measurement = ({ const color = failure === true ? pageColors['error'] : pageColors[status] const info = scores?.msg ?? statusInfo return ( - + <> {headMetadata && - + ) }} /> )} - + ) } Measurement.propTypes = { anomaly: PropTypes.bool, confirmed: PropTypes.bool, - country: PropTypes.string, error: PropTypes.string, failure: PropTypes.bool, input: PropTypes.any, diff --git a/pages/network/[asn].js b/pages/network/[asn].js index af76a6eb9..3bf15eefb 100644 --- a/pages/network/[asn].js +++ b/pages/network/[asn].js @@ -6,7 +6,6 @@ import { useIntl } from 'react-intl' import NLink from 'next/link' import styled from 'styled-components' import dayjs from 'services/dayjs' -import countryUtil from 'country-util' import Layout from 'components/Layout' import NavBar from 'components/NavBar' import { MetaTags } from 'components/dashboard/MetaTags' @@ -16,6 +15,7 @@ import Calendar from 'components/network/Calendar' import FormattedMarkdown from 'components/FormattedMarkdown' import { FormattedMessage } from 'react-intl' import CallToActionBox from 'components/CallToActionBox' +import { getLocalisedRegionName } from '../../utils/i18nCountries' const Bold = styled.span` font-weight: bold @@ -52,13 +52,13 @@ const ChartsContainer = () => { const Summary = ({ measurementsTotal, firstMeasurement, countriesData }) => { const intl = useIntl() - const formattedDate = dayjs(firstMeasurement).format('MMMM DD, YYYY') + const formattedDate = new Intl.DateTimeFormat(intl.locale, { dateStyle: 'long', timeZone: 'UTC' }).format(new Date(firstMeasurement)) const sortedCountries = countriesData.sort((a, b) => b.measurements - a.measurements) const countriesList = sortedCountries.map(function(c){ return (
  • - {countryUtil.territoryNames[c.country]} + {getLocalisedRegionName(c.country, intl.locale)}
  • @@ -109,7 +109,7 @@ const NetworkDashboard = ({asn, calendarData = [], measurementsTotal, countriesD }, [router, query, asn]) return ( - + <> @@ -132,7 +132,7 @@ const NetworkDashboard = ({asn, calendarData = [], measurementsTotal, countriesD } - + ) } diff --git a/pages/search.js b/pages/search.js index 1ef1da629..172fb2a89 100644 --- a/pages/search.js +++ b/pages/search.js @@ -11,7 +11,7 @@ import { Heading, Text } from 'ooni-components' -import { FormattedMessage } from 'react-intl' +import { FormattedMessage, useIntl } from 'react-intl' import dayjs from 'services/dayjs' import NavBar from '../components/NavBar' @@ -187,6 +187,7 @@ const NoResults = () => ( const Search = ({testNames, testNamesKeyed, countries, query: queryProp }) => { const router = useRouter() + const intl = useIntl() const { query, replace, isReady } = router const [nextURL, setNextURL] = useState(null) @@ -288,9 +289,9 @@ const Search = ({testNames, testNamesKeyed, countries, query: queryProp }) => { } return ( - + <> - Search through millions of Internet censorship measurements | OONI Explorer + {intl.formatMessage({id: 'Search.PageTitle'})} @@ -319,7 +320,7 @@ const Search = ({testNames, testNamesKeyed, countries, query: queryProp }) => { {loading && } {!error && !loading && results.length === 0 && } - {!error && !loading && results.length > 0 && + {!error && !loading && results.length > 0 && <> {nextURL && @@ -328,11 +329,11 @@ const Search = ({testNames, testNamesKeyed, countries, query: queryProp }) => { } - } + } - + ) } diff --git a/public/static/lang/en.json b/public/static/lang/en.json index d9649cde0..2a5f0d6f0 100644 --- a/public/static/lang/en.json +++ b/public/static/lang/en.json @@ -1,4 +1,16 @@ { + "General.OoniExplorer": "OONI Explorer", + "General.OK": "OK", + "General.Error": "Error", + "General.Anomaly": "Anomaly", + "General.Accessible": "Accessible", + "General.Failed": "Failed", + "General.Loading": "Loading\u2026", + "General.NoData": "No data", + "General.Apply": "Apply", + "General.Reset": "Reset", + "SocialButtons.CTA": "Share on Facebook or Twitter", + "SocialButtons.Text": "Data from OONI Explorer", "Tests.WebConnectivity.Name": "Web Connectivity Test", "Tests.Telegram.Name": "Telegram Test", "Tests.Facebook.Name": "Facebook Messenger Test", @@ -27,9 +39,8 @@ "Tests.Groups.Circumvention.Name": "Circumvention", "Tests.Groups.Experimental.Name": "Experimental", "Tests.Groups.Legacy.Name": "Legacy", - "Measurement.Hero.Status.Anomaly": "Anomaly", - "Measurement.Hero.Status.Reachable": "OK", - "Measurement.Hero.Status.Error": "Error", + "Measurement.MetaDescription": "OONI data suggests {description} on {formattedDate}, find more open data on internet censorship on OONI Explorer.", + "Measurement.NotFound": "Measurement not found", "Measurement.Hero.Status.Confirmed": "Confirmed Blocked", "Measurement.Hero.Status.Down": "Website Down", "Measurement.Hero.Status.Anomaly.DNS": "DNS", @@ -48,7 +59,7 @@ "Measurement.Status.Hint.Websites.TCPBlock": "TCP/IP blocking", "Measurement.Status.Hint.Websites.Unavailable": "Website down", "Measurement.SummaryText.Websites.Accessible": "On {date}, {WebsiteURL} was accessible when tested on {network} in {country}.", - "Measurement.SummaryText.Websites.Anomaly": "On {date}, {WebsiteURL} presented signs of {reason} on {network} in {country}.\n\nThis might mean that {WebsiteURL} was blocked, but false positives can occur. \n\nPlease explore the network measurement data below.", + "Measurement.SummaryText.Websites.Anomaly": "On {date}, {WebsiteURL} presented signs of {reason} on {network} in {country}.\n\nThis might mean that {WebsiteURL} was blocked, but false positives can occur.\n\nPlease explore the network measurement data below.", "Measurement.SummaryText.Websites.Anomaly.BlockingReason.DNS": "DNS tampering", "Measurement.SummaryText.Websites.Anomaly.BlockingReason.TCP": "TCP/IP blocking", "Measurement.SummaryText.Websites.Anomaly.BlockingReason.HTTP-failure": "HTTP blocking (HTTP requests failed)", @@ -60,20 +71,16 @@ "Measurement.Details.Websites.Failures.Label.HTTP": "HTTP Experiment", "Measurement.Details.Websites.Failures.Label.DNS": "DNS Experiment", "Measurement.Details.Websites.Failures.Label.Control": "Control", - "Measurement.Details.Websites.Failures.Values.Unknown": "No data", "Measurement.Details.Websites.Failures.Values.Null": "null", "Measurement.Details.Websites.DNSQueries.Heading": "DNS Queries", "Measurement.Details.Websites.DNSQueries.Label.Resolver": "Resolver", - "Measurement.Details.Websites.DNSQueries.NoData": "No data.", "Measurement.Details.Websites.TCP.Heading": "TCP Connections", - "Measurement.Details.Websites.TCP.NoData": "No data.", "Measurement.Details.Websites.TCP.ConnectionTo": "Connection to {destination} {connectionStatus}.", "Measurement.Details.Websites.TCP.ConnectionTo.Success": "succeeded", "Measurement.Details.Websites.TCP.ConnectionTo.Failed": "failed", "Measurement.Details.Websites.TCP.ConnectionTo.Blocked": "was blocked", "Measurement.Details.Websites.HTTP.Heading": "HTTP Requests", "Measurement.Details.Websites.HTTP.Label.Response": "Response", - "Measurement.Details.Websites.HTTP.NoData": "No Data", "Measurement.Details.Websites.HTTP.Request.URL": "URL", "Measurement.Details.Websites.HTTP.Response.Body": "Response Body", "Measurement.Details.Websites.HTTP.Response.Headers": "Response Headers", @@ -117,8 +124,6 @@ "Measurement.Details.SummaryText.Telegram.DesktopAndAppFailure": "On {date}, the testing of Telegram's mobile app and web interface (web.telegram.org) presented signs of blocking on {network} in {country}.\n\nThis might mean that both Telegram's mobile app and web interface were blocked, but [false positives](https://ooni.org/support/faq/#why-do-false-positives-occur) can occur.\n\nPlease explore the network measurement data below. Check other Telegram measurements from the same network during the same time period (if they're available).", "Measurement.Details.Telegram.Endpoint.Label.Mobile": "Mobile App", "Measurement.Details.Telegram.Endpoint.Label.Web": "Telegram Web", - "Measurement.Details.Endpoint.Status.Okay": "Okay", - "Measurement.Details.Endpoint.Status.Failed": "Failed", "Measurement.Details.Endpoint.Status.Unknown": "Unknown", "Measurement.Details.Telegram.Endpoint.Status.Heading": "Endpoint Status", "Measurement.Details.Telegram.Endpoint.ConnectionTo.Failed": "Connection to {destination} failed.", @@ -146,10 +151,6 @@ "Measurement.Details.SummaryText.FacebookMessenger.TCPSuccess": "TCP connections to Facebook's enpoints succeeded.", "Measurement.Details.FacebookMessenger.TCP.Label.Title": "TCP connections", "Measurement.Details.FacebookMessenger.DNS.Label.Title": "DNS lookups", - "Measurement.Details.FacebookMessenger.TCP.Label.Okay": "OK", - "Measurement.Details.FacebookMessenger.TCP.Label.Failed": "Failed", - "Measurement.Details.FacebookMessenger.DNS.Label.Okay": "OK", - "Measurement.Details.FacebookMessenger.DNS.Label.Failed": "Failed", "Measurement.Details.FacebookMessenger.TCPFailed": "TCP connections failed", "Measurement.Details.FacebookMessenger.DNSFailed": "DNS lookups failed", "Measurement.Details.FacebookMessenger.Endpoint.Status.Heading": "Endpoint Status", @@ -195,12 +196,11 @@ "Measurement.Details.Tor.Table.Header.Name": "Name", "Measurement.Details.Tor.Table.Header.Address": "Address", "Measurement.Details.Tor.Table.Header.Type": "Type", - "Measurement.Details.Tor.Table.Header.Accessible": "Accessible", - "Measurement.Status.Hint.TorSnowflake.Reachable": "Tor snowflake works", - "Measurement.Status.Hint.TorSnowflake.Blocked": "Tor snowflake does not work", - "Measurement.Status.Hint.TorSnowflake.Error": "Tor snowflake test failed", - "Measurement.Details.SummaryText.TorSnowflake.OK": "On {date}, [Tor Snowflake](https://www.torproject.org/) worked on {network} in {country}.\n\nThe [OONI Probe Tor Snowflake test](https://ooni.org/nettest/torsf/) was able to successfully bootstrap snowflake.", - "Measurement.Details.SummaryText.TorSnowflake.Blocked": "On {date}, [Tor Snowflake](https://www.torproject.org/) does not work on {network} in {country}.\n\nThe [OONI Probe Tor Snowflake test](https://ooni.org/nettest/torsf/) failed to bootstrap snowflake.", + "Measurement.Status.Hint.TorSnowflake.Reachable": "Tor Snowflake works", + "Measurement.Status.Hint.TorSnowflake.Blocked": "Tor Snowflake does not work", + "Measurement.Status.Hint.TorSnowflake.Error": "Tor Snowflake test failed", + "Measurement.Details.SummaryText.TorSnowflake.OK": "On {date}, [Tor Snowflake](https://www.torproject.org/) worked on {network} in {country}.\n\nThe [OONI Probe Tor Snowflake test](https://ooni.org/nettest/torsf/) was able to successfully bootstrap Snowflake.", + "Measurement.Details.SummaryText.TorSnowflake.Blocked": "On {date}, [Tor Snowflake](https://www.torproject.org/) does not work on {network} in {country}.\n\nThe [OONI Probe Tor Snowflake test](https://ooni.org/nettest/torsf/) failed to bootstrap Snowflake.", "Measurement.Details.SummaryText.TorSnowflake.Error": "On {date}, the Tor Snowflake test failed on {network} in {country}.", "Measurement.Details.TorSnowflake.BootstrapTime.Label": "Bootstrap Time", "Measurement.Details.TorSnowflake.Error.Label": "Failure", @@ -224,7 +224,7 @@ "Network.Summary.Countries": "Network observed in countries:", "Network.Summary.Country.Measurements": "({measurementsTotal} measurements)", "Network.NoData.Title": "Let's collect more data!", - "Network.NoData.Text": "We don\u2019t have enough data for this network to show the charts. Run OONI Probe to collect more measurements.", + "Network.NoData.Text": "We don't have enough data for this network to show the charts. Please run OONI Probe to collect more measurements.", "Footer.Text.Slogan": "Global community measuring internet censorship around the world.", "Footer.Heading.About": "About", "Footer.Heading.OONIProbe": "OONI Probe", @@ -245,7 +245,7 @@ "Footer.Text.Copyright": "\u00a9 {currentYear} Open Observatory of Network Interference (OONI)", "Footer.Text.CCommons": "Content available under a Creative Commons license.", "Footer.Text.Version": "Version", - "CategoryCode.ALDR.Name": "Drugs & Alcohol", + "CategoryCode.ALDR.Name": "Alcohol & Drugs", "CategoryCode.REL.Name": "Religion", "CategoryCode.PORN.Name": "Pornography", "CategoryCode.PROV.Name": "Provocative Attire", @@ -258,7 +258,7 @@ "CategoryCode.XED.Name": "Sex Education", "CategoryCode.PUBH.Name": "Public Health", "CategoryCode.GMB.Name": "Gambling", - "CategoryCode.ANON.Name": "Circumvention tools", + "CategoryCode.ANON.Name": "Anonymization and circumvention tools", "CategoryCode.DATE.Name": "Online Dating", "CategoryCode.GRP.Name": "Social Networking", "CategoryCode.LGBT.Name": "LGBTQ+", @@ -266,7 +266,7 @@ "CategoryCode.HACK.Name": "Hacking Tools", "CategoryCode.COMT.Name": "Communication Tools", "CategoryCode.MMED.Name": "Media sharing", - "CategoryCode.HOST.Name": "Hosting and Blogging", + "CategoryCode.HOST.Name": "Hosting and Blogging Platforms", "CategoryCode.SRCH.Name": "Search Engines", "CategoryCode.GAME.Name": "Gaming", "CategoryCode.CULTR.Name": "Culture", @@ -274,39 +274,39 @@ "CategoryCode.GOVT.Name": "Government", "CategoryCode.COMM.Name": "E-commerce", "CategoryCode.CTRL.Name": "Control content", - "CategoryCode.IGO.Name": "Intergovernmental Orgs.", + "CategoryCode.IGO.Name": "Intergovernmental Organizations", "CategoryCode.MISC.Name": "Miscellaneous content", - "CategoryCode.ALDR.Description": "Use and sale of drugs and alcohol", - "CategoryCode.REL.Description": "Religious issues, both supportive and critical", - "CategoryCode.PORN.Description": "Hard-core and soft-core pornography", - "CategoryCode.PROV.Description": "Provocative attire and portrayal of women wearing minimal clothing", - "CategoryCode.POLR.Description": "Critical political viewpoints", - "CategoryCode.HUMR.Description": "Human rights issues", - "CategoryCode.ENV.Description": "Discussions on environmental issues", - "CategoryCode.MILX.Description": "Terrorism, violent militant or separatist movements", - "CategoryCode.HATE.Description": "Disparaging of particular groups based on race, sex, sexuality or other characteristics", - "CategoryCode.NEWS.Description": "Major news websites, regional news outlets and independent media", - "CategoryCode.XED.Description": "Sexual health issues including contraception, STD's, rape prevention and abortion", - "CategoryCode.PUBH.Description": "Public health issues including HIV, SARS, bird flu, World Health Organization", - "CategoryCode.GMB.Description": "Online gambling and betting", - "CategoryCode.ANON.Description": "Anonymization, censorship circumvention and encryption", - "CategoryCode.DATE.Description": "Online dating sites", - "CategoryCode.GRP.Description": "Online social networking tools and platforms", - "CategoryCode.LGBT.Description": "LGBTQ+ communities discussing related issues (excluding pornography)", - "CategoryCode.FILE.Description": "File sharing including cloud-based file storage, torrents and P2P", - "CategoryCode.HACK.Description": "Computer security tools and news", - "CategoryCode.COMT.Description": "Individual and group communication tools including VoIP, messaging and webmail", - "CategoryCode.MMED.Description": "Video, audio and photo sharing", - "CategoryCode.HOST.Description": "Web hosting, blogging and other online publishing", - "CategoryCode.SRCH.Description": "Search engines and portals", - "CategoryCode.GAME.Description": "Online games and gaming platforms (excluding gambling sites)", - "CategoryCode.CULTR.Description": "Entertainment including history, literature, music, film, satire and humour", - "CategoryCode.ECON.Description": "General economic development and poverty", - "CategoryCode.GOVT.Description": "Government-run websites, including military", - "CategoryCode.COMM.Description": "Commercial services and products", - "CategoryCode.CTRL.Description": "Benign or innocuous content used for control", - "CategoryCode.IGO.Description": "Intergovernmental organizations including The United Nations", - "CategoryCode.MISC.Description": "Sites that haven't been categorized yet", + "CategoryCode.ALDR.Description": "Sites devoted to the use, paraphernalia, and sale of drugs and alcohol irrespective of the local legality.", + "CategoryCode.REL.Description": "Sites devoted to discussion of religious issues, both supportive and critical, as well as discussion of minority religious groups.", + "CategoryCode.PORN.Description": "Hard-core and soft-core pornography.", + "CategoryCode.PROV.Description": "Websites which show provocative attire and portray women in a sexual manner, wearing minimal clothing.", + "CategoryCode.POLR.Description": "Content that offers critical political viewpoints. Includes critical authors and bloggers, as well as oppositional political organizations. Includes pro-democracy content, anti-corruption content as well as content calling for changes in leadership, governance issues, legal reform, etc.", + "CategoryCode.HUMR.Description": "Sites dedicated to discussing human rights issues in various forms. Includes women's rights and rights of minority ethnic groups.", + "CategoryCode.ENV.Description": "Pollution, international environmental treaties, deforestation, environmental justice, disasters, etc.", + "CategoryCode.MILX.Description": "Sites promoting terrorism, violent militant or separatist movements.", + "CategoryCode.HATE.Description": "Content that disparages particular groups or persons based on race, sex, sexuality or other characteristics.", + "CategoryCode.NEWS.Description": "This category includes major news outlets (BBC, CNN, etc.) as well as regional news outlets and independent media.", + "CategoryCode.XED.Description": "Includes contraception, abstinence, STDs, healthy sexuality, teen pregnancy, rape prevention, abortion, sexual rights, and sexual health services.", + "CategoryCode.PUBH.Description": "HIV, SARS, bird flu, centers for disease control, World Health Organization, etc.", + "CategoryCode.GMB.Description": "Online gambling sites. Includes casino games, sports betting, etc.", + "CategoryCode.ANON.Description": "Sites that provide tools used for anonymization, circumvention, proxy-services and encryption.", + "CategoryCode.DATE.Description": "Online dating services which can be used to meet people, post profiles, chat, etc.", + "CategoryCode.GRP.Description": "Social networking tools and platforms.", + "CategoryCode.LGBT.Description": "A range of gay-lesbian-bisexual-transgender queer issues. (Excluding pornography)", + "CategoryCode.FILE.Description": "Sites and tools used to share files, including cloud-based file storage, torrents and P2P file-sharing tools.", + "CategoryCode.HACK.Description": "Sites dedicated to computer security, including news and tools. Includes malicious and non-malicious content.", + "CategoryCode.COMT.Description": "Sites and tools for individual and group communications. Includes webmail, VoIP, instant messaging, chat and mobile messaging applications.", + "CategoryCode.MMED.Description": "Video, audio or photo sharing platforms.", + "CategoryCode.HOST.Description": "Web hosting services, blogging and other online publishing platforms.", + "CategoryCode.SRCH.Description": "Search engines and portals.", + "CategoryCode.GAME.Description": "Online games and gaming platforms, excluding gambling sites.", + "CategoryCode.CULTR.Description": "Content relating to entertainment, history, literature, music, film, books, satire and humour.", + "CategoryCode.ECON.Description": "General economic development and poverty related topics, agencies and funding opportunities.", + "CategoryCode.GOVT.Description": "Government-run websites, including military sites.", + "CategoryCode.COMM.Description": "Websites of commercial services and products.", + "CategoryCode.CTRL.Description": "Benign or innocuous content used as a control.", + "CategoryCode.IGO.Description": "Websites of intergovernmental organizations such as the United Nations.", + "CategoryCode.MISC.Description": "Sites that don't fit in any category. (XXX Things in here should be categorised)", "Country.Heading.Overview": "Overview", "Country.Heading.Websites": "Websites", "Country.Heading.Apps": "Apps", @@ -353,8 +353,6 @@ "Country.Websites.Labels.ResultsPerPage": "Results per page", "Country.Websites.URLSearch.Placeholder": "Search for URL", "Country.Websites.URLCharts.Legend.Label.Blocked": "Confirmed Blocked", - "Country.Websites.URLCharts.Legend.Label.Anomaly": "Anomaly", - "Country.Websites.URLCharts.Legend.Label.Accessible": "Accessible", "Country.Websites.URLCharts.ExploreMoreMeasurements": "Explore more measurements", "Country.Websites.URLCharts.Pagination.Previous": "Previous Page", "Country.Websites.URLCharts.Pagination.Next": "Next Page", @@ -379,11 +377,12 @@ "Country.NetworkProperties.InfoBox.Label.Middleboxes.NotFound": "No middleboxes detected", "Country.NetworkProperties.Button.ShowMore": "Show more networks", "Country.Label.NoData": "No Data Available", + "Search.PageTitle": "Search through millions of Internet censorship measurements", "Search.Sidebar.Domain": "Domain", "Search.Sidebar.Domain.Placeholder": "e.g. twitter.com or 1.1.1.1", "Search.Sidebar.Domain.Error": "Please enter a valid domain name or IP address, such as twitter.com or 1.1.1.1", "Search.Sidebar.Input": "Input", - "Search.Sidebar.Input.Placeholder": "e.g. https://fbcdn.net/robots.txt", + "Search.Sidebar.Input.Placeholder": "e.g., https://fbcdn.net/robots.txt", "Search.Sidebar.Input.Error": "Please enter full URL or IP address, such as https://fbcdn.net/robots.txt", "Search.Sidebar.Categories": "Website Categories", "Search.Sidebar.Categories.All": "Any", @@ -403,49 +402,13 @@ "Search.FilterButton.Confirmed": "Confirmed", "Search.FilterButton.Anomalies": "Anomalies", "Search.FilterButton.Search": "Search", - "Search.Bullet.Reachable": "Accessible", - "Search.Bullet.Anomaly": "Anomaly", - "Search.Bullet.Blocked": "Confirmed blocked", - "Search.Bullet.Error": "Error", "Search.Filter.SortBy": "Sort by", "Search.Filter.SortBy.Date": "Date", - "Search.WebConnectivity.Results.Reachable": "Accessible", - "Search.WebConnectivity.Results.Anomaly": "Anomaly", "Search.WebConnectivity.Results.Blocked": "Confirmed", - "Search.WebConnectivity.Results.Error": "Error", "Search.HTTPRequests.Results.Anomaly": "", "Search.HTTPRequests.Results.Blocked": "", "Search.HTTPRequests.Results.Error": "", "Search.HTTPRequests.Results.Reachable": "", - "Search.WhatsApp.Results.Reachable": "Accessible", - "Search.WhatsApp.Results.Anomaly": "Anomaly", - "Search.WhatsApp.Results.Error": "Error", - "Search.FacebookMessenger.Results.Reachable": "Accessible", - "Search.FacebookMessenger.Results.Anomaly": "Anomaly", - "Search.FacebookMessenger.Results.Error": "Error", - "Search.Telegram.Results.Reachable": "Accessible", - "Search.Telegram.Results.Anomaly": "Anomaly", - "Search.Telegram.Results.Error": "Error", - "Search.Signal.Results.Reachable": "Accessible", - "Search.Signal.Results.Anomaly": "Anomaly", - "Search.Signal.Results.Error": "Error", - "Search.HTTPInvalidRequestLine.Results.Anomaly": "Anomaly", - "Search.HTTPInvalidRequestLine.Results.Reachable": "OK", - "Search.HTTPInvalidRequestLine.Results.Error": "Error", - "Search.HTTPHeaderFieldManipulation.Results.Anomaly": "Anomaly", - "Search.HTTPHeaderFieldManipulation.Results.Reachable": "OK", - "Search.HTTPHeaderFieldManipulation.Results.Error": "Error", - "Search.Tor.Results.Reachable": "OK", - "Search.Tor.Results.Anomaly": "Anomaly", - "Search.Tor.Results.Error": "Error", - "Search.Psiphon.Results.Reachable": "Ok", - "Search.Psiphon.Results.Anomaly": "Anomaly", - "Search.Psiphon.Results.Error": "Error", - "Search.RiseupVPN.Results.Reachable": "Accessible", - "Search.RiseupVPN.Results.Anomaly": "Anomaly", - "Search.RiseupVPN.Results.Error": "Error", - "Search.Test.Results.OK": "OK", - "Search.Test.Results.Error": "Error", "Search.NDT.Results": "", "Search.DASH.Results": "", "Search.VanillaTor.Results": "", @@ -471,6 +434,7 @@ "Home.NetworkProperties.SummaryText": "Check the speed and performance of thousands of networks around the world. Explore data on video streaming performance.", "Home.MonthlyStats.Title": "Monthly coverage worldwide", "Home.MonthlyStats.SummaryText": "OONI Explorer hosts millions of network measurements collected from more than 200 countries since 2012. Every day, OONI Explorer is updated with new measurements!\n\nLast month, {measurementCount} OONI Probe measurements were collected from {networkCount} networks in {countryCount} countries. Explore the monthly usage of [OONI Probe](https://ooni.org/install/) through the stats below.", + "Home.Highlights.CTA": "We encourage you to explore OONI measurements to find more highlights!", "Home.Highlights.Title": "Highlights", "Home.Highlights.Description": "What can you learn from OONI Explorer? \n\nBelow we share some stories from [research reports](https://ooni.org/post/) based on OONI data.\n\nWe share these case studies to demonstrate how OONI's openly available data can be used and what types of stories can be told. \n\nWe encourage you to explore OONI data, discover more censorship cases, and to use OONI data as part of your research and/or advocacy.", "Home.Highlights.Political": "Censorship during political events", @@ -481,10 +445,14 @@ "Home.Highlights.LGBTQI.Description": "Minority group sites are blocked around the world. Below we share a few cases on the blocking of LGBTQI sites.", "Home.Highlights.Changes": "Censorship changes", "Home.Highlights.Changes.Description": "OONI measurements have been collected on a continuous basis since 2012, enabling the identification of censorship changes around the world. Some examples include:", - "Home.Meta.Description": "OONI Explorer is an open data resource on Internet censorship around the world consisting of millions of measurements on network inteference.", + "Home.Meta.Description": "OONI Explorer is an open data resource on Internet censorship around the world consisting of millions of measurements on network interference.", + "Home.Highlights.Explore": "Explore", + "Home.Highlights.ReadReport": "Read report", "Countries.Heading.JumpToContinent": "Jump to continent", - "Countries.Search.NoCountriesFound": "No countries found with \"{searchTerm}\"", + "Countries.Search.NoCountriesFound": "No countries found with {searchTerm}", "Countries.Search.Placeholder": "Search for countries", + "Countries.PageTitle": "Internet Censorship around the world", + "Error.404.PageNotFound": "Page Not Found", "Error.404.GoBack": "Go back", "Error.404.Heading": "The requested page does not exist", "Error.404.Message": "We could not find the content you were looking for. Maybe try {measurmentLink} or look at {homePageLink}.", @@ -492,6 +460,8 @@ "Error.404.HomepageLinkText": "the homepage", "MAT.Title": "OONI Measurement Aggregation Toolkit (MAT)", "MAT.SubTitle": "Create charts based on aggregate views of real-time OONI data from around the world", + "MAT.JSONData": "JSON Data", + "MAT.CSVData": "CSV Data", "MAT.Form.Label.XAxis": "X Axis", "MAT.Form.Label.YAxis": "Y Axis", "MAT.Form.Label.AxisOption.domain": "Domain", @@ -505,6 +475,9 @@ "MAT.Form.ConfirmationModal.No": "No", "MAT.Form.ConfirmationModal.Button.Yes": "Yes", "MAT.Form.Submit": "Show Chart", + "MAT.Form.All": "All", + "MAT.Form.AllCountries": "All Countries", + "MAT.Table.Header.ok_count": "OK Count", "MAT.Table.Header.anomaly_count": "Anomaly Count", "MAT.Table.Header.confirmed_count": "Confirmed Count", "MAT.Table.Header.failure_count": "Failure Count", @@ -515,24 +488,61 @@ "MAT.Table.Header.probe_asn": "ASN", "MAT.Table.Header.blocking_type": "Blocking Type", "MAT.Table.Header.domain": "Domain", + "MAT.Table.FilterPlaceholder": "Search {count} records\u2026", + "MAT.Table.Search": "Search:", + "MAT.Table.Filters": "Filters", "MAT.Charts.NoData.Title": "No Data Found", "MAT.Charts.NoData.Description": "We are not able to produce a chart based on the selected filters. Please change the filters and try again.", + "MAT.Charts.NoData.Details": "Details:", "MAT.Help.Box.Title": "Help", "MAT.Help.Title": "FAQs", "MAT.Help.Content": "# What is the MAT?\n\nOONI's Measurement Aggregation Toolkit (MAT) is a tool that enables you to generate your own custom charts based on **aggregate views of real-time OONI data** collected from around the world.\n\nOONI data consists of network measurements collected by [OONI Probe](https://ooni.org/install/) users around the world. \n\nThese measurements contain information about various types of **internet censorship**, such as the [blocking of websites and apps](https://ooni.org/nettest/) around the world. \n\n# Who is the MAT for?\n\nThe MAT was built for researchers, journalists, and human rights defenders interested in examining internet censorship around the world.\n\n# Why use the MAT?\n\nWhen examining cases of internet censorship, it's important to **look at many measurements at once** (\"in aggregate\") in order to answer key questions like the following:\n\n* Does the testing of a service (e.g. Facebook) present **signs of blocking every time that it is tested** in a country? This can be helpful for ruling out [false positives](https://ooni.org/support/faq/#what-are-false-positives).\n* What types of websites (e.g. human rights websites) are blocked in each country?\n* In which countries is a specific website (e.g. `bbc.com`) blocked?\n* How does the blocking of different apps (e.g. WhatsApp or Telegram) vary across countries?\n* How does the blocking of a service vary across countries and [ASNs](https://ooni.org/support/glossary/#asn)?\n* How does the blocking of a service change over time?\n\nWhen trying to answer questions like the above, we normally perform relevant data analysis (instead of inspecting measurements one by one). \n\nThe MAT incorporates our data analysis techniques, enabling you to answer such questions without any data analysis skills, and with the click of a button!\n\n# How to use the MAT?\n\nThrough the filters at the start of the page, select the parameters you care about in order to plot charts based on aggregate views of OONI data.\n\nThe MAT includes the following filters:\n\n* **Countries:** Select a country through the drop-down menu (the \"All Countries\" option will show global coverage)\n* **Test Name:** Select an [OONI Probe test](https://ooni.org/nettest/) based on which you would like to get measurements (for example, select `Web Connectivity` to view the testing of websites)\n* **Domain:** Type the domain for the website you would like to get measurements (e.g. `twitter.com`)\n* **Website categories:** Select the [website category](https://github.com/citizenlab/test-lists/blob/master/lists/00-LEGEND-new_category_codes.csv) for which you would like to get measurements (e.g. `News Media` for news media websites)\n* **ASN:** Type the [ASN](https://ooni.org/support/glossary/#asn) of the network for which you would like to get measurements (e.g. `AS30722` for Vodafone Italia)\n* **Date range:** Select the date range of the measurements by adjusting the `Since` and `Until` filters\n* **X axis:** Select the values that you would like to appear on the horizontal axis of your chart\n* **Y axis:** Select the values that you would like to appear on the vertical axis of your chart\n\nDepending on what you would like to explore, adjust the MAT filters accordingly and click `Show Chart`. \n\nFor example, if you would like to check the testing of BBC in all countries around the world:\n\n* Type `www.bbc.com` under `Domain`\n* Select `Countries` under the `Y axis`\n* Click `Show Chart`\n\nThis will plot numerous charts based on the OONI Probe testing of `www.bbc.com` worldwide.\n\n# Interpreting MAT charts\n\nThe MAT charts (and associated tables) include the following values:\n\n* **OK count:** Successful measurements (i.e. NO sign of internet censorship)\n* **Confirmed count:** Measurements from automatically **confirmed blocked websites** (e.g. a [block page](https://ooni.org/support/glossary/#block-page) was served)\n* **Anomaly count:** Measurements that provided **signs of potential blocking** (however, [false positives](https://ooni.org/support/faq/#what-are-false-positives) can occur) \n* **Failure count:** Failed experiments that should be discarded\n* **Measurement count:** Total volume of OONI measurements (pertaining to the selected country, resource, etc.)\n\nWhen trying to identify the blocking of a service (e.g. `twitter.com`), it's useful to check whether:\n\n* Measurements are annotated as `confirmed`, automatically confirming the blocking of websites\n* A large volume of measurements (in comparison to the overall measurement count) present `anomalies` (i.e. signs of potential censorship)\n\nYou can access the raw data by clicking on the bars of charts, and subsequently clicking on the relevant measurement links. \n\n# Website categories\n\n[OONI Probe](https://ooni.org/install/) users test a wide range of [websites](https://ooni.org/support/faq/#which-websites-will-i-test-for-censorship-with-ooni-probe) that fall under the following [30 standardized categories](https://github.com/citizenlab/test-lists/blob/master/lists/00-LEGEND-new_category_codes.csv).", "MAT.Help.Subtitle.Categories": "Categories", + "MAT.CustomTooltip.ViewMeasurements": "View measurements", "ReachabilityDash.Heading.CircumventionTools": "Reachability of Censorship Circumvention Tools", "ReachabilityDash.CircumventionTools.Description": "The charts below display aggregate views of OONI data based on the testing of the following circumvention tools:\n\n* [Psiphon](https://ooni.org/nettest/psiphon)\n\n* [Tor](https://ooni.org/nettest/tor)\n\n* [Tor Snowflake](https://ooni.org/nettest/tor-snowflake/)\n\nPlease note that the presence of [anomalous measurements](https://ooni.org/support/faq/#what-do-you-mean-by-anomalies) is not always indicative of blocking, as [false positives](https://ooni.org/support/faq/#what-are-false-positives) can occur. Moreover, circumvention tools often have built-in circumvention techniques for evading censorship. \n\nWe therefore recommend referring to **[Tor Metrics](https://metrics.torproject.org/)** and to the **[Psiphon Data Engine](https://psix.ca/)** to view usage stats and gain a more comprehensive understanding of whether these tools work in each country.", "ReachabilityDash.Form.Label.CountrySelect.AllSelected": "All countries selected", "ReachabilityDash.Form.Label.CountrySelect.SearchPlaceholder": "Search", "ReachabilityDash.Form.Label.CountrySelect.SelectAll": "Select All", "ReachabilityDash.Form.Label.CountrySelect.SelectAllFiltered": "Select All (Filtered)", - "ReachabilityDash.Form.Label.CountrySelect.InputPlaceholder": "Select Countries...", + "ReachabilityDash.Form.Label.CountrySelect.InputPlaceholder": "Select Countries\u2026", "ReachabilityDash.Meta.Description": "View the accessibility of censorship circumvention tools around the world through OONI data.", "DateRange.Apply": "Apply", "DateRange.Cancel": "Cancel", "DateRange.Today": "Today", "DateRange.LastWeek": "Last Week", "DateRange.LastMonth": "Last Month", - "DateRange.LastYear": "Last Year" -} + "DateRange.LastYear": "Last Year", + "Highlights.Political.CubaReferendum2019.Title": "2019 Constitutional Referendum", + "Highlights.Political.CubaReferendum2019.Text": "Blocking of independent media", + "Highlights.Political.VenezuelaCrisis2019.Title": "2019 Presidential Crisis", + "Highlights.Political.VenezuelaCrisis2019.Text": "Blocking of Wikipedia and social media", + "Highlights.Political.ZimbabweProtests2019.Title": "2019 Fuel Protests", + "Highlights.Political.ZimbabweProtests2019.Text": "Social media blocking and internet blackouts", + "Highlights.Political.MaliElection2018.Title": "2018 Presidential Election", + "Highlights.Political.MaliElection2018.Text": "Blocking of WhatsApp and Twitter", + "Highlights.Political.CataloniaReferendum2017.Title": "Catalonia 2017 Independence Referendum", + "Highlights.Political.CataloniaReferendum2017.Text": "Blocking of sites related to the referendum", + "Highlights.Political.IranProtests2018.Title": "2018 Anti-government Protests", + "Highlights.Political.IranProtests2018.Text": "Blocking of Telegram, Instagram and Tor", + "Highlights.Political.EthiopiaProtests2016.Title": "2016 Wave of Protests", + "Highlights.Political.EthiopiaProtests2016.Text": "Blocking of news websites and social media", + "Highlights.Political.PakistanProtests2017.Title": "2017 Protests", + "Highlights.Political.PakistanProtests2017.Text": "Blocking of news websites and social media", + "Highlights.Media.Egypt.Title": "Pervasive media censorship", + "Highlights.Media.Egypt.Text": "Blocking of hundreds of media websites", + "Highlights.Media.Venezuela.Title": "Blocking of independent media websites", + "Highlights.Media.Venezuela.Text": "Venezuela's economic and political crisis", + "Highlights.Media.SouthSudan.Title": "Blocking of foreign-based media", + "Highlights.Media.SouthSudan.Text": "Media accused of hostile reporting against the government", + "Highlights.Media.Malaysia.Title": "Blocking of media", + "Highlights.Media.Malaysia.Text": "1MDB scandal", + "Highlights.Media.Iran.Title": "Pervasive media censorship", + "Highlights.Media.Iran.Text": "Blocking of at least 121 news outlets", + "Highlights.Lgbtqi.Indonesia.Text": "Blocking of LGBTQI sites", + "Highlights.Lgbtqi.Iran.Text": "Blocking of Grindr", + "Highlights.Lgbtqi.Ethiopia.Text": "Blocking of QueerNet", + "Highlights.Changes.Cuba.Text": "Cuba [used to primarily serve blank block pages](https://ooni.torproject.org/post/cuba-internet-censorship-2017/), only blocking the HTTP version of websites. Now they censor access to sites that support HTTPS by means of [IP blocking](https://ooni.org/post/cuba-referendum/).", + "Highlights.Changes.Venezuela.Text": "Venezuelan ISPs used to primarily block sites by means of [DNS tampering](https://ooni.torproject.org/post/venezuela-internet-censorship/). Now state-owned CANTV also implements [SNI-based filtering](https://ooni.torproject.org/post/venezuela-blocking-wikipedia-and-social-media-2019/).", + "Highlights.Changes.Ethiopia.Text": "Ethiopia [used to block](https://ooni.org/post/ethiopia-report/) numerous news websites, LGBTQI, political opposition, and circumvention tool sites. As part of the 2018 political reforms, most of these sites have been [unblocked](https://ooni.org/post/ethiopia-unblocking/)." +} \ No newline at end of file diff --git a/public/static/lang/translations.js b/public/static/lang/translations.js index 8784affab..22c9a02cf 100644 --- a/public/static/lang/translations.js +++ b/public/static/lang/translations.js @@ -1 +1 @@ -window.OONITranslations = {"en":{"Tests.WebConnectivity.Name":"Web Connectivity Test","Tests.Telegram.Name":"Telegram Test","Tests.Facebook.Name":"Facebook Messenger Test","Tests.WhatsApp.Name":"WhatsApp Test","Tests.Signal.Name":"Signal Test","Tests.HTTPInvalidReqLine.Name":"HTTP Invalid Request Line Test","Tests.HTTPHeaderManipulation.Name":"HTTP Header Field Manipulation Test","Tests.NDT.Name":"NDT Speed Test","Tests.Dash.Name":"DASH Video Streaming Test","Tests.TorVanilla.Name":"Tor (Vanilla) Test","Tests.BridgeReachability.Name":"Tor Bridge Reachability Test","Tests.TCPConnect.Name":"TCP Connect Test","Tests.DNSConsistency.Name":"DNS Consistency Test","Tests.HTTPRequests.Name":"HTTP Requests Test","Tests.Psiphon.Name":"Psiphon Test","Tests.Tor.Name":"Tor Test","Tests.RiseupVPN.Name":"Riseup VPN Test","Tests.TorSnowflake.Name":"Tor Snowflake Test","Tests.DNSCheck.Name":"DNS Check","Tests.StunReachability.Name":"STUN Reachability","Tests.URLGetter.Name":"URL Getter","Tests.Groups.Webistes.Name":"Websites","Tests.Groups.Instant Messagging.Name":"Instant Messaging","Tests.Groups.Middlebox.Name":"Middleboxes","Tests.Groups.Performance.Name":"Performance","Tests.Groups.Circumvention.Name":"Circumvention","Tests.Groups.Experimental.Name":"Experimental","Tests.Groups.Legacy.Name":"Legacy","Measurement.Hero.Status.Anomaly":"Anomaly","Measurement.Hero.Status.Reachable":"OK","Measurement.Hero.Status.Error":"Error","Measurement.Hero.Status.Confirmed":"Confirmed Blocked","Measurement.Hero.Status.Down":"Website Down","Measurement.Hero.Status.Anomaly.DNS":"DNS","Measurement.Hero.Status.Anomaly.HTTP":"HTTP","Measurement.Hero.Status.Anomaly.TCP":"TCP/IP","Measurement.CommonSummary.Label.ASN":"Network","Measurement.CommonSummary.Label.Country":"Country","Measurement.CommonSummary.Label.DateTime":"Date & Time","Measurement.DetailsHeader.Runtime":"Runtime","Measurement.Status.Hint.Websites.Censorship":"","Measurement.Status.Hint.Websites.DNS":"DNS tampering","Measurement.Status.Hint.Websites.Error":"Error in detection","Measurement.Status.Hint.Websites.HTTPdiff":"HTTP blocking (a blockpage might be served)","Measurement.Status.Hint.Websites.HTTPfail":"HTTP blocking (HTTP requests failed)","Measurement.Status.Hint.Websites.NoCensorship":"No blocking detected","Measurement.Status.Hint.Websites.TCPBlock":"TCP/IP blocking","Measurement.Status.Hint.Websites.Unavailable":"Website down","Measurement.SummaryText.Websites.Accessible":"On {date}, {WebsiteURL} was accessible when tested on {network} in {country}.","Measurement.SummaryText.Websites.Anomaly":"On {date}, {WebsiteURL} presented signs of {BlockingReason} on {network} in {country}.\n\nThis might mean that {WebsiteURL} was blocked, but [false positives](https://ooni.org/support/faq/#why-do-false-positives-occur) can occur. \n\nPlease explore the network measurement data below.","Measurement.SummaryText.Websites.Anomaly.BlockingReason.DNS":"DNS tampering","Measurement.SummaryText.Websites.Anomaly.BlockingReason.TCP":"TCP/IP blocking","Measurement.SummaryText.Websites.Anomaly.BlockingReason.HTTP-failure":"HTTP blocking (HTTP requests failed)","Measurement.SummaryText.Websites.Anomaly.BlockingReason.HTTP-diff":"HTTP blocking (a blockpage might be served)","Measurement.SummaryText.Websites.ConfirmedBlocked":"On {date}, {WebsiteURL} was blocked on {network} in {country}.\n\nThis is confirmed because a block page was served, as illustrated through the network measurement data below.","Measurement.SummaryText.Websites.Failed":"On {date}, the test for {WebsiteURL} failed on {network} in {country}.","Measurement.SummaryText.Websites.Down":"On {date}, {WebsiteURL} was down on {network} in {country}.","Measurement.Details.Websites.Failures.Heading":"Failures","Measurement.Details.Websites.Failures.Label.HTTP":"HTTP Experiment","Measurement.Details.Websites.Failures.Label.DNS":"DNS Experiment","Measurement.Details.Websites.Failures.Label.Control":"Control","Measurement.Details.Websites.Failures.Values.Unknown":"No data","Measurement.Details.Websites.Failures.Values.Null":"null","Measurement.Details.Websites.DNSQueries.Heading":"DNS Queries","Measurement.Details.Websites.DNSQueries.Label.Resolver":"Resolver","Measurement.Details.Websites.DNSQueries.NoData":"No data.","Measurement.Details.Websites.TCP.Heading":"TCP Connections","Measurement.Details.Websites.TCP.NoData":"No data.","Measurement.Details.Websites.TCP.ConnectionTo":"Connection to {destination} {connectionStatus}.","Measurement.Details.Websites.TCP.ConnectionTo.Success":"succeeded","Measurement.Details.Websites.TCP.ConnectionTo.Failed":"failed","Measurement.Details.Websites.TCP.ConnectionTo.Blocked":"was blocked","Measurement.Details.Websites.HTTP.Heading":"HTTP Requests","Measurement.Details.Websites.HTTP.Label.Response":"Response","Measurement.Details.Websites.HTTP.NoData":"No Data","Measurement.Details.Websites.HTTP.Request.URL":"URL","Measurement.Details.Websites.HTTP.Response.Body":"Response Body","Measurement.Details.Websites.HTTP.Response.Headers":"Response Headers","Measurement.CommonDetails.Label.MsmtID":"Report ID","Measurement.CommonDetails.Label.Platform":"Platform","Measurement.CommonDetails.Label.Software":"Software Name","Measurement.CommonDetails.Label.Engine":"Measurement Engine","Measurement.CommonDetails.Value.Unavailable":"Unavailable","Measurement.CommonDetails.RawMeasurement.Heading":"Raw Measurement Data","Measurement.CommonDetails.RawMeasurement.Download":"Download JSON","Measurement.CommonDetails.RawMeasurement.Unavailable":"Unavailable","Measurement.CommonDetails.RawMeasurement.Expand":"Expand All","Measurement.CommonDetails.Label.Resolver":"Resolver","Measurement.CommonDetails.Label.ResolverASN":"Resolver ASN","Measurement.CommonDetails.Label.ResolverIP":"Resolver IP","Measurement.CommonDetails.Label.ResolverNetworkName":"Resolver Network Name","Measurement.Hero.Status.NDT.Title":"Results","Measurement.Status.Info.Label.Download":"Download","Measurement.Status.Info.Label.Upload":"Upload","Measurement.Status.Info.Label.Ping":"Ping","Measurement.Status.Info.Label.Server":"Server","Measurement.Status.Info.NDT.Error":"Failed Test","Measurement.Details.Performance.Heading":"Performance Details","Measurement.Details.Performance.Label.AvgPing":"Average Ping","Measurement.Details.Performance.Label.MaxPing":"Max Ping","Measurement.Details.Performance.Label.MSS":"MSS","Measurement.Details.Performance.Label.RetransmitRate":"Retransmission Rate","Measurement.Details.Performance.Label.PktLoss":"Packet Loss","Measurement.Details.Performance.Label.OutOfOrder":"Out of Order","Measurement.Details.Performance.Label.Timeouts":"Timeouts","Measurement.Hero.Status.Dash.Title":"Results","Measurement.Status.Info.Label.VideoQuality":"Video Quality","Measurement.Status.Info.Label.Bitrate":"Median Bitrate","Measurement.Status.Info.Label.Delay":"Playout Delay","Measurement.Status.Hint.Telegram.Blocked":"Telegram is likely blocked","Measurement.Status.Hint.Telegram.Reachable":"Telegram is accessible","Measurement.Status.Hint.Telegram.Failed":"The Telegram test failed","Measurement.Details.SummaryText.Telegram.Reachable":"On {date}, Telegram was reachable on {network} in {country}. \n\nOONI's Telegram test successfully connected to Telegram's endpoints and web interface (web.telegram.org).","Measurement.Details.SummaryText.Telegram.AppFailure":"On {date}, the testing of Telegram's mobile app presented signs of blocking on {network} in {country}.\n\nThis might mean that Telegram's mobile app was blocked, but [false positives](https://ooni.org/support/faq/#why-do-false-positives-occur) can occur. \n\nPlease explore the network measurement data below. Check other Telegram measurements from the same network during the same time period (if they're available).","Measurement.Details.SummaryText.Telegram.DesktopFailure":"On {date}, the testing of Telegram's web interface (web.telegram.org) presented signs of blocking on {network} in {country}.\n\nThis might mean that web.telegram.org was blocked, but [false positives](https://ooni.org/support/faq/#why-do-false-positives-occur) can occur.\n\nPlease explore the network measurement data below. Check other Telegram measurements from the same network during the same time period (if they're available).","Measurement.Details.SummaryText.Telegram.DesktopAndAppFailure":"On {date}, the testing of Telegram's mobile app and web interface (web.telegram.org) presented signs of blocking on {network} in {country}.\n\nThis might mean that both Telegram's mobile app and web interface were blocked, but [false positives](https://ooni.org/support/faq/#why-do-false-positives-occur) can occur.\n\nPlease explore the network measurement data below. Check other Telegram measurements from the same network during the same time period (if they're available).","Measurement.Details.Telegram.Endpoint.Label.Mobile":"Mobile App","Measurement.Details.Telegram.Endpoint.Label.Web":"Telegram Web","Measurement.Details.Endpoint.Status.Okay":"Okay","Measurement.Details.Endpoint.Status.Failed":"Failed","Measurement.Details.Endpoint.Status.Unknown":"Unknown","Measurement.Details.Telegram.Endpoint.Status.Heading":"Endpoint Status","Measurement.Details.Telegram.Endpoint.ConnectionTo.Failed":"Connection to {destination} failed.","Measurement.Details.Telegram.Endpoint.ConnectionTo.Successful":"Connection to {destination} was successful.","Measurement.Details.Hint.WhatsApp.Reachable":"WhatsApp is accessible","Measurement.Status.Hint.WhatsApp.Blocked":"WhatsApp is likely blocked","Measurement.Status.Hint.WhatsApp.Failed":"The WhatsApp test failed","Measurement.Details.SummaryText.WhatsApp.Reachable":"On {date}, WhatsApp was reachable on {network} in {country}. \n\nOONI's WhatsApp test successfully connected to WhatsApp's endpoints, registration service and web interface (web.whatsapp.com).","Measurement.Details.SummaryText.WhatsApp.AppFailure":"On {date}, the testing of WhatsApp's mobile app presented signs of blocking on {network} in {country}.\n\nThis might mean that WhatsApp's mobile app was blocked, but [false positives](https://ooni.org/support/faq/#why-do-false-positives-occur) can occur. \n\nPlease explore the network measurement data below. Check other WhatsApp measurements from the same network during the same time period (if they're available).","Measurement.Details.SummaryText.WhatsApp.DesktopFailure":"On {date}, the testing of WhatsApp's web interface (web.whatsapp.com) presented signs of blocking on {network} in {country}.\n\nThis might mean that web.whatsapp.com was blocked, but [false positives](https://ooni.org/support/faq/#why-do-false-positives-occur) can occur.\n\nPlease explore the network measurement data below. Check other WhatsApp measurements from the same network during the same time period (if they're available).","Measurement.Details.SummaryText.WhatsApp.DesktopAndAppFailure":"On {date}, the testing of WhatsApp's mobile app and web interface (web.whatsapp.com) presented signs of blocking on {network} in {country}.\n\nThis might mean that both WhatsApp's mobile app and web interface were blocked, but [false positives](https://ooni.org/support/faq/#why-do-false-positives-occur) can occur.\n\nPlease explore the network measurement data below. Check other WhatsApp measurements from the same network during the same time period (if they're available).","Measurement.Details.WhatsApp.Endpoint.Label.Mobile":"Mobile App","Measurement.Details.WhatsApp.Endpoint.Label.Web":"WhatsApp Web","Measurement.Details.WhatsApp.Endpoint.Label.Registration":"Registration","Measurement.Details.WhatsApp.Endpoint.Status.Heading":"Endpoint Status","Measurement.Details.WhatsApp.Endpoint.ConnectionTo.Failed":"Connection to {destination} failed.","Measurement.Details.WhatsApp.Endpoint.ConnectionTo.Successful":"Connection to {destination} was successful.","Measurement.Status.Hint.FacebookMessenger.Reachable":"Facebook Messenger is accessible","Measurement.Status.Hint.FacebookMessenger.Blocked":"Facebook Messenger is likely blocked","Measurement.Status.Hint.FacebookMessenger.Failed":"The Facebook Messenger test failed","Measurement.Details.SummaryText.FacebookMessenger.Reachable":"On {date}, Facebook Messenger was reachable on {network} in {country}.","Measurement.Details.SummaryText.FacebookMessenger.TCPFailure":"TCP connections to Facebook's endpoints failed.","Measurement.Details.SummaryText.FacebookMessenger.DNSFailure":"DNS lookups did not resolve to Facebook IP addresses.","Measurement.Details.SummaryText.FacebookMessenger.DNSSuccess":"DNS lookups resolved to Facebook IP addresses.","Measurement.Details.SummaryText.FacebookMessenger.TCPSuccess":"TCP connections to Facebook's enpoints succeeded.","Measurement.Details.FacebookMessenger.TCP.Label.Title":"TCP connections","Measurement.Details.FacebookMessenger.DNS.Label.Title":"DNS lookups","Measurement.Details.FacebookMessenger.TCP.Label.Okay":"OK","Measurement.Details.FacebookMessenger.TCP.Label.Failed":"Failed","Measurement.Details.FacebookMessenger.DNS.Label.Okay":"OK","Measurement.Details.FacebookMessenger.DNS.Label.Failed":"Failed","Measurement.Details.FacebookMessenger.Endpoint.Status.Heading":"Endpoint Status","Measurement.Details.FacebookMessenger.Endpoint.ConnectionTo.Failed":"Connection to {destination} failed.","Measurement.Details.FacebookMessenger.Endpoint.ConnectionTo.Successful":"Connection to {destination} was successful.","Measurement.Status.Hint.Signal.Blocked":"Signal is likely blocked","Measurement.Status.Hint.Signal.Reachable":"Signal is accessible","Measurement.Status.Hint.Signal.Failed":"The Signal test failed","Measurement.Details.SummaryText.Signal.Reachable":"On {date}, [Signal](https://signal.org/) was reachable on {network} in {country}. \n\nThe [OONI Probe Signal test](https://ooni.org/nettest/signal) successfully connected to Signal's endpoints.","Measurement.Details.SummaryText.Signal.Blocked":"On {date}, the testing of the [Signal app](https://signal.org/) presented signs of blocking on {network} in {country}.\n\nThis might mean that Signal was blocked, but [false positives](https://ooni.org/support/faq/#why-do-false-positives-occur) can occur. \n\nPlease explore the network measurement data below. Check other Signal measurements from the same network during the same time period (if they're available).","Measurement.Hero.Status.HTTPHeaderManipulation.NoMiddleBoxes":"No middleboxes detected","Measurement.HTTPHeaderManipulation.NoMiddleBoxes.SummaryText":"On {date}, no network anomaly was detected on {network} in {country} when communicating with our servers.","Measurement.Hero.Status.HTTPHeaderManipulation.MiddleboxesDetected":"Network tampering","Measurement.HTTPHeaderManipulation.MiddleBoxesDetected.SummaryText":"On {date}, network traffic was manipulated when contacting our control servers. \n\nThis means that there might be a middlebox on {network} in {country}, which could be responsible for censorship and/or surveillance.","Measurement.Hero.Status.HTTPInvalidReqLine.NoMiddleBoxes":"No middleboxes detected","Measurement.HTTPInvalidReqLine.NoMiddleBoxes.SummaryText":"On {date}, no network anomaly was detected on {network} in {country} when communicating with our servers.","Measurement.Hero.Status.HTTPInvalidReqLine.MiddleboxesDetected":"Network tampering","Measurement.HTTPInvalidReqLine.MiddleboxesDetected.SummaryText":"On {date}, network traffic was manipulated when contacting our control servers. \n\nThis means that there might be a middlebox on {network} in {country}, which could be responsible for censorship and/or surveillance.","Measurement.HTTPInvalidReqLine.YouSent":"You Sent","Measurement.HTTPInvalidReqLine.YouReceived":"You Received","Measurement.Hero.Status.TorVanilla.Blocked":"Tor is likely blocked","Measurement.Hero.Status.TorVanilla.Reachable":"Tor is accessible","Measurement.Details.SummaryText.TorVanilla.Blocked":"On {date}, OONI's Vanilla Tor test did not manage to bootstrap a connection to the [Tor network](https://www.torproject.org/).\n\nThis might mean that access to the Tor network was blocked on {network} in {country}, but [false positives can occur](https://ooni.org/support/faq/#why-do-false-positives-occur).\n\nPlease explore the network measurement data below. Check other Tor measurements from the same network during the same time period (if they're available).","Measurement.Details.SummaryText.TorVanilla.Reachable":"OONI's Vanilla Tor test successfully bootstrapped a connection to the [Tor network](https://www.torproject.org/).\n\nThis means that the Tor network was reachable from {network} in {country} on {date}.","Measurement.Details.VanillaTor.Endpoint.Label.Reachability":"Reachability","Measurement.Status.Hint.Psiphon.Reachable":"Psiphon works","Measurement.Status.Hint.Psiphon.Blocked":"Psiphon is likely blocked","Measurement.Status.Hint.Psiphon.BootstrappingError":"Psiphon is likely blocked (bootstrap error)","Measurement.Details.SummaryText.Psiphon.OK":"On {date}, [Psiphon](https://psiphon.ca/) worked on {network} in {country}.\n\nThe [OONI Probe Psiphon test](https://ooni.org/nettest/psiphon/) was able to successfully bootstrap Psiphon and ensure that the app works.","Measurement.Details.SummaryText.Psiphon.Blocked":"On {date}, [Psiphon](https://psiphon.ca/) did not appear to work on {network} in {country}.\n\nWhile the [OONI Probe Psiphon test](https://ooni.org/nettest/psiphon/) was able to bootstrap Psiphon, it was unable to fetch a webpage from the internet. \n\nThis suggests that the Psiphon app may have been blocked on this network. \n\nHowever, [false positives](https://ooni.org/support/faq/#why-do-false-positives-occur) can occur. Please explore the network measurement data below and compare it with other relevant measurements (if they're available).","Measurement.Details.SummaryText.Psiphon.BootstrappingError":"On {date}, [Psiphon](https://psiphon.ca/) did not work on {network} in {country}.\n\nThe [OONI Probe Psiphon test](https://ooni.org/nettest/psiphon/) was unable to bootstrap Psiphon.\n\nThis suggests that the Psiphon app may have been blocked on this network.\n\nHowever, [false positives](https://ooni.org/support/faq/#why-do-false-positives-occur) can occur. Please explore the network measurement data below and compare it with other relevant measurements (if they're available).","Measurement.Details.Psiphon.BootstrapTime.Label":"Bootstrap Time","Measurement.Status.Hint.Tor.Reachable":"Tor works","Measurement.Status.Hint.Tor.Blocked":"Tor is likely blocked","Measurement.Status.Hint.Tor.Error":"Tor test failed","Measurement.Details.SummaryText.Tor.OK":"On {date}, [Tor](https://www.torproject.org/) worked on {network} in {country}.\n\nAs part of [OONI Probe Tor testing](https://ooni.org/nettest/tor/), all reachability measurements of selected Tor directory authorities and bridges were successful.","Measurement.Details.SummaryText.Tor.Blocked":"On {date}, [Tor](https://www.torproject.org/) did not appear to work on {network} in {country}.\n\nThe [OONI Probe Tor test](https://ooni.org/nettest/tor/) failed in performing certain measurements. More details are available through the network measurement data provided below.\n\nThis suggests that Tor may have been blocked on this network.\n\nHowever, [false positives](https://ooni.org/support/faq/#why-do-false-positives-occur) can occur.","Measurement.Details.SummaryText.Tor.Error":"On {date}, the Tor test failed on {network} in {country}.","Measurement.Details.Tor.Bridges.Label.Title":"Tor Browser Bridges","Measurement.Details.Tor.Bridges.Label.OK":"{bridgesAccessible}/{bridgesTotal} OK","Measurement.Details.Tor.DirAuth.Label.Title":"Tor Directory Authorities","Measurement.Details.Tor.DirAuth.Label.OK":"{dirAuthAccessible}/{dirAuthTotal} OK","Measurement.Details.Tor.Table.Header.Name":"Name","Measurement.Details.Tor.Table.Header.Address":"Address","Measurement.Details.Tor.Table.Header.Type":"Type","Measurement.Details.Tor.Table.Header.Connect":"Connect","Measurement.Details.Tor.Table.Header.Handshake":"Handshake","Measurement.Status.Hint.TorSnowflake.Reachable":"Tor snowflake works","Measurement.Status.Hint.TorSnowflake.Blocked":"Tor snowflake does not work","Measurement.Status.Hint.TorSnowflake.Error":"Tor snowflake test failed","Measurement.Details.SummaryText.TorSnowflake.OK":"On {date}, [Tor Snowflake](https://www.torproject.org/) worked on {network} in {country}.\n\nThe [OONI Probe Tor Snowflake test](https://ooni.org/nettest/torsf/) was able to successfully bootstrap snowflake.","Measurement.Details.SummaryText.TorSnowflake.Blocked":"On {date}, [Tor Snowflake](https://www.torproject.org/) does not work on {network} in {country}.\n\nThe [OONI Probe Tor Snowflake test](https://ooni.org/nettest/torsf/) failed to bootstrap snowflake.","Measurement.Details.SummaryText.TorSnowflake.Error":"On {date}, the Tor Snowflake test failed on {network} in {country}.","Measurement.Details.TorSnowflake.BootstrapTime.Label":"Bootstrap Time","Measurement.Details.TorSnowflake.Error.Label":"Failure","Measurement.Metadata.TorSnowflake.Reachable":"Tor Snowflake was reachable in {country}","Measurement.Metadata.TorSnowflake.UnReachable":"Tor Snowflake was NOT reachable in {country}","Measurement.Metadata.TorSnowflake.Error":"Tor Snowflake test failed in {country}","Measurement.Status.Hint.RiseupVPN.Reachable":"RiseupVPN works","Measurement.Status.Hint.RiseupVPN.Blocked":"RiseupVPN is likely blocked","Measurement.Status.Hint.RiseupVPN.Failed":"The RiseupVPN test failed","Measurement.Details.SummaryText.RiseupVPN.OK":"On {date}, [RiseupVPN](https://riseup.net/vpn) was reachable on {network} in {country}. \n\nThe [OONI Probe RiseupVPN test](https://ooni.org/nettest/riseupvpn/) successfully connected to RiseupVPN's bootstrap servers and gateways.","Measurement.Details.SummaryText.RiseupVPN.Blocked":"On {date}, the testing of [RiseupVPN](https://riseup.net/vpn) presented signs of blocking on {network} in {country}.\n\nThis might mean that RiseupVPN was blocked, but [false positives](https://ooni.org/support/faq/#why-do-false-positives-occur) can occur. \n\nPlease explore the network measurement data below. Check other RiseupVPN measurements from the same network during the same time period (if they're available).","Measurement.Metadata.RiseupVPN.Reachable":"RiseupVPN was reachable in {country}","Measurement.Metadata.RiseupVPN.Blocked":"RiseupVPN was not reachable in {country}","Measurement.Hero.Status.Default":"Measurement Report","Navbar.Search":"Search","Navbar.Countries":"Countries","Navbar.Charts.Circumvention":"Circumvention Charts","Navbar.Charts.MAT":"MAT Charts","Footer.Text.Slogan":"Global community measuring internet censorship around the world.","Footer.Heading.About":"About","Footer.Heading.OONIProbe":"OONI Probe","Footer.Heading.Updates":"Updates","Footer.Heading.SocialLinks":"","Footer.Link.About":"OONI","Footer.Link.DataPolicy":"Data Policy","Footer.Link.DataLicense":"Data License","Footer.Link.Contact":"Contact","Footer.Link.Probe":"Install","Footer.Link.Tests":"Tests","Footer.Link.Code":"Source code","Footer.Link.API":"API","Footer.Link.Blog":"Blog","Footer.Link.Twitter":"Twitter","Footer.Link.MailingList":"Mailing list","Footer.Link.Slack":"Slack","Footer.Text.Copyright":"© 2019 Open Observatory of Network Interference (OONI)","Footer.Text.CCommons":"Content available under a Creative Commons license.","Footer.Text.Version":"Version","CategoryCode.ALDR.Name":"Drugs & Alcohol","CategoryCode.REL.Name":"Religion","CategoryCode.PORN.Name":"Pornography","CategoryCode.PROV.Name":"Provocative Attire","CategoryCode.POLR.Name":"Political Criticism","CategoryCode.HUMR.Name":"Human Rights Issues","CategoryCode.ENV.Name":"Environment","CategoryCode.MILX.Name":"Terrorism and Militants","CategoryCode.HATE.Name":"Hate Speech","CategoryCode.NEWS.Name":"News Media","CategoryCode.XED.Name":"Sex Education","CategoryCode.PUBH.Name":"Public Health","CategoryCode.GMB.Name":"Gambling","CategoryCode.ANON.Name":"Circumvention tools","CategoryCode.DATE.Name":"Online Dating","CategoryCode.GRP.Name":"Social Networking","CategoryCode.LGBT.Name":"LGBTQ+","CategoryCode.FILE.Name":"File-sharing","CategoryCode.HACK.Name":"Hacking Tools","CategoryCode.COMT.Name":"Communication Tools","CategoryCode.MMED.Name":"Media sharing","CategoryCode.HOST.Name":"Hosting and Blogging","CategoryCode.SRCH.Name":"Search Engines","CategoryCode.GAME.Name":"Gaming","CategoryCode.CULTR.Name":"Culture","CategoryCode.ECON.Name":"Economics","CategoryCode.GOVT.Name":"Government","CategoryCode.COMM.Name":"E-commerce","CategoryCode.CTRL.Name":"Control content","CategoryCode.IGO.Name":"Intergovernmental Orgs.","CategoryCode.MISC.Name":"Miscellaneous content","CategoryCode.ALDR.Description":"Use and sale of drugs and alcohol","CategoryCode.REL.Description":"Religious issues, both supportive and critical","CategoryCode.PORN.Description":"Hard-core and soft-core pornography","CategoryCode.PROV.Description":"Provocative attire and portrayal of women wearing minimal clothing","CategoryCode.POLR.Description":"Critical political viewpoints","CategoryCode.HUMR.Description":"Human rights issues","CategoryCode.ENV.Description":"Discussions on environmental issues","CategoryCode.MILX.Description":"Terrorism, violent militant or separatist movements","CategoryCode.HATE.Description":"Disparaging of particular groups based on race, sex, sexuality or other characteristics","CategoryCode.NEWS.Description":"Major news websites, regional news outlets and independent media","CategoryCode.XED.Description":"Sexual health issues including contraception, STD's, rape prevention and abortion","CategoryCode.PUBH.Description":"Public health issues including HIV, SARS, bird flu, World Health Organization","CategoryCode.GMB.Description":"Online gambling and betting","CategoryCode.ANON.Description":"Anonymization, censorship circumvention and encryption","CategoryCode.DATE.Description":"Online dating sites","CategoryCode.GRP.Description":"Online social networking tools and platforms","CategoryCode.LGBT.Description":"LGBTQ+ communities discussing related issues (excluding pornography)","CategoryCode.FILE.Description":"File sharing including cloud-based file storage, torrents and P2P","CategoryCode.HACK.Description":"Computer security tools and news","CategoryCode.COMT.Description":"Individual and group communication tools including VoIP, messaging and webmail","CategoryCode.MMED.Description":"Video, audio and photo sharing","CategoryCode.HOST.Description":"Web hosting, blogging and other online publishing","CategoryCode.SRCH.Description":"Search engines and portals","CategoryCode.GAME.Description":"Online games and gaming platforms (excluding gambling sites)","CategoryCode.CULTR.Description":"Entertainment including history, literature, music, film, satire and humour","CategoryCode.ECON.Description":"General economic development and poverty","CategoryCode.GOVT.Description":"Government-run websites, including military","CategoryCode.COMM.Description":"Commercial services and products","CategoryCode.CTRL.Description":"Benign or innocuous content used for control","CategoryCode.IGO.Description":"Intergovernmental organizations including The United Nations","CategoryCode.MISC.Description":"Sites that haven't been categorized yet","Country.Heading.Overview":"Overview","Country.Heading.Websites":"Websites","Country.Heading.Apps":"Apps","Country.Heading.NetworkProperties":"Networks","Country.Overview.Heading.NwInterference":"In a nutshell","Country.Overview.NwInterference.Middleboxes.Blocked":"Middleboxes were detected on {middleboxCount} network(s)","Country.Overview.NwInterference.Middleboxes.Normal":"No middleboxes were detected on tested networks","Country.Overview.NwInterference.Middleboxes.NoData":"Not enough data available on middleboxes","Country.Overview.NwInterference.IM.Blocked":"Instant messaging apps were likely blocked","Country.Overview.NwInterference.IM.Normal":"No instant messaging apps were blocked on tested networks","Country.Overview.NwInterference.IM.NoData":"Not enough data available on instant messaging apps","Country.Overview.NwInterference.CircumventionTools.Blocked":"Circumvention tools were likely blocked","Country.Overview.NwInterference.CircumventionTools.Normal":"No circumvention tools were blocked on tested networks","Country.Overview.NwInterference.CircumventionTools.NoData":"Not enough data available on circumvention tools","Country.Overview.NwInterference.Websites.Blocked":"OONI data confirms the blocking of websites","Country.Overview.NwInterference.Websites.Normal":"The blocking of websites is not confirmed","Country.Overview.NwInterference.Websites.NoData":"Not enough data available on blocked websites","Country.Overview.Heading.TestsByClass":"Measurement Coverage","Country.Overview.Heading.TestsByClass.Description":"The graph below provides an overview of OONI Probe measurement coverage. It shows how many results have been collected from each OONI Probe test category, as well as how many networks have been covered by tests. \n\nBy looking at this graph, you can understand if there is enough data to draw meaningful conclusions. If there is not enough data and you are in the country in question, [install OONI Probe](https://ooni.org/install), run tests, and contribute data!","Country.Overview.TestsByClass.Websites":"Websites","Country.Overview.TestsByClass.InstantMessaging":"Instant Messaging","Country.Overview.TestsByClass.Performance":"Performance","Country.Overview.TestsByClass.Middleboxes":"Middleboxes","Country.Overview.TestsByClass.Circumvention":"Circumvention Tools","Country.Overview.FeaturedResearch":"Research Reports","Country.Overview.FeaturedResearch.None":"We haven't published a research report based on OONI data from this country yet. \n\nWe encourage you to use OONI data for your research!","Country.Overview.SummaryTextTemplate":"OONI Probe users in **{countryName}** have collected [**{measurementCount}** measurements]({linkToMeasurements}) from **{networkCovered}** local networks.\n\nExplore the data below to check the accessibility and/or blocking of sites and services.","Country.Overview.NoData.Title":"Let's collect more data!","Country.Overview.NoData.CallToAction":"We don’t have enough measurements for **{country}** to show a chart. If you are in {country} or know people there, tell them to run OONI Probe to collect more measurements.","Country.Overview.NoData.Button.InstallProbe":"Install OONI Probe","Country.Overview.NoData.Button.OoniRunLink":"Create OONI Run Link","Country.Meta.Title":"Internet Censorship in {countryName} - OONI Explorer","Country.Meta.Description":"OONI Probe users in {countryName} have collected {measurementCount} measurements from {networkCount} local networks. Explore the data on OONI Explorer.","Country.PeriodFilter.Label":"Show results from","Country.PeriodFilter.Option.30Days":"Last 30 Days","Country.PeriodFilter.Option.2Months":"Last 2 Months","Country.PeriodFilter.Option.3Months":"Last 3 Months","Country.PeriodFilter.Option.6Months":"Last 6 Months","Country.Websites.Description":"Check whether websites have been blocked.\n\nTesting methodology: OONI's [Web Connectivity test](https://ooni.org/nettest/web-connectivity/), designed to measure the DNS, HTTP, and TCP/IP blocking of websites. \n\nTested websites: [Citizen Lab test lists](https://github.com/citizenlab/test-lists)\n\nIf you'd like to see results on the testing of different websites, please [contribute to test lists](https://ooni.org/get-involved/contribute-test-lists/) or test the sites of your choice via the [OONI Probe mobile app](https://ooni.org/install/). \n\nPlease note that unless a block page is served, some anomalous measurements may contain [false positives](https://ooni.org/support/faq/#why-do-false-positives-occur). We therefore encourage you to examine anomalous measurements in depth and over time.","Country.Websites.Description.MoreLinkText":"","Country.Websites.Heading.BlockedByCategory":"Categories of Blocked Websites","Country.Websites.BlockedByCategory.Description":"Websites that fall under the following categories were blocked on the {selectedASN} network.","Country.Websites.TestedWebsitesCount":"URLs tested","Country.Websites.Labels.ResultsPerPage":"Results per page","Country.Websites.URLSearch.Placeholder":"Search for URL","Country.Websites.URLCharts.Legend.Label.Blocked":"Confirmed Blocked","Country.Websites.URLCharts.Legend.Label.Anomaly":"Anomaly","Country.Websites.URLCharts.Legend.Label.Accessible":"Accessible","Country.Websites.URLCharts.ExploreMoreMeasurements":"Explore more measurements","Country.Websites.URLCharts.Pagination.Previous":"Previous Page","Country.Websites.URLCharts.Pagination.Next":"Next Page","Country.Apps.Description":"Check whether instant messaging apps and circumvention tools are blocked.\n\nThe following results were collected through the use of [OONI Probe tests](https://ooni.org/nettest/) designed to measure the blocking of WhatsApp, Facebook Messenger, and Telegram. \n\nWe also share results on the testing of circumvention tools, like [Tor](https://www.torproject.org/).","Country.Apps.Label.LastTested":"Last tested","Country.Apps.Label.TestedNetworks":"tested networks","Country.Apps.Button.ShowMore":"Show More","Country.NetworkProperties.Description":"Check the speed and performance of networks.\n\nThe following results were collected through the use of [OONI Probe's performance and middlebox tests](https://ooni.org/nettest/). You can check the speed and performance of tested networks, as well as video streaming performance. \n\nYou can also learn whether middleboxes were detected in tested networks. Middleboxes are network appliances that can be used for a variety of networking purposes (such as caching), but sometimes they're used to implement internet censorship and/or surveillance.","Country.NetworkProperties.Heading.Summary":"Summary","Country.NetworkProperties.Heading.Networks":"Networks","Country.NetworkProperties.InfoBox.Label.AverageDownload":"Average Download","Country.NetworkProperties.InfoBox.Label.AverageUpload":"Average Upload","Country.NetworkProperties.InfoBox.Label.Covered":"Covered","Country.NetworkProperties.InfoBox.Label.Middleboxes":"Middleboxes detected","Country.NetworkProperties.InfoBox.Units.Mbits":"Mbit/s","Country.NetworkProperties.InfoBox.Units.Networks.Singular":"Network","Country.NetworkProperties.InfoBox.Units.Networks.Plural":"Networks","Country.NetworkProperties.InfoBox.Label.AverageStreaming":"Average Streaming","Country.NetworkProperties.InfoBox.Label.AveragePing":"Average Ping","Country.NetworkProperties.InfoBox.Units.Milliseconds":"ms","Country.NetworkProperties.InfoBox.Label.Middleboxes.Found":"Middleboxes detected","Country.NetworkProperties.InfoBox.Label.Middleboxes.NotFound":"No middleboxes detected","Country.NetworkProperties.Button.ShowMore":"Show more networks","Country.Label.NoData":"No Data Available","Search.Sidebar.Domain":"Domain","Search.Sidebar.Domain.Placeholder":"e.g. twitter.com or 1.1.1.1","Search.Sidebar.Domain.Error":"Please enter a valid domain name or IP address, such as twitter.com or 1.1.1.1","Search.Sidebar.Categories":"Website Categories","Search.Sidebar.Categories.All":"Any","Search.Sidebar.Status":"Status","Search.Sidebar.TestName":"Test Name","Search.Sidebar.TestName.AllTests":"Any","Search.Sidebar.Country":"Country","Search.Sidebar.Country.AllCountries":"Any","Search.Sidebar.ASN":"ASN","Search.Sidebar.ASN.example":"e.g. AS30722","Search.Sidebar.ASN.Error":"Valid formats: AS1234, 1234","Search.Sidebar.From":"From","Search.Sidebar.Until":"Until","Search.Sidebar.HideFailed":"Hide failed measurements","Search.Sidebar.Button.FilterResults":"Filter Results","Search.FilterButton.AllResults":"All Results","Search.FilterButton.Confirmed":"Confirmed","Search.FilterButton.Anomalies":"Anomalies","Search.FilterButton.Search":"Search","Search.Bullet.Reachable":"Accessible","Search.Bullet.Anomaly":"Anomaly","Search.Bullet.Blocked":"Confirmed blocked","Search.Bullet.Error":"Error","Search.Filter.SortBy":"Sort by","Search.Filter.SortBy.Date":"Date","Search.WebConnectivity.Results.Reachable":"Accessible","Search.WebConnectivity.Results.Anomaly":"Anomaly","Search.WebConnectivity.Results.Blocked":"Confirmed","Search.WebConnectivity.Results.Error":"Error","Search.HTTPRequests.Results.Anomaly":"","Search.HTTPRequests.Results.Blocked":"","Search.HTTPRequests.Results.Error":"","Search.HTTPRequests.Results.Reachable":"","Search.WhatsApp.Results.Reachable":"Accessible","Search.WhatsApp.Results.Anomaly":"Anomaly","Search.WhatsApp.Results.Error":"Error","Search.FacebookMessenger.Results.Reachable":"Accessible","Search.FacebookMessenger.Results.Anomaly":"Anomaly","Search.FacebookMessenger.Results.Error":"Error","Search.Telegram.Results.Reachable":"Accessible","Search.Telegram.Results.Anomaly":"Anomaly","Search.Telegram.Results.Error":"Error","Search.Signal.Results.Reachable":"Accessible","Search.Signal.Results.Anomaly":"Anomaly","Search.Signal.Results.Error":"Error","Search.HTTPInvalidRequestLine.Results.Anomaly":"Anomaly","Search.HTTPInvalidRequestLine.Results.Reachable":"OK","Search.HTTPInvalidRequestLine.Results.Error":"Error","Search.HTTPHeaderFieldManipulation.Results.Anomaly":"Anomaly","Search.HTTPHeaderFieldManipulation.Results.Reachable":"OK","Search.HTTPHeaderFieldManipulation.Results.Error":"Error","Search.Tor.Results.Reachable":"OK","Search.Tor.Results.Anomaly":"Anomaly","Search.Tor.Results.Error":"Error","Search.Psiphon.Results.Reachable":"Ok","Search.Psiphon.Results.Anomaly":"Anomaly","Search.Psiphon.Results.Error":"Error","Search.RiseupVPN.Results.Reachable":"Accessible","Search.RiseupVPN.Results.Anomaly":"Anomaly","Search.RiseupVPN.Results.Error":"Error","Search.Test.Results.OK":"OK","Search.Test.Results.Error":"Error","Search.NDT.Results":"","Search.DASH.Results":"","Search.VanillaTor.Results":"","Search.BridgeReachability.Results":"","Search.LegacyTests.Results":"","Search.Results.Empty.Heading":"No Results Found","Search.Results.Empty.Description":"Please try changing the filters to get better results.","Search.Button.LoadMore":"Load more","Search.Error.Message":"This query took too long to complete. Please try adjusting the search filters or view the example queries in the [Highlights section of the homepage](/#highlights).\n\nIf you are interested in using OONI data in batch, we recommend the [ooni-data Amazon S3 bucket](https://ooni.org/post/mining-ooni-data/) or the [aggregation API](https://api.ooni.io/apidocs/#/default/get_api_v1_aggregation).\n\nWe are working on improving the performance of OONI Explorer. To track our work on this, [see the open issues on the ooni/api repository](https://github.com/ooni/api/issues?q=is%3Aissue+is%3Aopen+label%3Aoptimization).","Search.Error.Details.Label":"Server Response","Home.Banner.Title.UncoverEvidence":"Uncover evidence of internet censorship worldwide","Home.Banner.Subtitle.ExploreCensorshipEvents":"Open data collected by the global OONI community","Home.Banner.Button.Explore":"Explore","Home.Banner.Stats.Measurements":"Measurements","Home.Banner.Stats.Countries":"Countries","Home.Banner.Stats.Networks":"Networks","Home.About.SummaryText":"OONI Explorer is an open data resource on internet censorship around the world. \n\nSince 2012, millions of network measurements have been collected from more than 200 countries. OONI Explorer sheds light on internet censorship and other forms of network interference worldwide.\n\nTo contribute to this open dataset, [install OONI Probe](https://ooni.org/install/) and run tests!","Home.Websites&Apps.Title":"Blocking of Websites & Apps","Home.Websites&Apps.SummaryText":"Discover blocked websites around the world. Check whether WhatsApp, Facebook Messenger, and Telegram are blocked.","Home.Search&Filter.Title":"Search","Home.Search&Filter.SummaryText":"Explore OONI measurements with a powerful search tool. View the most recently blocked websites. Compare internet censorship across networks.","Home.NetworkProperties.Title":"Network Performance","Home.NetworkProperties.SummaryText":"Check the speed and performance of thousands of networks around the world. Explore data on video streaming performance.","Home.MonthlyStats.Title":"Monthly coverage worldwide","Home.MonthlyStats.SummaryText":"OONI Explorer hosts millions of network measurements collected from more than 200 countries since 2012. Every day, OONI Explorer is updated with new measurements!\n\nLast month, {measurementCount} OONI Probe measurements were collected from {networkCount} networks in {countryCount} countries. Explore the monthly usage of [OONI Probe](https://ooni.org/install/) through the stats below.","Home.Highlights.Title":"Highlights","Home.Highlights.Description":"What can you learn from OONI Explorer? \n\nBelow we share some stories from [research reports](https://ooni.org/post/) based on OONI data.\n\nWe share these case studies to demonstrate how OONI's openly available data can be used and what types of stories can be told. \n\nWe encourage you to explore OONI data, discover more censorship cases, and to use OONI data as part of your research and/or advocacy.","Home.Highlights.Political":"Censorship during political events","Home.Highlights.Political.Description":"Internet censorship sometimes occurs in response to or in anticipation of political events, such as elections, protests, and riots. Below we share a few cases detected via OONI data and correlated with political events.","Home.Highlights.Media":"Media censorship","Home.Highlights.Media.Description":"Press freedom is threatened in countries that experience the blocking of media websites. Below we share a few cases detected through OONI data.","Home.Highlights.LGBTQI":"Blocking of LGBTQI sites","Home.Highlights.LGBTQI.Description":"Minority group sites are blocked around the world. Below we share a few cases on the blocking of LGBTQI sites.","Home.Highlights.Changes":"Censorship changes","Home.Highlights.Changes.Description":"OONI measurements have been collected on a continuous basis since 2012, enabling the identification of censorship changes around the world. Some examples include:","Home.Meta.Description":"OONI Explorer is an open data resource on Internet censorship around the world consisting of millions of measurements on network inteference.","Countries.Heading.JumpToContinent":"Jump to continent","Countries.Search.NoCountriesFound":"No countries found with '{searchTerm}'","Countries.Search.Placeholder":"Search for countries","Error.404.GoBack":"Go back","Error.404.Heading":"The requested page does not exist","Error.404.Message":"We could not find the content you were looking for. Maybe try {measurmentLink} or look at {homePageLink}.","Error.404.MeasurmentLinkText":"exploring some measurement","Error.404.HomepageLinkText":"the homepage","MAT.Title":"OONI Measurement Aggregation Toolkit (MAT)","MAT.SubTitle":"Create charts based on aggregate views of real-time OONI data from around the world","MAT.Form.Label.XAxis":"X Axis","MAT.Form.Label.YAxis":"Y Axis","MAT.Form.Label.AxisOption.domain":"Domain","MAT.Form.Label.AxisOption.measurement_start_day":"Measurement Day","MAT.Form.Label.AxisOption.probe_cc":"Countries","MAT.Form.Label.AxisOption.category_code":"Website Categories","MAT.Form.Label.AxisOption.probe_asn":"ASN","MAT.Form.ConfirmationModal.Title":"Are you sure?","MAT.Form.ConfirmationModal.Message":"Duration too long. This can potentially slow down the page","MAT.Form.ConfirmationModal.No":"No","MAT.Form.ConfirmationModal.Button.Yes":"Yes","MAT.Form.Submit":"Show Chart","MAT.Table.Header.anomaly_count":"Anomaly Count","MAT.Table.Header.confirmed_count":"Confirmed Count","MAT.Table.Header.failure_count":"Failure Count","MAT.Table.Header.measurement_count":"Measurement Count","MAT.Table.Header.input":"URL","MAT.Table.Header.category_code":"Category Code","MAT.Table.Header.probe_cc":"Country","MAT.Table.Header.probe_asn":"ASN","MAT.Table.Header.blocking_type":"Blocking Type","MAT.Table.Header.domain":"Domain","MAT.Charts.NoData.Title":"No Data Found","MAT.Charts.NoData.Description":"We are not able to produce a chart based on the selected filters. Please change the filters and try again.","MAT.Help.Box.Title":"Help","MAT.Help.Title":"FAQs","MAT.Help.Content":"# What is the MAT?\n\nOONI's Measurement Aggregation Toolkit (MAT) is a tool that enables you to generate your own custom charts based on **aggregate views of real-time OONI data** collected from around the world.\n\nOONI data consists of network measurements collected by [OONI Probe](https://ooni.org/install/) users around the world. \n\nThese measurements contain information about various types of **internet censorship**, such as the [blocking of websites and apps](https://ooni.org/nettest/) around the world. \n\n# Who is the MAT for?\n\nThe MAT was built for researchers, journalists, and human rights defenders interested in examining internet censorship around the world.\n\n# Why use the MAT?\n\nWhen examining cases of internet censorship, it's important to **look at many measurements at once** (\"in aggregate\") in order to answer key questions like the following:\n\n* Does the testing of a service (e.g. Facebook) present **signs of blocking every time that it is tested** in a country? This can be helpful for ruling out [false positives](https://ooni.org/support/faq/#what-are-false-positives).\n* What types of websites (e.g. human rights websites) are blocked in each country?\n* In which countries is a specific website (e.g. `bbc.com`) blocked?\n* How does the blocking of different apps (e.g. WhatsApp or Telegram) vary across countries?\n* How does the blocking of a service vary across countries and [ASNs](https://ooni.org/support/glossary/#asn)?\n* How does the blocking of a service change over time?\n\nWhen trying to answer questions like the above, we normally perform relevant data analysis (instead of inspecting measurements one by one). \n\nThe MAT incorporates our data analysis techniques, enabling you to answer such questions without any data analysis skills, and with the click of a button!\n\n# How to use the MAT?\n\nThrough the filters at the start of the page, select the parameters you care about in order to plot charts based on aggregate views of OONI data.\n\nThe MAT includes the following filters:\n\n* **Countries:** Select a country through the drop-down menu (the \"All Countries\" option will show global coverage)\n* **Test Name:** Select an [OONI Probe test](https://ooni.org/nettest/) based on which you would like to get measurements (for example, select `Web Connectivity` to view the testing of websites)\n* **Domain:** Type the domain for the website you would like to get measurements (e.g. `twitter.com`)\n* **Website categories:** Select the [website category](https://github.com/citizenlab/test-lists/blob/master/lists/00-LEGEND-new_category_codes.csv) for which you would like to get measurements (e.g. `News Media` for news media websites)\n* **ASN:** Type the [ASN](https://ooni.org/support/glossary/#asn) of the network for which you would like to get measurements (e.g. `AS30722` for Vodafone Italia)\n* **Date range:** Select the date range of the measurements by adjusting the `Since` and `Until` filters\n* **X axis:** Select the values that you would like to appear on the horizontal axis of your chart\n* **Y axis:** Select the values that you would like to appear on the vertical axis of your chart\n\nDepending on what you would like to explore, adjust the MAT filters accordingly and click `Show Chart`. \n\nFor example, if you would like to check the testing of BBC in all countries around the world:\n\n* Type `www.bbc.com` under `Domain`\n* Select `Countries` under the `Y axis`\n* Click `Show Chart`\n\nThis will plot numerous charts based on the OONI Probe testing of `www.bbc.com` worldwide.\n\n# Interpreting MAT charts\n\nThe MAT charts (and associated tables) include the following values:\n\n* **OK count:** Successful measurements (i.e. NO sign of internet censorship)\n* **Confirmed count:** Measurements from automatically **confirmed blocked websites** (e.g. a [block page](https://ooni.org/support/glossary/#block-page) was served)\n* **Anomaly count:** Measurements that provided **signs of potential blocking** (however, [false positives](https://ooni.org/support/faq/#what-are-false-positives) can occur) \n* **Failure count:** Failed experiments that should be discarded\n* **Measurement count:** Total volume of OONI measurements (pertaining to the selected country, resource, etc.)\n\nWhen trying to identify the blocking of a service (e.g. `twitter.com`), it's useful to check whether:\n\n* Measurements are annotated as `confirmed`, automatically confirming the blocking of websites\n* A large volume of measurements (in comparison to the overall measurement count) present `anomalies` (i.e. signs of potential censorship)\n\nYou can access the raw data by clicking on the bars of charts, and subsequently clicking on the relevant measurement links. \n\n# Website categories\n\n[OONI Probe](https://ooni.org/install/) users test a wide range of [websites](https://ooni.org/support/faq/#which-websites-will-i-test-for-censorship-with-ooni-probe) that fall under the following [30 standardized categories](https://github.com/citizenlab/test-lists/blob/master/lists/00-LEGEND-new_category_codes.csv).","MAT.Help.Subtitle.Categories":"Categories","ReachabilityDash.Heading.CircumventionTools":"Reachability of Censorship Circumvention Tools","ReachabilityDash.CircumventionTools.Description":"The charts below display aggregate views of OONI data based on the testing of the following circumvention tools:\n\n* [Psiphon](https://ooni.org/nettest/psiphon)\n\n* [Tor](https://ooni.org/nettest/tor)\n\n* [Tor Snowflake](https://ooni.org/nettest/tor-snowflake/)\n\nPlease note that the presence of [anomalous measurements](https://ooni.org/support/faq/#what-do-you-mean-by-anomalies) is not always indicative of blocking, as [false positives](https://ooni.org/support/faq/#what-are-false-positives) can occur. Moreover, circumvention tools often have built-in circumvention techniques for evading censorship. \n\nWe therefore recommend referring to **[Tor Metrics](https://metrics.torproject.org/)** and to the **[Psiphon Data Engine](https://psix.ca/)** to view usage stats and gain a more comprehensive understanding of whether these tools work in each country.","ReachabilityDash.Form.Label.CountrySelect.AllSelected":"All countries selected","ReachabilityDash.Form.Label.CountrySelect.SearchPlaceholder":"Search","ReachabilityDash.Form.Label.CountrySelect.SelectAll":"Select All","ReachabilityDash.Form.Label.CountrySelect.SelectAllFiltered":"Select All (Filtered)","ReachabilityDash.Form.Label.CountrySelect.InputPlaceholder":"Select Countries...","ReachabilityDash.Meta.Description":"View the accessibility of censorship circumvention tools around the world through OONI data."}} \ No newline at end of file +window.OONITranslations = {"en":{"General.OoniExplorer":"OONI Explorer","General.OK":"OK","General.Error":"Error","General.Anomaly":"Anomaly","General.Accessible":"Accessible","General.Failed":"Failed","General.Loading":"Loading…","General.NoData":"No data","General.Apply":"Apply","General.Reset":"Reset","SocialButtons.CTA":"Share on Facebook or Twitter","SocialButtons.Text":"Data from OONI Explorer","Tests.WebConnectivity.Name":"Web Connectivity Test","Tests.Telegram.Name":"Telegram Test","Tests.Facebook.Name":"Facebook Messenger Test","Tests.WhatsApp.Name":"WhatsApp Test","Tests.Signal.Name":"Signal Test","Tests.HTTPInvalidReqLine.Name":"HTTP Invalid Request Line Test","Tests.HTTPHeaderManipulation.Name":"HTTP Header Field Manipulation Test","Tests.NDT.Name":"NDT Speed Test","Tests.Dash.Name":"DASH Video Streaming Test","Tests.TorVanilla.Name":"Tor (Vanilla) Test","Tests.BridgeReachability.Name":"Tor Bridge Reachability Test","Tests.TCPConnect.Name":"TCP Connect Test","Tests.DNSConsistency.Name":"DNS Consistency Test","Tests.HTTPRequests.Name":"HTTP Requests Test","Tests.Psiphon.Name":"Psiphon Test","Tests.Tor.Name":"Tor Test","Tests.RiseupVPN.Name":"Riseup VPN Test","Tests.TorSnowflake.Name":"Tor Snowflake Test","Tests.DNSCheck.Name":"DNS Check","Tests.StunReachability.Name":"STUN Reachability","Tests.URLGetter.Name":"URL Getter","Tests.Groups.Webistes.Name":"Websites","Tests.Groups.Instant Messagging.Name":"Instant Messaging","Tests.Groups.Middlebox.Name":"Middleboxes","Tests.Groups.Performance.Name":"Performance","Tests.Groups.Circumvention.Name":"Circumvention","Tests.Groups.Experimental.Name":"Experimental","Tests.Groups.Legacy.Name":"Legacy","Measurement.MetaDescription":"OONI data suggests {description} on {formattedDate}, find more open data on internet censorship on OONI Explorer.","Measurement.NotFound":"Measurement not found","Measurement.Hero.Status.Confirmed":"Confirmed Blocked","Measurement.Hero.Status.Down":"Website Down","Measurement.Hero.Status.Anomaly.DNS":"DNS","Measurement.Hero.Status.Anomaly.HTTP":"HTTP","Measurement.Hero.Status.Anomaly.TCP":"TCP/IP","Measurement.CommonSummary.Label.ASN":"Network","Measurement.CommonSummary.Label.Country":"Country","Measurement.CommonSummary.Label.DateTime":"Date & Time","Measurement.DetailsHeader.Runtime":"Runtime","Measurement.Status.Hint.Websites.Censorship":"","Measurement.Status.Hint.Websites.DNS":"DNS tampering","Measurement.Status.Hint.Websites.Error":"Error in detection","Measurement.Status.Hint.Websites.HTTPdiff":"HTTP blocking (a blockpage might be served)","Measurement.Status.Hint.Websites.HTTPfail":"HTTP blocking (HTTP requests failed)","Measurement.Status.Hint.Websites.NoCensorship":"No blocking detected","Measurement.Status.Hint.Websites.TCPBlock":"TCP/IP blocking","Measurement.Status.Hint.Websites.Unavailable":"Website down","Measurement.SummaryText.Websites.Accessible":"On {date}, {WebsiteURL} was accessible when tested on {network} in {country}.","Measurement.SummaryText.Websites.Anomaly":"On {date}, {WebsiteURL} presented signs of {reason} on {network} in {country}.\n\nThis might mean that {WebsiteURL} was blocked, but false positives can occur.\n\nPlease explore the network measurement data below.","Measurement.SummaryText.Websites.Anomaly.BlockingReason.DNS":"DNS tampering","Measurement.SummaryText.Websites.Anomaly.BlockingReason.TCP":"TCP/IP blocking","Measurement.SummaryText.Websites.Anomaly.BlockingReason.HTTP-failure":"HTTP blocking (HTTP requests failed)","Measurement.SummaryText.Websites.Anomaly.BlockingReason.HTTP-diff":"HTTP blocking (a blockpage might be served)","Measurement.SummaryText.Websites.ConfirmedBlocked":"On {date}, {WebsiteURL} was blocked on {network} in {country}.\n\nThis is confirmed because a block page was served, as illustrated through the network measurement data below.","Measurement.SummaryText.Websites.Failed":"On {date}, the test for {WebsiteURL} failed on {network} in {country}.","Measurement.SummaryText.Websites.Down":"On {date}, {WebsiteURL} was down on {network} in {country}.","Measurement.Details.Websites.Failures.Heading":"Failures","Measurement.Details.Websites.Failures.Label.HTTP":"HTTP Experiment","Measurement.Details.Websites.Failures.Label.DNS":"DNS Experiment","Measurement.Details.Websites.Failures.Label.Control":"Control","Measurement.Details.Websites.Failures.Values.Null":"null","Measurement.Details.Websites.DNSQueries.Heading":"DNS Queries","Measurement.Details.Websites.DNSQueries.Label.Resolver":"Resolver","Measurement.Details.Websites.TCP.Heading":"TCP Connections","Measurement.Details.Websites.TCP.ConnectionTo":"Connection to {destination} {connectionStatus}.","Measurement.Details.Websites.TCP.ConnectionTo.Success":"succeeded","Measurement.Details.Websites.TCP.ConnectionTo.Failed":"failed","Measurement.Details.Websites.TCP.ConnectionTo.Blocked":"was blocked","Measurement.Details.Websites.HTTP.Heading":"HTTP Requests","Measurement.Details.Websites.HTTP.Label.Response":"Response","Measurement.Details.Websites.HTTP.Request.URL":"URL","Measurement.Details.Websites.HTTP.Response.Body":"Response Body","Measurement.Details.Websites.HTTP.Response.Headers":"Response Headers","Measurement.CommonDetails.Label.MsmtID":"Report ID","Measurement.CommonDetails.Label.Platform":"Platform","Measurement.CommonDetails.Label.Software":"Software Name","Measurement.CommonDetails.Label.Engine":"Measurement Engine","Measurement.CommonDetails.Value.Unavailable":"Unavailable","Measurement.CommonDetails.RawMeasurement.Heading":"Raw Measurement Data","Measurement.CommonDetails.RawMeasurement.Download":"Download JSON","Measurement.CommonDetails.RawMeasurement.Unavailable":"Unavailable","Measurement.CommonDetails.RawMeasurement.Expand":"Expand All","Measurement.CommonDetails.Label.Resolver":"Resolver","Measurement.CommonDetails.Label.ResolverASN":"Resolver ASN","Measurement.CommonDetails.Label.ResolverIP":"Resolver IP","Measurement.CommonDetails.Label.ResolverNetworkName":"Resolver Network Name","Measurement.Hero.Status.NDT.Title":"Results","Measurement.Status.Info.Label.Download":"Download","Measurement.Status.Info.Label.Upload":"Upload","Measurement.Status.Info.Label.Ping":"Ping","Measurement.Status.Info.Label.Server":"Server","Measurement.Status.Info.NDT.Error":"Failed Test","Measurement.Details.Performance.Heading":"Performance Details","Measurement.Details.Performance.Label.AvgPing":"Average Ping","Measurement.Details.Performance.Label.MaxPing":"Max Ping","Measurement.Details.Performance.Label.MSS":"MSS","Measurement.Details.Performance.Label.RetransmitRate":"Retransmission Rate","Measurement.Details.Performance.Label.PktLoss":"Packet Loss","Measurement.Details.Performance.Label.OutOfOrder":"Out of Order","Measurement.Details.Performance.Label.Timeouts":"Timeouts","Measurement.Hero.Status.Dash.Title":"Results","Measurement.Status.Info.Label.VideoQuality":"Video Quality","Measurement.Status.Info.Label.Bitrate":"Median Bitrate","Measurement.Status.Info.Label.Delay":"Playout Delay","Measurement.Status.Hint.Telegram.Blocked":"Telegram is likely blocked","Measurement.Status.Hint.Telegram.Reachable":"Telegram is accessible","Measurement.Status.Hint.Telegram.Failed":"The Telegram test failed","Measurement.Details.SummaryText.Telegram.Reachable":"On {date}, Telegram was reachable on {network} in {country}. \n\nOONI's Telegram test successfully connected to Telegram's endpoints and web interface (web.telegram.org).","Measurement.Details.SummaryText.Telegram.AppFailure":"On {date}, the testing of Telegram's mobile app presented signs of blocking on {network} in {country}.\n\nThis might mean that Telegram's mobile app was blocked, but [false positives](https://ooni.org/support/faq/#why-do-false-positives-occur) can occur. \n\nPlease explore the network measurement data below. Check other Telegram measurements from the same network during the same time period (if they're available).","Measurement.Details.SummaryText.Telegram.DesktopFailure":"On {date}, the testing of Telegram's web interface (web.telegram.org) presented signs of blocking on {network} in {country}.\n\nThis might mean that web.telegram.org was blocked, but [false positives](https://ooni.org/support/faq/#why-do-false-positives-occur) can occur.\n\nPlease explore the network measurement data below. Check other Telegram measurements from the same network during the same time period (if they're available).","Measurement.Details.SummaryText.Telegram.DesktopAndAppFailure":"On {date}, the testing of Telegram's mobile app and web interface (web.telegram.org) presented signs of blocking on {network} in {country}.\n\nThis might mean that both Telegram's mobile app and web interface were blocked, but [false positives](https://ooni.org/support/faq/#why-do-false-positives-occur) can occur.\n\nPlease explore the network measurement data below. Check other Telegram measurements from the same network during the same time period (if they're available).","Measurement.Details.Telegram.Endpoint.Label.Mobile":"Mobile App","Measurement.Details.Telegram.Endpoint.Label.Web":"Telegram Web","Measurement.Details.Endpoint.Status.Unknown":"Unknown","Measurement.Details.Telegram.Endpoint.Status.Heading":"Endpoint Status","Measurement.Details.Telegram.Endpoint.ConnectionTo.Failed":"Connection to {destination} failed.","Measurement.Details.Telegram.Endpoint.ConnectionTo.Successful":"Connection to {destination} was successful.","Measurement.Details.Hint.WhatsApp.Reachable":"WhatsApp is accessible","Measurement.Status.Hint.WhatsApp.Blocked":"WhatsApp is likely blocked","Measurement.Status.Hint.WhatsApp.Failed":"The WhatsApp test failed","Measurement.Details.SummaryText.WhatsApp.Reachable":"On {date}, WhatsApp was reachable on {network} in {country}. \n\nOONI's WhatsApp test successfully connected to WhatsApp's endpoints, registration service and web interface (web.whatsapp.com).","Measurement.Details.SummaryText.WhatsApp.AppFailure":"On {date}, the testing of WhatsApp's mobile app presented signs of blocking on {network} in {country}.\n\nThis might mean that WhatsApp's mobile app was blocked, but [false positives](https://ooni.org/support/faq/#why-do-false-positives-occur) can occur. \n\nPlease explore the network measurement data below. Check other WhatsApp measurements from the same network during the same time period (if they're available).","Measurement.Details.SummaryText.WhatsApp.DesktopFailure":"On {date}, the testing of WhatsApp's web interface (web.whatsapp.com) presented signs of blocking on {network} in {country}.\n\nThis might mean that web.whatsapp.com was blocked, but [false positives](https://ooni.org/support/faq/#why-do-false-positives-occur) can occur.\n\nPlease explore the network measurement data below. Check other WhatsApp measurements from the same network during the same time period (if they're available).","Measurement.Details.SummaryText.WhatsApp.DesktopAndAppFailure":"On {date}, the testing of WhatsApp's mobile app and web interface (web.whatsapp.com) presented signs of blocking on {network} in {country}.\n\nThis might mean that both WhatsApp's mobile app and web interface were blocked, but [false positives](https://ooni.org/support/faq/#why-do-false-positives-occur) can occur.\n\nPlease explore the network measurement data below. Check other WhatsApp measurements from the same network during the same time period (if they're available).","Measurement.Details.WhatsApp.Endpoint.Label.Mobile":"Mobile App","Measurement.Details.WhatsApp.Endpoint.Label.Web":"WhatsApp Web","Measurement.Details.WhatsApp.Endpoint.Label.Registration":"Registration","Measurement.Details.WhatsApp.Endpoint.Status.Heading":"Endpoint Status","Measurement.Details.WhatsApp.Endpoint.ConnectionTo.Failed":"Connection to {destination} failed.","Measurement.Details.WhatsApp.Endpoint.ConnectionTo.Successful":"Connection to {destination} was successful.","Measurement.Status.Hint.FacebookMessenger.Reachable":"Facebook Messenger is accessible","Measurement.Status.Hint.FacebookMessenger.Blocked":"Facebook Messenger is likely blocked","Measurement.Status.Hint.FacebookMessenger.Failed":"The Facebook Messenger test failed","Measurement.Details.SummaryText.FacebookMessenger.Reachable":"On {date}, Facebook Messenger was reachable on {network} in {country}.","Measurement.Details.SummaryText.FacebookMessenger.TCPFailure":"TCP connections to Facebook's endpoints failed.","Measurement.Details.SummaryText.FacebookMessenger.DNSFailure":"DNS lookups did not resolve to Facebook IP addresses.","Measurement.Details.SummaryText.FacebookMessenger.DNSSuccess":"DNS lookups resolved to Facebook IP addresses.","Measurement.Details.SummaryText.FacebookMessenger.TCPSuccess":"TCP connections to Facebook's enpoints succeeded.","Measurement.Details.FacebookMessenger.TCP.Label.Title":"TCP connections","Measurement.Details.FacebookMessenger.DNS.Label.Title":"DNS lookups","Measurement.Details.FacebookMessenger.TCPFailed":"TCP connections failed","Measurement.Details.FacebookMessenger.DNSFailed":"DNS lookups failed","Measurement.Details.FacebookMessenger.Endpoint.Status.Heading":"Endpoint Status","Measurement.Details.FacebookMessenger.Endpoint.ConnectionTo.Failed":"Connection to {destination} failed.","Measurement.Details.FacebookMessenger.Endpoint.ConnectionTo.Successful":"Connection to {destination} was successful.","Measurement.Status.Hint.Signal.Blocked":"Signal is likely blocked","Measurement.Status.Hint.Signal.Reachable":"Signal is accessible","Measurement.Status.Hint.Signal.Failed":"The Signal test failed","Measurement.Details.SummaryText.Signal.Reachable":"On {date}, [Signal](https://signal.org/) was reachable on {network} in {country}. \n\nThe [OONI Probe Signal test](https://ooni.org/nettest/signal) successfully connected to Signal's endpoints.","Measurement.Details.SummaryText.Signal.Blocked":"On {date}, the testing of the [Signal app](https://signal.org/) presented signs of blocking on {network} in {country}.\n\nThis might mean that Signal was blocked, but [false positives](https://ooni.org/support/faq/#why-do-false-positives-occur) can occur. \n\nPlease explore the network measurement data below. Check other Signal measurements from the same network during the same time period (if they're available).","Measurement.Hero.Status.HTTPHeaderManipulation.NoMiddleBoxes":"No middleboxes detected","Measurement.HTTPHeaderManipulation.NoMiddleBoxes.SummaryText":"On {date}, no network anomaly was detected on {network} in {country} when communicating with our servers.","Measurement.Hero.Status.HTTPHeaderManipulation.MiddleboxesDetected":"Network tampering","Measurement.HTTPHeaderManipulation.MiddleBoxesDetected.SummaryText":"On {date}, network traffic was manipulated when contacting our control servers. \n\nThis means that there might be a middlebox on {network} in {country}, which could be responsible for censorship and/or surveillance.","Measurement.Hero.Status.HTTPInvalidReqLine.NoMiddleBoxes":"No middleboxes detected","Measurement.HTTPInvalidReqLine.NoMiddleBoxes.SummaryText":"On {date}, no network anomaly was detected on {network} in {country} when communicating with our servers.","Measurement.Hero.Status.HTTPInvalidReqLine.MiddleboxesDetected":"Network tampering","Measurement.HTTPInvalidReqLine.MiddleboxesDetected.SummaryText":"On {date}, network traffic was manipulated when contacting our control servers. \n\nThis means that there might be a middlebox on {network} in {country}, which could be responsible for censorship and/or surveillance.","Measurement.HTTPInvalidReqLine.YouSent":"You Sent","Measurement.HTTPInvalidReqLine.YouReceived":"You Received","Measurement.Hero.Status.TorVanilla.Blocked":"Tor is likely blocked","Measurement.Hero.Status.TorVanilla.Reachable":"Tor is accessible","Measurement.Details.SummaryText.TorVanilla.Blocked":"On {date}, OONI's Vanilla Tor test did not manage to bootstrap a connection to the [Tor network](https://www.torproject.org/).\n\nThis might mean that access to the Tor network was blocked on {network} in {country}, but [false positives can occur](https://ooni.org/support/faq/#why-do-false-positives-occur).\n\nPlease explore the network measurement data below. Check other Tor measurements from the same network during the same time period (if they're available).","Measurement.Details.SummaryText.TorVanilla.Reachable":"OONI's Vanilla Tor test successfully bootstrapped a connection to the [Tor network](https://www.torproject.org/).\n\nThis means that the Tor network was reachable from {network} in {country} on {date}.","Measurement.Details.VanillaTor.Endpoint.Label.Reachability":"Reachability","Measurement.Status.Hint.Psiphon.Reachable":"Psiphon works","Measurement.Status.Hint.Psiphon.Blocked":"Psiphon is likely blocked","Measurement.Status.Hint.Psiphon.BootstrappingError":"Psiphon is likely blocked (bootstrap error)","Measurement.Details.SummaryText.Psiphon.OK":"On {date}, [Psiphon](https://psiphon.ca/) worked on {network} in {country}.\n\nThe [OONI Probe Psiphon test](https://ooni.org/nettest/psiphon/) was able to successfully bootstrap Psiphon and ensure that the app works.","Measurement.Details.SummaryText.Psiphon.Blocked":"On {date}, [Psiphon](https://psiphon.ca/) did not appear to work on {network} in {country}.\n\nWhile the [OONI Probe Psiphon test](https://ooni.org/nettest/psiphon/) was able to bootstrap Psiphon, it was unable to fetch a webpage from the internet. \n\nThis suggests that the Psiphon app may have been blocked on this network. \n\nHowever, [false positives](https://ooni.org/support/faq/#why-do-false-positives-occur) can occur. Please explore the network measurement data below and compare it with other relevant measurements (if they're available).","Measurement.Details.SummaryText.Psiphon.BootstrappingError":"On {date}, [Psiphon](https://psiphon.ca/) did not work on {network} in {country}.\n\nThe [OONI Probe Psiphon test](https://ooni.org/nettest/psiphon/) was unable to bootstrap Psiphon.\n\nThis suggests that the Psiphon app may have been blocked on this network.\n\nHowever, [false positives](https://ooni.org/support/faq/#why-do-false-positives-occur) can occur. Please explore the network measurement data below and compare it with other relevant measurements (if they're available).","Measurement.Details.Psiphon.BootstrapTime.Label":"Bootstrap Time","Measurement.Status.Hint.Tor.Reachable":"Tor works","Measurement.Status.Hint.Tor.Blocked":"Tor is likely blocked","Measurement.Status.Hint.Tor.Error":"Tor test failed","Measurement.Details.SummaryText.Tor.OK":"On {date}, [Tor](https://www.torproject.org/) worked on {network} in {country}.\n\nAs part of [OONI Probe Tor testing](https://ooni.org/nettest/tor/), all reachability measurements of selected Tor directory authorities and bridges were successful.","Measurement.Details.SummaryText.Tor.Blocked":"On {date}, [Tor](https://www.torproject.org/) did not appear to work on {network} in {country}.\n\nThe [OONI Probe Tor test](https://ooni.org/nettest/tor/) failed in performing certain measurements. More details are available through the network measurement data provided below.\n\nThis suggests that Tor may have been blocked on this network.\n\nHowever, [false positives](https://ooni.org/support/faq/#why-do-false-positives-occur) can occur.","Measurement.Details.SummaryText.Tor.Error":"On {date}, the Tor test failed on {network} in {country}.","Measurement.Details.Tor.Bridges.Label.Title":"Tor Browser Bridges","Measurement.Details.Tor.Bridges.Label.OK":"{bridgesAccessible}/{bridgesTotal} OK","Measurement.Details.Tor.DirAuth.Label.Title":"Tor Directory Authorities","Measurement.Details.Tor.DirAuth.Label.OK":"{dirAuthAccessible}/{dirAuthTotal} OK","Measurement.Details.Tor.Table.Header.Name":"Name","Measurement.Details.Tor.Table.Header.Address":"Address","Measurement.Details.Tor.Table.Header.Type":"Type","Measurement.Status.Hint.TorSnowflake.Reachable":"Tor Snowflake works","Measurement.Status.Hint.TorSnowflake.Blocked":"Tor Snowflake does not work","Measurement.Status.Hint.TorSnowflake.Error":"Tor Snowflake test failed","Measurement.Details.SummaryText.TorSnowflake.OK":"On {date}, [Tor Snowflake](https://www.torproject.org/) worked on {network} in {country}.\n\nThe [OONI Probe Tor Snowflake test](https://ooni.org/nettest/torsf/) was able to successfully bootstrap Snowflake.","Measurement.Details.SummaryText.TorSnowflake.Blocked":"On {date}, [Tor Snowflake](https://www.torproject.org/) does not work on {network} in {country}.\n\nThe [OONI Probe Tor Snowflake test](https://ooni.org/nettest/torsf/) failed to bootstrap Snowflake.","Measurement.Details.SummaryText.TorSnowflake.Error":"On {date}, the Tor Snowflake test failed on {network} in {country}.","Measurement.Details.TorSnowflake.BootstrapTime.Label":"Bootstrap Time","Measurement.Details.TorSnowflake.Error.Label":"Failure","Measurement.Metadata.TorSnowflake.Reachable":"Tor Snowflake was reachable in {country}","Measurement.Metadata.TorSnowflake.UnReachable":"Tor Snowflake was NOT reachable in {country}","Measurement.Metadata.TorSnowflake.Error":"Tor Snowflake test failed in {country}","Measurement.Status.Hint.RiseupVPN.Reachable":"RiseupVPN works","Measurement.Status.Hint.RiseupVPN.Blocked":"RiseupVPN is likely blocked","Measurement.Status.Hint.RiseupVPN.Failed":"The RiseupVPN test failed","Measurement.Details.SummaryText.RiseupVPN.OK":"On {date}, [RiseupVPN](https://riseup.net/vpn) was reachable on {network} in {country}. \n\nThe [OONI Probe RiseupVPN test](https://ooni.org/nettest/riseupvpn/) successfully connected to RiseupVPN's bootstrap servers and gateways.","Measurement.Details.SummaryText.RiseupVPN.Blocked":"On {date}, the testing of [RiseupVPN](https://riseup.net/vpn) presented signs of blocking on {network} in {country}.\n\nThis might mean that RiseupVPN was blocked, but [false positives](https://ooni.org/support/faq/#why-do-false-positives-occur) can occur. \n\nPlease explore the network measurement data below. Check other RiseupVPN measurements from the same network during the same time period (if they're available).","Measurement.Metadata.RiseupVPN.Reachable":"RiseupVPN was reachable in {country}","Measurement.Metadata.RiseupVPN.Blocked":"RiseupVPN was not reachable in {country}","Measurement.Hero.Status.Default":"Measurement Report","Navbar.Search":"Search","Navbar.Countries":"Countries","Navbar.Charts.Circumvention":"Circumvention Charts","Navbar.Charts.MAT":"MAT Charts","Network.Summary.TotalMeasurements":"Total number of measurements: **{measurementsTotal}**","Network.Summary.FirstMeasurement":"Date of the first measurement: **{formattedDate}**","Network.Summary.Countries":"Network observed in countries:","Network.Summary.Country.Measurements":"({measurementsTotal} measurements)","Network.NoData.Title":"Let's collect more data!","Network.NoData.Text":"We don't have enough data for this network to show the charts. Please run OONI Probe to collect more measurements.","Footer.Text.Slogan":"Global community measuring internet censorship around the world.","Footer.Heading.About":"About","Footer.Heading.OONIProbe":"OONI Probe","Footer.Heading.Updates":"Updates","Footer.Heading.SocialLinks":"","Footer.Link.About":"OONI","Footer.Link.DataPolicy":"Data Policy","Footer.Link.DataLicense":"Data License","Footer.Link.Contact":"Contact","Footer.Link.Probe":"Install","Footer.Link.Tests":"Tests","Footer.Link.Code":"Source code","Footer.Link.API":"API","Footer.Link.Blog":"Blog","Footer.Link.Twitter":"Twitter","Footer.Link.MailingList":"Mailing list","Footer.Link.Slack":"Slack","Footer.Text.Copyright":"© {currentYear} Open Observatory of Network Interference (OONI)","Footer.Text.CCommons":"Content available under a Creative Commons license.","Footer.Text.Version":"Version","CategoryCode.ALDR.Name":"Alcohol & Drugs","CategoryCode.REL.Name":"Religion","CategoryCode.PORN.Name":"Pornography","CategoryCode.PROV.Name":"Provocative Attire","CategoryCode.POLR.Name":"Political Criticism","CategoryCode.HUMR.Name":"Human Rights Issues","CategoryCode.ENV.Name":"Environment","CategoryCode.MILX.Name":"Terrorism and Militants","CategoryCode.HATE.Name":"Hate Speech","CategoryCode.NEWS.Name":"News Media","CategoryCode.XED.Name":"Sex Education","CategoryCode.PUBH.Name":"Public Health","CategoryCode.GMB.Name":"Gambling","CategoryCode.ANON.Name":"Anonymization and circumvention tools","CategoryCode.DATE.Name":"Online Dating","CategoryCode.GRP.Name":"Social Networking","CategoryCode.LGBT.Name":"LGBTQ+","CategoryCode.FILE.Name":"File-sharing","CategoryCode.HACK.Name":"Hacking Tools","CategoryCode.COMT.Name":"Communication Tools","CategoryCode.MMED.Name":"Media sharing","CategoryCode.HOST.Name":"Hosting and Blogging Platforms","CategoryCode.SRCH.Name":"Search Engines","CategoryCode.GAME.Name":"Gaming","CategoryCode.CULTR.Name":"Culture","CategoryCode.ECON.Name":"Economics","CategoryCode.GOVT.Name":"Government","CategoryCode.COMM.Name":"E-commerce","CategoryCode.CTRL.Name":"Control content","CategoryCode.IGO.Name":"Intergovernmental Organizations","CategoryCode.MISC.Name":"Miscellaneous content","CategoryCode.ALDR.Description":"Sites devoted to the use, paraphernalia, and sale of drugs and alcohol irrespective of the local legality.","CategoryCode.REL.Description":"Sites devoted to discussion of religious issues, both supportive and critical, as well as discussion of minority religious groups.","CategoryCode.PORN.Description":"Hard-core and soft-core pornography.","CategoryCode.PROV.Description":"Websites which show provocative attire and portray women in a sexual manner, wearing minimal clothing.","CategoryCode.POLR.Description":"Content that offers critical political viewpoints. Includes critical authors and bloggers, as well as oppositional political organizations. Includes pro-democracy content, anti-corruption content as well as content calling for changes in leadership, governance issues, legal reform, etc.","CategoryCode.HUMR.Description":"Sites dedicated to discussing human rights issues in various forms. Includes women's rights and rights of minority ethnic groups.","CategoryCode.ENV.Description":"Pollution, international environmental treaties, deforestation, environmental justice, disasters, etc.","CategoryCode.MILX.Description":"Sites promoting terrorism, violent militant or separatist movements.","CategoryCode.HATE.Description":"Content that disparages particular groups or persons based on race, sex, sexuality or other characteristics.","CategoryCode.NEWS.Description":"This category includes major news outlets (BBC, CNN, etc.) as well as regional news outlets and independent media.","CategoryCode.XED.Description":"Includes contraception, abstinence, STDs, healthy sexuality, teen pregnancy, rape prevention, abortion, sexual rights, and sexual health services.","CategoryCode.PUBH.Description":"HIV, SARS, bird flu, centers for disease control, World Health Organization, etc.","CategoryCode.GMB.Description":"Online gambling sites. Includes casino games, sports betting, etc.","CategoryCode.ANON.Description":"Sites that provide tools used for anonymization, circumvention, proxy-services and encryption.","CategoryCode.DATE.Description":"Online dating services which can be used to meet people, post profiles, chat, etc.","CategoryCode.GRP.Description":"Social networking tools and platforms.","CategoryCode.LGBT.Description":"A range of gay-lesbian-bisexual-transgender queer issues. (Excluding pornography)","CategoryCode.FILE.Description":"Sites and tools used to share files, including cloud-based file storage, torrents and P2P file-sharing tools.","CategoryCode.HACK.Description":"Sites dedicated to computer security, including news and tools. Includes malicious and non-malicious content.","CategoryCode.COMT.Description":"Sites and tools for individual and group communications. Includes webmail, VoIP, instant messaging, chat and mobile messaging applications.","CategoryCode.MMED.Description":"Video, audio or photo sharing platforms.","CategoryCode.HOST.Description":"Web hosting services, blogging and other online publishing platforms.","CategoryCode.SRCH.Description":"Search engines and portals.","CategoryCode.GAME.Description":"Online games and gaming platforms, excluding gambling sites.","CategoryCode.CULTR.Description":"Content relating to entertainment, history, literature, music, film, books, satire and humour.","CategoryCode.ECON.Description":"General economic development and poverty related topics, agencies and funding opportunities.","CategoryCode.GOVT.Description":"Government-run websites, including military sites.","CategoryCode.COMM.Description":"Websites of commercial services and products.","CategoryCode.CTRL.Description":"Benign or innocuous content used as a control.","CategoryCode.IGO.Description":"Websites of intergovernmental organizations such as the United Nations.","CategoryCode.MISC.Description":"Sites that don't fit in any category. (XXX Things in here should be categorised)","Country.Heading.Overview":"Overview","Country.Heading.Websites":"Websites","Country.Heading.Apps":"Apps","Country.Heading.NetworkProperties":"Networks","Country.Overview.Heading.NwInterference":"In a nutshell","Country.Overview.NwInterference.Middleboxes.Blocked":"Middleboxes were detected on {middleboxCount} network(s)","Country.Overview.NwInterference.Middleboxes.Normal":"No middleboxes were detected on tested networks","Country.Overview.NwInterference.Middleboxes.NoData":"Not enough data available on middleboxes","Country.Overview.NwInterference.IM.Blocked":"Instant messaging apps were likely blocked","Country.Overview.NwInterference.IM.Normal":"No instant messaging apps were blocked on tested networks","Country.Overview.NwInterference.IM.NoData":"Not enough data available on instant messaging apps","Country.Overview.NwInterference.CircumventionTools.Blocked":"Circumvention tools were likely blocked","Country.Overview.NwInterference.CircumventionTools.Normal":"No circumvention tools were blocked on tested networks","Country.Overview.NwInterference.CircumventionTools.NoData":"Not enough data available on circumvention tools","Country.Overview.NwInterference.Websites.Blocked":"OONI data confirms the blocking of websites","Country.Overview.NwInterference.Websites.Normal":"The blocking of websites is not confirmed","Country.Overview.NwInterference.Websites.NoData":"Not enough data available on blocked websites","Country.Overview.Heading.TestsByClass":"Measurement Coverage","Country.Overview.Heading.TestsByClass.Description":"The graph below provides an overview of OONI Probe measurement coverage. It shows how many results have been collected from each OONI Probe test category, as well as how many networks have been covered by tests. \n\nBy looking at this graph, you can understand if there is enough data to draw meaningful conclusions. If there is not enough data and you are in the country in question, [install OONI Probe](https://ooni.org/install), run tests, and contribute data!","Country.Overview.TestsByClass.Websites":"Websites","Country.Overview.TestsByClass.InstantMessaging":"Instant Messaging","Country.Overview.TestsByClass.Performance":"Performance","Country.Overview.TestsByClass.Middleboxes":"Middleboxes","Country.Overview.TestsByClass.Circumvention":"Circumvention Tools","Country.Overview.FeaturedResearch":"Research Reports","Country.Overview.FeaturedResearch.None":"We haven't published a research report based on OONI data from this country yet. \n\nWe encourage you to use OONI data for your research!","Country.Overview.SummaryTextTemplate":"OONI Probe users in **{countryName}** have collected [**{measurementCount}** measurements]({linkToMeasurements}) from **{networkCovered}** local networks.\n\nExplore the data below to check the accessibility and/or blocking of sites and services.","Country.Overview.NoData.Title":"Let's collect more data!","Country.Overview.NoData.CallToAction":"We don’t have enough measurements for **{country}** to show a chart. If you are in {country} or know people there, tell them to run OONI Probe to collect more measurements.","Country.Overview.NoData.Button.InstallProbe":"Install OONI Probe","Country.Overview.NoData.Button.OoniRunLink":"Create OONI Run Link","Country.Meta.Title":"Internet Censorship in {countryName} - OONI Explorer","Country.Meta.Description":"OONI Probe users in {countryName} have collected {measurementCount} measurements from {networkCount} local networks. Explore the data on OONI Explorer.","Country.PeriodFilter.Label":"Show results from","Country.PeriodFilter.Option.30Days":"Last 30 Days","Country.PeriodFilter.Option.2Months":"Last 2 Months","Country.PeriodFilter.Option.3Months":"Last 3 Months","Country.PeriodFilter.Option.6Months":"Last 6 Months","Country.Websites.Description":"Check whether websites have been blocked.\n\nTesting methodology: OONI's [Web Connectivity test](https://ooni.org/nettest/web-connectivity/), designed to measure the DNS, HTTP, and TCP/IP blocking of websites. \n\nTested websites: [Citizen Lab test lists](https://github.com/citizenlab/test-lists)\n\nIf you'd like to see results on the testing of different websites, please [contribute to test lists](https://ooni.org/get-involved/contribute-test-lists/) or test the sites of your choice via the [OONI Probe mobile app](https://ooni.org/install/). \n\nPlease note that unless a block page is served, some anomalous measurements may contain [false positives](https://ooni.org/support/faq/#why-do-false-positives-occur). We therefore encourage you to examine anomalous measurements in depth and over time.","Country.Websites.Description.MoreLinkText":"","Country.Websites.Heading.BlockedByCategory":"Categories of Blocked Websites","Country.Websites.BlockedByCategory.Description":"Websites that fall under the following categories were blocked on the {selectedASN} network.","Country.Websites.TestedWebsitesCount":"URLs tested","Country.Websites.Labels.ResultsPerPage":"Results per page","Country.Websites.URLSearch.Placeholder":"Search for URL","Country.Websites.URLCharts.Legend.Label.Blocked":"Confirmed Blocked","Country.Websites.URLCharts.ExploreMoreMeasurements":"Explore more measurements","Country.Websites.URLCharts.Pagination.Previous":"Previous Page","Country.Websites.URLCharts.Pagination.Next":"Next Page","Country.Apps.Description":"Check whether instant messaging apps and circumvention tools are blocked.\n\nThe following results were collected through the use of [OONI Probe tests](https://ooni.org/nettest/) designed to measure the blocking of WhatsApp, Facebook Messenger, and Telegram. \n\nWe also share results on the testing of circumvention tools, like [Tor](https://www.torproject.org/).","Country.Apps.Label.LastTested":"Last tested","Country.Apps.Label.TestedNetworks":"tested networks","Country.Apps.Button.ShowMore":"Show More","Country.NetworkProperties.Description":"Check the speed and performance of networks.\n\nThe following results were collected through the use of [OONI Probe's performance and middlebox tests](https://ooni.org/nettest/). You can check the speed and performance of tested networks, as well as video streaming performance. \n\nYou can also learn whether middleboxes were detected in tested networks. Middleboxes are network appliances that can be used for a variety of networking purposes (such as caching), but sometimes they're used to implement internet censorship and/or surveillance.","Country.NetworkProperties.Heading.Summary":"Summary","Country.NetworkProperties.Heading.Networks":"Networks","Country.NetworkProperties.InfoBox.Label.AverageDownload":"Average Download","Country.NetworkProperties.InfoBox.Label.AverageUpload":"Average Upload","Country.NetworkProperties.InfoBox.Label.Covered":"Covered","Country.NetworkProperties.InfoBox.Label.Middleboxes":"Middleboxes detected","Country.NetworkProperties.InfoBox.Units.Mbits":"Mbit/s","Country.NetworkProperties.InfoBox.Units.Networks.Singular":"Network","Country.NetworkProperties.InfoBox.Units.Networks.Plural":"Networks","Country.NetworkProperties.InfoBox.Label.AverageStreaming":"Average Streaming","Country.NetworkProperties.InfoBox.Label.AveragePing":"Average Ping","Country.NetworkProperties.InfoBox.Units.Milliseconds":"ms","Country.NetworkProperties.InfoBox.Label.Middleboxes.Found":"Middleboxes detected","Country.NetworkProperties.InfoBox.Label.Middleboxes.NotFound":"No middleboxes detected","Country.NetworkProperties.Button.ShowMore":"Show more networks","Country.Label.NoData":"No Data Available","Search.PageTitle":"Search through millions of Internet censorship measurements","Search.Sidebar.Domain":"Domain","Search.Sidebar.Domain.Placeholder":"e.g. twitter.com or 1.1.1.1","Search.Sidebar.Domain.Error":"Please enter a valid domain name or IP address, such as twitter.com or 1.1.1.1","Search.Sidebar.Input":"Input","Search.Sidebar.Input.Placeholder":"e.g., https://fbcdn.net/robots.txt","Search.Sidebar.Input.Error":"Please enter full URL or IP address, such as https://fbcdn.net/robots.txt","Search.Sidebar.Categories":"Website Categories","Search.Sidebar.Categories.All":"Any","Search.Sidebar.Status":"Status","Search.Sidebar.TestName":"Test Name","Search.Sidebar.TestName.AllTests":"Any","Search.Sidebar.Country":"Country","Search.Sidebar.Country.AllCountries":"Any","Search.Sidebar.ASN":"ASN","Search.Sidebar.ASN.example":"e.g. AS30722","Search.Sidebar.ASN.Error":"Valid formats: AS1234, 1234","Search.Sidebar.From":"From","Search.Sidebar.Until":"Until","Search.Sidebar.HideFailed":"Hide failed measurements","Search.Sidebar.Button.FilterResults":"Filter Results","Search.FilterButton.AllResults":"All Results","Search.FilterButton.Confirmed":"Confirmed","Search.FilterButton.Anomalies":"Anomalies","Search.FilterButton.Search":"Search","Search.Filter.SortBy":"Sort by","Search.Filter.SortBy.Date":"Date","Search.WebConnectivity.Results.Blocked":"Confirmed","Search.HTTPRequests.Results.Anomaly":"","Search.HTTPRequests.Results.Blocked":"","Search.HTTPRequests.Results.Error":"","Search.HTTPRequests.Results.Reachable":"","Search.NDT.Results":"","Search.DASH.Results":"","Search.VanillaTor.Results":"","Search.BridgeReachability.Results":"","Search.LegacyTests.Results":"","Search.Results.Empty.Heading":"No Results Found","Search.Results.Empty.Description":"Please try changing the filters to get better results.","Search.Button.LoadMore":"Load more","Search.Error.Message":"This query took too long to complete. Please try adjusting the search filters or view the example queries in the [Highlights section of the homepage](/#highlights).\n\nIf you are interested in using OONI data in batch, we recommend the [ooni-data Amazon S3 bucket](https://ooni.org/post/mining-ooni-data/) or the [aggregation API](https://api.ooni.io/apidocs/#/default/get_api_v1_aggregation).\n\nWe are working on improving the performance of OONI Explorer. To track our work on this, [see the open issues on the ooni/api repository](https://github.com/ooni/api/issues?q=is%3Aissue+is%3Aopen+label%3Aoptimization).","Search.Error.Details.Label":"Server Response","Home.Banner.Title.UncoverEvidence":"Uncover evidence of internet censorship worldwide","Home.Banner.Subtitle.ExploreCensorshipEvents":"Open data collected by the global OONI community","Home.Banner.Button.Explore":"Explore","Home.Banner.Stats.Measurements":"Measurements","Home.Banner.Stats.Countries":"Countries","Home.Banner.Stats.Networks":"Networks","Home.About.SummaryText":"OONI Explorer is an open data resource on internet censorship around the world. \n\nSince 2012, millions of network measurements have been collected from more than 200 countries. OONI Explorer sheds light on internet censorship and other forms of network interference worldwide.\n\nTo contribute to this open dataset, [install OONI Probe](https://ooni.org/install/) and run tests!","Home.Websites&Apps.Title":"Blocking of Websites & Apps","Home.Websites&Apps.SummaryText":"Discover blocked websites around the world. Check whether WhatsApp, Facebook Messenger, and Telegram are blocked.","Home.Search&Filter.Title":"Search","Home.Search&Filter.SummaryText":"Explore OONI measurements with a powerful search tool. View the most recently blocked websites. Compare internet censorship across networks.","Home.NetworkProperties.Title":"Network Performance","Home.NetworkProperties.SummaryText":"Check the speed and performance of thousands of networks around the world. Explore data on video streaming performance.","Home.MonthlyStats.Title":"Monthly coverage worldwide","Home.MonthlyStats.SummaryText":"OONI Explorer hosts millions of network measurements collected from more than 200 countries since 2012. Every day, OONI Explorer is updated with new measurements!\n\nLast month, {measurementCount} OONI Probe measurements were collected from {networkCount} networks in {countryCount} countries. Explore the monthly usage of [OONI Probe](https://ooni.org/install/) through the stats below.","Home.Highlights.CTA":"We encourage you to explore OONI measurements to find more highlights!","Home.Highlights.Title":"Highlights","Home.Highlights.Description":"What can you learn from OONI Explorer? \n\nBelow we share some stories from [research reports](https://ooni.org/post/) based on OONI data.\n\nWe share these case studies to demonstrate how OONI's openly available data can be used and what types of stories can be told. \n\nWe encourage you to explore OONI data, discover more censorship cases, and to use OONI data as part of your research and/or advocacy.","Home.Highlights.Political":"Censorship during political events","Home.Highlights.Political.Description":"Internet censorship sometimes occurs in response to or in anticipation of political events, such as elections, protests, and riots. Below we share a few cases detected via OONI data and correlated with political events.","Home.Highlights.Media":"Media censorship","Home.Highlights.Media.Description":"Press freedom is threatened in countries that experience the blocking of media websites. Below we share a few cases detected through OONI data.","Home.Highlights.LGBTQI":"Blocking of LGBTQI sites","Home.Highlights.LGBTQI.Description":"Minority group sites are blocked around the world. Below we share a few cases on the blocking of LGBTQI sites.","Home.Highlights.Changes":"Censorship changes","Home.Highlights.Changes.Description":"OONI measurements have been collected on a continuous basis since 2012, enabling the identification of censorship changes around the world. Some examples include:","Home.Meta.Description":"OONI Explorer is an open data resource on Internet censorship around the world consisting of millions of measurements on network interference.","Home.Highlights.Explore":"Explore","Home.Highlights.ReadReport":"Read report","Countries.Heading.JumpToContinent":"Jump to continent","Countries.Search.NoCountriesFound":"No countries found with {searchTerm}","Countries.Search.Placeholder":"Search for countries","Countries.PageTitle":"Internet Censorship around the world","Error.404.PageNotFound":"Page Not Found","Error.404.GoBack":"Go back","Error.404.Heading":"The requested page does not exist","Error.404.Message":"We could not find the content you were looking for. Maybe try {measurmentLink} or look at {homePageLink}.","Error.404.MeasurmentLinkText":"exploring some measurement","Error.404.HomepageLinkText":"the homepage","MAT.Title":"OONI Measurement Aggregation Toolkit (MAT)","MAT.SubTitle":"Create charts based on aggregate views of real-time OONI data from around the world","MAT.JSONData":"JSON Data","MAT.CSVData":"CSV Data","MAT.Form.Label.XAxis":"X Axis","MAT.Form.Label.YAxis":"Y Axis","MAT.Form.Label.AxisOption.domain":"Domain","MAT.Form.Label.AxisOption.input":"Input","MAT.Form.Label.AxisOption.measurement_start_day":"Measurement Day","MAT.Form.Label.AxisOption.probe_cc":"Countries","MAT.Form.Label.AxisOption.category_code":"Website Categories","MAT.Form.Label.AxisOption.probe_asn":"ASN","MAT.Form.ConfirmationModal.Title":"Are you sure?","MAT.Form.ConfirmationModal.Message":"Duration too long. This can potentially slow down the page","MAT.Form.ConfirmationModal.No":"No","MAT.Form.ConfirmationModal.Button.Yes":"Yes","MAT.Form.Submit":"Show Chart","MAT.Form.All":"All","MAT.Form.AllCountries":"All Countries","MAT.Table.Header.ok_count":"OK Count","MAT.Table.Header.anomaly_count":"Anomaly Count","MAT.Table.Header.confirmed_count":"Confirmed Count","MAT.Table.Header.failure_count":"Failure Count","MAT.Table.Header.measurement_count":"Measurement Count","MAT.Table.Header.input":"URL","MAT.Table.Header.category_code":"Category Code","MAT.Table.Header.probe_cc":"Country","MAT.Table.Header.probe_asn":"ASN","MAT.Table.Header.blocking_type":"Blocking Type","MAT.Table.Header.domain":"Domain","MAT.Table.FilterPlaceholder":"Search {count} records…","MAT.Table.Search":"Search:","MAT.Table.Filters":"Filters","MAT.Charts.NoData.Title":"No Data Found","MAT.Charts.NoData.Description":"We are not able to produce a chart based on the selected filters. Please change the filters and try again.","MAT.Charts.NoData.Details":"Details:","MAT.Help.Box.Title":"Help","MAT.Help.Title":"FAQs","MAT.Help.Content":"# What is the MAT?\n\nOONI's Measurement Aggregation Toolkit (MAT) is a tool that enables you to generate your own custom charts based on **aggregate views of real-time OONI data** collected from around the world.\n\nOONI data consists of network measurements collected by [OONI Probe](https://ooni.org/install/) users around the world. \n\nThese measurements contain information about various types of **internet censorship**, such as the [blocking of websites and apps](https://ooni.org/nettest/) around the world. \n\n# Who is the MAT for?\n\nThe MAT was built for researchers, journalists, and human rights defenders interested in examining internet censorship around the world.\n\n# Why use the MAT?\n\nWhen examining cases of internet censorship, it's important to **look at many measurements at once** (\"in aggregate\") in order to answer key questions like the following:\n\n* Does the testing of a service (e.g. Facebook) present **signs of blocking every time that it is tested** in a country? This can be helpful for ruling out [false positives](https://ooni.org/support/faq/#what-are-false-positives).\n* What types of websites (e.g. human rights websites) are blocked in each country?\n* In which countries is a specific website (e.g. `bbc.com`) blocked?\n* How does the blocking of different apps (e.g. WhatsApp or Telegram) vary across countries?\n* How does the blocking of a service vary across countries and [ASNs](https://ooni.org/support/glossary/#asn)?\n* How does the blocking of a service change over time?\n\nWhen trying to answer questions like the above, we normally perform relevant data analysis (instead of inspecting measurements one by one). \n\nThe MAT incorporates our data analysis techniques, enabling you to answer such questions without any data analysis skills, and with the click of a button!\n\n# How to use the MAT?\n\nThrough the filters at the start of the page, select the parameters you care about in order to plot charts based on aggregate views of OONI data.\n\nThe MAT includes the following filters:\n\n* **Countries:** Select a country through the drop-down menu (the \"All Countries\" option will show global coverage)\n* **Test Name:** Select an [OONI Probe test](https://ooni.org/nettest/) based on which you would like to get measurements (for example, select `Web Connectivity` to view the testing of websites)\n* **Domain:** Type the domain for the website you would like to get measurements (e.g. `twitter.com`)\n* **Website categories:** Select the [website category](https://github.com/citizenlab/test-lists/blob/master/lists/00-LEGEND-new_category_codes.csv) for which you would like to get measurements (e.g. `News Media` for news media websites)\n* **ASN:** Type the [ASN](https://ooni.org/support/glossary/#asn) of the network for which you would like to get measurements (e.g. `AS30722` for Vodafone Italia)\n* **Date range:** Select the date range of the measurements by adjusting the `Since` and `Until` filters\n* **X axis:** Select the values that you would like to appear on the horizontal axis of your chart\n* **Y axis:** Select the values that you would like to appear on the vertical axis of your chart\n\nDepending on what you would like to explore, adjust the MAT filters accordingly and click `Show Chart`. \n\nFor example, if you would like to check the testing of BBC in all countries around the world:\n\n* Type `www.bbc.com` under `Domain`\n* Select `Countries` under the `Y axis`\n* Click `Show Chart`\n\nThis will plot numerous charts based on the OONI Probe testing of `www.bbc.com` worldwide.\n\n# Interpreting MAT charts\n\nThe MAT charts (and associated tables) include the following values:\n\n* **OK count:** Successful measurements (i.e. NO sign of internet censorship)\n* **Confirmed count:** Measurements from automatically **confirmed blocked websites** (e.g. a [block page](https://ooni.org/support/glossary/#block-page) was served)\n* **Anomaly count:** Measurements that provided **signs of potential blocking** (however, [false positives](https://ooni.org/support/faq/#what-are-false-positives) can occur) \n* **Failure count:** Failed experiments that should be discarded\n* **Measurement count:** Total volume of OONI measurements (pertaining to the selected country, resource, etc.)\n\nWhen trying to identify the blocking of a service (e.g. `twitter.com`), it's useful to check whether:\n\n* Measurements are annotated as `confirmed`, automatically confirming the blocking of websites\n* A large volume of measurements (in comparison to the overall measurement count) present `anomalies` (i.e. signs of potential censorship)\n\nYou can access the raw data by clicking on the bars of charts, and subsequently clicking on the relevant measurement links. \n\n# Website categories\n\n[OONI Probe](https://ooni.org/install/) users test a wide range of [websites](https://ooni.org/support/faq/#which-websites-will-i-test-for-censorship-with-ooni-probe) that fall under the following [30 standardized categories](https://github.com/citizenlab/test-lists/blob/master/lists/00-LEGEND-new_category_codes.csv).","MAT.Help.Subtitle.Categories":"Categories","MAT.CustomTooltip.ViewMeasurements":"View measurements","ReachabilityDash.Heading.CircumventionTools":"Reachability of Censorship Circumvention Tools","ReachabilityDash.CircumventionTools.Description":"The charts below display aggregate views of OONI data based on the testing of the following circumvention tools:\n\n* [Psiphon](https://ooni.org/nettest/psiphon)\n\n* [Tor](https://ooni.org/nettest/tor)\n\n* [Tor Snowflake](https://ooni.org/nettest/tor-snowflake/)\n\nPlease note that the presence of [anomalous measurements](https://ooni.org/support/faq/#what-do-you-mean-by-anomalies) is not always indicative of blocking, as [false positives](https://ooni.org/support/faq/#what-are-false-positives) can occur. Moreover, circumvention tools often have built-in circumvention techniques for evading censorship. \n\nWe therefore recommend referring to **[Tor Metrics](https://metrics.torproject.org/)** and to the **[Psiphon Data Engine](https://psix.ca/)** to view usage stats and gain a more comprehensive understanding of whether these tools work in each country.","ReachabilityDash.Form.Label.CountrySelect.AllSelected":"All countries selected","ReachabilityDash.Form.Label.CountrySelect.SearchPlaceholder":"Search","ReachabilityDash.Form.Label.CountrySelect.SelectAll":"Select All","ReachabilityDash.Form.Label.CountrySelect.SelectAllFiltered":"Select All (Filtered)","ReachabilityDash.Form.Label.CountrySelect.InputPlaceholder":"Select Countries…","ReachabilityDash.Meta.Description":"View the accessibility of censorship circumvention tools around the world through OONI data.","DateRange.Apply":"Apply","DateRange.Cancel":"Cancel","DateRange.Today":"Today","DateRange.LastWeek":"Last Week","DateRange.LastMonth":"Last Month","DateRange.LastYear":"Last Year","Highlights.Political.CubaReferendum2019.Title":"2019 Constitutional Referendum","Highlights.Political.CubaReferendum2019.Text":"Blocking of independent media","Highlights.Political.VenezuelaCrisis2019.Title":"2019 Presidential Crisis","Highlights.Political.VenezuelaCrisis2019.Text":"Blocking of Wikipedia and social media","Highlights.Political.ZimbabweProtests2019.Title":"2019 Fuel Protests","Highlights.Political.ZimbabweProtests2019.Text":"Social media blocking and internet blackouts","Highlights.Political.MaliElection2018.Title":"2018 Presidential Election","Highlights.Political.MaliElection2018.Text":"Blocking of WhatsApp and Twitter","Highlights.Political.CataloniaReferendum2017.Title":"Catalonia 2017 Independence Referendum","Highlights.Political.CataloniaReferendum2017.Text":"Blocking of sites related to the referendum","Highlights.Political.IranProtests2018.Title":"2018 Anti-government Protests","Highlights.Political.IranProtests2018.Text":"Blocking of Telegram, Instagram and Tor","Highlights.Political.EthiopiaProtests2016.Title":"2016 Wave of Protests","Highlights.Political.EthiopiaProtests2016.Text":"Blocking of news websites and social media","Highlights.Political.PakistanProtests2017.Title":"2017 Protests","Highlights.Political.PakistanProtests2017.Text":"Blocking of news websites and social media","Highlights.Media.Egypt.Title":"Pervasive media censorship","Highlights.Media.Egypt.Text":"Blocking of hundreds of media websites","Highlights.Media.Venezuela.Title":"Blocking of independent media websites","Highlights.Media.Venezuela.Text":"Venezuela's economic and political crisis","Highlights.Media.SouthSudan.Title":"Blocking of foreign-based media","Highlights.Media.SouthSudan.Text":"Media accused of hostile reporting against the government","Highlights.Media.Malaysia.Title":"Blocking of media","Highlights.Media.Malaysia.Text":"1MDB scandal","Highlights.Media.Iran.Title":"Pervasive media censorship","Highlights.Media.Iran.Text":"Blocking of at least 121 news outlets","Highlights.Lgbtqi.Indonesia.Text":"Blocking of LGBTQI sites","Highlights.Lgbtqi.Iran.Text":"Blocking of Grindr","Highlights.Lgbtqi.Ethiopia.Text":"Blocking of QueerNet","Highlights.Changes.Cuba.Text":"Cuba [used to primarily serve blank block pages](https://ooni.torproject.org/post/cuba-internet-censorship-2017/), only blocking the HTTP version of websites. Now they censor access to sites that support HTTPS by means of [IP blocking](https://ooni.org/post/cuba-referendum/).","Highlights.Changes.Venezuela.Text":"Venezuelan ISPs used to primarily block sites by means of [DNS tampering](https://ooni.torproject.org/post/venezuela-internet-censorship/). Now state-owned CANTV also implements [SNI-based filtering](https://ooni.torproject.org/post/venezuela-blocking-wikipedia-and-social-media-2019/).","Highlights.Changes.Ethiopia.Text":"Ethiopia [used to block](https://ooni.org/post/ethiopia-report/) numerous news websites, LGBTQI, political opposition, and circumvention tool sites. As part of the 2018 political reforms, most of these sites have been [unblocked](https://ooni.org/post/ethiopia-unblocking/)."}} \ No newline at end of file diff --git a/public/static/locale-data.js b/public/static/locale-data.js deleted file mode 100644 index 596871707..000000000 --- a/public/static/locale-data.js +++ /dev/null @@ -1 +0,0 @@ -!function(e,a){"object"==typeof exports&&"undefined"!=typeof module?module.exports=a():"function"==typeof define&&define.amd?define(a):(e.ReactIntlLocaleData=e.ReactIntlLocaleData||{},e.ReactIntlLocaleData.en=a())}(this,function(){"use strict";return[{locale:"en",pluralRuleFunction:function(e,a){var n=String(e).split("."),l=!n[1],o=Number(n[0])==e,t=o&&n[0].slice(-1),r=o&&n[0].slice(-2);return a?1==t&&11!=r?"one":2==t&&12!=r?"two":3==t&&13!=r?"few":"other":1==e&&l?"one":"other"},fields:{year:{displayName:"year",relative:{0:"this year",1:"next year","-1":"last year"},relativeTime:{future:{one:"in {0} year",other:"in {0} years"},past:{one:"{0} year ago",other:"{0} years ago"}}},month:{displayName:"month",relative:{0:"this month",1:"next month","-1":"last month"},relativeTime:{future:{one:"in {0} month",other:"in {0} months"},past:{one:"{0} month ago",other:"{0} months ago"}}},day:{displayName:"day",relative:{0:"today",1:"tomorrow","-1":"yesterday"},relativeTime:{future:{one:"in {0} day",other:"in {0} days"},past:{one:"{0} day ago",other:"{0} days ago"}}},hour:{displayName:"hour",relative:{0:"this hour"},relativeTime:{future:{one:"in {0} hour",other:"in {0} hours"},past:{one:"{0} hour ago",other:"{0} hours ago"}}},minute:{displayName:"minute",relative:{0:"this minute"},relativeTime:{future:{one:"in {0} minute",other:"in {0} minutes"},past:{one:"{0} minute ago",other:"{0} minutes ago"}}},second:{displayName:"second",relative:{0:"now"},relativeTime:{future:{one:"in {0} second",other:"in {0} seconds"},past:{one:"{0} second ago",other:"{0} seconds ago"}}}}},{locale:"en-001",parentLocale:"en"},{locale:"en-150",parentLocale:"en-001"},{locale:"en-AG",parentLocale:"en-001"},{locale:"en-AI",parentLocale:"en-001"},{locale:"en-AS",parentLocale:"en"},{locale:"en-AT",parentLocale:"en-150"},{locale:"en-AU",parentLocale:"en-001"},{locale:"en-BB",parentLocale:"en-001"},{locale:"en-BE",parentLocale:"en-001"},{locale:"en-BI",parentLocale:"en"},{locale:"en-BM",parentLocale:"en-001"},{locale:"en-BS",parentLocale:"en-001"},{locale:"en-BW",parentLocale:"en-001"},{locale:"en-BZ",parentLocale:"en-001"},{locale:"en-CA",parentLocale:"en-001"},{locale:"en-CC",parentLocale:"en-001"},{locale:"en-CH",parentLocale:"en-150"},{locale:"en-CK",parentLocale:"en-001"},{locale:"en-CM",parentLocale:"en-001"},{locale:"en-CX",parentLocale:"en-001"},{locale:"en-CY",parentLocale:"en-001"},{locale:"en-DE",parentLocale:"en-150"},{locale:"en-DG",parentLocale:"en-001"},{locale:"en-DK",parentLocale:"en-150"},{locale:"en-DM",parentLocale:"en-001"},{locale:"en-Dsrt",pluralRuleFunction:function(e,a){return"other"},fields:{year:{displayName:"Year",relative:{0:"this year",1:"next year","-1":"last year"},relativeTime:{future:{other:"+{0} y"},past:{other:"-{0} y"}}},month:{displayName:"Month",relative:{0:"this month",1:"next month","-1":"last month"},relativeTime:{future:{other:"+{0} m"},past:{other:"-{0} m"}}},day:{displayName:"Day",relative:{0:"today",1:"tomorrow","-1":"yesterday"},relativeTime:{future:{other:"+{0} d"},past:{other:"-{0} d"}}},hour:{displayName:"Hour",relative:{0:"this hour"},relativeTime:{future:{other:"+{0} h"},past:{other:"-{0} h"}}},minute:{displayName:"Minute",relative:{0:"this minute"},relativeTime:{future:{other:"+{0} min"},past:{other:"-{0} min"}}},second:{displayName:"Second",relative:{0:"now"},relativeTime:{future:{other:"+{0} s"},past:{other:"-{0} s"}}}}},{locale:"en-ER",parentLocale:"en-001"},{locale:"en-FI",parentLocale:"en-150"},{locale:"en-FJ",parentLocale:"en-001"},{locale:"en-FK",parentLocale:"en-001"},{locale:"en-FM",parentLocale:"en-001"},{locale:"en-GB",parentLocale:"en-001"},{locale:"en-GD",parentLocale:"en-001"},{locale:"en-GG",parentLocale:"en-001"},{locale:"en-GH",parentLocale:"en-001"},{locale:"en-GI",parentLocale:"en-001"},{locale:"en-GM",parentLocale:"en-001"},{locale:"en-GU",parentLocale:"en"},{locale:"en-GY",parentLocale:"en-001"},{locale:"en-HK",parentLocale:"en-001"},{locale:"en-IE",parentLocale:"en-001"},{locale:"en-IL",parentLocale:"en-001"},{locale:"en-IM",parentLocale:"en-001"},{locale:"en-IN",parentLocale:"en-001"},{locale:"en-IO",parentLocale:"en-001"},{locale:"en-JE",parentLocale:"en-001"},{locale:"en-JM",parentLocale:"en-001"},{locale:"en-KE",parentLocale:"en-001"},{locale:"en-KI",parentLocale:"en-001"},{locale:"en-KN",parentLocale:"en-001"},{locale:"en-KY",parentLocale:"en-001"},{locale:"en-LC",parentLocale:"en-001"},{locale:"en-LR",parentLocale:"en-001"},{locale:"en-LS",parentLocale:"en-001"},{locale:"en-MG",parentLocale:"en-001"},{locale:"en-MH",parentLocale:"en"},{locale:"en-MO",parentLocale:"en-001"},{locale:"en-MP",parentLocale:"en"},{locale:"en-MS",parentLocale:"en-001"},{locale:"en-MT",parentLocale:"en-001"},{locale:"en-MU",parentLocale:"en-001"},{locale:"en-MW",parentLocale:"en-001"},{locale:"en-MY",parentLocale:"en-001"},{locale:"en-NA",parentLocale:"en-001"},{locale:"en-NF",parentLocale:"en-001"},{locale:"en-NG",parentLocale:"en-001"},{locale:"en-NL",parentLocale:"en-150"},{locale:"en-NR",parentLocale:"en-001"},{locale:"en-NU",parentLocale:"en-001"},{locale:"en-NZ",parentLocale:"en-001"},{locale:"en-PG",parentLocale:"en-001"},{locale:"en-PH",parentLocale:"en-001"},{locale:"en-PK",parentLocale:"en-001"},{locale:"en-PN",parentLocale:"en-001"},{locale:"en-PR",parentLocale:"en"},{locale:"en-PW",parentLocale:"en-001"},{locale:"en-RW",parentLocale:"en-001"},{locale:"en-SB",parentLocale:"en-001"},{locale:"en-SC",parentLocale:"en-001"},{locale:"en-SD",parentLocale:"en-001"},{locale:"en-SE",parentLocale:"en-150"},{locale:"en-SG",parentLocale:"en-001"},{locale:"en-SH",parentLocale:"en-001"},{locale:"en-SI",parentLocale:"en-150"},{locale:"en-SL",parentLocale:"en-001"},{locale:"en-SS",parentLocale:"en-001"},{locale:"en-SX",parentLocale:"en-001"},{locale:"en-SZ",parentLocale:"en-001"},{locale:"en-Shaw",pluralRuleFunction:function(e,a){return"other"},fields:{year:{displayName:"Year",relative:{0:"this year",1:"next year","-1":"last year"},relativeTime:{future:{other:"+{0} y"},past:{other:"-{0} y"}}},month:{displayName:"Month",relative:{0:"this month",1:"next month","-1":"last month"},relativeTime:{future:{other:"+{0} m"},past:{other:"-{0} m"}}},day:{displayName:"Day",relative:{0:"today",1:"tomorrow","-1":"yesterday"},relativeTime:{future:{other:"+{0} d"},past:{other:"-{0} d"}}},hour:{displayName:"Hour",relative:{0:"this hour"},relativeTime:{future:{other:"+{0} h"},past:{other:"-{0} h"}}},minute:{displayName:"Minute",relative:{0:"this minute"},relativeTime:{future:{other:"+{0} min"},past:{other:"-{0} min"}}},second:{displayName:"Second",relative:{0:"now"},relativeTime:{future:{other:"+{0} s"},past:{other:"-{0} s"}}}}},{locale:"en-TC",parentLocale:"en-001"},{locale:"en-TK",parentLocale:"en-001"},{locale:"en-TO",parentLocale:"en-001"},{locale:"en-TT",parentLocale:"en-001"},{locale:"en-TV",parentLocale:"en-001"},{locale:"en-TZ",parentLocale:"en-001"},{locale:"en-UG",parentLocale:"en-001"},{locale:"en-UM",parentLocale:"en"},{locale:"en-US",parentLocale:"en"},{locale:"en-VC",parentLocale:"en-001"},{locale:"en-VG",parentLocale:"en-001"},{locale:"en-VI",parentLocale:"en"},{locale:"en-VU",parentLocale:"en-001"},{locale:"en-WS",parentLocale:"en-001"},{locale:"en-ZA",parentLocale:"en-001"},{locale:"en-ZM",parentLocale:"en-001"},{locale:"en-ZW",parentLocale:"en-001"}]}); diff --git a/scripts/build-translations.js b/scripts/build-translations.js index fbf2864c2..facc73b76 100644 --- a/scripts/build-translations.js +++ b/scripts/build-translations.js @@ -1,12 +1,12 @@ /* eslint-disable no-console */ const glob = require('glob') -const { basename } = require('path') +const { dirname, basename, resolve } = require('path') const { readFileSync, writeFileSync } = require('fs') const LANG_DIR = './public/static/lang/' const TRANSLATED_STRINGS_DIR = '../translations/explorer' -const supportedLanguages = glob.sync(`${TRANSLATED_STRINGS_DIR}/*`).map((f) => basename(f, '.json')) +const supportedLanguages = glob.sync(`${TRANSLATED_STRINGS_DIR}/**/*.json`).map((f) => basename(dirname(f, '.json'))) // Copy latest files from `translations` supportedLanguages.forEach((lang) => { diff --git a/services/dayjs.js b/services/dayjs.js index b8ff0a8c3..2f00be990 100644 --- a/services/dayjs.js +++ b/services/dayjs.js @@ -2,6 +2,14 @@ import dayjs from 'dayjs' import utc from 'dayjs/plugin/utc' import relativeTime from 'dayjs/plugin/relativeTime' import isSameOrBefore from 'dayjs/plugin/isSameOrBefore' +import('dayjs/locale/de') +import('dayjs/locale/es') +import('dayjs/locale/fa') +import('dayjs/locale/fr') +import('dayjs/locale/is') +import('dayjs/locale/ru') +import('dayjs/locale/tr') +import('dayjs/locale/zh') dayjs .extend(utc) diff --git a/utils/i18nCountries.js b/utils/i18nCountries.js new file mode 100644 index 000000000..52ed93bb2 --- /dev/null +++ b/utils/i18nCountries.js @@ -0,0 +1,34 @@ +import { countryList } from 'country-util' +import '@formatjs/intl-displaynames/polyfill' + +// eventually we can remove this, but currently Chrome doesn't have the translations for UN M.49 area codes implemented so we need to polyfill +process.env.LOCALES.forEach((locale) => { + // if (locale === 'zh_CN') locale = 'zh-Hant' + // if (locale === 'zh_HK') locale = 'zh-Hant-HK' + + require(`@formatjs/intl-displaynames/locale-data/${locale}`) +}) + +export const getLocalisedRegionName = (regionCode, locale) => { + try { + return new Intl.DisplayNames([locale], { type: 'region' }).of(String(regionCode)) + } catch (e) { + return regionCode + } +} + +export const getLocalisedLanguageName = (regionCode, locale) => { + try { + return new Intl.DisplayNames([locale], { type: 'language' }).of(String(regionCode)) + } catch (e) { + return regionCode + } +} + +export const localisedCountries = (locale) => { + return countryList.map((c) => ({ + ...c, + localisedCountryName: getLocalisedRegionName(c.iso3166_alpha2, locale) + }) + ) +} \ No newline at end of file diff --git a/yarn.lock b/yarn.lock index 00f47bde2..f83d4465f 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1542,6 +1542,11 @@ minimatch "^3.1.2" strip-json-comments "^3.1.1" +"@fontsource/fira-sans@^4.5.9": + version "4.5.9" + resolved "https://registry.yarnpkg.com/@fontsource/fira-sans/-/fira-sans-4.5.9.tgz#b2e8e68c4ff566cc366504d87a75d33723f367d6" + integrity sha512-wGh4mUHjjWzMwJMCo3z4GOYe9a2QKgvg1bge0gIg8Je6LKNID+/EFmcXuUDyk1KbUKHpWJIquVM9kFFyJyRY2A== + "@formatjs/ecma402-abstract@1.11.8": version "1.11.8" resolved "https://registry.yarnpkg.com/@formatjs/ecma402-abstract/-/ecma402-abstract-1.11.8.tgz#f4015dfb6a837369d94c6ba82455c609e45bce20" @@ -4689,11 +4694,6 @@ follow-redirects@^1.14.9: resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.15.1.tgz#0ca6a452306c9b276e4d3127483e29575e207ad5" integrity sha512-yLAMQs+k0b2m7cVxpS1VKJVvoz7SS9Td1zss3XRwXj+ZDH00RJgnuLx7E44wx02kQLrdM3aOOy+FpzS7+8OizA== -fontsource-fira-sans@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/fontsource-fira-sans/-/fontsource-fira-sans-4.0.0.tgz#98bad402b7b3797871420028657e3ae6663363f8" - integrity sha512-kVc8mR+Xr8R6cpwvy37gwxKOwKHuMttWIDtmDBSkRkAaks/hDCwBGQpMC4KcTCkShg2naaRXRypIqXGVPMdw3w== - for-in@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/for-in/-/for-in-1.0.2.tgz#81068d295a8142ec0ac726c6e2200c30fb6d5e80" From 801980a5c91f7d8a6388b22d07c272750c2923fd Mon Sep 17 00:00:00 2001 From: Maja Komel Date: Tue, 27 Dec 2022 16:27:39 +0100 Subject: [PATCH 17/41] Move vanilla tor from legacy to experimental category (#825) --- components/test-info.js | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/components/test-info.js b/components/test-info.js index d3d4de91b..1f86b64ef 100644 --- a/components/test-info.js +++ b/components/test-info.js @@ -131,12 +131,6 @@ export const testNames = { }, /* Censorship circumvention */ - 'vanilla_tor': { - group: 'legacy', - name: , - id: 'Tests.TorVanilla.Name', - info: 'https://ooni.org/nettest/vanilla-tor/' - }, 'bridge_reachability': { group: 'legacy', name: , @@ -189,6 +183,12 @@ export const testNames = { }, /* Experimental tests */ + 'vanilla_tor': { + group: 'experimental', + name: , + id: 'Tests.TorVanilla.Name', + info: 'https://ooni.org/nettest/vanilla-tor/' + }, 'dnscheck': { group: 'experimental', name: , From f9d169652b4890d21659ca41334ace5607643204 Mon Sep 17 00:00:00 2001 From: Maja Komel Date: Tue, 27 Dec 2022 16:32:22 +0100 Subject: [PATCH 18/41] Fix back button where params applied programmatically (#815) * Fix back button where params applied programmatically * Better fix * Localized labels * Cleanup --- components/dashboard/Form.js | 22 ++++++++++++++-------- components/network/Form.js | 20 +++++++++++--------- pages/chart/circumvention.js | 27 ++++++++++++++++++++------- pages/chart/mat.js | 3 +-- pages/network/[asn].js | 31 ++++++++++++++++++++----------- 5 files changed, 66 insertions(+), 37 deletions(-) diff --git a/components/dashboard/Form.js b/components/dashboard/Form.js index 71aae8c26..3521f22e8 100644 --- a/components/dashboard/Form.js +++ b/components/dashboard/Form.js @@ -1,6 +1,6 @@ import { useEffect, useMemo, useState } from 'react' import { useForm, Controller } from 'react-hook-form' -import { Box, Flex, Input } from 'ooni-components' +import { Box, Flex, Input, Button } from 'ooni-components' import { MultiSelect } from 'react-multi-select-component' import { useIntl } from 'react-intl' import dayjs from 'services/dayjs' @@ -35,7 +35,7 @@ export const Form = ({ onChange, query, availableCountries }) => { return { since: query?.since ?? defaultDefaultValues.since, until: query?.until ?? defaultDefaultValues.until, - probe_cc: countryOptions.filter(country => countriesInQuery.includes(country.value)) + probe_cc: countryOptions.filter(country => countriesInQuery.includes(country.value)), } } @@ -71,21 +71,22 @@ export const Form = ({ onChange, query, availableCountries }) => { } const {since, until, probe_cc} = watch() - - useEffect(() => { + + const submit = (e) => { + e.preventDefault() const cleanedUpData = { since, until, probe_cc: probe_cc.length > 0 ? probe_cc.map(d => d.value).join(',') : undefined } onChange(cleanedUpData) - }, [onChange, since, until, probe_cc]) + } return ( - Country + {intl.formatMessage({id: 'Search.Sidebar.Country'})} { ( { - Since + {intl.formatMessage({id: 'Search.Sidebar.From'})} { /> - Until + {intl.formatMessage({id: 'Search.Sidebar.Until'})} { )} /> + + + + + { showDatePicker && { } setShowDatePicker(false) } - - useEffect(() => { - const cleanedUpData = { - since, - until, - } - onChange(cleanedUpData) - }, [onChange, since, until]) + + const submit = (e) => { + e.preventDefault() + onChange({since, until}) + } return ( @@ -88,6 +85,11 @@ const Form = ({ onChange, query }) => { )} /> + + + + + { showDatePicker && { const router = useRouter() const query = router.query + useEffect(() => { + const { query } = router + if (Object.keys(query).length === 0) { + const tomorrow = dayjs.utc().add(1, 'day').format('YYYY-MM-DD') + const monthAgo = dayjs.utc().subtract(30, 'day').format('YYYY-MM-DD') + const probe_cc = ['CN', 'IR', 'RU'].join(',') + const href = { + query: { + since: monthAgo, + until: tomorrow, + probe_cc + }, + } + router.replace(href, undefined, { shallow: true }) + } + }, []) + // Sync page URL params with changes from form values const onChange = useCallback(({ since, until, probe_cc }) => { // since: "2022-01-02", @@ -26,17 +44,12 @@ const DashboardCircumvention = ({ availableCountries }) => { if (probe_cc) { params['probe_cc'] = probe_cc } - const href = { - pathname: router.pathname, - query: params, - } if (query.since !== since || query.until !== until || query.probe_cc !== probe_cc ) { - router.push(href, href, { shallow: true }) + router.push({ query: params }, undefined, { shallow: true }) } - }, [router, query]) return ( diff --git a/pages/chart/mat.js b/pages/chart/mat.js index 2df1e6497..d1ee11e99 100644 --- a/pages/chart/mat.js +++ b/pages/chart/mat.js @@ -97,7 +97,6 @@ const MeasurementAggregationToolkit = ({ testNames }) => { const today = dayjs.utc().add(1, 'day') const monthAgo = dayjs.utc(today).subtract(1, 'month') const href = { - pathname: router.pathname, query: { test_name: 'web_connectivity', axis_x: 'measurement_start_day', @@ -105,7 +104,7 @@ const MeasurementAggregationToolkit = ({ testNames }) => { until: today.format('YYYY-MM-DD'), }, } - router.push(href, href, { shallow: true }) + router.replace(href, undefined, { shallow: true }) } // Ignore the dependency on `router` because we want // this effect to run only once, on mount, if query is empty. diff --git a/pages/network/[asn].js b/pages/network/[asn].js index 3bf15eefb..9c2619936 100644 --- a/pages/network/[asn].js +++ b/pages/network/[asn].js @@ -1,4 +1,4 @@ -import React, { useCallback } from 'react' +import React, { useCallback, useEffect } from 'react' import { useRouter } from 'next/router' import axios from 'axios' import { Container, Heading, Box, Flex, Text, Link } from 'ooni-components' @@ -85,9 +85,24 @@ const Summary = ({ measurementsTotal, firstMeasurement, countriesData }) => { const NetworkDashboard = ({asn, calendarData = [], measurementsTotal, countriesData}) => { const router = useRouter() - const query = router.query + const { query } = router const displayASN = asn.replace('AS', '') + useEffect(() => { + if (Object.keys(query).length < 3) { + const today = dayjs.utc().add(1, 'day') + const monthAgo = dayjs.utc(today).subtract(1, 'month') + const href = { + query: { + since: monthAgo.format('YYYY-MM-DD'), + until: today.format('YYYY-MM-DD'), + asn: query.asn + }, + } + router.replace(href, undefined, { shallow: true }) + } + }, []) + // Sync page URL params with changes from form values const onChange = useCallback(({ since, until }) => { // since: "2022-01-02", @@ -95,17 +110,11 @@ const NetworkDashboard = ({asn, calendarData = [], measurementsTotal, countriesD const params = { since, until, + asn } - const href = { - pathname: router.pathname.replace('[asn]', asn), - query: params, + if (query.since !== since || query.until !== until) { + router.push({ query: params }, undefined, { shallow: true }) } - if (query.since !== since - || query.until !== until - ) { - router.push(href, href, { shallow: true }) - } - }, [router, query, asn]) return ( From 9c5dfddeb031a7b32b1990c2b215930ac8cc9d10 Mon Sep 17 00:00:00 2001 From: Sarath Date: Wed, 28 Dec 2022 06:55:05 -0500 Subject: [PATCH 19/41] Improve MAT conditional filters (#752) * Drop fields from MAT form state when they are hidden * Hide irrelevant options in X and Y axis based on test name * Show Y axis countries option when All Countries selected * Remove console log * Improve handling of conditional filters Co-authored-by: Maja Komel --- components/aggregation/mat/Form.js | 75 ++++++++++++++++++------------ pages/_app.js | 2 +- public/static/lang/en.json | 6 +-- 3 files changed, 49 insertions(+), 34 deletions(-) diff --git a/components/aggregation/mat/Form.js b/components/aggregation/mat/Form.js index d9863b560..9f7b0202d 100644 --- a/components/aggregation/mat/Form.js +++ b/components/aggregation/mat/Form.js @@ -1,4 +1,4 @@ -import React, { useCallback, useEffect, useState, useRef, useLayoutEffect } from 'react' +import React, { useCallback, useEffect, useState, useMemo } from 'react' import PropTypes from 'prop-types' import { useForm, Controller } from 'react-hook-form' import styled from 'styled-components' @@ -55,18 +55,17 @@ const messages = defineMessages({ const xAxisOptions = [ - 'measurement_start_day', - 'category_code', - 'probe_cc', + ['measurement_start_day', [], false], + ['category_code', ['web_connectivity'], false], + ['probe_cc', [], true], ] const yAxisOptions = [ - 'domain', - 'input', - 'category_code', - 'probe_cc', - 'probe_asn', - '' + ['domain', ['web_connectivity'], false], + ['category_code', ['web_connectivity'], false], + ['probe_cc', [], true], + ['probe_asn', [], false], + ['', [], false] ] const testsWithValidDomainFilter = [ @@ -76,6 +75,15 @@ const testsWithValidDomainFilter = [ 'tcp_connect' ] +const filterAxisOptions = (options, countryValue, testNameValue) => { + return options + .filter(([option, validTestNames, hideForSingleCountry]) => { + if (hideForSingleCountry && countryValue !== '') return false + return validTestNames.length === 0 || validTestNames.includes(testNameValue) + }) + .map(([option]) => option) +} + function isValidFilterForTestname(testName = 'XX', arrayWithMapping) { // whether the dependent filter is valid to show along with `testName` return arrayWithMapping.includes(testName) @@ -99,36 +107,27 @@ const defaultDefaultValues = { } export const Form = ({ onSubmit, testNames, query }) => { - const isInitialMount = useRef(true) const intl = useIntl() const [showConfirmation, setShowConfirmation] = useState(false) const defaultValues = Object.assign({}, defaultDefaultValues, query) const { handleSubmit, control, getValues, watch, reset, setValue } = useForm({ - defaultValues + defaultValues, + shouldUnregister: true, }) - // If `query` changes after the page mounts, reset the form to use default - // values based on new `query` - useEffect(() => { - // Skip running this on mount to avoid unnecessary re-renders - // Based on: https://reactjs.org/docs/hooks-faq.html#can-i-run-an-effect-only-on-updates - if (isInitialMount.current) { - isInitialMount.current = false - } else { - reset(Object.assign({}, defaultDefaultValues, query)) - } - }, [reset, query]) - const sortedCountries = localisedCountries(intl.locale) .sort((a,b) => new Intl.Collator(intl.locale).compare(a.localisedCountryName, b.localisedCountryName)) const testNameValue = watch('test_name') - const showWebConnectivityFilters = isValidFilterForTestname(testNameValue, testsWithValidDomainFilter) + const countryValue = watch('probe_cc') + const showWebConnectivityFilters = useMemo(() => (isValidFilterForTestname(testNameValue, testsWithValidDomainFilter)), [testNameValue]) // reset domain and input when web_connectivity is deselected - useLayoutEffect(() => { - setValue('domain', '') - setValue('input', '') + useEffect(() => { + if (!showWebConnectivityFilters) { + setValue('domain', '') + setValue('input', '') + } }, [setValue, showWebConnectivityFilters]) const [showDatePicker, setShowDatePicker] = useState(false) @@ -170,6 +169,22 @@ export const Form = ({ onSubmit, testNames, query }) => { } }, [getValues, onConfirm]) + const xAxisOptionsFiltered = useMemo(() => { + return filterAxisOptions(xAxisOptions, countryValue, testNameValue) + }, [testNameValue, countryValue]) + + useEffect(() => { + if (!xAxisOptionsFiltered.includes(getValues('axis_x'))) setValue('axis_x', 'measurement_start_day') + }, [setValue, getValues, xAxisOptionsFiltered]) + + const yAxisOptionsFiltered = useMemo(() => { + return filterAxisOptions(yAxisOptions, countryValue, testNameValue) + }, [testNameValue, countryValue]) + + useEffect(() => { + if (!yAxisOptionsFiltered.includes(getValues('axis_y'))) setValue('axis_y', '') + }, [setValue, getValues, yAxisOptionsFiltered]) + return ( @@ -259,7 +274,7 @@ export const Form = ({ onSubmit, testNames, query }) => { control={control} render={({field}) => ( @@ -275,7 +290,7 @@ export const Form = ({ onSubmit, testNames, query }) => { control={control} render={({field}) => ( diff --git a/pages/_app.js b/pages/_app.js index ef3425645..05eeb51c9 100644 --- a/pages/_app.js +++ b/pages/_app.js @@ -3,7 +3,7 @@ // https://github.com/vercel/next.js/blob/canary/examples/with-loading/pages/_app.js import 'scripts/wdyr' import 'regenerator-runtime/runtime' -import { useEffect, useId, useMemo, useState } from 'react' +import { useEffect } from 'react' import 'regenerator-runtime/runtime' import NProgress from 'nprogress' import { useRouter } from 'next/router' diff --git a/public/static/lang/en.json b/public/static/lang/en.json index 2a5f0d6f0..651a5b61b 100644 --- a/public/static/lang/en.json +++ b/public/static/lang/en.json @@ -462,8 +462,8 @@ "MAT.SubTitle": "Create charts based on aggregate views of real-time OONI data from around the world", "MAT.JSONData": "JSON Data", "MAT.CSVData": "CSV Data", - "MAT.Form.Label.XAxis": "X Axis", - "MAT.Form.Label.YAxis": "Y Axis", + "MAT.Form.Label.XAxis": "Columns", + "MAT.Form.Label.YAxis": "Rows", "MAT.Form.Label.AxisOption.domain": "Domain", "MAT.Form.Label.AxisOption.input": "Input", "MAT.Form.Label.AxisOption.measurement_start_day": "Measurement Day", @@ -496,7 +496,7 @@ "MAT.Charts.NoData.Details": "Details:", "MAT.Help.Box.Title": "Help", "MAT.Help.Title": "FAQs", - "MAT.Help.Content": "# What is the MAT?\n\nOONI's Measurement Aggregation Toolkit (MAT) is a tool that enables you to generate your own custom charts based on **aggregate views of real-time OONI data** collected from around the world.\n\nOONI data consists of network measurements collected by [OONI Probe](https://ooni.org/install/) users around the world. \n\nThese measurements contain information about various types of **internet censorship**, such as the [blocking of websites and apps](https://ooni.org/nettest/) around the world. \n\n# Who is the MAT for?\n\nThe MAT was built for researchers, journalists, and human rights defenders interested in examining internet censorship around the world.\n\n# Why use the MAT?\n\nWhen examining cases of internet censorship, it's important to **look at many measurements at once** (\"in aggregate\") in order to answer key questions like the following:\n\n* Does the testing of a service (e.g. Facebook) present **signs of blocking every time that it is tested** in a country? This can be helpful for ruling out [false positives](https://ooni.org/support/faq/#what-are-false-positives).\n* What types of websites (e.g. human rights websites) are blocked in each country?\n* In which countries is a specific website (e.g. `bbc.com`) blocked?\n* How does the blocking of different apps (e.g. WhatsApp or Telegram) vary across countries?\n* How does the blocking of a service vary across countries and [ASNs](https://ooni.org/support/glossary/#asn)?\n* How does the blocking of a service change over time?\n\nWhen trying to answer questions like the above, we normally perform relevant data analysis (instead of inspecting measurements one by one). \n\nThe MAT incorporates our data analysis techniques, enabling you to answer such questions without any data analysis skills, and with the click of a button!\n\n# How to use the MAT?\n\nThrough the filters at the start of the page, select the parameters you care about in order to plot charts based on aggregate views of OONI data.\n\nThe MAT includes the following filters:\n\n* **Countries:** Select a country through the drop-down menu (the \"All Countries\" option will show global coverage)\n* **Test Name:** Select an [OONI Probe test](https://ooni.org/nettest/) based on which you would like to get measurements (for example, select `Web Connectivity` to view the testing of websites)\n* **Domain:** Type the domain for the website you would like to get measurements (e.g. `twitter.com`)\n* **Website categories:** Select the [website category](https://github.com/citizenlab/test-lists/blob/master/lists/00-LEGEND-new_category_codes.csv) for which you would like to get measurements (e.g. `News Media` for news media websites)\n* **ASN:** Type the [ASN](https://ooni.org/support/glossary/#asn) of the network for which you would like to get measurements (e.g. `AS30722` for Vodafone Italia)\n* **Date range:** Select the date range of the measurements by adjusting the `Since` and `Until` filters\n* **X axis:** Select the values that you would like to appear on the horizontal axis of your chart\n* **Y axis:** Select the values that you would like to appear on the vertical axis of your chart\n\nDepending on what you would like to explore, adjust the MAT filters accordingly and click `Show Chart`. \n\nFor example, if you would like to check the testing of BBC in all countries around the world:\n\n* Type `www.bbc.com` under `Domain`\n* Select `Countries` under the `Y axis`\n* Click `Show Chart`\n\nThis will plot numerous charts based on the OONI Probe testing of `www.bbc.com` worldwide.\n\n# Interpreting MAT charts\n\nThe MAT charts (and associated tables) include the following values:\n\n* **OK count:** Successful measurements (i.e. NO sign of internet censorship)\n* **Confirmed count:** Measurements from automatically **confirmed blocked websites** (e.g. a [block page](https://ooni.org/support/glossary/#block-page) was served)\n* **Anomaly count:** Measurements that provided **signs of potential blocking** (however, [false positives](https://ooni.org/support/faq/#what-are-false-positives) can occur) \n* **Failure count:** Failed experiments that should be discarded\n* **Measurement count:** Total volume of OONI measurements (pertaining to the selected country, resource, etc.)\n\nWhen trying to identify the blocking of a service (e.g. `twitter.com`), it's useful to check whether:\n\n* Measurements are annotated as `confirmed`, automatically confirming the blocking of websites\n* A large volume of measurements (in comparison to the overall measurement count) present `anomalies` (i.e. signs of potential censorship)\n\nYou can access the raw data by clicking on the bars of charts, and subsequently clicking on the relevant measurement links. \n\n# Website categories\n\n[OONI Probe](https://ooni.org/install/) users test a wide range of [websites](https://ooni.org/support/faq/#which-websites-will-i-test-for-censorship-with-ooni-probe) that fall under the following [30 standardized categories](https://github.com/citizenlab/test-lists/blob/master/lists/00-LEGEND-new_category_codes.csv).", + "MAT.Help.Content": "# What is the MAT?\n\nOONI's Measurement Aggregation Toolkit (MAT) is a tool that enables you to generate your own custom charts based on **aggregate views of real-time OONI data** collected from around the world.\n\nOONI data consists of network measurements collected by [OONI Probe](https://ooni.org/install/) users around the world. \n\nThese measurements contain information about various types of **internet censorship**, such as the [blocking of websites and apps](https://ooni.org/nettest/) around the world. \n\n# Who is the MAT for?\n\nThe MAT was built for researchers, journalists, and human rights defenders interested in examining internet censorship around the world.\n\n# Why use the MAT?\n\nWhen examining cases of internet censorship, it's important to **look at many measurements at once** (\"in aggregate\") in order to answer key questions like the following:\n\n* Does the testing of a service (e.g. Facebook) present **signs of blocking every time that it is tested** in a country? This can be helpful for ruling out [false positives](https://ooni.org/support/faq/#what-are-false-positives).\n* What types of websites (e.g. human rights websites) are blocked in each country?\n* In which countries is a specific website (e.g. `bbc.com`) blocked?\n* How does the blocking of different apps (e.g. WhatsApp or Telegram) vary across countries?\n* How does the blocking of a service vary across countries and [ASNs](https://ooni.org/support/glossary/#asn)?\n* How does the blocking of a service change over time?\n\nWhen trying to answer questions like the above, we normally perform relevant data analysis (instead of inspecting measurements one by one). \n\nThe MAT incorporates our data analysis techniques, enabling you to answer such questions without any data analysis skills, and with the click of a button!\n\n# How to use the MAT?\n\nThrough the filters at the start of the page, select the parameters you care about in order to plot charts based on aggregate views of OONI data.\n\nThe MAT includes the following filters:\n\n* **Countries:** Select a country through the drop-down menu (the \"All Countries\" option will show global coverage)\n* **Test Name:** Select an [OONI Probe test](https://ooni.org/nettest/) based on which you would like to get measurements (for example, select `Web Connectivity` to view the testing of websites)\n* **Domain:** Type the domain for the website you would like to get measurements (e.g. `twitter.com`)\n* **Website categories:** Select the [website category](https://github.com/citizenlab/test-lists/blob/master/lists/00-LEGEND-new_category_codes.csv) for which you would like to get measurements (e.g. `News Media` for news media websites)\n* **ASN:** Type the [ASN](https://ooni.org/support/glossary/#asn) of the network for which you would like to get measurements (e.g. `AS30722` for Vodafone Italia)\n* **Date range:** Select the date range of the measurements by adjusting the `Since` and `Until` filters\n* **Columns:** Select the values that you would like to appear on the horizontal axis of your chart\n* **Rows:** Select the values that you would like to appear on the vertical axis of your chart\n\nDepending on what you would like to explore, adjust the MAT filters accordingly and click `Show Chart`. \n\nFor example, if you would like to check the testing of BBC in all countries around the world:\n\n* Type `www.bbc.com` under `Domain`\n* Select `Countries` under the `Rows`\n* Click `Show Chart`\n\nThis will plot numerous charts based on the OONI Probe testing of `www.bbc.com` worldwide.\n\n# Interpreting MAT charts\n\nThe MAT charts (and associated tables) include the following values:\n\n* **OK count:** Successful measurements (i.e. NO sign of internet censorship)\n* **Confirmed count:** Measurements from automatically **confirmed blocked websites** (e.g. a [block page](https://ooni.org/support/glossary/#block-page) was served)\n* **Anomaly count:** Measurements that provided **signs of potential blocking** (however, [false positives](https://ooni.org/support/faq/#what-are-false-positives) can occur) \n* **Failure count:** Failed experiments that should be discarded\n* **Measurement count:** Total volume of OONI measurements (pertaining to the selected country, resource, etc.)\n\nWhen trying to identify the blocking of a service (e.g. `twitter.com`), it's useful to check whether:\n\n* Measurements are annotated as `confirmed`, automatically confirming the blocking of websites\n* A large volume of measurements (in comparison to the overall measurement count) present `anomalies` (i.e. signs of potential censorship)\n\nYou can access the raw data by clicking on the bars of charts, and subsequently clicking on the relevant measurement links. \n\n# Website categories\n\n[OONI Probe](https://ooni.org/install/) users test a wide range of [websites](https://ooni.org/support/faq/#which-websites-will-i-test-for-censorship-with-ooni-probe) that fall under the following [30 standardized categories](https://github.com/citizenlab/test-lists/blob/master/lists/00-LEGEND-new_category_codes.csv).", "MAT.Help.Subtitle.Categories": "Categories", "MAT.CustomTooltip.ViewMeasurements": "View measurements", "ReachabilityDash.Heading.CircumventionTools": "Reachability of Censorship Circumvention Tools", From ebcabdc10f005e86073c2cdd8e73aa44196a0933 Mon Sep 17 00:00:00 2001 From: GermaVinsmoke Date: Sun, 8 Jan 2023 20:05:10 +0530 Subject: [PATCH 20/41] replaced older IM and circumvention charts (#785) Signed-off-by: GermaVinsmoke Co-authored-by: Maja Komel --- components/{network => }/Chart.js | 43 +-- components/aggregation/mat/XAxis.js | 2 +- components/country/ASNSelector.js | 31 -- components/country/Apps.js | 60 ++-- components/country/AppsStats.js | 77 ----- components/country/AppsStatsChart.js | 111 ------- components/country/AppsStatsCircumvention.js | 73 ----- .../country/AppsStatsCircumventionRow.js | 174 ----------- components/country/AppsStatsRow.js | 179 ----------- .../country/ConfirmedBlockedCategory.js | 123 ++++++++ components/country/CountryContext.js | 18 -- components/country/NetworkProperties.js | 162 ---------- components/country/NetworkStats.js | 91 ------ components/country/Overview.js | 6 - components/country/OverviewCharts.js | 3 +- components/country/PageNavMenu.js | 3 - components/country/PeriodFilter.js | 17 -- components/country/URLChart.js | 287 ------------------ components/country/WebsiteChartLoader.js | 87 ------ components/country/Websites.js | 119 +++----- components/country/WebsitesCharts.js | 165 ---------- components/network/Form.js | 6 +- cypress/support/e2e.js | 10 + pages/country/[countryCode].js | 74 +++-- pages/network/[asn].js | 71 +++-- 25 files changed, 337 insertions(+), 1655 deletions(-) rename components/{network => }/Chart.js (65%) delete mode 100644 components/country/ASNSelector.js delete mode 100644 components/country/AppsStats.js delete mode 100644 components/country/AppsStatsChart.js delete mode 100644 components/country/AppsStatsCircumvention.js delete mode 100644 components/country/AppsStatsCircumventionRow.js delete mode 100644 components/country/AppsStatsRow.js create mode 100644 components/country/ConfirmedBlockedCategory.js delete mode 100644 components/country/NetworkProperties.js delete mode 100644 components/country/NetworkStats.js delete mode 100644 components/country/PeriodFilter.js delete mode 100644 components/country/URLChart.js delete mode 100644 components/country/WebsiteChartLoader.js delete mode 100644 components/country/WebsitesCharts.js diff --git a/components/network/Chart.js b/components/Chart.js similarity index 65% rename from components/network/Chart.js rename to components/Chart.js index b517d15d2..847f781cf 100644 --- a/components/network/Chart.js +++ b/components/Chart.js @@ -1,6 +1,6 @@ import React, { useMemo } from 'react' -import { useIntl } from 'react-intl' import { useRouter } from 'next/router' +import { FormattedMessage } from 'react-intl' import { Heading, Box, Flex } from 'ooni-components' import useSWR from 'swr' import GridChart, { prepareDataForGridChart } from 'components/aggregation/mat/GridChart' @@ -14,33 +14,16 @@ const swrOptions = { } const Chart = React.memo(function Chart({testName, testGroup = null, title, queryParams = {}}) { - const intl = useIntl() - const router = useRouter() - const { query: {since, until, asn} } = router - const name = testName || testGroup.name - const params = useMemo(() => ({ - ...queryParams, - axis_x: 'measurement_start_day' - }), [queryParams]) - - const query = useMemo(() => ({ - ...params, - probe_asn: asn, - since: since, - until: until, - ...testName && {test_name: testName} - }), [since, until, asn, params, testName]) - const apiQuery = useMemo(() => { - const qs = new URLSearchParams(query).toString() + const qs = new URLSearchParams(queryParams).toString() return qs - }, [query]) + }, [queryParams]) const { data, error } = useSWR( - testGroup ? { query: apiQuery, - testNames: testGroup.tests, + testGroup ? { query: apiQuery, + testNames: testGroup.tests, groupKey: name } : apiQuery, testGroup ? MATMultipleFetcher : MATFetcher, @@ -51,23 +34,23 @@ const Chart = React.memo(function Chart({testName, testGroup = null, title, quer return [null, 0] } let chartData = testGroup ? data : data.data - const graphQuery = testGroup ? {...query, axis_y: name} : query - const [reshapedData, rowKeys, rowLabels] = prepareDataForGridChart(chartData, graphQuery, intl.locale) + const graphQuery = testGroup ? {...queryParams, axis_y: name} : queryParams + const [reshapedData, rowKeys, rowLabels] = prepareDataForGridChart(chartData, graphQuery) return [reshapedData, rowKeys, rowLabels] - }, [data, query, name, testGroup, intl]) + }, [data, queryParams, name, testGroup]) const headerOptions = { probe_cc: false, subtitle: false } return ( - + {title} {(!chartData && !error) ? ( -
    {intl.formatMessage({id: 'General.Loading'})}
    + ) : ( chartData === null || chartData.length === 0 ? ( - {intl.formatMessage({id: 'General.NoData'})} + ) : (
    - {intl.formatMessage({id: 'General.Error'})}: {error.message} + Error: {error.message} {JSON.stringify(error, null, 2)} @@ -95,4 +78,4 @@ const Chart = React.memo(function Chart({testName, testGroup = null, title, quer ) }) -export default Chart \ No newline at end of file +export default Chart diff --git a/components/aggregation/mat/XAxis.js b/components/aggregation/mat/XAxis.js index 36b6ac095..0b38ce8eb 100644 --- a/components/aggregation/mat/XAxis.js +++ b/components/aggregation/mat/XAxis.js @@ -45,4 +45,4 @@ export const XAxis = ({ data }) => { ) -} \ No newline at end of file +} diff --git a/components/country/ASNSelector.js b/components/country/ASNSelector.js deleted file mode 100644 index d318ba2de..000000000 --- a/components/country/ASNSelector.js +++ /dev/null @@ -1,31 +0,0 @@ -import React from 'react' -import PropTypes from 'prop-types' -import { Select } from 'ooni-components' -import styled from 'styled-components' - -const StyledSelect = styled(Select)` - font-family: 'Fira Sans'; -` - -const ASNSelector = ({ networks, onNetworkChange, selectedNetwork }) => ( - onNetworkChange(e.target.value)} defaultValue={selectedNetwork}> - { - networks.map((network, index) => ( - - )) - } - -) - -ASNSelector.propTypes = { - networks: PropTypes.arrayOf(PropTypes.shape({ - probe_asn: PropTypes.number, - count: PropTypes.number - })), - onNetworkChange: PropTypes.func, - selectedNetwork: PropTypes.number -} - -export default ASNSelector diff --git a/components/country/Apps.js b/components/country/Apps.js index 16c446692..04368d9c6 100644 --- a/components/country/Apps.js +++ b/components/country/Apps.js @@ -1,13 +1,50 @@ -import React from 'react' -import { FormattedMessage } from 'react-intl' +import { useMemo } from 'react' +import { FormattedMessage, useIntl } from 'react-intl' import { Text } from 'ooni-components' import SectionHeader from './SectionHeader' import { SimpleBox } from './boxes' -// import PeriodFilter from './PeriodFilter' -import AppsStatsGroup from './AppsStats' -import AppsStatsCircumvention from './AppsStatsCircumvention' +import Chart from 'components/Chart' import FormattedMarkdown from '../FormattedMarkdown' +import { useRouter } from 'next/router' + +const messagingTestNames = ['signal', 'telegram', 'whatsapp', 'facebook_messenger'] +const circumventionTestNames = ['vanilla_tor', 'psiphon', 'tor', 'torsf'] + +const ChartsContainer = () => { + const intl = useIntl() + const router = useRouter() + const { query: { since, until, countryCode } } = router + + const queryMessagingApps = useMemo(() => ({ + axis_x: 'measurement_start_day', + probe_cc: countryCode, + since, + until, + }), [countryCode, since, until]) + + const queryCircumventionTools = useMemo(() => ({ + axis_x: 'measurement_start_day', + probe_cc: countryCode, + since, + until, + }), [countryCode, since, until]) + + return ( + <> + + + + ) +} const AppsSection = () => ( <> @@ -15,24 +52,13 @@ const AppsSection = () => ( - {/* - - */} - {/* App-wise graphs */} - } - testGroup='im' - /> - {} - testGroup='circumvention' - />} + ) diff --git a/components/country/AppsStats.js b/components/country/AppsStats.js deleted file mode 100644 index 60578ac65..000000000 --- a/components/country/AppsStats.js +++ /dev/null @@ -1,77 +0,0 @@ -import React from 'react' -import { Box, Heading, Text } from 'ooni-components' -import styled from 'styled-components' -import axios from 'axios' -import { FormattedMessage } from 'react-intl' - -import { inCountry } from './CountryContext' -import AppsStatRow from './AppsStatsRow' -import { AppSectionLoader } from './WebsiteChartLoader' - -const AppGroupHeading = styled(Box)` - border: 1px solid ${props => props.theme.colors.gray3}; - border-left: 12px solid ${props => props.theme.colors.cyan6}; -` - -const defaultState = { - data: null, - fetching: true -} - -class AppsStatsGroup extends React.Component { - constructor(props) { - super(props) - this.state = { - fetching: false, - ...defaultState - } - } - - componentDidMount() { - this.fetchIMNetworks() - } - - async fetchIMNetworks() { - const { countryCode } = this.props - const client = axios.create({baseURL: process.env.NEXT_PUBLIC_OONI_API}) // eslint-disable-line - const result = await client.get('/api/_/im_networks', { - params: { - probe_cc: countryCode - } - }) - - this.setState({ - data: result.data, - fetching: false - }) - } - - render() { - const { title } = this.props - const { data, fetching } = this.state - if (fetching) { - return ( - - ) - } - return ( - - - {title} - - {data && Object.keys(data).length === 0 && - - - - - - } - {Object.keys(data).map((im, index) => ( - - ))} - - ) - } -} - -export default inCountry(AppsStatsGroup) diff --git a/components/country/AppsStatsChart.js b/components/country/AppsStatsChart.js deleted file mode 100644 index a846d6a83..000000000 --- a/components/country/AppsStatsChart.js +++ /dev/null @@ -1,111 +0,0 @@ -import React from 'react' -import PropTypes from 'prop-types' -import axios from 'axios' -import { - VictoryChart, - VictoryBar, - VictoryAxis, - VictoryVoronoiContainer -} from 'victory' -import { theme } from 'ooni-components' - -import { inCountry } from './CountryContext' -import Tooltip from './Tooltip' -import { AppsChartLoader } from './WebsiteChartLoader' - -class AppsStatChart extends React.Component { - constructor(props) { - super(props) - this.state = { - data: null, - fetching: true - } - } - - componentDidMount() { - this.fetchAppNetworkStats() - } - - async fetchAppNetworkStats() { - const { countryCode, app, asn } = this.props - const client = axios.create({baseURL: process.env.NEXT_PUBLIC_OONI_API}) // eslint-disable-line - const result = await client.get('/api/_/im_stats', { - params: { - probe_cc: countryCode, - probe_asn: asn, - test_name: app - } - }) - - this.setState({ - data: result.data.results, - fetching: false - }) - } - - render() { - const { data, fetching } = this.state - - if (fetching) { - return () - } - - const yMax = data.reduce((max, item) => ( - (item.total_count > max) ? item.total_count : max - ), 0) - - return ( - <> - - } - > - {}} - /> - yMax} - style={{ - data: { - fill: theme.colors.gray3, - } - }} - /> - { - let s = `${new Date(d.test_day).toLocaleDateString()}` - s += `\n${d.total_count} Total` - return s - }} - labelComponent={} - /> - - - ) - } -} - -export default inCountry(AppsStatChart) diff --git a/components/country/AppsStatsCircumvention.js b/components/country/AppsStatsCircumvention.js deleted file mode 100644 index 2e191f07a..000000000 --- a/components/country/AppsStatsCircumvention.js +++ /dev/null @@ -1,73 +0,0 @@ -import React from 'react' -import { Box, Heading } from 'ooni-components' -import styled from 'styled-components' -import axios from 'axios' - -import { inCountry } from './CountryContext' -import AppsStatsRowCircumvention from './AppsStatsCircumventionRow' -import { AppSectionLoader } from './WebsiteChartLoader' - -const AppGroupHeading = styled(Box)` - border: 1px solid ${props => props.theme.colors.gray3}; - border-left: 12px solid ${props => props.theme.colors.cyan6}; -` - -const defaultState = { - data: null, - fetching: true -} - -class AppsStatsCircumvention extends React.Component { - constructor(props) { - super(props) - this.state = { - fetching: false, - ...defaultState - } - } - - componentDidMount() { - this.fetchCircumventionStats() - } - - async fetchCircumventionStats() { - const { countryCode } = this.props - const client = axios.create({baseURL: process.env.NEXT_PUBLIC_OONI_API}) // eslint-disable-line - const result = await client.get('/api/_/vanilla_tor_stats', { - params: { - probe_cc: countryCode - } - }) - - this.setState({ - data: result.data, - fetching: false - }) - } - - static getDerivedStateFromprops() { - return defaultState - } - - render() { - const { title } = this.props - const { data, fetching } = this.state - - if (fetching) { - return ( - - ) - } - - return ( - - - {title} - - - - ) - } -} - -export default inCountry(AppsStatsCircumvention) diff --git a/components/country/AppsStatsCircumventionRow.js b/components/country/AppsStatsCircumventionRow.js deleted file mode 100644 index 0181dd7c6..000000000 --- a/components/country/AppsStatsCircumventionRow.js +++ /dev/null @@ -1,174 +0,0 @@ -import React from 'react' -import PropTypes from 'prop-types' -import { FormattedMessage } from 'react-intl' -import { Flex, Box, Link, Text, theme } from 'ooni-components' -import styled from 'styled-components' -import { - NettestVanillaTor -} from 'ooni-components/dist/icons' -import dayjs from 'services/dayjs' - -import { testNames } from '../test-info' -import { CountryContext } from './CountryContext' -import { CollapseTrigger } from '../CollapseTrigger' - -const NETWORK_STATS_PER_PAGE = 4 - -const StyledRow = styled(Box)` - border: 1px solid ${props => props.theme.colors.gray3}; -` - -const NetworkRow = ({ asn, data }) => ( - - - - AS{ asn } - - - {data.last_tested} Last tested - - - {data.failure_count} Not OK - - - {data.success_count} OK - - - {data.total_count} Total - - - - - {data.test_runtime_avg} Runtime avg - - - {data.test_runtime_max} Runtime max - - - {data.test_runtime_min} Runtime min - - - - - -) - -class AppsStatsCircumventionRow extends React.Component { - constructor(props) { - super(props) - this.state = { - minimized: true, - visibleNetworks: 0 - } - this.toggleMinimize = this.toggleMinimize.bind(this) - this.showMore = this.showMore.bind(this) - } - - toggleMinimize() { - const { totalNetworks } = this.state - const networksToShow = Math.min(totalNetworks, NETWORK_STATS_PER_PAGE) - this.setState((state) => ({ - minimized: !state.minimized, - visibleNetworks: state.minimized ? networksToShow : 0 - })) - } - - showMore() { - // Ensure not to add more rows than available - const { visibleNetworks, totalNetworks } = this.state - - let rowsToAdd = NETWORK_STATS_PER_PAGE - - if (visibleNetworks + NETWORK_STATS_PER_PAGE > totalNetworks) { - rowsToAdd = totalNetworks - visibleNetworks - } - - this.setState((state) => ({ - visibleNetworks: state.visibleNetworks + rowsToAdd - })) - } - - - static getDerivedStateFromProps(props, state) { - const totalNetworks = props.data.networks.length - return { - totalNetworks, - ...state - } - } - - renderCharts() { - const { data } = this.props - const { totalNetworks, visibleNetworks } = this.state - const networks = data.networks - const content = [] - - for (let i = 0; i < networks.length && i < visibleNetworks ; i ++) { - content.push() - } - - return ( - <> - - {content} - - {(visibleNetworks < totalNetworks) && - - { - e.preventDefault() - this.showMore() - }}> - - - - } - - ) - } - - render () { - const { data } = this.props - const { minimized, totalNetworks } = this.state - - return ( - - - - - - - Vanilla Tor - - - {data.networks.length === 0 && - - - - } - {data.networks.length > 0 && `${data.networks.length} Networks Tested`} - - {totalNetworks > 0 && - <> - - - {' '} - {dayjs.utc(data.last_tested).fromNow()} - - - - - - } - - {!minimized && this.renderCharts()} - - ) - } -} - -export default AppsStatsCircumventionRow diff --git a/components/country/AppsStatsRow.js b/components/country/AppsStatsRow.js deleted file mode 100644 index 6acc04626..000000000 --- a/components/country/AppsStatsRow.js +++ /dev/null @@ -1,179 +0,0 @@ -import React, { useContext } from 'react' -import PropTypes from 'prop-types' -import { FormattedMessage } from 'react-intl' -import { Flex, Box, Link, theme } from 'ooni-components' -import styled from 'styled-components' -import { - NettestWhatsApp, - NettestTelegram, - NettestFacebookMessenger -} from 'ooni-components/dist/icons' -import dayjs from 'services/dayjs' - -import { testNames } from '../test-info' -import AppsStatChart from './AppsStatsChart' -import { CountryContext } from './CountryContext' -import { CollapseTrigger } from '../CollapseTrigger' - -const NETWORK_STATS_PER_PAGE = 4 - -const AppIcon = ({ app, size }) => { - switch(app) { - case 'whatsapp': - return - case 'telegram': - return - case 'facebook_messenger': - return - default: - return - } -} - -const StyledRow = styled(Box)` - border: 1px solid ${props => props.theme.colors.gray3}; -` - -const NetworkRow = ({ asn, app }) => { - const { countryCode } = useContext(CountryContext) - const until = dayjs.utc().add(1, 'day').format('YYYY-MM-DD') - const since = dayjs.utc().subtract(30, 'day').format('YYYY-MM-DD') - - const linkToMeasurements = `/search?probe_cc=${countryCode}&probe_asn=AS${asn}&test_name=${app}&since=${since}&until=${until}` - - return ( - - - - - - - AS{ asn } - - - - - - - - - - - - - - - ) -} - -class AppsStatRow extends React.Component { - constructor(props) { - super(props) - this.state = { - minimized: true, - visibleNetworks: 0 - } - this.toggleMinimize = this.toggleMinimize.bind(this) - this.showMore = this.showMore.bind(this) - } - - toggleMinimize() { - const { totalNetworks } = this.state - const networksToShow = Math.min(totalNetworks, NETWORK_STATS_PER_PAGE) - this.setState((state) => ({ - minimized: !state.minimized, - visibleNetworks: state.minimized ? networksToShow : 0 - })) - } - - showMore() { - // Ensure not to add more rows than available - const { visibleNetworks, totalNetworks } = this.state - - let rowsToAdd = NETWORK_STATS_PER_PAGE - - if (visibleNetworks + NETWORK_STATS_PER_PAGE > totalNetworks) { - rowsToAdd = totalNetworks - visibleNetworks - } - - this.setState((state) => ({ - visibleNetworks: state.visibleNetworks + rowsToAdd - })) - } - - - static getDerivedStateFromProps(props, state) { - const totalNetworks = props.data.anomaly_networks.length + props.data.ok_networks.length - return { - totalNetworks, - ...state - } - } - - renderCharts() { - const { data, app } = this.props - const { totalNetworks, visibleNetworks } = this.state - const networks = [...data.anomaly_networks, ...data.ok_networks] - const content = [] - - for (let i = 0; i < networks.length && i < visibleNetworks ; i ++) { - content.push() - } - - return ( - <> - - {content} - - {(visibleNetworks < totalNetworks) && - - {e.preventDefault(); this.showMore()}}> - - - - } - - ) - } - - render () { - const { app, data } = this.props - const { minimized } = this.state - return ( - - - - - - - {testNames[app].name} - - - {`${data.anomaly_networks.length + data.ok_networks.length} Networks Tested`} - - - - {' '} - {dayjs.utc(data.last_tested).fromNow()} - - - - - - {!minimized && this.renderCharts()} - - ) - } -} - -export default AppsStatRow diff --git a/components/country/ConfirmedBlockedCategory.js b/components/country/ConfirmedBlockedCategory.js new file mode 100644 index 000000000..1458335be --- /dev/null +++ b/components/country/ConfirmedBlockedCategory.js @@ -0,0 +1,123 @@ +import React, { useMemo } from 'react' +import { useRouter } from 'next/router' +import { FormattedMessage } from 'react-intl' +import { Heading, Box, Flex, Text, theme } from 'ooni-components' +import useSWR from 'swr' +import { DetailsBox } from 'components/measurement/DetailsBox' +import { MATFetcher } from 'services/fetchers' +import * as icons from 'ooni-components/dist/icons' +import Badge from 'components/Badge' +import { getCategoryCodesMap } from 'components/utils/categoryCodes' + +const swrOptions = { + revalidateOnFocus: false, + dedupingInterval: 10 * 60 * 1000, +} + +const ConfirmedBlockedCategory = React.memo(function Chart({testName, title, queryParams = {}}) { + const router = useRouter() + const { query: { countryCode } } = router + + const categoryCodeMap = getCategoryCodesMap() + + const params = useMemo(() => ({ + ...queryParams, + axis_x: 'category_code' + }), [queryParams]) + + const query = useMemo(() => ({ + ...params, + probe_cc: countryCode, + ...testName && {test_name: testName} + }), [countryCode, params, testName]) + + const apiQuery = useMemo(() => { + const qs = new URLSearchParams(query).toString() + return qs + }, [query]) + + const prepareDataForBadge = (categoriesData) => { + return categoriesData.filter(category => category.confirmed_count > 0) + } + + const { data, error } = useSWR( + apiQuery, + MATFetcher, + swrOptions + ) + + const blockedCategoriesData = useMemo(() => { + if (!data) { + return null + } + + const categoriesData = prepareDataForBadge(data.data) + + return categoriesData + }, [data]) + + return ( + + {title} + + {(!blockedCategoriesData && !error) ? ( +
    Loading ...
    + ) : ( + blockedCategoriesData === null || blockedCategoriesData.length === 0 ? ( + + ) : ( + + {blockedCategoriesData && blockedCategoriesData.map(category => ( + + ))} + + ) + )} +
    + {error && + +
    + Error: {error.message} + + {JSON.stringify(error, null, 2)} + +
    + }/> + } + +
    + ) +}) + +const CategoryBadge = ({ categoryCode, categoryCodeMap, confirmedCount }) => { + const categoryDesc = categoryCodeMap.get(categoryCode) + const CategoryIcon = icons[`CategoryCode${categoryCode}`] + + if (categoryDesc === undefined && confirmedCount === 0) { + return null + } + + if (CategoryIcon === undefined) { + return null + } + + return ( + + + + + + + + + + + ) +} + +export default ConfirmedBlockedCategory diff --git a/components/country/CountryContext.js b/components/country/CountryContext.js index 7c501a794..21c8ac17c 100644 --- a/components/country/CountryContext.js +++ b/components/country/CountryContext.js @@ -25,24 +25,6 @@ CountryContextProvider.propTypes = { children: PropTypes.any } - -/* HoC to inject Country context into wrapped components */ -export const inCountry = (WrappedComponent) => { - return function InjectCountry(props) { - return ( - - {({countryCode, countryName}) => ( - - )} - - ) - } -} - /* Custom Hook to use CountryContext */ export const useCountry = () => { return useContext(CountryContext) diff --git a/components/country/NetworkProperties.js b/components/country/NetworkProperties.js deleted file mode 100644 index 9c0137d12..000000000 --- a/components/country/NetworkProperties.js +++ /dev/null @@ -1,162 +0,0 @@ -import React from 'react' -import { FormattedMessage } from 'react-intl' -import { Flex, Box, Heading, Text, Link } from 'ooni-components' -import axios from 'axios' - -import SectionHeader from './SectionHeader' -import { SimpleBox } from './boxes' -// import PeriodFilter from './PeriodFilter' -import NetworkStats from './NetworkStats' -import SpinLoader from '../vendor/SpinLoader' -import FormattedMarkdown from '../FormattedMarkdown' - -const NETWORK_STATS_PER_PAGE = 4 - -class NetworkPropertiesSection extends React.Component { - constructor(props) { - super(props) - this.state = { - fetching: true, - visibleNetworks: 0, - data: [], - currentPage: 0 - } - this.showMoreNetworks = this.showMoreNetworks.bind(this) - } - - showMoreNetworks() { - // Ensure not to add more rows than available - const { visibleNetworks, totalNetworks } = this.state - - let rowsToAdd = NETWORK_STATS_PER_PAGE - - if (visibleNetworks + NETWORK_STATS_PER_PAGE > totalNetworks) { - rowsToAdd = totalNetworks - visibleNetworks - } - - this.setState((state) => ({ - visibleNetworks: state.visibleNetworks + rowsToAdd - })) - } - - componentDidMount() { - this.fetchNetworkStats() - } - - componentDidUpdate(prevProps, prevState) { - const { data, totalNetworks } = this.state - if (prevState.data.length === data.length && data.length < totalNetworks) { - this.fetchNetworkStats() - } - } - - async fetchNetworkStats() { - const { countryCode } = this.props - const { currentPage } = this.state - const client = axios.create({baseURL: process.env.NEXT_PUBLIC_OONI_API}) // eslint-disable-line - const result = await client.get('/api/_/network_stats', { - params: { - probe_cc: countryCode, - limit: NETWORK_STATS_PER_PAGE, - offset: (currentPage > 0 ? currentPage : 0) * NETWORK_STATS_PER_PAGE - } - }) - - this.setState((state) => ({ - data: [...state.data, ...result.data.results], - totalNetworks: result.data.metadata.total_count, - currentPage: result.data.metadata.current_page, - fetching: false, - visibleNetworks: state.data.length + result.data.results.length - })) - } - - renderStats() { - const { fetching, visibleNetworks, totalNetworks, data } = this.state - - if (fetching) { - return () - } - - if (data.length === 0) { - return ( - - - - - - ) - } - - const content = [] - - for ( let i = 0; i < data.length && i < visibleNetworks; i++) { - content.push( - - ) - } - return ( - <> - {content} - {(visibleNetworks < totalNetworks) && - - { - e.preventDefault() - this.showMoreNetworks() - }}> - - - - } - - ) - } - - render() { - return ( - <> - - - - - {/* - {}} /> - */} - - - - - - - {/* Country Level Summary - - - - - - - - - - - - */} - {/* Network-wise infoboxes */} - - {this.renderStats()} - - - - ) - } -} - -export default NetworkPropertiesSection diff --git a/components/country/NetworkStats.js b/components/country/NetworkStats.js deleted file mode 100644 index 8aec06c84..000000000 --- a/components/country/NetworkStats.js +++ /dev/null @@ -1,91 +0,0 @@ -import React from 'react' -import PropTypes from 'prop-types' -import { Flex, Box, theme } from 'ooni-components' -import { - NettestGroupMiddleBoxes, -} from 'ooni-components/dist/icons' -import { MdFileDownload, MdFileUpload } from 'react-icons/md' -import { FormattedMessage } from 'react-intl' -import styled from 'styled-components' - -import { testGroups } from '../test-info' - - -const BorderedBox = styled(Flex)` - border: 1px solid ${props => props.theme.colors.gray3}; - padding: 12px; -` - -const StyledStat = styled(Flex)` - font-size: 28px; - font-weight: 300; - line-height: 40px; -` - -const StatBox = ({ - label = 'Average Download', - value = '50 Mbit/s', - ...props -}) => ( - - - {value} - - - {label} - - -) - -const NetworkStats = ({ - asn, - asnName, - avgDownload, - avgUpload, - avgPing, - middleboxes -}) => ( - <> - - AS{asn} - {asnName} - - - } - value={( - - - {avgDownload} - - - )} - /> - } - value={( - - - {avgUpload} - - - )} - /> - } - value={( - - {avgPing} - - - )} - /> - } - value={} - /> - - -) - -export default NetworkStats diff --git a/components/country/Overview.js b/components/country/Overview.js index da2f6728f..5b2b9f226 100644 --- a/components/country/Overview.js +++ b/components/country/Overview.js @@ -7,12 +7,6 @@ import { BoxWithTitle } from './boxes' import TestsByGroup from './OverviewCharts' import FormattedMarkdown from '../FormattedMarkdown' import { useCountry } from './CountryContext' -import { - NettestGroupWebsites, - NettestGroupInstantMessaging, - NettestGroupMiddleBoxes, -} from 'ooni-components/dist/icons' - const NwInterferenceStatus = styled(Box)` color: ${props => props.color || props.theme.colors.gray5}; diff --git a/components/country/OverviewCharts.js b/components/country/OverviewCharts.js index 97a18f4ff..00136942e 100644 --- a/components/country/OverviewCharts.js +++ b/components/country/OverviewCharts.js @@ -1,6 +1,6 @@ import React from 'react' import styled from 'styled-components' -import { Heading, Button, Flex, Box, Text, theme } from 'ooni-components' +import { Flex, Box, theme } from 'ooni-components' import { VictoryChart, VictoryBar, @@ -11,7 +11,6 @@ import { VictoryVoronoiContainer } from 'victory' import { FormattedMessage, injectIntl } from 'react-intl' -import NLink from 'next/link' import Tooltip from './Tooltip' import VictoryTheme from '../VictoryTheme' diff --git a/components/country/PageNavMenu.js b/components/country/PageNavMenu.js index c9a91de64..8f4cdcba3 100644 --- a/components/country/PageNavMenu.js +++ b/components/country/PageNavMenu.js @@ -55,9 +55,6 @@ const PageNavMenu = ({ countryCode }) => { - - - } diff --git a/components/country/PeriodFilter.js b/components/country/PeriodFilter.js deleted file mode 100644 index 25ec9b7bc..000000000 --- a/components/country/PeriodFilter.js +++ /dev/null @@ -1,17 +0,0 @@ -import React from 'react' -import { Flex, Select } from 'ooni-components' -import { FormattedMessage } from 'react-intl' - -const PeriodFilter = () => ( - - {(msg) => } - - -) - -export default PeriodFilter diff --git a/components/country/URLChart.js b/components/country/URLChart.js deleted file mode 100644 index fca084f7a..000000000 --- a/components/country/URLChart.js +++ /dev/null @@ -1,287 +0,0 @@ -import React from 'react' -import PropTypes from 'prop-types' -import axios from 'axios' -import { Flex, Box, Link } from 'ooni-components' -import { - VictoryChart, - VictoryStack, - VictoryBar, - VictoryAxis, - VictoryVoronoiContainer -} from 'victory' -import { FormattedMessage } from 'react-intl' -import styled from 'styled-components' -import dayjs from 'services/dayjs' - -import { - colorNormal, - colorError, - colorConfirmed, - colorAnomaly, - colorEmpty -} from '../colors' - -import Tooltip from './Tooltip' -import { WebsiteChartLoader } from './WebsiteChartLoader' - -const Circle = styled.div` - position: relative; - top: 0; - right: 0; - background-color: ${props => props.theme.colors.gray3}; - padding: 6px; - border-radius: 50%; - :hover { - background-color: ${props => props.theme.colors.gray4}; - } -` -/* CSS Triangle from CSS-Tricks: https://css-tricks.com/snippets/css/css-triangle/#article-header-id-1 */ -// TOOD: Improve toggle using transforms and animation -const Triangle = styled.div` - width: 0; - height: 0; - border-left: 6px solid transparent; - border-right: 6px solid transparent; - border-top: ${props => props.down ? '12px solid ' + props.theme.colors.gray7 : 'none'}; - border-bottom: ${props => !props.down ? '12px solid ' + props.theme.colors.gray7 : 'none'}; -` - -const WrappedText = styled.div` - overflow-wrap: break-word; - min-height: 2em; -` - -const TruncatedURL = ({ url }) => { - const MAX_URL_LENGTH = 60 - try { - const urlObj = new URL(url) - const domain = urlObj.origin - const path = urlObj.pathname - let endOfPath = path.split('/').pop() - if (domain.length + endOfPath.length > MAX_URL_LENGTH) { - endOfPath = endOfPath.substring(0, MAX_URL_LENGTH - domain.length) + '...' - } - return ( - - - {`${domain}${endOfPath.length > 5 ? '/...' : ''}/${endOfPath}`} - - - ) - } catch (e) { - return ( - - {url} - - ) - } -} - -const StyledChartRow = styled(Flex)` - border: 1px solid ${props => props.theme.colors.gray3}; - border-radius: 5px; -` - -const ToggleMinimizeButton = ({ minimized, onToggle }) => ( - -) - -const defaultState = { - data: null, - minimized: true, - fetching: true -} - -class URLChart extends React.Component { - constructor(props) { - super(props) - this.state = defaultState - this.onToggleMinimize = this.onToggleMinimize.bind(this) - } - - onToggleMinimize() { - this.setState((state) => ({ - minimized: !state.minimized - })) - } - - componentDidMount() { - this.fetchURLChartData() - } - - componentDidUpdate(prevProps, prevState) { - if(this.state.data === null) { - this.fetchURLChartData() - } - } - - async fetchURLChartData() { - const { metadata, network, countryCode } = this.props - const client = axios.create({baseURL: process.env.NEXT_PUBLIC_OONI_API}) // eslint-disable-line - const result = await client.get('/api/_/website_stats', { - params: { - probe_cc: countryCode, - probe_asn: network, - input: metadata.input - } - }) - // HACK: Temporary fix to workaround backend bug showing wrong anomaly and confirmed counts - const fixedData = result.data.results.map(d => { - d.anomaly_count = d.anomaly_count - d.confirmed_count - return d - }) - this.setState({ - data: result.data.results, - fetching: false - }) - } - - static getDerivedStateFromProps(props, state) { - if (props.metadata.input !== state.prevTestUrl) { - return { - prevTestUrl: props.metadata.input, - ...defaultState - } - } - return null - } - - render() { - const { metadata, countryCode, network } = this.props - const { data, minimized, fetching } = this.state - const dataColorMap = { - total_count: colorNormal, - confirmed_count: colorConfirmed, - anomaly_count: colorAnomaly, - failure_count: colorError, - empty: colorEmpty - } - - if (fetching) { - return ( - - ) - } - - const until = dayjs.utc().add(1, 'day').format('YYYY-MM-DD') - const since30days = dayjs.utc().subtract(30, 'days').format('YYYY-MM-DD') - - const yMax = data.reduce((max, item) => ( - (item.total_count > max) ? item.total_count : max - ), 0) - - const domainToExplore = new URL(metadata.input).hostname - - return ( - - - - - - - - - {/* TODO: Show percentages - - - - - - */} - - - { - data && - - } - > - {}} - /> - yMax} - style={{ - data: { - fill: dataColorMap.empty - } - }} - /> - - { - let s = `${new Date(d.test_day).toLocaleDateString()}` - if (d.confirmed_count > 0) { - s += `\n${d.confirmed_count} Confirmed` - } - if (d.anomaly_count > 0) { - s += `\n${d.anomaly_count} Anomalies` - } - if (d.failure_count > 0) { - s += `\n${d.failure_count} Failures` - } - s += `\n${d.total_count} Total` - return s - }} - labelComponent={} - data={data} - x='test_day' - y={(d) => (d.total_count - d.confirmed_count - d.anomaly_count - d.failure_count)} - style={{ - data: { - fill: dataColorMap.total_count, - } - }} - /> - { - ['confirmed_count', 'anomaly_count', 'failure_count'].map((type, index) => ( - - )) - } - - - } - - - - {/* - - - - */} - - ) - } -} - -export default URLChart diff --git a/components/country/WebsiteChartLoader.js b/components/country/WebsiteChartLoader.js deleted file mode 100644 index 6022d15fd..000000000 --- a/components/country/WebsiteChartLoader.js +++ /dev/null @@ -1,87 +0,0 @@ -import React from 'react' -import PropTypes from 'prop-types' -import ContentLoader from 'react-content-loader' -import { theme } from 'ooni-components' - -export const WebsiteChartLoader = (props) => { - const random = Math.floor(Math.random() * (20 - 14) + 14) - return ( - - - {Array(random).fill('').map((e, i) => ( - - )) - } - - ) -} - -export const WebsiteSectionLoader = ({ rows = 5 }) => ( - <> - {Array(rows) - .fill('') - .map((e, i) => ( - - )) - } - -) - -WebsiteSectionLoader.propTypes = { - rows: PropTypes.number -} - -export const AppsChartLoader = ({xOffset = 50, barWidth = 10, barHeight = 30, ...props}) => { - const random = Math.floor(Math.random() * (20 - 16) + 16) - return ( - - {Array(random).fill('').map((e, i) => ( - - )) - } - - ) -} - -export const AppSectionLoader = ({ rows = 1 }) => { - const Row = ({ y }) => ( - [ - , - , - , - - ] - ) - return ( - - - {Array(rows).fill('').map((e, i) => ( - - ))} - - ) -} - -AppSectionLoader.propTypes = { - rows: PropTypes.number -} diff --git a/components/country/Websites.js b/components/country/Websites.js index 51e65351e..363618007 100644 --- a/components/country/Websites.js +++ b/components/country/Websites.js @@ -1,88 +1,47 @@ -import React from 'react' -import { inCountry } from './CountryContext' -import { FormattedMessage } from 'react-intl' -import axios from 'axios' -import { Flex, Box, Heading, Text, Input } from 'ooni-components' - +import React, {useCallback, useMemo} from 'react' +import { useIntl, FormattedMessage } from 'react-intl' +import { Box, Text } from 'ooni-components' +import ChartCountry from 'components/Chart' import SectionHeader from './SectionHeader' import { SimpleBox } from './boxes' -// import PeriodFilter from './PeriodFilter' -import TestsByCategoryInNetwork from './WebsitesCharts' import FormattedMarkdown from '../FormattedMarkdown' +import ConfirmedBlockedCategory from './ConfirmedBlockedCategory' +import { useRouter } from 'next/router' -class WebsitesSection extends React.Component { - constructor(props) { - super(props) - this.state = { - noData: false, - selectedNetwork: null, - networks: [] - } - this.onNetworkChange = this.onNetworkChange.bind(this) - } - - onNetworkChange(asn) { - this.setState({ - selectedNetwork: Number(asn) - }) - } - - async componentDidMount() { - const { countryCode } = this.props - const client = axios.create({baseURL: process.env.NEXT_PUBLIC_OONI_API}) // eslint-disable-line - const result = await client.get('/api/_/website_networks', { - params: { - probe_cc: countryCode - } - }) - if (result.data.results.length > 0) { - this.setState({ - networks: result.data.results, - selectedNetwork: Number(result.data.results[0].probe_asn) - }) - } else { - this.setState({ - noData: true, - networks: null - }) - } - } +const WebsitesSection = ({ countryCode }) => { + const router = useRouter() + const { query: { since, until } } = router - render () { - const { onPeriodChange, countryCode } = this.props - const { noData, selectedNetwork } = this.state - return ( - <> - - - - - - {/* - - */} - - - - - + const query = useMemo(() => ({ + axis_y: 'domain', + axis_x: 'measurement_start_day', + probe_cc: countryCode, + since, + until, + test_name: 'web_connectivity', + }), [countryCode, since, until]) - - {noData && - - - - } - - - - ) - } + return ( + <> + + + + + + + + + + + + + + + + ) } -export default inCountry(WebsitesSection) +export default WebsitesSection \ No newline at end of file diff --git a/components/country/WebsitesCharts.js b/components/country/WebsitesCharts.js deleted file mode 100644 index 005da24e0..000000000 --- a/components/country/WebsitesCharts.js +++ /dev/null @@ -1,165 +0,0 @@ -import React from 'react' -import PropTypes from 'prop-types' -import { FormattedMessage } from 'react-intl' -import { Flex, Box, Heading, Text, Link } from 'ooni-components' -import axios from 'axios' -import URLChart from './URLChart' -import ASNSelector from './ASNSelector' -import { WebsiteSectionLoader, WebsiteChartLoader } from './WebsiteChartLoader' - -const defaultState = { - resultsPerPage: 5, - testedUrlsCount: 0, - testedUrls: null, - fetching: true -} - -class TestsByCategoryInNetwork extends React.Component { - constructor(props) { - super(props) - this.state = defaultState - } - - // This is dead code now. Made so to ensure the loader - // is rendered even when is not ready to render because - // list of networks is still being fetched by the parent component. - // This prevents the jump in the layout. - // - // componentDidMount() { - // if (this.props.network !== null) { - // this.fetchUrlsInNetwork() - // } - // } - - componentDidUpdate(prevProps) { - if (prevProps.networks !== this.props.networks && this.props.networks === null) { - this.setState({ - fetching: false - }) - } else if (this.state.testedUrls === null && this.props.network !== null) { - this.fetchUrlsInNetwork() - } - } - - async fetchUrlsInNetwork() { - const { network, countryCode } = this.props - const { resultsPerPage, currentPage } = this.state - const client = axios.create({baseURL: process.env.NEXT_PUBLIC_OONI_API}) // eslint-disable-line - const result = await client.get('/api/_/website_urls', { - params: { - probe_cc: countryCode, - probe_asn: network, - limit: resultsPerPage, - offset: (currentPage > 0 ? (currentPage - 1) : 0) * resultsPerPage - } - }) - this.setState({ - testedUrlsCount: result.data.metadata.total_count, - testedUrls: result.data.results, - currentPage: result.data.metadata.current_page, - fetching: false - }) - } - - prevPage() { - this.setState((state) => ({ - currentPage: state.currentPage - 1 - })) - } - - nextPage() { - this.setState((state) => ({ - currentPage: state.currentPage + 1 - })) - } - - static getDerivedStateFromProps(props, state) { - if (props.network !== state.prevNetwork) { - return { - prevNetwork: props.network, - currentPage: 1, - prevPage: 1, - ...defaultState - } - } - if (state.currentPage !== state.prevPage) { - return { - prevPage: state.currentPage || 1, - ...defaultState - } - } - return null - } - - render() { - const { network, countryCode, networks, onNetworkChange } = this.props - const { testedUrlsCount, testedUrls, currentPage, resultsPerPage, fetching } = this.state - - const renderLoader = () => ( - - ) - - return ( - <> - {/* */} - {/* {'AS'+network} - }} - /> */} - {/* Category Selection */} - - {(network !== null && networks !== null) ? - <> - - - - - {testedUrlsCount} - - - : - - } - {/* Results per page dropdown - - - - */} - - {/* Hide until API is available - - {(msg) => ( - - )} - - */} - {/* URL-wise barcharts Start */} - {fetching && renderLoader()} - {(!fetching && testedUrls && testedUrls.length === 0) && - - - - - - } - {(!fetching && testedUrls && testedUrls.length > 0) && - testedUrls.map((testedUrl, index) => ( - - ))} - {(!fetching && testedUrlsCount > 0) && - {e.preventDefault(); this.prevPage()}}>{'< '} - {currentPage} of { Math.ceil(testedUrlsCount / resultsPerPage)} pages - {e.preventDefault(); this.nextPage()}}>{' >'} - } - {/* URL-wise barcharts End */} - - ) - } -} - -export default TestsByCategoryInNetwork diff --git a/components/network/Form.js b/components/network/Form.js index 2db164899..aa5c2323c 100644 --- a/components/network/Form.js +++ b/components/network/Form.js @@ -16,7 +16,7 @@ const defaultDefaultValues = { until: tomorrow, } -const Form = ({ onChange, query }) => { +const Form = ({ onSubmit, query }) => { const intl = useIntl() const query2formValues = (query) => { @@ -49,11 +49,11 @@ const Form = ({ onChange, query }) => { const submit = (e) => { e.preventDefault() - onChange({since, until}) + onSubmit({since, until}) } return ( - + diff --git a/cypress/support/e2e.js b/cypress/support/e2e.js index d68db96df..5fae3fc7b 100644 --- a/cypress/support/e2e.js +++ b/cypress/support/e2e.js @@ -18,3 +18,13 @@ import './commands' // Alternatively you can use CommonJS syntax: // require('./commands') + +Cypress.on('uncaught:exception', (err, runnable) => { + // we expect a 3rd party library error with message 'ResizeObserver loop limit exceeded' + // and don't want to fail the test so we return false + if (err.message.includes('ResizeObserver loop limit exceeded')) { + return false + } + // we still want to ensure there are no other unexpected + // errors, so we let them fail the test +}) diff --git a/pages/country/[countryCode].js b/pages/country/[countryCode].js index c569c4c04..b6a77d7b6 100644 --- a/pages/country/[countryCode].js +++ b/pages/country/[countryCode].js @@ -1,25 +1,28 @@ /* global process */ -import React, { useCallback, useState } from 'react' +import React, { useCallback, useState, useEffect } from 'react' import axios from 'axios' +import { useRouter } from 'next/router' import { Container, Heading, Flex, Box } from 'ooni-components' import styled from 'styled-components' +import { useIntl } from 'react-intl' import { StickyContainer, Sticky } from 'react-sticky' import { getLocalisedRegionName } from '../../utils/i18nCountries' +import dayjs from 'services/dayjs' -import NavBar from '../../components/NavBar' -import Flag from '../../components/Flag' -import PageNavMenu from '../../components/country/PageNavMenu' -import Overview from '../../components/country/Overview' -import WebsitesSection from '../../components/country/Websites' -import AppsSection from '../../components/country/Apps' -// import NetworkPropertiesSection from '../../components/country/NetworkProperties' -import { CountryContextProvider } from '../../components/country/CountryContext' -import CountryHead from '../../components/country/CountryHead' -import { useIntl } from 'react-intl' +import Form from 'components/network/Form' +import NavBar from 'components/NavBar' +import Flag from 'components/Flag' +import Layout from 'components/Layout' +import PageNavMenu from 'components/country/PageNavMenu' +import Overview from 'components/country/Overview' +import WebsitesSection from 'components/country/Websites' +import AppsSection from 'components/country/Apps' +import { CountryContextProvider } from 'components/country/CountryContext' +import CountryHead from 'components/country/CountryHead' const getCountryReports = (countryCode, data) => { const reports = data.filter((article) => ( @@ -77,14 +80,30 @@ export async function getServerSideProps ({ res, query }) { } } - const Country = ({ countryCode, overviewStats, reports, ...coverageDataSSR }) => { const intl = useIntl() - const [newData, setNewData] = useState(false) const countryName = getLocalisedRegionName(countryCode, intl.locale) + const [newData, setNewData] = useState(false) + const router = useRouter() + const query = router.query + + useEffect(() => { + if (Object.keys(query).length === 1) { + const today = dayjs.utc().add(1, 'day') + const monthAgo = dayjs.utc(today).subtract(1, 'month') + const href = { + pathname: router.pathname, + query: { + since: monthAgo.format('YYYY-MM-DD'), + until: today.format('YYYY-MM-DD'), + countryCode + }, + } + router.replace(href, undefined, { shallow: true }) + } + }, []) const fetchTestCoverageData = useCallback((testGroupList) => { - console.log(testGroupList) const fetcher = async (testGroupList) => { let client = axios.create({baseURL: process.env.NEXT_PUBLIC_OONI_API}) // eslint-disable-line const result = await client.get('/api/_/test_coverage', { @@ -100,9 +119,29 @@ const Country = ({ countryCode, overviewStats, reports, ...coverageDataSSR }) => }) } fetcher(testGroupList) - + }, [countryCode, setNewData]) + // Sync page URL params with changes from form values + const onSubmit = ({ since, until }) => { + const params = { + since, + until, + } + + const href = { + pathname: router.pathname.replace('[countryCode]', countryCode), + query: params, + } + + if (query.since !== since + || query.until !== until + ) { + router.push(href, href, { shallow: true }) + } + + } + const { testCoverage, networkCoverage } = newData !== false ? newData : coverageDataSSR return ( @@ -153,7 +192,8 @@ const Country = ({ countryCode, overviewStats, reports, ...coverageDataSSR }) => fetchTestCoverageData={fetchTestCoverageData} featuredArticles={reports} /> - + + @@ -164,4 +204,4 @@ const Country = ({ countryCode, overviewStats, reports, ...coverageDataSSR }) => ) } -export default Country \ No newline at end of file +export default Country diff --git a/pages/network/[asn].js b/pages/network/[asn].js index 9c2619936..81fb67c53 100644 --- a/pages/network/[asn].js +++ b/pages/network/[asn].js @@ -1,26 +1,21 @@ -import React, { useCallback, useEffect } from 'react' +import React, { useCallback, useEffect, useMemo } from 'react' import { useRouter } from 'next/router' import axios from 'axios' -import { Container, Heading, Box, Flex, Text, Link } from 'ooni-components' +import { Container, Heading, Box, Text, Link } from 'ooni-components' import { useIntl } from 'react-intl' import NLink from 'next/link' -import styled from 'styled-components' import dayjs from 'services/dayjs' import Layout from 'components/Layout' import NavBar from 'components/NavBar' import { MetaTags } from 'components/dashboard/MetaTags' import Form from 'components/network/Form' -import Chart from 'components/network/Chart' +import Chart from 'components/Chart' import Calendar from 'components/network/Calendar' import FormattedMarkdown from 'components/FormattedMarkdown' import { FormattedMessage } from 'react-intl' import CallToActionBox from 'components/CallToActionBox' import { getLocalisedRegionName } from '../../utils/i18nCountries' -const Bold = styled.span` - font-weight: bold -` - const prepareDataForCalendar = (data) => { return data.map((r) => ({ value: r.measurement_count, @@ -34,18 +29,46 @@ const circumventionTestNames = ['psiphon', 'tor', 'torsf'] const ChartsContainer = () => { const intl = useIntl() + const router = useRouter() + const { query: { since, until, asn } } = router + + const queryWebsites = useMemo(() => ({ + axis_y: 'domain', + axis_x: 'measurement_start_day', + asn, + since, + until, + test_name: 'web_connectivity', + }), [asn, since, until]) + + const queryMessagingApps = useMemo(() => ({ + axis_x: 'measurement_start_day', + asn, + since, + until, + }), [asn, since, until]) + + const queryCircumventionTools = useMemo(() => ({ + axis_x: 'measurement_start_day', + asn, + since, + until, + }), [asn, since, until]) + return ( - <> + <> + queryParams={queryWebsites} /> + title={intl.formatMessage({id: 'Tests.Groups.Instant Messagging.Name'})} + queryParams={queryMessagingApps} /> + title={intl.formatMessage({id: 'Tests.Groups.Circumvention.Name'})} + queryParams={queryCircumventionTools} /> ) } @@ -89,7 +112,7 @@ const NetworkDashboard = ({asn, calendarData = [], measurementsTotal, countriesD const displayASN = asn.replace('AS', '') useEffect(() => { - if (Object.keys(query).length < 3) { + if (Object.keys(query).length < 3) { const today = dayjs.utc().add(1, 'day') const monthAgo = dayjs.utc(today).subtract(1, 'month') const href = { @@ -104,7 +127,7 @@ const NetworkDashboard = ({asn, calendarData = [], measurementsTotal, countriesD }, []) // Sync page URL params with changes from form values - const onChange = useCallback(({ since, until }) => { + const onSubmit = ({ since, until }) => { // since: "2022-01-02", // until: "2022-02-01", const params = { @@ -115,7 +138,7 @@ const NetworkDashboard = ({asn, calendarData = [], measurementsTotal, countriesD if (query.since !== since || query.until !== until) { router.push({ query: params }, undefined, { shallow: true }) } - }, [router, query, asn]) + } return ( <> @@ -125,17 +148,17 @@ const NetworkDashboard = ({asn, calendarData = [], measurementsTotal, countriesD AS{displayASN} {router.isReady && <> - {!!calendarData.length ? + {!!calendarData.length ? <> - + : - } - text={} + text={} /> } @@ -154,8 +177,8 @@ export const getServerSideProps = async (context) => { const measurementsTotal = await client .get(path, {params: {'probe_asn': asn}}) - .then((response)=> response?.data?.result.measurement_count) - + .then((response) => response?.data?.result.measurement_count) + const calendarData = await client.get(path, { params: { probe_asn: asn, since: dayjs.utc().subtract(10, 'year').format('YYYY-MM-DD'), @@ -168,8 +191,8 @@ export const getServerSideProps = async (context) => { axis_x: 'probe_cc' }}).then((response) => (response.data.result.map(res => ({country: res.probe_cc, measurements: res.measurement_count})))) - return { - props: { + return { + props: { asn, calendarData, measurementsTotal, @@ -186,4 +209,4 @@ export const getServerSideProps = async (context) => { } } -export default NetworkDashboard \ No newline at end of file +export default NetworkDashboard From 674fd8e53d7b84517b04ba4b21d63e7458f725e1 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sun, 8 Jan 2023 15:35:48 +0100 Subject: [PATCH 21/41] Bump json5 from 1.0.1 to 1.0.2 (#829) Bumps [json5](https://github.com/json5/json5) from 1.0.1 to 1.0.2. - [Release notes](https://github.com/json5/json5/releases) - [Changelog](https://github.com/json5/json5/blob/main/CHANGELOG.md) - [Commits](https://github.com/json5/json5/compare/v1.0.1...v1.0.2) --- updated-dependencies: - dependency-name: json5 dependency-type: indirect ... Signed-off-by: dependabot[bot] Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- yarn.lock | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/yarn.lock b/yarn.lock index f83d4465f..83c67e69f 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5606,9 +5606,9 @@ json-stringify-safe@~5.0.1: integrity sha1-Epai1Y/UXxmg9s4B1lcB4sc1tus= json5@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/json5/-/json5-1.0.1.tgz#779fb0018604fa854eacbf6252180d83543e3dbe" - integrity sha512-aKS4WQjPenRxiQsC93MNfjx+nbF4PAdYzmd/1JIj8HYzqfbu86beTuNgXDzPknWk0n0uARlyewZo4s++ES36Ow== + version "1.0.2" + resolved "https://registry.yarnpkg.com/json5/-/json5-1.0.2.tgz#63d98d60f21b313b77c4d6da18bfa69d80e1d593" + integrity sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA== dependencies: minimist "^1.2.0" @@ -5966,9 +5966,9 @@ minimatch@^5.0.1: brace-expansion "^2.0.1" minimist@1.2.0, minimist@^1.2.0, minimist@^1.2.5, minimist@^1.2.6: - version "1.2.6" - resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.6.tgz#8637a5b759ea0d6e98702cfb3a9283323c93af44" - integrity sha512-Jsjnk4bw3YJqYzbdyBiNsPWHPfO++UGG749Cxs6peCu5Xg4nrena6OVxOYxrQTqww0Jmwt+Ref8rggumkTLz9Q== + version "1.2.7" + resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.7.tgz#daa1c4d91f507390437c6a8bc01078e7000c4d18" + integrity sha512-bzfL1YUZsP41gmu/qjrEk0Q6i2ix/cVeAhbCbqH9u3zYutS1cLg00qhrD0M2MVdCcx4Sc0UpP2eBWo9rotpq6g== minipass@^2.6.0, minipass@^2.9.0: version "2.9.0" From 24b519f55c1106a5810039fac3c38c13cabefc13 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 1 Feb 2023 17:45:05 +0100 Subject: [PATCH 22/41] Bump ua-parser-js from 0.7.31 to 0.7.33 (#835) Bumps [ua-parser-js](https://github.com/faisalman/ua-parser-js) from 0.7.31 to 0.7.33. - [Release notes](https://github.com/faisalman/ua-parser-js/releases) - [Changelog](https://github.com/faisalman/ua-parser-js/blob/master/changelog.md) - [Commits](https://github.com/faisalman/ua-parser-js/compare/0.7.31...0.7.33) --- updated-dependencies: - dependency-name: ua-parser-js dependency-type: indirect ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- yarn.lock | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/yarn.lock b/yarn.lock index 83c67e69f..dd83dc8fa 100644 --- a/yarn.lock +++ b/yarn.lock @@ -7783,9 +7783,9 @@ typescript@^4.5: integrity sha512-C0WQT0gezHuw6AdY1M2jxUO83Rjf0HP7Sk1DtXj6j1EwkQNZrHAg2XPWlq62oqEhYvONq5pkC2Y9oPljWToLmQ== ua-parser-js@^0.7.30: - version "0.7.31" - resolved "https://registry.yarnpkg.com/ua-parser-js/-/ua-parser-js-0.7.31.tgz#649a656b191dffab4f21d5e053e27ca17cbff5c6" - integrity sha512-qLK/Xe9E2uzmYI3qLeOmI0tEOt+TBBQyUIAh4aAgU05FVYzeZrKUdkAZfBNVGRaHVgV0TDkdEngJSw/SyQchkQ== + version "0.7.33" + resolved "https://registry.yarnpkg.com/ua-parser-js/-/ua-parser-js-0.7.33.tgz#1d04acb4ccef9293df6f70f2c3d22f3030d8b532" + integrity sha512-s8ax/CeZdK9R/56Sui0WM6y9OFREJarMRHqLB2EwkovemBxNQ+Bqu8GAsUnVcXKgphb++ghr/B2BZx4mahujPw== unbox-primitive@^1.0.1: version "1.0.1" From 05f908f665bd081854c2c2d3e1fc3a27e898f2d1 Mon Sep 17 00:00:00 2001 From: Simon Leinen Date: Wed, 1 Feb 2023 18:20:00 +0100 Subject: [PATCH 23/41] DNS Answers: Add ancillary information (#828) * Rename FiveColRow to DNSAnswerRow We may want to add some columns in the future. * DNSAnswerRow -> DnsAnswerRow For consistency with the pre-existing DnsAnswerCell. * DnsAnswerRow: Add ancillary information If the measurement JSON for a DNS answer contains "asn" and/or "as_org_name" fields, we use that to populate the new "Ancillary info" column in human-readable form. * DnsAnswerRow: Improve readability by varying column widths The TTL/class/type values are always short, so make the corresponding fields narrower to leave more space for the other fields, which can be longer. * Renamed "ancillary information" to "Answer IP Info" Also improve output in the (impossible?) case where "asn" is not set, but "as_org_name" is. * DnsAnswerIpInfo: New utility function Factored out from QueryContainer. * Rename DnsAnswerIpInfo to dnsAnswerIpInfo Normal functions seem to use camelCase by convention. * Simplify dnsAnswerIpInfo() Use auxiliary variables to avoid nested conditionals. --- .../measurement/nettests/WebConnectivity.js | 30 ++++++++++++++----- 1 file changed, 22 insertions(+), 8 deletions(-) diff --git a/components/measurement/nettests/WebConnectivity.js b/components/measurement/nettests/WebConnectivity.js index 667002aa2..3b261dfbc 100644 --- a/components/measurement/nettests/WebConnectivity.js +++ b/components/measurement/nettests/WebConnectivity.js @@ -167,32 +167,45 @@ FailureString.propTypes = { failure: PropTypes.string } +const DnsNarrowAnswerCell = (props) => ( + {props.children} +) + const DnsAnswerCell = (props) => ( - {props.children} + {props.children} ) DnsAnswerCell.propTypes = { children: PropTypes.any } -const FiveColRow = ({ name = 'Name', netClass = 'Class', ttl = 'TTL', type = 'Type', data = 'DATA', header = false}) => ( +const dnsAnswerIpInfo = (dnsAnswer) => { + const asn = dnsAnswer.asn ? `AS${dnsAnswer.asn}` : 'Unknown AS' + const asOrgName = dnsAnswer.as_org_name ? `(${dnsAnswer.as_org_name})` : '' + + return `${asn} ${asOrgName}`.trim() +} + +const DnsAnswerRow = ({ name = 'Name', netClass = 'Class', ttl = 'TTL', type = 'Type', data = 'DATA', answer_ip_info = 'Answer IP Info', header = false}) => ( {name} - {netClass} - {ttl} - {type} + {netClass} + {ttl} + {type} {data} + {answer_ip_info} ) -FiveColRow.propTypes = { +DnsAnswerRow.propTypes = { name: PropTypes.string, netClass: PropTypes.string, ttl: PropTypes.number, type: PropTypes.string, data: PropTypes.string, + answer_ip_info: PropTypes.string, header: PropTypes.bool } @@ -228,9 +241,9 @@ const QueryContainer = ({query}) => { {failure && } {!failure && - + {Array.isArray(answers) && answers.map((dnsAnswer, index) => ( - { ? dnsAnswer.hostname : null // for any other answer_type, DATA column will be empty } + answer_ip_info={dnsAnswerIpInfo(dnsAnswer)} /> ))} From 44c93f13053088d90ff784d897806ffa07ad813e Mon Sep 17 00:00:00 2001 From: GermaVinsmoke Date: Thu, 9 Feb 2023 16:55:22 +0530 Subject: [PATCH 24/41] Search page filter values (#836) * passing correct query parameter to filter Signed-off-by: GermaVinsmoke * asn and failure params Signed-off-by: GermaVinsmoke --------- Signed-off-by: GermaVinsmoke --- pages/search.js | 28 ++++++---------------------- 1 file changed, 6 insertions(+), 22 deletions(-) diff --git a/pages/search.js b/pages/search.js index 172fb2a89..cd5846105 100644 --- a/pages/search.js +++ b/pages/search.js @@ -70,7 +70,7 @@ const queryToParams = ({ query }) => { let params = {}, show = 50 const supportedParams = ['probe_cc', 'domain', 'input','category_code', 'probe_asn', 'test_name', 'since', 'until', 'failure'] - + if (query.show) { show = parseInt(query.show) } @@ -102,22 +102,6 @@ const getMeasurements = (query) => { return client.get('/api/v1/measurements', {params}) } -// Handle circular structures when stringifying error responses -// From: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Errors/Cyclic_object_value#Examples -const getCircularReplacer = () => { - const seen = new WeakSet() - return (key, value) => { - if (typeof value === 'object' && value !== null) { - if (seen.has(value)) { - return - } - seen.add(value) - } - return value - } -} - - const serializeError = (err) => { const { name, message, stack, config = {} } = err.toJSON() const { baseURL, url, params } = config @@ -302,14 +286,14 @@ const Search = ({testNames, testNamesKeyed, countries, query: queryProp }) => { Date: Fri, 10 Feb 2023 10:09:06 +0100 Subject: [PATCH 25/41] Update coverage chart color scale and add a legend (#823) * Update coverage chart color scale and add a legend * Update ranges --- components/network/Calendar.js | 72 +++++++++++++++++++++++----------- 1 file changed, 50 insertions(+), 22 deletions(-) diff --git a/components/network/Calendar.js b/components/network/Calendar.js index 27ff7d17c..f9c69441a 100644 --- a/components/network/Calendar.js +++ b/components/network/Calendar.js @@ -1,7 +1,7 @@ import React, { useState } from 'react' import { ResponsiveCalendar } from '@nivo/calendar' import styled from 'styled-components' -import { Flex, theme } from 'ooni-components' +import { Flex, Box, theme } from 'ooni-components' import { getRange } from 'utils' const StyledCalendar = styled.div` @@ -9,15 +9,24 @@ height: 180px; margin-bottom: 10px; margin-top: 40px; ` -const colors = theme.colors +const { colors } = theme +const chartColors = [colors.blue2, colors.blue4, colors.blue5, colors.blue7] + const findColor = number => { - if (number === 0) return colors.gray4 - if (number <= 10) return colors.orange3 - if (number <= 100) return colors.green2 - if (number <= 1000) return colors.green5 - return colors.green8 + if (number === 0) return colors.gray1 + if (number <= 50) return chartColors[0] + if (number <= 500) return chartColors[1] + if (number <= 5000) return chartColors[2] + return chartColors[3] } +const colorLegend = [ + {color: chartColors[0], range: '1-50'}, + {color: chartColors[1], range: '51-100'}, + {color: chartColors[2], range: '501-5000'}, + {color: chartColors[3], range: '>5000'}, +] + const dateRange = (startDate, endDate) => { if (!startDate || !endDate) return const start = new Date(new Date(startDate.getFullYear(), 0, 0, 0).setUTCHours(0, 0, 0, 0)) @@ -61,21 +70,40 @@ const Calendar = React.memo(function Calendar({asn, data}) { dayBorderColor="#ffffff" /> - - {yearsOptions.map(year => ( - setSelectedYear(year)} - > - {year} - - ))} + + + {colorLegend.map(item => ( + + + {item.range} + + ))} + + + {yearsOptions.map(year => ( + setSelectedYear(year)} + > + {year} + + ))} + ) From daee5f6ebddddf13b88da60f955019b3b2bc9491 Mon Sep 17 00:00:00 2001 From: Maja Komel Date: Mon, 13 Feb 2023 11:19:49 +0100 Subject: [PATCH 26/41] Update copy --- public/static/lang/en.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/public/static/lang/en.json b/public/static/lang/en.json index 651a5b61b..43dab8759 100644 --- a/public/static/lang/en.json +++ b/public/static/lang/en.json @@ -382,8 +382,8 @@ "Search.Sidebar.Domain.Placeholder": "e.g. twitter.com or 1.1.1.1", "Search.Sidebar.Domain.Error": "Please enter a valid domain name or IP address, such as twitter.com or 1.1.1.1", "Search.Sidebar.Input": "Input", - "Search.Sidebar.Input.Placeholder": "e.g., https://fbcdn.net/robots.txt", - "Search.Sidebar.Input.Error": "Please enter full URL or IP address, such as https://fbcdn.net/robots.txt", + "Search.Sidebar.Input.Placeholder": "e.g., https://twitter.com/", + "Search.Sidebar.Input.Error": "Please enter a full URL (e.g. https://twitter.com/) with a trailing slash (`/`) or an IP address", "Search.Sidebar.Categories": "Website Categories", "Search.Sidebar.Categories.All": "Any", "Search.Sidebar.Status": "Status", From 8d039a8a45ef954a7af4e7dd72e0e1f5d7e3cf8b Mon Sep 17 00:00:00 2001 From: Maja Komel Date: Wed, 15 Feb 2023 09:49:32 +0100 Subject: [PATCH 27/41] Update inputs when navigating back circumvention page (#837) * Update inputs when navigating back circumvention page * Fixed time grain --- components/dashboard/Charts.js | 5 ++-- components/dashboard/Form.js | 50 ++++++++++++++++++++-------------- components/network/Form.js | 12 ++------ package.json | 2 +- yarn.lock | 8 +++--- 5 files changed, 41 insertions(+), 36 deletions(-) diff --git a/components/dashboard/Charts.js b/components/dashboard/Charts.js index 3ba6faa9b..46978ca39 100644 --- a/components/dashboard/Charts.js +++ b/components/dashboard/Charts.js @@ -55,7 +55,8 @@ const Chart = React.memo(function Chart({ testName }) { ...fixedQuery, test_name: testName, since: since, - until: until + until: until, + time_grain: 'day' }), [since, testName, until]) const apiQuery = useMemo(() => { @@ -64,7 +65,7 @@ const Chart = React.memo(function Chart({ testName }) { }, [query]) const { data, error } = useSWR( - apiQuery, + () => since && until ? apiQuery : null, fetcher, swrOptions ) diff --git a/components/dashboard/Form.js b/components/dashboard/Form.js index 3521f22e8..6657ee7c0 100644 --- a/components/dashboard/Form.js +++ b/components/dashboard/Form.js @@ -30,14 +30,14 @@ export const Form = ({ onChange, query, availableCountries }) => { .sort((a, b) => (new Intl.Collator(intl.locale).compare(a.label, b.label))) , [availableCountries, intl]) - const query2formValues = (query) => { + const query2formValues = useMemo(() => { const countriesInQuery = query.probe_cc?.split(',') ?? defaultDefaultValues.probe_cc return { since: query?.since ?? defaultDefaultValues.since, until: query?.until ?? defaultDefaultValues.until, probe_cc: countryOptions.filter(country => countriesInQuery.includes(country.value)), } - } + }, [countryOptions, query]) const multiSelectStrings = useMemo(() => ({ 'allItemsAreSelected': intl.formatMessage({ id: 'ReachabilityDash.Form.Label.CountrySelect.AllSelected' }), @@ -51,10 +51,23 @@ export const Form = ({ onChange, query, availableCountries }) => { // 'create': 'Create', }), [intl]) - const { control, getValues, watch, setValue } = useForm({ - defaultValues: query2formValues(query) + const { control, getValues, watch, setValue, reset } = useForm({ + defaultValues: query2formValues }) + useEffect(()=> { + reset(query2formValues) + }, [query2formValues, reset]) + + const cleanedUpData = (values) => { + const { since, until, probe_cc } = values + return { + since, + until, + probe_cc: probe_cc.length > 0 ? probe_cc.map(d => d.value).join(',') : undefined + } + } + const [showDatePicker, setShowDatePicker] = useState(false) const handleRangeSelect = (range) => { if (range?.from) { @@ -68,19 +81,15 @@ export const Form = ({ onChange, query, availableCountries }) => { setValue('until', '') } setShowDatePicker(false) + onChange(cleanedUpData(getValues())) } - const {since, until, probe_cc} = watch() - - const submit = (e) => { - e.preventDefault() - const cleanedUpData = { - since, - until, - probe_cc: probe_cc.length > 0 ? probe_cc.map(d => d.value).join(',') : undefined - } - onChange(cleanedUpData) - } + useEffect(() => { + const subscription = watch((value, { name, type }) => { + if (name === 'probe_cc' && type === 'change') onChange(cleanedUpData(getValues())) + }) + return () => subscription.unsubscribe() + }, [watch, getValues]) return ( @@ -113,6 +122,9 @@ export const Form = ({ onChange, query, availableCountries }) => { {...field} onFocus={() => setShowDatePicker(true)} onKeyDown={() => setShowDatePicker(false)} + name={field.name} + value={field.value} + onChange={field.onChange} /> )} /> @@ -127,15 +139,13 @@ export const Form = ({ onChange, query, availableCountries }) => { {...field} onFocus={() => setShowDatePicker(true)} onKeyDown={() => setShowDatePicker(false)} + name={field.name} + value={field.value} + onChange={field.onChange} /> )} /> - - - - - { showDatePicker && { setShowDatePicker(false) } - const submit = (e) => { - e.preventDefault() + useEffect(() => { onSubmit({since, until}) - } + }, [onSubmit, since, until]) return ( - + @@ -85,11 +84,6 @@ const Form = ({ onSubmit, query }) => { )} /> - - - - - { showDatePicker && Date: Wed, 15 Feb 2023 10:06:06 +0100 Subject: [PATCH 28/41] Add support for selecting time granularity on MAT (#818) * Add support for selecting time granularity on MAT * Fix week granularity scale * Hour formatting, conditional dropdown options * Fix missing messages --- components/Chart.js | 10 ++-- components/aggregation/mat/Form.js | 56 +++++++++++++++++++- components/aggregation/mat/RowChart.js | 33 +++++++----- components/aggregation/mat/computations.js | 27 ++++++++-- components/aggregation/mat/timeScaleXAxis.js | 28 +++++++++- pages/chart/mat.js | 5 +- public/static/lang/en.json | 5 ++ 7 files changed, 138 insertions(+), 26 deletions(-) diff --git a/components/Chart.js b/components/Chart.js index 847f781cf..d78f1aebf 100644 --- a/components/Chart.js +++ b/components/Chart.js @@ -22,10 +22,12 @@ const Chart = React.memo(function Chart({testName, testGroup = null, title, quer }, [queryParams]) const { data, error } = useSWR( - testGroup ? { query: apiQuery, - testNames: testGroup.tests, - groupKey: name - } : apiQuery, + testGroup ? + { query: apiQuery, + testNames: testGroup.tests, + groupKey: name + } : + apiQuery, testGroup ? MATMultipleFetcher : MATFetcher, swrOptions ) diff --git a/components/aggregation/mat/Form.js b/components/aggregation/mat/Form.js index 9f7b0202d..be43b004c 100644 --- a/components/aggregation/mat/Form.js +++ b/components/aggregation/mat/Form.js @@ -51,6 +51,22 @@ const messages = defineMessages({ id: 'MAT.Form.Label.AxisOption.probe_asn', defaultMessage: '' }, + 'hour': { + id: 'MAT.Form.TimeGrainOption.hour', + defaultMessage: '' + }, + 'day': { + id: 'MAT.Form.TimeGrainOption.day', + defaultMessage: '' + }, + 'week': { + id: 'MAT.Form.TimeGrainOption.week', + defaultMessage: '' + }, + 'month': { + id: 'MAT.Form.TimeGrainOption.month', + defaultMessage: '' + } }) @@ -103,7 +119,8 @@ const defaultDefaultValues = { since: lastMonthToday, until: tomorrow, axis_x: 'measurement_start_day', - axis_y: '' + axis_y: '', + time_grain: 'day' } export const Form = ({ onSubmit, testNames, query }) => { @@ -185,6 +202,26 @@ export const Form = ({ onSubmit, testNames, query }) => { if (!yAxisOptionsFiltered.includes(getValues('axis_y'))) setValue('axis_y', '') }, [setValue, getValues, yAxisOptionsFiltered]) + const since = watch('since') + const until = watch('until') + const timeGrainOptions = useMemo(() => { + if (!since || !until) return ['hour', 'day', 'week', 'month'] + const diff = dayjs(until).diff(dayjs(since), 'day') + if (diff < 8) { + const availableValues = ['hour', 'day'] + if (!availableValues.includes(getValues('time_grain'))) setValue('time_grain', 'hour') + return availableValues + } else if (diff >= 8 && diff < 31) { + const availableValues = ['day', 'week'] + if (!availableValues.includes(getValues('time_grain'))) setValue('time_grain', 'day') + return availableValues + } else if (diff >= 31 ) { + const availableValues = ['day', 'week', 'month'] + if (!availableValues.includes(getValues('time_grain'))) setValue('time_grain', 'day') + return availableValues + } + }, [setValue, getValues, since, until]) + return ( @@ -265,6 +302,22 @@ export const Form = ({ onSubmit, testNames, query }) => { /> } + + + + + ( + + )} + /> + @@ -390,5 +443,6 @@ Form.propTypes = { input: PropTypes.string, probe_cc: PropTypes.string, category_code: PropTypes.string, + time_grain: PropTypes.string, }) } diff --git a/components/aggregation/mat/RowChart.js b/components/aggregation/mat/RowChart.js index 4a808fff8..bf6cfb3d9 100644 --- a/components/aggregation/mat/RowChart.js +++ b/components/aggregation/mat/RowChart.js @@ -28,7 +28,18 @@ const colorFunc = (d) => colorMap[d.id] || '#ccc' const barLayers = ['grid', 'axes', 'bars'] export const chartMargins = { top: 4, right: 50, bottom: 4, left: 0 } -const chartProps1D = { +const formatXAxisValues = (value, query, intl) => { + if (query.axis_x === 'measurement_start_day' && Date.parse(value)) { + if (query.time_grain === 'hour') { + const dateTime = new Date(value) + return new Intl.DateTimeFormat(intl.locale, { dateStyle: 'short', timeStyle: 'short', timeZone: 'UTC', hourCycle: 'h23' }).format(dateTime) + } + } else { + return value + } +} + +const chartProps1D = (query, intl) => ({ colors: colorFunc, indexScale: { type: 'band', @@ -49,7 +60,10 @@ const chartProps1D = { tickPadding: 5, tickRotation: 45, legendPosition: 'middle', - legendOffset: 60 + legendOffset: 70, + tickValues: getXAxisTicks(query), + legend: query.axis_x ? intl.formatMessage({id: `MAT.Form.Label.AxisOption.${query.axis_x}`, defaultMessage: '' }) : '', + format: (values) => formatXAxisValues(values, query, intl), }, axisLeft: { tickSize: 5, @@ -64,9 +78,9 @@ const chartProps1D = { animate: true, motionStiffness: 90, motionDamping: 15, -} +}) -const chartProps2D = { +const chartProps2D = (query) => ({ // NOTE: These dimensions are linked to accuracy of the custom axes rendered in // margin: chartMargins, @@ -98,7 +112,7 @@ const chartProps2D = { animate: false, isInteractive: true, layers: barLayers, -} +}) const RowChart = ({ data, indexBy, label, height, rowIndex /* width, first, last */}) => { const intl = useIntl() @@ -128,9 +142,6 @@ const RowChart = ({ data, indexBy, label, height, rowIndex /* width, first, last // react-spring from working on the actual data during // first render. This forces an update after 1ms with // real data, which appears quick enough with animation disabled - // const [chartData, setChartData] = useState([]) - // useEffect(() => { - // let animation = setTimeout(() => setChartData(data), 1) const [chartData, setChartData] = useState([]) useEffect(() => { let animation = setTimeout(() => setChartData(data), 1) @@ -140,11 +151,9 @@ const RowChart = ({ data, indexBy, label, height, rowIndex /* width, first, last } }, [data]) + const chartProps = useMemo(() => { - const xAxisTicks = getXAxisTicks(query) - chartProps1D.axisBottom.tickValues = xAxisTicks - chartProps1D.axisBottom.legend = query.axis_x ? intl.formatMessage({id: `MAT.Form.Label.AxisOption.${query.axis_x}`, defaultMessage: ''}) : '' - return label === undefined ? chartProps1D : chartProps2D + return label === undefined ? chartProps1D(query, intl) : chartProps2D(query) }, [intl, label, query]) return ( diff --git a/components/aggregation/mat/computations.js b/components/aggregation/mat/computations.js index 0e65736e4..f7596c4ce 100644 --- a/components/aggregation/mat/computations.js +++ b/components/aggregation/mat/computations.js @@ -1,14 +1,33 @@ import { getCategoryCodesMap } from '../../utils/categoryCodes' import { getLocalisedRegionName } from 'utils/i18nCountries' +import dayjs from 'services/dayjs' const categoryCodesMap = getCategoryCodesMap() -export function getDatesBetween(startDate, endDate) { +export function getDatesBetween(startDate, endDate, timeGrain) { const dateSet = new Set() var currentDate = startDate while (currentDate < endDate) { - dateSet.add(currentDate.toISOString().slice(0, 10)) - currentDate.setDate(currentDate.getDate() + 1) + if (timeGrain === 'hour') { + let startOfDay = dayjs(currentDate).utc().startOf('day') + const nextDay = startOfDay.add(1, 'day') + while (startOfDay.toDate() < nextDay.toDate()) { + dateSet.add(startOfDay.toISOString().split('.')[0] + 'Z') + startOfDay = startOfDay.utc().add(1, 'hours') + } + currentDate = dayjs(currentDate).utc().add(1, 'day') + } else if (timeGrain === 'month') { + const monthStart = dayjs(currentDate).utc().startOf('month') + dateSet.add(monthStart.toISOString().slice(0, 10)) + currentDate = monthStart.add(1, 'month').toDate() + } else if (timeGrain === 'week') { + const weekStart = dayjs(currentDate).utc().startOf('week') + dateSet.add(weekStart.toISOString().slice(0, 10)) + currentDate = weekStart.add(1, 'week').toDate() + } else if (timeGrain === 'day') { + dateSet.add(currentDate.toISOString().slice(0, 10)) + currentDate.setDate(currentDate.getDate() + 1) + } } return dateSet } @@ -20,7 +39,7 @@ export function fillRowHoles (data, query, locale) { switch(query.axis_x) { case 'measurement_start_day': - domain = getDatesBetween(new Date(query.since), new Date(query.until)) + domain = getDatesBetween(new Date(query.since), new Date(query.until), query.time_grain) break case 'category_code': domain = [...getCategoryCodesMap().keys()] diff --git a/components/aggregation/mat/timeScaleXAxis.js b/components/aggregation/mat/timeScaleXAxis.js index 52418e401..7c96f14ad 100644 --- a/components/aggregation/mat/timeScaleXAxis.js +++ b/components/aggregation/mat/timeScaleXAxis.js @@ -1,19 +1,43 @@ import { scaleUtc } from 'd3-scale' import { getDatesBetween } from './computations' +import dayjs from 'services/dayjs' const defaultCount = 20 +const getIntervalTicks = (data, count = defaultCount) => { + if (!(data && data.length)) return [] + + const start = data[0] + const end = data[data.length - 1] + const intervalType = 'week' + const intervalCount = Math.floor(dayjs(end).diff(start, intervalType)) + return data.reduce((accum, point, index) => { + const divisor = Math.ceil(intervalCount / count) + if (index % divisor === 0) + accum.push(point) + return accum + }, []) +} + + export function getXAxisTicks (query, count = defaultCount) { if (query.axis_x === 'measurement_start_day') { - const dateDomain = [...getDatesBetween(new Date(query.since), new Date(query.until))].map(d => new Date(d)) + const dateDomain = [...getDatesBetween(new Date(query.since), new Date(query.until), query.time_grain)].map(d => new Date(d)) const xScale = scaleUtc().domain([dateDomain[0], dateDomain[dateDomain.length-1]]) - const xAxisTickValues = dateDomain.length < 30 ? dateDomain : [ ...xScale.ticks(count), ] + + if (query.time_grain === 'hour') { + return Array.from(xAxisTickValues).map(d => d.toISOString().split('.')[0] + 'Z') + } else if (query.time_grain === 'week') { + return dateDomain.length < 30 ? + Array.from(dateDomain).map(d => d.toISOString().split('T')[0]) : + getIntervalTicks(dateDomain.map((d) => d.toISOString().split('T')[0]), count) + } return Array.from(xAxisTickValues).map(d => d.toISOString().split('T')[0]) } diff --git a/pages/chart/mat.js b/pages/chart/mat.js index d1ee11e99..c621b9c95 100644 --- a/pages/chart/mat.js +++ b/pages/chart/mat.js @@ -102,6 +102,7 @@ const MeasurementAggregationToolkit = ({ testNames }) => { axis_x: 'measurement_start_day', since: monthAgo.format('YYYY-MM-DD'), until: today.format('YYYY-MM-DD'), + time_grain: 'day', }, } router.replace(href, undefined, { shallow: true }) @@ -112,9 +113,7 @@ const MeasurementAggregationToolkit = ({ testNames }) => { }, []) const shouldFetchData = router.pathname !== router.asPath - // THIS IS TEMPORARY - in the next iteration users will be - // able to set time_grain themselves - const query = {...router.query, time_grain: 'day'} + const query = {...router.query} const { data, error, isValidating } = useSWR( () => shouldFetchData ? [query] : null, diff --git a/public/static/lang/en.json b/public/static/lang/en.json index 43dab8759..5a735eb72 100644 --- a/public/static/lang/en.json +++ b/public/static/lang/en.json @@ -464,12 +464,17 @@ "MAT.CSVData": "CSV Data", "MAT.Form.Label.XAxis": "Columns", "MAT.Form.Label.YAxis": "Rows", + "MAT.Form.Label.TimeGrain": "Time Granularity", "MAT.Form.Label.AxisOption.domain": "Domain", "MAT.Form.Label.AxisOption.input": "Input", "MAT.Form.Label.AxisOption.measurement_start_day": "Measurement Day", "MAT.Form.Label.AxisOption.probe_cc": "Countries", "MAT.Form.Label.AxisOption.category_code": "Website Categories", "MAT.Form.Label.AxisOption.probe_asn": "ASN", + "MAT.Form.TimeGrainOption.hour": "Hour", + "MAT.Form.TimeGrainOption.day": "Day", + "MAT.Form.TimeGrainOption.week": "Week", + "MAT.Form.TimeGrainOption.month": "Month", "MAT.Form.ConfirmationModal.Title": "Are you sure?", "MAT.Form.ConfirmationModal.Message": "Duration too long. This can potentially slow down the page", "MAT.Form.ConfirmationModal.No": "No", From 0541f87cfa1b892d261ea00ddbb0864b2c0c0164 Mon Sep 17 00:00:00 2001 From: Maja Komel Date: Wed, 15 Feb 2023 11:21:47 +0100 Subject: [PATCH 29/41] Default to day granularity for charts on thematic pages --- components/country/Apps.js | 2 ++ components/country/Websites.js | 1 + cypress/e2e/mat.e2e.cy.js | 2 +- pages/network/[asn].js | 3 +++ 4 files changed, 7 insertions(+), 1 deletion(-) diff --git a/components/country/Apps.js b/components/country/Apps.js index 04368d9c6..e5e5bfa79 100644 --- a/components/country/Apps.js +++ b/components/country/Apps.js @@ -21,6 +21,7 @@ const ChartsContainer = () => { probe_cc: countryCode, since, until, + time_grain: 'day', }), [countryCode, since, until]) const queryCircumventionTools = useMemo(() => ({ @@ -28,6 +29,7 @@ const ChartsContainer = () => { probe_cc: countryCode, since, until, + time_grain: 'day', }), [countryCode, since, until]) return ( diff --git a/components/country/Websites.js b/components/country/Websites.js index 363618007..a739809cb 100644 --- a/components/country/Websites.js +++ b/components/country/Websites.js @@ -19,6 +19,7 @@ const WebsitesSection = ({ countryCode }) => { since, until, test_name: 'web_connectivity', + time_grain: 'day', }), [countryCode, since, until]) return ( diff --git a/cypress/e2e/mat.e2e.cy.js b/cypress/e2e/mat.e2e.cy.js index 8c70b82bb..5656a6232 100644 --- a/cypress/e2e/mat.e2e.cy.js +++ b/cypress/e2e/mat.e2e.cy.js @@ -20,7 +20,7 @@ describe('MAT Tests', () => { describe('MAT Basics', () => { before(() => { - cy.visit('http://localhost:3100/chart/mat?test_name=web_connectivity&since=2022-03-01&until=2022-04-01&axis_x=measurement_start_day') + cy.visit('http://localhost:3100/chart/mat?test_name=web_connectivity&since=2022-03-01&until=2022-04-01&axis_x=measurement_start_day&time_grain=day') }) it('it loads', () => { diff --git a/pages/network/[asn].js b/pages/network/[asn].js index 81fb67c53..4d9697b1d 100644 --- a/pages/network/[asn].js +++ b/pages/network/[asn].js @@ -39,6 +39,7 @@ const ChartsContainer = () => { since, until, test_name: 'web_connectivity', + time_grain: 'day', }), [asn, since, until]) const queryMessagingApps = useMemo(() => ({ @@ -46,6 +47,7 @@ const ChartsContainer = () => { asn, since, until, + time_grain: 'day', }), [asn, since, until]) const queryCircumventionTools = useMemo(() => ({ @@ -53,6 +55,7 @@ const ChartsContainer = () => { asn, since, until, + time_grain: 'day', }), [asn, since, until]) return ( From 5db15aac1419d272ffe7d70374280706716cff2e Mon Sep 17 00:00:00 2001 From: Maja Komel Date: Wed, 15 Feb 2023 12:46:44 +0100 Subject: [PATCH 30/41] Fix mat chart if time grain is missing --- pages/chart/mat.js | 31 +++++++++++++++---------------- 1 file changed, 15 insertions(+), 16 deletions(-) diff --git a/pages/chart/mat.js b/pages/chart/mat.js index c621b9c95..c87e1f4ef 100644 --- a/pages/chart/mat.js +++ b/pages/chart/mat.js @@ -93,23 +93,22 @@ const MeasurementAggregationToolkit = ({ testNames }) => { // In that case, trigger a shallow navigation that shows a chart useEffect(() => { const { query } = router - if (Object.keys(query).length === 0) { - const today = dayjs.utc().add(1, 'day') - const monthAgo = dayjs.utc(today).subtract(1, 'month') - const href = { - query: { - test_name: 'web_connectivity', - axis_x: 'measurement_start_day', - since: monthAgo.format('YYYY-MM-DD'), - until: today.format('YYYY-MM-DD'), - time_grain: 'day', - }, - } - router.replace(href, undefined, { shallow: true }) + const today = dayjs.utc().add(1, 'day') + const monthAgo = dayjs.utc(today).subtract(1, 'month') + const href = { + query: { + test_name: 'web_connectivity', + axis_x: 'measurement_start_day', + since: monthAgo.format('YYYY-MM-DD'), + until: today.format('YYYY-MM-DD'), + time_grain: 'day', + ...query + }, } - // Ignore the dependency on `router` because we want - // this effect to run only once, on mount, if query is empty. - // eslint-disable-next-line react-hooks/exhaustive-deps + router.replace(href, undefined, { shallow: true }) + // Ignore the dependency on `router` because we want + // this effect to run only once, on mount, if query is empty. + // eslint-disable-next-line react-hooks/exhaustive-deps }, []) const shouldFetchData = router.pathname !== router.asPath From 54471a9e14e59174f4f750aabd0033e1deaa92eb Mon Sep 17 00:00:00 2001 From: Maja Komel Date: Wed, 15 Feb 2023 16:43:09 +0100 Subject: [PATCH 31/41] Add missing dependency --- components/aggregation/mat/computations.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/components/aggregation/mat/computations.js b/components/aggregation/mat/computations.js index f7596c4ce..9065be27d 100644 --- a/components/aggregation/mat/computations.js +++ b/components/aggregation/mat/computations.js @@ -1,5 +1,5 @@ import { getCategoryCodesMap } from '../../utils/categoryCodes' -import { getLocalisedRegionName } from 'utils/i18nCountries' +import { getLocalisedRegionName, localisedCountries } from 'utils/i18nCountries' import dayjs from 'services/dayjs' const categoryCodesMap = getCategoryCodesMap() From 7ca542d7a9ec446cde77a0952ac257beb564ed13 Mon Sep 17 00:00:00 2001 From: Maja Komel Date: Mon, 20 Feb 2023 14:27:41 +0100 Subject: [PATCH 32/41] Fix MAT subtitle for categories --- components/aggregation/mat/ChartHeader.js | 2 +- components/aggregation/mat/labels.js | 3 +-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/components/aggregation/mat/ChartHeader.js b/components/aggregation/mat/ChartHeader.js index 138bbfa76..9d28d3d4e 100644 --- a/components/aggregation/mat/ChartHeader.js +++ b/components/aggregation/mat/ChartHeader.js @@ -44,7 +44,7 @@ export const SubtitleStr = ({ query }) => { params.add(query.input) } if (query.category_code) { - params.add(getRowLabel(query.category_code, 'category_code')) + params.add(intl.formatMessage({id: `CategoryCode.${query.category_code}.Name`, defaultMessage: query.category_code})) } if (query.probe_asn) { params.add(query.probe_asn) diff --git a/components/aggregation/mat/labels.js b/components/aggregation/mat/labels.js index 48f480188..de1960245 100644 --- a/components/aggregation/mat/labels.js +++ b/components/aggregation/mat/labels.js @@ -4,7 +4,7 @@ import { Box } from 'ooni-components' import { testNames } from '../../test-info' import { getCategoryCodesMap } from '../../utils/categoryCodes' import { getLocalisedRegionName } from 'utils/i18nCountries' -import { FormattedMessage, useIntl } from 'react-intl' +import { FormattedMessage } from 'react-intl' const InputRowLabel = ({ input }) => { const truncatedInput = input @@ -33,7 +33,6 @@ const blockingTypeLabels = { } const CategoryLabel = ({ code }) => { - const intl = useIntl() return ( ) From 8ae9d1868f0fa9af495f32efd13adde3ea23eb98 Mon Sep 17 00:00:00 2001 From: Maja Komel Date: Mon, 20 Feb 2023 20:52:24 +0100 Subject: [PATCH 33/41] Return default value for getDatesBetween --- components/aggregation/mat/computations.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/components/aggregation/mat/computations.js b/components/aggregation/mat/computations.js index 9065be27d..1216caf9c 100644 --- a/components/aggregation/mat/computations.js +++ b/components/aggregation/mat/computations.js @@ -24,7 +24,7 @@ export function getDatesBetween(startDate, endDate, timeGrain) { const weekStart = dayjs(currentDate).utc().startOf('week') dateSet.add(weekStart.toISOString().slice(0, 10)) currentDate = weekStart.add(1, 'week').toDate() - } else if (timeGrain === 'day') { + } else { dateSet.add(currentDate.toISOString().slice(0, 10)) currentDate.setDate(currentDate.getDate() + 1) } From 4f40115e11e8a0c01ea89167c721a9637fe78b39 Mon Sep 17 00:00:00 2001 From: Maja Komel Date: Thu, 23 Feb 2023 23:26:27 +0100 Subject: [PATCH 34/41] Update document.js --- pages/_document.js | 37 ++++++++++++++++++------------------- 1 file changed, 18 insertions(+), 19 deletions(-) diff --git a/pages/_document.js b/pages/_document.js index a3615cc17..c645f3c12 100644 --- a/pages/_document.js +++ b/pages/_document.js @@ -1,27 +1,26 @@ -import React from 'react' -import Document, { Html, Head, Main, NextScript } from 'next/document' +// updated based on documentation: https://github.com/vercel/next.js/blob/canary/examples/with-styled-components/pages/_document.tsx +import Document, { DocumentContext, Html, Head, Main, NextScript } from 'next/document' import { ServerStyleSheet } from 'styled-components' export default class MyDocument extends Document { static async getInitialProps (ctx) { const sheet = new ServerStyleSheet() - const page = ctx.renderPage(App => props => sheet.collectStyles()) - const styleTags = sheet.getStyleElement() - const initialProps = await Document.getInitialProps(ctx) - return { ...page, styleTags, ...initialProps } - } + const originalRenderPage = ctx.renderPage + + try { + ctx.renderPage = () => + originalRenderPage({ + enhanceApp: (App) => (props) => + sheet.collectStyles(), + }) - render () { - return ( - - - {this.props.styleTags} - - -
    - - - - ) + const initialProps = await Document.getInitialProps(ctx) + return { + ...initialProps, + styles: [initialProps.styles, sheet.getStyleElement()], + } + } finally { + sheet.seal() + } } } From 131dc0b21b5393494d63eab5b573c5b3ae706581 Mon Sep 17 00:00:00 2001 From: Maja Komel Date: Thu, 23 Feb 2023 23:31:37 +0100 Subject: [PATCH 35/41] Remove redundant babel related packages --- package.json | 4 -- yarn.lock | 170 ++++++--------------------------------------------- 2 files changed, 18 insertions(+), 156 deletions(-) diff --git a/package.json b/package.json index 3478dfd39..8d6a199b9 100644 --- a/package.json +++ b/package.json @@ -6,7 +6,6 @@ "main": "index.js", "repository": "https://github.com/ooni/explorer", "dependencies": { - "@babel/core": "7.18.10", "@datapunt/matomo-tracker-react": "^0.5.1", "@fontsource/fira-sans": "^4.5.9", "@nivo/bar": "^0.80.0", @@ -17,8 +16,6 @@ "@rebass/forms": "^4.0.6", "@sentry/nextjs": "^7.11.1", "axios": "^0.27.2", - "babel-plugin-inline-react-svg": "2.0.1", - "babel-plugin-styled-components": "^1.10.7", "buffer-from": "^1.1.2", "country-util": "^0.2.0", "date-fns": "^2.29.1", @@ -68,7 +65,6 @@ "devDependencies": { "@svgr/webpack": "^6.5.0", "@welldone-software/why-did-you-render": "^7.0.1", - "babel-plugin-formatjs": "^10.3.25", "cypress": "^10.6.0", "eslint": "^8.22.0", "eslint-config-next": "^12.2.5", diff --git a/yarn.lock b/yarn.lock index 31a2fc892..1c6a4f84a 100644 --- a/yarn.lock +++ b/yarn.lock @@ -41,7 +41,7 @@ resolved "https://registry.yarnpkg.com/@babel/compat-data/-/compat-data-7.18.8.tgz#2483f565faca607b8535590e84e7de323f27764d" integrity sha512-HSmX4WZPPK3FUxYp7g2T6EyO8j96HlZJlxmKPSh6KAcqwyDrfx7hKjXpAW/0FhFfTJsR0Yt4lAjLI2coMptIHQ== -"@babel/core@7.18.10", "@babel/core@^7.10.4", "@babel/core@^7.13.16": +"@babel/core@^7.13.16": version "7.18.10" resolved "https://registry.yarnpkg.com/@babel/core/-/core-7.18.10.tgz#39ad504991d77f1f3da91be0b8b949a5bc466fb8" integrity sha512-JQM6k6ENcBFKVtWvLavlvi/mPcpYZ3+R+2EySDEMSMbp7Mn4FexlbbJVrx2R7Ijhr01T8gyqrOaABWIOgxeUyw== @@ -110,13 +110,6 @@ "@jridgewell/gen-mapping" "^0.3.2" jsesc "^2.5.1" -"@babel/helper-annotate-as-pure@^7.0.0": - version "7.0.0" - resolved "https://registry.yarnpkg.com/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.0.0.tgz#323d39dd0b50e10c7c06ca7d7638e6864d8c5c32" - integrity sha512-3UYcJUj9kvSLbLbUIfQTqzcy5VX7GRZ/CCDrnOaZorFFM01aXp1+GJwuFGV4NDDoAS+mOUyHcO6UD/RfqOks3Q== - dependencies: - "@babel/types" "^7.0.0" - "@babel/helper-annotate-as-pure@^7.16.0": version "7.16.7" resolved "https://registry.yarnpkg.com/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.16.7.tgz#bb2339a7534a9c128e3102024c60760a3a7f3862" @@ -483,21 +476,16 @@ chalk "^2.0.0" js-tokens "^4.0.0" -"@babel/parser@^7.0.0": - version "7.4.4" - resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.4.4.tgz#5977129431b8fe33471730d255ce8654ae1250b6" - integrity sha512-5pCS4mOsL+ANsFZGdvNLybx4wtqAZJ0MJjMHxvzI3bvIsz6sQvzW8XX92EYIkiPtIvcfG3Aj+Ir5VNyjnZhP7w== - -"@babel/parser@^7.1.0", "@babel/parser@^7.13.16", "@babel/parser@^7.18.10", "@babel/parser@^7.18.11": - version "7.18.11" - resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.18.11.tgz#68bb07ab3d380affa9a3f96728df07969645d2d9" - integrity sha512-9JKn5vN+hDt0Hdqn1PiJ2guflwP+B6Ga8qbDuoF0PzzVhrzsKIJo8yGqVk6CmMHiMei9w1C1Bp9IMJSIK+HPIQ== - "@babel/parser@^7.12.13", "@babel/parser@^7.13.0": version "7.13.12" resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.13.12.tgz#ba320059420774394d3b0c0233ba40e4250b81d1" integrity sha512-4T7Pb244rxH24yR116LAuJ+adxXXnHhZaLJjegJVKSdoNCe4x1eDBaud5YIcQFcqzsaD5BHvJw5BQ0AZapdCRw== +"@babel/parser@^7.13.16", "@babel/parser@^7.18.10", "@babel/parser@^7.18.11": + version "7.18.11" + resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.18.11.tgz#68bb07ab3d380affa9a3f96728df07969645d2d9" + integrity sha512-9JKn5vN+hDt0Hdqn1PiJ2guflwP+B6Ga8qbDuoF0PzzVhrzsKIJo8yGqVk6CmMHiMei9w1C1Bp9IMJSIK+HPIQ== + "@babel/parser@^7.19.6": version "7.19.6" resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.19.6.tgz#b923430cb94f58a7eae8facbffa9efd19130e7f8" @@ -704,7 +692,7 @@ dependencies: "@babel/helper-plugin-utils" "^7.8.0" -"@babel/plugin-syntax-jsx@7", "@babel/plugin-syntax-jsx@^7.18.6": +"@babel/plugin-syntax-jsx@^7.18.6": version "7.18.6" resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.18.6.tgz#a8feef63b010150abd97f1649ec296e849943ca0" integrity sha512-6mmljtAedFGTWu2p/8WIORGwy+61PLgOMPOdazc7YoJ9ZCWUyFy3A6CpPkRKLKD1ToAesxX8KGEViAiLo9N+7Q== @@ -1278,7 +1266,7 @@ "@babel/parser" "^7.18.10" "@babel/types" "^7.18.10" -"@babel/traverse@7", "@babel/traverse@^7.18.10", "@babel/traverse@^7.18.9": +"@babel/traverse@^7.18.10", "@babel/traverse@^7.18.9": version "7.18.11" resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.18.11.tgz#3d51f2afbd83ecf9912bcbb5c4d94e3d2ddaa16f" integrity sha512-TG9PiM2R/cWCAy6BPJKeHzNbu4lPzOSZpeMfeNErskGpTJx6trEvFaVCbDvpcxwy49BKWmEPwiW8mrysNiDvIQ== @@ -1334,15 +1322,6 @@ lodash "^4.17.13" to-fast-properties "^2.0.0" -"@babel/types@^7.12.11", "@babel/types@^7.18.10", "@babel/types@^7.18.6", "@babel/types@^7.18.9", "@babel/types@^7.3.0": - version "7.18.10" - resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.18.10.tgz#4908e81b6b339ca7c6b7a555a5fc29446f26dde6" - integrity sha512-MJvnbEiiNkpjo+LknnmRrqbY1GPUUggjv+wQVjetM/AONoupqRALB7I6jGqNUAZsKcRIEu2J6FRFvsczljjsaQ== - dependencies: - "@babel/helper-string-parser" "^7.18.10" - "@babel/helper-validator-identifier" "^7.18.6" - to-fast-properties "^2.0.0" - "@babel/types@^7.12.13", "@babel/types@^7.13.0": version "7.13.12" resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.13.12.tgz#edbf99208ef48852acdff1c8a681a1e4ade580cd" @@ -1360,6 +1339,15 @@ "@babel/helper-validator-identifier" "^7.16.7" to-fast-properties "^2.0.0" +"@babel/types@^7.18.10", "@babel/types@^7.18.6", "@babel/types@^7.18.9": + version "7.18.10" + resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.18.10.tgz#4908e81b6b339ca7c6b7a555a5fc29446f26dde6" + integrity sha512-MJvnbEiiNkpjo+LknnmRrqbY1GPUUggjv+wQVjetM/AONoupqRALB7I6jGqNUAZsKcRIEu2J6FRFvsczljjsaQ== + dependencies: + "@babel/helper-string-parser" "^7.18.10" + "@babel/helper-validator-identifier" "^7.18.6" + to-fast-properties "^2.0.0" + "@babel/types@^7.18.4", "@babel/types@^7.19.0", "@babel/types@^7.19.4", "@babel/types@^7.4.4": version "7.19.4" resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.19.4.tgz#0dd5c91c573a202d600490a35b33246fed8a41c7" @@ -1617,19 +1605,6 @@ intl-messageformat "10.1.1" tslib "2.4.0" -"@formatjs/ts-transformer@3.9.9": - version "3.9.9" - resolved "https://registry.yarnpkg.com/@formatjs/ts-transformer/-/ts-transformer-3.9.9.tgz#4804e0560e1944407e9dc5f5ae568a329d5668e4" - integrity sha512-V5BDpn5XW1wWBkpDn5SFHRH4Ndf3oyjxmuqWMxe2EwOKkV4XJvzZI73k3p/Hut3Xg55AxBQQmkFK9hyeBJPyIg== - dependencies: - "@formatjs/icu-messageformat-parser" "2.1.4" - "@types/json-stable-stringify" "^1.0.32" - "@types/node" "14 || 16 || 17" - chalk "^4.0.0" - json-stable-stringify "^1.0.1" - tslib "2.4.0" - typescript "^4.5" - "@hapi/hoek@^9.0.0": version "9.2.1" resolved "https://registry.yarnpkg.com/@hapi/hoek/-/hoek-9.2.1.tgz#9551142a1980503752536b5050fd99f4a7f13b17" @@ -2353,46 +2328,6 @@ resolved "https://registry.yarnpkg.com/@trysound/sax/-/sax-0.2.0.tgz#cccaab758af56761eb7bf37af6f03f326dd798ad" integrity sha512-L7z9BgrNEcYyUYtF+HaEfiS5ebkh9jXqbszz7pC0hRBPaatV0XjSD3+eHrpqFemQfgwiFF0QPIarnIihIDn7OA== -"@types/babel__core@*", "@types/babel__core@^7.1.7": - version "7.1.19" - resolved "https://registry.yarnpkg.com/@types/babel__core/-/babel__core-7.1.19.tgz#7b497495b7d1b4812bdb9d02804d0576f43ee460" - integrity sha512-WEOTgRsbYkvA/KCsDwVEGkd7WAr1e3g31VHQ8zy5gul/V1qKullU/BU5I68X5v7V3GnB9eotmom4v5a5gjxorw== - dependencies: - "@babel/parser" "^7.1.0" - "@babel/types" "^7.0.0" - "@types/babel__generator" "*" - "@types/babel__template" "*" - "@types/babel__traverse" "*" - -"@types/babel__generator@*": - version "7.6.4" - resolved "https://registry.yarnpkg.com/@types/babel__generator/-/babel__generator-7.6.4.tgz#1f20ce4c5b1990b37900b63f050182d28c2439b7" - integrity sha512-tFkciB9j2K755yrTALxD44McOrk+gfpIpvC3sxHjRawj6PfnQxrse4Clq5y/Rq+G3mrBurMax/lG8Qn2t9mSsg== - dependencies: - "@babel/types" "^7.0.0" - -"@types/babel__helper-plugin-utils@^7.10.0": - version "7.10.0" - resolved "https://registry.yarnpkg.com/@types/babel__helper-plugin-utils/-/babel__helper-plugin-utils-7.10.0.tgz#dcd2416f9c189d5837ab2a276368cf67134efe78" - integrity sha512-60YtHzhQ9HAkToHVV+TB4VLzBn9lrfgrsOjiJMtbv/c1jPdekBxaByd6DMsGBzROXWoIL6U3lEFvvbu69RkUoA== - dependencies: - "@types/babel__core" "*" - -"@types/babel__template@*": - version "7.4.1" - resolved "https://registry.yarnpkg.com/@types/babel__template/-/babel__template-7.4.1.tgz#3d1a48fd9d6c0edfd56f2ff578daed48f36c8969" - integrity sha512-azBFKemX6kMg5Io+/rdGT0dkGreboUVR0Cdm3fz9QJWpaQGJRQXl7C+6hOTCZcMll7KFyEQpgbYI2lHdsS4U7g== - dependencies: - "@babel/parser" "^7.1.0" - "@babel/types" "^7.0.0" - -"@types/babel__traverse@*": - version "7.18.0" - resolved "https://registry.yarnpkg.com/@types/babel__traverse/-/babel__traverse-7.18.0.tgz#8134fd78cb39567465be65b9fdc16d378095f41f" - integrity sha512-v4Vwdko+pgymgS+A2UIaJru93zQd85vIGWObM5ekZNdXCKtDYqATlEYnWgfo86Q6I1Lh0oXnksDnMU1cwmlPDw== - dependencies: - "@babel/types" "^7.3.0" - "@types/hoist-non-react-statics@^3.3.1": version "3.3.1" resolved "https://registry.yarnpkg.com/@types/hoist-non-react-statics/-/hoist-non-react-statics-3.3.1.tgz#1124aafe5118cb591977aeb1ceaaed1070eb039f" @@ -2401,11 +2336,6 @@ "@types/react" "*" hoist-non-react-statics "^3.3.0" -"@types/json-stable-stringify@^1.0.32": - version "1.0.34" - resolved "https://registry.yarnpkg.com/@types/json-stable-stringify/-/json-stable-stringify-1.0.34.tgz#c0fb25e4d957e0ee2e497c1f553d7f8bb668fd75" - integrity sha512-s2cfwagOQAS8o06TcwKfr9Wx11dNGbH2E9vJz1cqV+a/LOyhWNLUNd6JSRYNzvB4d29UuJX2M0Dj9vE1T8fRXw== - "@types/json5@^0.0.29": version "0.0.29" resolved "https://registry.yarnpkg.com/@types/json5/-/json5-0.0.29.tgz#ee28707ae94e11d2b827bcbe5270bcea7f3e71ee" @@ -2416,11 +2346,6 @@ resolved "https://registry.yarnpkg.com/@types/node/-/node-17.0.23.tgz#3b41a6e643589ac6442bdbd7a4a3ded62f33f7da" integrity sha512-UxDxWn7dl97rKVeVS61vErvw086aCYhDLyvRQZ5Rk65rZKepaFdm53GeqXaKBuOhED4e9uWq34IC3TdSdJJ2Gw== -"@types/node@14 || 16 || 17": - version "17.0.45" - resolved "https://registry.yarnpkg.com/@types/node/-/node-17.0.45.tgz#2c0fafd78705e7a18b7906b5201a522719dc5190" - integrity sha512-w+tIMs3rq2afQdsPJlODhoUEKzFP1ayaoyl1CcnwtIlsVe7K7bA1NGm4s3PraqTLlXnbIN84zuBlxBWo1u9BLw== - "@types/node@^14.14.31": version "14.18.9" resolved "https://registry.yarnpkg.com/@types/node/-/node-14.18.9.tgz#0e5944eefe2b287391279a19b407aa98bd14436d" @@ -2877,33 +2802,6 @@ babel-plugin-emotion@^10.0.27: find-root "^1.1.0" source-map "^0.5.7" -babel-plugin-formatjs@^10.3.25: - version "10.3.25" - resolved "https://registry.yarnpkg.com/babel-plugin-formatjs/-/babel-plugin-formatjs-10.3.25.tgz#711736c63c92cf383a5f1eb0649824cee53af22c" - integrity sha512-9mn/n9V3BTgnDseO5CGstZTmaxvZY7NzTnIZEYLktqWVsrfk9TaOqWckOBc1govXoha49X41ZT+XujP7Uh8aPA== - dependencies: - "@babel/core" "^7.10.4" - "@babel/helper-plugin-utils" "^7.10.4" - "@babel/plugin-syntax-jsx" "7" - "@babel/traverse" "7" - "@babel/types" "^7.12.11" - "@formatjs/icu-messageformat-parser" "2.1.4" - "@formatjs/ts-transformer" "3.9.9" - "@types/babel__core" "^7.1.7" - "@types/babel__helper-plugin-utils" "^7.10.0" - tslib "2.4.0" - -babel-plugin-inline-react-svg@2.0.1: - version "2.0.1" - resolved "https://registry.yarnpkg.com/babel-plugin-inline-react-svg/-/babel-plugin-inline-react-svg-2.0.1.tgz#68c9c119d643a8f2d7bf939b942534d89ae3ade9" - integrity sha512-aD4gy2G3gNVDaw97LtoixzWbaOcSEnOb4KJPe8kZedSeqxY3v71KsBs8DGmButGZtEloCRhRRuU2TpW1hIPXig== - dependencies: - "@babel/helper-plugin-utils" "^7.0.0" - "@babel/parser" "^7.0.0" - lodash.isplainobject "^4.0.6" - resolve "^1.20.0" - svgo "^2.0.3" - babel-plugin-macros@^2.0.0: version "2.8.0" resolved "https://registry.yarnpkg.com/babel-plugin-macros/-/babel-plugin-macros-2.8.0.tgz#0f958a7cc6556b1e65344465d99111a1e5e10138" @@ -2948,16 +2846,6 @@ babel-plugin-polyfill-regenerator@^0.4.1: lodash "^4.17.11" picomatch "^2.3.0" -babel-plugin-styled-components@^1.10.7: - version "1.10.7" - resolved "https://registry.yarnpkg.com/babel-plugin-styled-components/-/babel-plugin-styled-components-1.10.7.tgz#3494e77914e9989b33cc2d7b3b29527a949d635c" - integrity sha512-MBMHGcIA22996n9hZRf/UJLVVgkEOITuR2SvjHLb5dSTUyR4ZRGn+ngITapes36FI3WLxZHfRhkA1ffHxihOrg== - dependencies: - "@babel/helper-annotate-as-pure" "^7.0.0" - "@babel/helper-module-imports" "^7.0.0" - babel-plugin-syntax-jsx "^6.18.0" - lodash "^4.17.11" - babel-plugin-syntax-jsx@^6.18.0: version "6.18.0" resolved "https://registry.npmjs.org/babel-plugin-syntax-jsx/-/babel-plugin-syntax-jsx-6.18.0.tgz#0af32a9a6e13ca7a3fd5069e62d7b0f58d0d8946" @@ -5593,13 +5481,6 @@ json-stable-stringify-without-jsonify@^1.0.1: resolved "https://registry.yarnpkg.com/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz#9db7b59496ad3f3cfef30a75142d2d930ad72651" integrity sha1-nbe1lJatPzz+8wp1FC0tkwrXJlE= -json-stable-stringify@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/json-stable-stringify/-/json-stable-stringify-1.0.1.tgz#9a759d39c5f2ff503fd5300646ed445f88c4f9af" - integrity sha512-i/J297TW6xyj7sDFa7AmBPkQvLIxWr2kKPWI26tXydnZrzVAocNqn5DMNT1Mzk0vit1V5UkRM7C1KdVNp7Lmcg== - dependencies: - jsonify "~0.0.0" - json-stringify-safe@~5.0.1: version "5.0.1" resolved "https://registry.yarnpkg.com/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz#1296a2d58fd45f19a0f6ce01d65701e2c735b6eb" @@ -5626,11 +5507,6 @@ jsonfile@^6.0.1: optionalDependencies: graceful-fs "^4.1.6" -jsonify@~0.0.0: - version "0.0.0" - resolved "https://registry.yarnpkg.com/jsonify/-/jsonify-0.0.0.tgz#2c74b6ee41d93ca51b7b5aaee8f503631d252a73" - integrity sha512-trvBk1ki43VZptdBI5rIlG4YOzyeH/WefQt5rj1grasPn4iiZWKet8nkgc4GlsAylaztn0qZfUYOiTsASJFdNA== - jsprim@^2.0.2: version "2.0.2" resolved "https://registry.yarnpkg.com/jsprim/-/jsprim-2.0.2.tgz#77ca23dbcd4135cd364800d22ff82c2185803d4d" @@ -5777,11 +5653,6 @@ lodash.flow@^3.3.0: resolved "https://registry.yarnpkg.com/lodash.flow/-/lodash.flow-3.5.0.tgz#87bf40292b8cf83e4e8ce1a3ae4209e20071675a" integrity sha1-h79AKSuM+D5OjOGjrkIJ4gBxZ1o= -lodash.isplainobject@^4.0.6: - version "4.0.6" - resolved "https://registry.yarnpkg.com/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz#7c526a52d89b45c45cc690b88163be0497f550cb" - integrity sha1-fFJqUtibRcRcxpC4gWO+BJf1UMs= - lodash.merge@^4.6.2: version "4.6.2" resolved "https://registry.yarnpkg.com/lodash.merge/-/lodash.merge-4.6.2.tgz#558aa53b43b661e1925a0afdfa36a9a1085fe57a" @@ -7606,7 +7477,7 @@ svg-parser@^2.0.4: resolved "https://registry.yarnpkg.com/svg-parser/-/svg-parser-2.0.4.tgz#fdc2e29e13951736140b76cb122c8ee6630eb6b5" integrity sha512-e4hG1hRwoOdRb37cIMSgzNsxyzKfayW6VOflrwvR+/bzrkyxY/31WkbgnQpgtrNp1SdpJvpUAGTa/ZoiPNDuRQ== -svgo@^2.0.3, svgo@^2.8.0: +svgo@^2.8.0: version "2.8.0" resolved "https://registry.yarnpkg.com/svgo/-/svgo-2.8.0.tgz#4ff80cce6710dc2795f0c7c74101e6764cfccd24" integrity sha512-+N/Q9kV1+F+UeWYoSiULYo4xYSDQlTgb+ayMobAXPwMnLvop7oxKMo9OzIrX5x3eS4L4f2UHhc9axXwY8DpChg== @@ -7777,11 +7648,6 @@ type-fest@^0.21.3: resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.21.3.tgz#d260a24b0198436e133fa26a524a6d65fa3b2e37" integrity sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w== -typescript@^4.5: - version "4.7.4" - resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.7.4.tgz#1a88596d1cf47d59507a1bcdfb5b9dfe4d488235" - integrity sha512-C0WQT0gezHuw6AdY1M2jxUO83Rjf0HP7Sk1DtXj6j1EwkQNZrHAg2XPWlq62oqEhYvONq5pkC2Y9oPljWToLmQ== - ua-parser-js@^0.7.30: version "0.7.33" resolved "https://registry.yarnpkg.com/ua-parser-js/-/ua-parser-js-0.7.33.tgz#1d04acb4ccef9293df6f70f2c3d22f3030d8b532" From 329cc9ac9b0277cc625ed29fcc50046adb5daccf Mon Sep 17 00:00:00 2001 From: Maja Komel Date: Fri, 24 Feb 2023 20:42:35 +0100 Subject: [PATCH 36/41] Fix error on MAT when dates typed in manually --- components/aggregation/mat/Form.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/components/aggregation/mat/Form.js b/components/aggregation/mat/Form.js index be43b004c..32a10e357 100644 --- a/components/aggregation/mat/Form.js +++ b/components/aggregation/mat/Form.js @@ -205,7 +205,8 @@ export const Form = ({ onSubmit, testNames, query }) => { const since = watch('since') const until = watch('until') const timeGrainOptions = useMemo(() => { - if (!since || !until) return ['hour', 'day', 'week', 'month'] + const dateRegex = /^\d{4}-\d{2}-\d{2}$/ + if (!until.match(dateRegex) || !since.match(dateRegex)) return ['hour', 'day', 'week', 'month'] const diff = dayjs(until).diff(dayjs(since), 'day') if (diff < 8) { const availableValues = ['hour', 'day'] From 7d2f2119d0941507481144c1a958d3c9a7affa2e Mon Sep 17 00:00:00 2001 From: Maja Komel Date: Mon, 27 Feb 2023 11:20:09 +0100 Subject: [PATCH 37/41] Correctly map test ids --- components/test-info.js | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/components/test-info.js b/components/test-info.js index 1f86b64ef..46c88bc00 100644 --- a/components/test-info.js +++ b/components/test-info.js @@ -152,12 +152,13 @@ export const testNames = { 'torsf': { group: 'circumvention', name: , - id: 'Tests.Tor.Name', + id: 'Tests.TorSnowflake.Name', info: 'https://ooni.org/nettest/torsf/' }, 'riseupvpn': { group: 'circumvention', name: , + id: 'Tests.RiseupVPN.Name', info: 'https://ooni.org/nettest/' }, @@ -192,7 +193,7 @@ export const testNames = { 'dnscheck': { group: 'experimental', name: , - id: 'Tests.HTTPRequests.Name', + id: 'Tests.DNSCheck.Name', info: 'https://ooni.org/nettest/http-requests/' }, 'stunreachability': { From 6a028651cb31546db84f91b8a0a098cfdc3a6001 Mon Sep 17 00:00:00 2001 From: Maja Komel Date: Mon, 27 Feb 2023 11:20:47 +0100 Subject: [PATCH 38/41] Pull latest translations --- public/static/lang/en.json | 48 ++++++++++++++++++++++++++++-- public/static/lang/translations.js | 2 +- 2 files changed, 47 insertions(+), 3 deletions(-) diff --git a/public/static/lang/en.json b/public/static/lang/en.json index 5a735eb72..85a9efc92 100644 --- a/public/static/lang/en.json +++ b/public/static/lang/en.json @@ -9,6 +9,17 @@ "General.NoData": "No data", "General.Apply": "Apply", "General.Reset": "Reset", + "General.Submit": "Submit", + "General.Close": "Close", + "General.Cancel": "Cancel", + "General.Login": "Login", + "General.Logout": "Logout", + "General.Edit": "Edit", + "Login.EnterEmail": "Add your email address and click the link sent to your email to log in.\n\nWe do not store email addresses.", + "Login.Submitted": "Your login request has been submitted. Please check your email for a link to activate and log in to your account.", + "Login.LoggingIn": "Logging in...", + "Login.Failure": "Try logging in again", + "Login.Success": "Successfully logged in. Redirecting back to the measurement...", "SocialButtons.CTA": "Share on Facebook or Twitter", "SocialButtons.Text": "Data from OONI Explorer", "Tests.WebConnectivity.Name": "Web Connectivity Test", @@ -97,6 +108,7 @@ "Measurement.CommonDetails.Label.ResolverASN": "Resolver ASN", "Measurement.CommonDetails.Label.ResolverIP": "Resolver IP", "Measurement.CommonDetails.Label.ResolverNetworkName": "Resolver Network Name", + "Measurement.CommonDetails.Label.UserFeedback": "User Feedback", "Measurement.Hero.Status.NDT.Title": "Results", "Measurement.Status.Info.Label.Download": "Download", "Measurement.Status.Info.Label.Upload": "Upload", @@ -215,6 +227,31 @@ "Measurement.Metadata.RiseupVPN.Reachable": "RiseupVPN was reachable in {country}", "Measurement.Metadata.RiseupVPN.Blocked": "RiseupVPN was not reachable in {country}", "Measurement.Hero.Status.Default": "Measurement Report", + "Measurement.Feedback.Title": "Verify the measurement", + "Measurement.Feedback.Description": "Please share feedback based on what you see in the measurement data.", + "Measurement.Feedback.ok": "It's OK", + "Measurement.Feedback.down": "It's down", + "Measurement.Feedback.down.unreachable": "It's unreachable", + "Measurement.Feedback.down.misconfigured": "It's misconfigured", + "Measurement.Feedback.blocked": "It's blocked", + "Measurement.Feedback.blocked.tcp": "TCP/IP blocking", + "Measurement.Feedback.blocked.tls": "TLS blocking", + "Measurement.Feedback.blocked.blockpage": "Block page", + "Measurement.Feedback.blocked.blockpage.http": "HTTP block page", + "Measurement.Feedback.blocked.blockpage.dns": "DNS block page", + "Measurement.Feedback.blocked.blockpage.server_side": "Server-side block page", + "Measurement.Feedback.blocked.blockpage.server_side.captcha": "CAPTCHA", + "Measurement.Feedback.blocked.dns": "DNS blocking without block page", + "Measurement.Feedback.blocked.dns.inconsistent": "Inconsistent DNS response", + "Measurement.Feedback.blocked.dns.nxdomain": "NXDOMAIN error", + "Measurement.Feedback.Success.Title": "Thank you!", + "Measurement.Feedback.Success.Description": "Your feedback will help improve OONI measurements.", + "Measurement.Feedback.Failure": "Something went wrong, please try again.", + "Measurement.Feedback.Login.Title": "Please log in to continue", + "Measurement.Feedback.Login.Confirmation.Title": "Login link sent", + "Measurement.Feedback.Login.Confirmation.Text": "Please check your email for a link to log in to your account.", + "Measurement.Feedback.Login.Description": "We will send a link to your email. We do not store emails.", + "Measurement.Feedback.ExistingFeedback": "Your previous feedback", "Navbar.Search": "Search", "Navbar.Countries": "Countries", "Navbar.Charts.Circumvention": "Circumvention Charts", @@ -229,7 +266,6 @@ "Footer.Heading.About": "About", "Footer.Heading.OONIProbe": "OONI Probe", "Footer.Heading.Updates": "Updates", - "Footer.Heading.SocialLinks": "", "Footer.Link.About": "OONI", "Footer.Link.DataPolicy": "Data Policy", "Footer.Link.DataLicense": "Data License", @@ -310,6 +346,7 @@ "Country.Heading.Overview": "Overview", "Country.Heading.Websites": "Websites", "Country.Heading.Apps": "Apps", + "Country.Heading.Shutdowns": "Shutdowns", "Country.Heading.NetworkProperties": "Networks", "Country.Overview.Heading.NwInterference": "In a nutshell", "Country.Overview.NwInterference.Middleboxes.Blocked": "Middleboxes were detected on {middleboxCount} network(s)", @@ -356,10 +393,12 @@ "Country.Websites.URLCharts.ExploreMoreMeasurements": "Explore more measurements", "Country.Websites.URLCharts.Pagination.Previous": "Previous Page", "Country.Websites.URLCharts.Pagination.Next": "Next Page", - "Country.Apps.Description": "Check whether instant messaging apps and circumvention tools are blocked.\n\nThe following results were collected through the use of [OONI Probe tests](https://ooni.org/nettest/) designed to measure the blocking of WhatsApp, Facebook Messenger, and Telegram. \n\nWe also share results on the testing of circumvention tools, like [Tor](https://www.torproject.org/).", + "Country.Apps.Description": "Check whether instant messaging apps and circumvention tools are blocked.\n\nThe following results were collected through the use of [OONI Probe tests](https://ooni.org/nettest/) designed to measure the blocking of WhatsApp, Facebook Messenger, Telegram and Signal. \n\nWe also share results on the testing of circumvention tools, like [Tor](https://www.torproject.org/) and [Psiphon](https://psiphon.ca/).", "Country.Apps.Label.LastTested": "Last tested", "Country.Apps.Label.TestedNetworks": "tested networks", "Country.Apps.Button.ShowMore": "Show More", + "Country.Shutdowns": "Internet shutdowns", + "Country.Shutdowns.Description": "Monitor Internet shutdowns through public, third-party data sources. \n\nThe following graph shares data from Georgia Tech's [Internet Outage Detection and Analysis (IODA)](https://ioda.inetintel.cc.gatech.edu/) project, [Google Transparency Reports (Google traffic data)](https://transparencyreport.google.com/traffic/overview?hl=en), and [Cloudflare Radar](https://radar.cloudflare.com/).\n\nIf you notice a drop in all signals, that might serve as an indicator of an Internet shutdown. \n\nLearn more about Internet shutdowns and their impact through the [#KeepItOn campaign](https://www.accessnow.org/keepiton/).", "Country.NetworkProperties.Description": "Check the speed and performance of networks.\n\nThe following results were collected through the use of [OONI Probe's performance and middlebox tests](https://ooni.org/nettest/). You can check the speed and performance of tested networks, as well as video streaming performance. \n\nYou can also learn whether middleboxes were detected in tested networks. Middleboxes are network appliances that can be used for a variety of networking purposes (such as caching), but sometimes they're used to implement internet censorship and/or surveillance.", "Country.NetworkProperties.Heading.Summary": "Summary", "Country.NetworkProperties.Heading.Networks": "Networks", @@ -518,6 +557,11 @@ "DateRange.LastWeek": "Last Week", "DateRange.LastMonth": "Last Month", "DateRange.LastYear": "Last Year", + "ThirdPartyGraph.Label.gtr": "Google (Search)", + "ThirdPartyGraph.Label.merit-nt": "Telescope", + "ThirdPartyGraph.Label.bgp": "BGP", + "ThirdPartyGraph.Label.ping-slash24": "Active Probing", + "ThirdPartyGraph.Label.cloudflare": "Cloudflare", "Highlights.Political.CubaReferendum2019.Title": "2019 Constitutional Referendum", "Highlights.Political.CubaReferendum2019.Text": "Blocking of independent media", "Highlights.Political.VenezuelaCrisis2019.Title": "2019 Presidential Crisis", diff --git a/public/static/lang/translations.js b/public/static/lang/translations.js index 22c9a02cf..de5afcdff 100644 --- a/public/static/lang/translations.js +++ b/public/static/lang/translations.js @@ -1 +1 @@ -window.OONITranslations = {"en":{"General.OoniExplorer":"OONI Explorer","General.OK":"OK","General.Error":"Error","General.Anomaly":"Anomaly","General.Accessible":"Accessible","General.Failed":"Failed","General.Loading":"Loading…","General.NoData":"No data","General.Apply":"Apply","General.Reset":"Reset","SocialButtons.CTA":"Share on Facebook or Twitter","SocialButtons.Text":"Data from OONI Explorer","Tests.WebConnectivity.Name":"Web Connectivity Test","Tests.Telegram.Name":"Telegram Test","Tests.Facebook.Name":"Facebook Messenger Test","Tests.WhatsApp.Name":"WhatsApp Test","Tests.Signal.Name":"Signal Test","Tests.HTTPInvalidReqLine.Name":"HTTP Invalid Request Line Test","Tests.HTTPHeaderManipulation.Name":"HTTP Header Field Manipulation Test","Tests.NDT.Name":"NDT Speed Test","Tests.Dash.Name":"DASH Video Streaming Test","Tests.TorVanilla.Name":"Tor (Vanilla) Test","Tests.BridgeReachability.Name":"Tor Bridge Reachability Test","Tests.TCPConnect.Name":"TCP Connect Test","Tests.DNSConsistency.Name":"DNS Consistency Test","Tests.HTTPRequests.Name":"HTTP Requests Test","Tests.Psiphon.Name":"Psiphon Test","Tests.Tor.Name":"Tor Test","Tests.RiseupVPN.Name":"Riseup VPN Test","Tests.TorSnowflake.Name":"Tor Snowflake Test","Tests.DNSCheck.Name":"DNS Check","Tests.StunReachability.Name":"STUN Reachability","Tests.URLGetter.Name":"URL Getter","Tests.Groups.Webistes.Name":"Websites","Tests.Groups.Instant Messagging.Name":"Instant Messaging","Tests.Groups.Middlebox.Name":"Middleboxes","Tests.Groups.Performance.Name":"Performance","Tests.Groups.Circumvention.Name":"Circumvention","Tests.Groups.Experimental.Name":"Experimental","Tests.Groups.Legacy.Name":"Legacy","Measurement.MetaDescription":"OONI data suggests {description} on {formattedDate}, find more open data on internet censorship on OONI Explorer.","Measurement.NotFound":"Measurement not found","Measurement.Hero.Status.Confirmed":"Confirmed Blocked","Measurement.Hero.Status.Down":"Website Down","Measurement.Hero.Status.Anomaly.DNS":"DNS","Measurement.Hero.Status.Anomaly.HTTP":"HTTP","Measurement.Hero.Status.Anomaly.TCP":"TCP/IP","Measurement.CommonSummary.Label.ASN":"Network","Measurement.CommonSummary.Label.Country":"Country","Measurement.CommonSummary.Label.DateTime":"Date & Time","Measurement.DetailsHeader.Runtime":"Runtime","Measurement.Status.Hint.Websites.Censorship":"","Measurement.Status.Hint.Websites.DNS":"DNS tampering","Measurement.Status.Hint.Websites.Error":"Error in detection","Measurement.Status.Hint.Websites.HTTPdiff":"HTTP blocking (a blockpage might be served)","Measurement.Status.Hint.Websites.HTTPfail":"HTTP blocking (HTTP requests failed)","Measurement.Status.Hint.Websites.NoCensorship":"No blocking detected","Measurement.Status.Hint.Websites.TCPBlock":"TCP/IP blocking","Measurement.Status.Hint.Websites.Unavailable":"Website down","Measurement.SummaryText.Websites.Accessible":"On {date}, {WebsiteURL} was accessible when tested on {network} in {country}.","Measurement.SummaryText.Websites.Anomaly":"On {date}, {WebsiteURL} presented signs of {reason} on {network} in {country}.\n\nThis might mean that {WebsiteURL} was blocked, but false positives can occur.\n\nPlease explore the network measurement data below.","Measurement.SummaryText.Websites.Anomaly.BlockingReason.DNS":"DNS tampering","Measurement.SummaryText.Websites.Anomaly.BlockingReason.TCP":"TCP/IP blocking","Measurement.SummaryText.Websites.Anomaly.BlockingReason.HTTP-failure":"HTTP blocking (HTTP requests failed)","Measurement.SummaryText.Websites.Anomaly.BlockingReason.HTTP-diff":"HTTP blocking (a blockpage might be served)","Measurement.SummaryText.Websites.ConfirmedBlocked":"On {date}, {WebsiteURL} was blocked on {network} in {country}.\n\nThis is confirmed because a block page was served, as illustrated through the network measurement data below.","Measurement.SummaryText.Websites.Failed":"On {date}, the test for {WebsiteURL} failed on {network} in {country}.","Measurement.SummaryText.Websites.Down":"On {date}, {WebsiteURL} was down on {network} in {country}.","Measurement.Details.Websites.Failures.Heading":"Failures","Measurement.Details.Websites.Failures.Label.HTTP":"HTTP Experiment","Measurement.Details.Websites.Failures.Label.DNS":"DNS Experiment","Measurement.Details.Websites.Failures.Label.Control":"Control","Measurement.Details.Websites.Failures.Values.Null":"null","Measurement.Details.Websites.DNSQueries.Heading":"DNS Queries","Measurement.Details.Websites.DNSQueries.Label.Resolver":"Resolver","Measurement.Details.Websites.TCP.Heading":"TCP Connections","Measurement.Details.Websites.TCP.ConnectionTo":"Connection to {destination} {connectionStatus}.","Measurement.Details.Websites.TCP.ConnectionTo.Success":"succeeded","Measurement.Details.Websites.TCP.ConnectionTo.Failed":"failed","Measurement.Details.Websites.TCP.ConnectionTo.Blocked":"was blocked","Measurement.Details.Websites.HTTP.Heading":"HTTP Requests","Measurement.Details.Websites.HTTP.Label.Response":"Response","Measurement.Details.Websites.HTTP.Request.URL":"URL","Measurement.Details.Websites.HTTP.Response.Body":"Response Body","Measurement.Details.Websites.HTTP.Response.Headers":"Response Headers","Measurement.CommonDetails.Label.MsmtID":"Report ID","Measurement.CommonDetails.Label.Platform":"Platform","Measurement.CommonDetails.Label.Software":"Software Name","Measurement.CommonDetails.Label.Engine":"Measurement Engine","Measurement.CommonDetails.Value.Unavailable":"Unavailable","Measurement.CommonDetails.RawMeasurement.Heading":"Raw Measurement Data","Measurement.CommonDetails.RawMeasurement.Download":"Download JSON","Measurement.CommonDetails.RawMeasurement.Unavailable":"Unavailable","Measurement.CommonDetails.RawMeasurement.Expand":"Expand All","Measurement.CommonDetails.Label.Resolver":"Resolver","Measurement.CommonDetails.Label.ResolverASN":"Resolver ASN","Measurement.CommonDetails.Label.ResolverIP":"Resolver IP","Measurement.CommonDetails.Label.ResolverNetworkName":"Resolver Network Name","Measurement.Hero.Status.NDT.Title":"Results","Measurement.Status.Info.Label.Download":"Download","Measurement.Status.Info.Label.Upload":"Upload","Measurement.Status.Info.Label.Ping":"Ping","Measurement.Status.Info.Label.Server":"Server","Measurement.Status.Info.NDT.Error":"Failed Test","Measurement.Details.Performance.Heading":"Performance Details","Measurement.Details.Performance.Label.AvgPing":"Average Ping","Measurement.Details.Performance.Label.MaxPing":"Max Ping","Measurement.Details.Performance.Label.MSS":"MSS","Measurement.Details.Performance.Label.RetransmitRate":"Retransmission Rate","Measurement.Details.Performance.Label.PktLoss":"Packet Loss","Measurement.Details.Performance.Label.OutOfOrder":"Out of Order","Measurement.Details.Performance.Label.Timeouts":"Timeouts","Measurement.Hero.Status.Dash.Title":"Results","Measurement.Status.Info.Label.VideoQuality":"Video Quality","Measurement.Status.Info.Label.Bitrate":"Median Bitrate","Measurement.Status.Info.Label.Delay":"Playout Delay","Measurement.Status.Hint.Telegram.Blocked":"Telegram is likely blocked","Measurement.Status.Hint.Telegram.Reachable":"Telegram is accessible","Measurement.Status.Hint.Telegram.Failed":"The Telegram test failed","Measurement.Details.SummaryText.Telegram.Reachable":"On {date}, Telegram was reachable on {network} in {country}. \n\nOONI's Telegram test successfully connected to Telegram's endpoints and web interface (web.telegram.org).","Measurement.Details.SummaryText.Telegram.AppFailure":"On {date}, the testing of Telegram's mobile app presented signs of blocking on {network} in {country}.\n\nThis might mean that Telegram's mobile app was blocked, but [false positives](https://ooni.org/support/faq/#why-do-false-positives-occur) can occur. \n\nPlease explore the network measurement data below. Check other Telegram measurements from the same network during the same time period (if they're available).","Measurement.Details.SummaryText.Telegram.DesktopFailure":"On {date}, the testing of Telegram's web interface (web.telegram.org) presented signs of blocking on {network} in {country}.\n\nThis might mean that web.telegram.org was blocked, but [false positives](https://ooni.org/support/faq/#why-do-false-positives-occur) can occur.\n\nPlease explore the network measurement data below. Check other Telegram measurements from the same network during the same time period (if they're available).","Measurement.Details.SummaryText.Telegram.DesktopAndAppFailure":"On {date}, the testing of Telegram's mobile app and web interface (web.telegram.org) presented signs of blocking on {network} in {country}.\n\nThis might mean that both Telegram's mobile app and web interface were blocked, but [false positives](https://ooni.org/support/faq/#why-do-false-positives-occur) can occur.\n\nPlease explore the network measurement data below. Check other Telegram measurements from the same network during the same time period (if they're available).","Measurement.Details.Telegram.Endpoint.Label.Mobile":"Mobile App","Measurement.Details.Telegram.Endpoint.Label.Web":"Telegram Web","Measurement.Details.Endpoint.Status.Unknown":"Unknown","Measurement.Details.Telegram.Endpoint.Status.Heading":"Endpoint Status","Measurement.Details.Telegram.Endpoint.ConnectionTo.Failed":"Connection to {destination} failed.","Measurement.Details.Telegram.Endpoint.ConnectionTo.Successful":"Connection to {destination} was successful.","Measurement.Details.Hint.WhatsApp.Reachable":"WhatsApp is accessible","Measurement.Status.Hint.WhatsApp.Blocked":"WhatsApp is likely blocked","Measurement.Status.Hint.WhatsApp.Failed":"The WhatsApp test failed","Measurement.Details.SummaryText.WhatsApp.Reachable":"On {date}, WhatsApp was reachable on {network} in {country}. \n\nOONI's WhatsApp test successfully connected to WhatsApp's endpoints, registration service and web interface (web.whatsapp.com).","Measurement.Details.SummaryText.WhatsApp.AppFailure":"On {date}, the testing of WhatsApp's mobile app presented signs of blocking on {network} in {country}.\n\nThis might mean that WhatsApp's mobile app was blocked, but [false positives](https://ooni.org/support/faq/#why-do-false-positives-occur) can occur. \n\nPlease explore the network measurement data below. Check other WhatsApp measurements from the same network during the same time period (if they're available).","Measurement.Details.SummaryText.WhatsApp.DesktopFailure":"On {date}, the testing of WhatsApp's web interface (web.whatsapp.com) presented signs of blocking on {network} in {country}.\n\nThis might mean that web.whatsapp.com was blocked, but [false positives](https://ooni.org/support/faq/#why-do-false-positives-occur) can occur.\n\nPlease explore the network measurement data below. Check other WhatsApp measurements from the same network during the same time period (if they're available).","Measurement.Details.SummaryText.WhatsApp.DesktopAndAppFailure":"On {date}, the testing of WhatsApp's mobile app and web interface (web.whatsapp.com) presented signs of blocking on {network} in {country}.\n\nThis might mean that both WhatsApp's mobile app and web interface were blocked, but [false positives](https://ooni.org/support/faq/#why-do-false-positives-occur) can occur.\n\nPlease explore the network measurement data below. Check other WhatsApp measurements from the same network during the same time period (if they're available).","Measurement.Details.WhatsApp.Endpoint.Label.Mobile":"Mobile App","Measurement.Details.WhatsApp.Endpoint.Label.Web":"WhatsApp Web","Measurement.Details.WhatsApp.Endpoint.Label.Registration":"Registration","Measurement.Details.WhatsApp.Endpoint.Status.Heading":"Endpoint Status","Measurement.Details.WhatsApp.Endpoint.ConnectionTo.Failed":"Connection to {destination} failed.","Measurement.Details.WhatsApp.Endpoint.ConnectionTo.Successful":"Connection to {destination} was successful.","Measurement.Status.Hint.FacebookMessenger.Reachable":"Facebook Messenger is accessible","Measurement.Status.Hint.FacebookMessenger.Blocked":"Facebook Messenger is likely blocked","Measurement.Status.Hint.FacebookMessenger.Failed":"The Facebook Messenger test failed","Measurement.Details.SummaryText.FacebookMessenger.Reachable":"On {date}, Facebook Messenger was reachable on {network} in {country}.","Measurement.Details.SummaryText.FacebookMessenger.TCPFailure":"TCP connections to Facebook's endpoints failed.","Measurement.Details.SummaryText.FacebookMessenger.DNSFailure":"DNS lookups did not resolve to Facebook IP addresses.","Measurement.Details.SummaryText.FacebookMessenger.DNSSuccess":"DNS lookups resolved to Facebook IP addresses.","Measurement.Details.SummaryText.FacebookMessenger.TCPSuccess":"TCP connections to Facebook's enpoints succeeded.","Measurement.Details.FacebookMessenger.TCP.Label.Title":"TCP connections","Measurement.Details.FacebookMessenger.DNS.Label.Title":"DNS lookups","Measurement.Details.FacebookMessenger.TCPFailed":"TCP connections failed","Measurement.Details.FacebookMessenger.DNSFailed":"DNS lookups failed","Measurement.Details.FacebookMessenger.Endpoint.Status.Heading":"Endpoint Status","Measurement.Details.FacebookMessenger.Endpoint.ConnectionTo.Failed":"Connection to {destination} failed.","Measurement.Details.FacebookMessenger.Endpoint.ConnectionTo.Successful":"Connection to {destination} was successful.","Measurement.Status.Hint.Signal.Blocked":"Signal is likely blocked","Measurement.Status.Hint.Signal.Reachable":"Signal is accessible","Measurement.Status.Hint.Signal.Failed":"The Signal test failed","Measurement.Details.SummaryText.Signal.Reachable":"On {date}, [Signal](https://signal.org/) was reachable on {network} in {country}. \n\nThe [OONI Probe Signal test](https://ooni.org/nettest/signal) successfully connected to Signal's endpoints.","Measurement.Details.SummaryText.Signal.Blocked":"On {date}, the testing of the [Signal app](https://signal.org/) presented signs of blocking on {network} in {country}.\n\nThis might mean that Signal was blocked, but [false positives](https://ooni.org/support/faq/#why-do-false-positives-occur) can occur. \n\nPlease explore the network measurement data below. Check other Signal measurements from the same network during the same time period (if they're available).","Measurement.Hero.Status.HTTPHeaderManipulation.NoMiddleBoxes":"No middleboxes detected","Measurement.HTTPHeaderManipulation.NoMiddleBoxes.SummaryText":"On {date}, no network anomaly was detected on {network} in {country} when communicating with our servers.","Measurement.Hero.Status.HTTPHeaderManipulation.MiddleboxesDetected":"Network tampering","Measurement.HTTPHeaderManipulation.MiddleBoxesDetected.SummaryText":"On {date}, network traffic was manipulated when contacting our control servers. \n\nThis means that there might be a middlebox on {network} in {country}, which could be responsible for censorship and/or surveillance.","Measurement.Hero.Status.HTTPInvalidReqLine.NoMiddleBoxes":"No middleboxes detected","Measurement.HTTPInvalidReqLine.NoMiddleBoxes.SummaryText":"On {date}, no network anomaly was detected on {network} in {country} when communicating with our servers.","Measurement.Hero.Status.HTTPInvalidReqLine.MiddleboxesDetected":"Network tampering","Measurement.HTTPInvalidReqLine.MiddleboxesDetected.SummaryText":"On {date}, network traffic was manipulated when contacting our control servers. \n\nThis means that there might be a middlebox on {network} in {country}, which could be responsible for censorship and/or surveillance.","Measurement.HTTPInvalidReqLine.YouSent":"You Sent","Measurement.HTTPInvalidReqLine.YouReceived":"You Received","Measurement.Hero.Status.TorVanilla.Blocked":"Tor is likely blocked","Measurement.Hero.Status.TorVanilla.Reachable":"Tor is accessible","Measurement.Details.SummaryText.TorVanilla.Blocked":"On {date}, OONI's Vanilla Tor test did not manage to bootstrap a connection to the [Tor network](https://www.torproject.org/).\n\nThis might mean that access to the Tor network was blocked on {network} in {country}, but [false positives can occur](https://ooni.org/support/faq/#why-do-false-positives-occur).\n\nPlease explore the network measurement data below. Check other Tor measurements from the same network during the same time period (if they're available).","Measurement.Details.SummaryText.TorVanilla.Reachable":"OONI's Vanilla Tor test successfully bootstrapped a connection to the [Tor network](https://www.torproject.org/).\n\nThis means that the Tor network was reachable from {network} in {country} on {date}.","Measurement.Details.VanillaTor.Endpoint.Label.Reachability":"Reachability","Measurement.Status.Hint.Psiphon.Reachable":"Psiphon works","Measurement.Status.Hint.Psiphon.Blocked":"Psiphon is likely blocked","Measurement.Status.Hint.Psiphon.BootstrappingError":"Psiphon is likely blocked (bootstrap error)","Measurement.Details.SummaryText.Psiphon.OK":"On {date}, [Psiphon](https://psiphon.ca/) worked on {network} in {country}.\n\nThe [OONI Probe Psiphon test](https://ooni.org/nettest/psiphon/) was able to successfully bootstrap Psiphon and ensure that the app works.","Measurement.Details.SummaryText.Psiphon.Blocked":"On {date}, [Psiphon](https://psiphon.ca/) did not appear to work on {network} in {country}.\n\nWhile the [OONI Probe Psiphon test](https://ooni.org/nettest/psiphon/) was able to bootstrap Psiphon, it was unable to fetch a webpage from the internet. \n\nThis suggests that the Psiphon app may have been blocked on this network. \n\nHowever, [false positives](https://ooni.org/support/faq/#why-do-false-positives-occur) can occur. Please explore the network measurement data below and compare it with other relevant measurements (if they're available).","Measurement.Details.SummaryText.Psiphon.BootstrappingError":"On {date}, [Psiphon](https://psiphon.ca/) did not work on {network} in {country}.\n\nThe [OONI Probe Psiphon test](https://ooni.org/nettest/psiphon/) was unable to bootstrap Psiphon.\n\nThis suggests that the Psiphon app may have been blocked on this network.\n\nHowever, [false positives](https://ooni.org/support/faq/#why-do-false-positives-occur) can occur. Please explore the network measurement data below and compare it with other relevant measurements (if they're available).","Measurement.Details.Psiphon.BootstrapTime.Label":"Bootstrap Time","Measurement.Status.Hint.Tor.Reachable":"Tor works","Measurement.Status.Hint.Tor.Blocked":"Tor is likely blocked","Measurement.Status.Hint.Tor.Error":"Tor test failed","Measurement.Details.SummaryText.Tor.OK":"On {date}, [Tor](https://www.torproject.org/) worked on {network} in {country}.\n\nAs part of [OONI Probe Tor testing](https://ooni.org/nettest/tor/), all reachability measurements of selected Tor directory authorities and bridges were successful.","Measurement.Details.SummaryText.Tor.Blocked":"On {date}, [Tor](https://www.torproject.org/) did not appear to work on {network} in {country}.\n\nThe [OONI Probe Tor test](https://ooni.org/nettest/tor/) failed in performing certain measurements. More details are available through the network measurement data provided below.\n\nThis suggests that Tor may have been blocked on this network.\n\nHowever, [false positives](https://ooni.org/support/faq/#why-do-false-positives-occur) can occur.","Measurement.Details.SummaryText.Tor.Error":"On {date}, the Tor test failed on {network} in {country}.","Measurement.Details.Tor.Bridges.Label.Title":"Tor Browser Bridges","Measurement.Details.Tor.Bridges.Label.OK":"{bridgesAccessible}/{bridgesTotal} OK","Measurement.Details.Tor.DirAuth.Label.Title":"Tor Directory Authorities","Measurement.Details.Tor.DirAuth.Label.OK":"{dirAuthAccessible}/{dirAuthTotal} OK","Measurement.Details.Tor.Table.Header.Name":"Name","Measurement.Details.Tor.Table.Header.Address":"Address","Measurement.Details.Tor.Table.Header.Type":"Type","Measurement.Status.Hint.TorSnowflake.Reachable":"Tor Snowflake works","Measurement.Status.Hint.TorSnowflake.Blocked":"Tor Snowflake does not work","Measurement.Status.Hint.TorSnowflake.Error":"Tor Snowflake test failed","Measurement.Details.SummaryText.TorSnowflake.OK":"On {date}, [Tor Snowflake](https://www.torproject.org/) worked on {network} in {country}.\n\nThe [OONI Probe Tor Snowflake test](https://ooni.org/nettest/torsf/) was able to successfully bootstrap Snowflake.","Measurement.Details.SummaryText.TorSnowflake.Blocked":"On {date}, [Tor Snowflake](https://www.torproject.org/) does not work on {network} in {country}.\n\nThe [OONI Probe Tor Snowflake test](https://ooni.org/nettest/torsf/) failed to bootstrap Snowflake.","Measurement.Details.SummaryText.TorSnowflake.Error":"On {date}, the Tor Snowflake test failed on {network} in {country}.","Measurement.Details.TorSnowflake.BootstrapTime.Label":"Bootstrap Time","Measurement.Details.TorSnowflake.Error.Label":"Failure","Measurement.Metadata.TorSnowflake.Reachable":"Tor Snowflake was reachable in {country}","Measurement.Metadata.TorSnowflake.UnReachable":"Tor Snowflake was NOT reachable in {country}","Measurement.Metadata.TorSnowflake.Error":"Tor Snowflake test failed in {country}","Measurement.Status.Hint.RiseupVPN.Reachable":"RiseupVPN works","Measurement.Status.Hint.RiseupVPN.Blocked":"RiseupVPN is likely blocked","Measurement.Status.Hint.RiseupVPN.Failed":"The RiseupVPN test failed","Measurement.Details.SummaryText.RiseupVPN.OK":"On {date}, [RiseupVPN](https://riseup.net/vpn) was reachable on {network} in {country}. \n\nThe [OONI Probe RiseupVPN test](https://ooni.org/nettest/riseupvpn/) successfully connected to RiseupVPN's bootstrap servers and gateways.","Measurement.Details.SummaryText.RiseupVPN.Blocked":"On {date}, the testing of [RiseupVPN](https://riseup.net/vpn) presented signs of blocking on {network} in {country}.\n\nThis might mean that RiseupVPN was blocked, but [false positives](https://ooni.org/support/faq/#why-do-false-positives-occur) can occur. \n\nPlease explore the network measurement data below. Check other RiseupVPN measurements from the same network during the same time period (if they're available).","Measurement.Metadata.RiseupVPN.Reachable":"RiseupVPN was reachable in {country}","Measurement.Metadata.RiseupVPN.Blocked":"RiseupVPN was not reachable in {country}","Measurement.Hero.Status.Default":"Measurement Report","Navbar.Search":"Search","Navbar.Countries":"Countries","Navbar.Charts.Circumvention":"Circumvention Charts","Navbar.Charts.MAT":"MAT Charts","Network.Summary.TotalMeasurements":"Total number of measurements: **{measurementsTotal}**","Network.Summary.FirstMeasurement":"Date of the first measurement: **{formattedDate}**","Network.Summary.Countries":"Network observed in countries:","Network.Summary.Country.Measurements":"({measurementsTotal} measurements)","Network.NoData.Title":"Let's collect more data!","Network.NoData.Text":"We don't have enough data for this network to show the charts. Please run OONI Probe to collect more measurements.","Footer.Text.Slogan":"Global community measuring internet censorship around the world.","Footer.Heading.About":"About","Footer.Heading.OONIProbe":"OONI Probe","Footer.Heading.Updates":"Updates","Footer.Heading.SocialLinks":"","Footer.Link.About":"OONI","Footer.Link.DataPolicy":"Data Policy","Footer.Link.DataLicense":"Data License","Footer.Link.Contact":"Contact","Footer.Link.Probe":"Install","Footer.Link.Tests":"Tests","Footer.Link.Code":"Source code","Footer.Link.API":"API","Footer.Link.Blog":"Blog","Footer.Link.Twitter":"Twitter","Footer.Link.MailingList":"Mailing list","Footer.Link.Slack":"Slack","Footer.Text.Copyright":"© {currentYear} Open Observatory of Network Interference (OONI)","Footer.Text.CCommons":"Content available under a Creative Commons license.","Footer.Text.Version":"Version","CategoryCode.ALDR.Name":"Alcohol & Drugs","CategoryCode.REL.Name":"Religion","CategoryCode.PORN.Name":"Pornography","CategoryCode.PROV.Name":"Provocative Attire","CategoryCode.POLR.Name":"Political Criticism","CategoryCode.HUMR.Name":"Human Rights Issues","CategoryCode.ENV.Name":"Environment","CategoryCode.MILX.Name":"Terrorism and Militants","CategoryCode.HATE.Name":"Hate Speech","CategoryCode.NEWS.Name":"News Media","CategoryCode.XED.Name":"Sex Education","CategoryCode.PUBH.Name":"Public Health","CategoryCode.GMB.Name":"Gambling","CategoryCode.ANON.Name":"Anonymization and circumvention tools","CategoryCode.DATE.Name":"Online Dating","CategoryCode.GRP.Name":"Social Networking","CategoryCode.LGBT.Name":"LGBTQ+","CategoryCode.FILE.Name":"File-sharing","CategoryCode.HACK.Name":"Hacking Tools","CategoryCode.COMT.Name":"Communication Tools","CategoryCode.MMED.Name":"Media sharing","CategoryCode.HOST.Name":"Hosting and Blogging Platforms","CategoryCode.SRCH.Name":"Search Engines","CategoryCode.GAME.Name":"Gaming","CategoryCode.CULTR.Name":"Culture","CategoryCode.ECON.Name":"Economics","CategoryCode.GOVT.Name":"Government","CategoryCode.COMM.Name":"E-commerce","CategoryCode.CTRL.Name":"Control content","CategoryCode.IGO.Name":"Intergovernmental Organizations","CategoryCode.MISC.Name":"Miscellaneous content","CategoryCode.ALDR.Description":"Sites devoted to the use, paraphernalia, and sale of drugs and alcohol irrespective of the local legality.","CategoryCode.REL.Description":"Sites devoted to discussion of religious issues, both supportive and critical, as well as discussion of minority religious groups.","CategoryCode.PORN.Description":"Hard-core and soft-core pornography.","CategoryCode.PROV.Description":"Websites which show provocative attire and portray women in a sexual manner, wearing minimal clothing.","CategoryCode.POLR.Description":"Content that offers critical political viewpoints. Includes critical authors and bloggers, as well as oppositional political organizations. Includes pro-democracy content, anti-corruption content as well as content calling for changes in leadership, governance issues, legal reform, etc.","CategoryCode.HUMR.Description":"Sites dedicated to discussing human rights issues in various forms. Includes women's rights and rights of minority ethnic groups.","CategoryCode.ENV.Description":"Pollution, international environmental treaties, deforestation, environmental justice, disasters, etc.","CategoryCode.MILX.Description":"Sites promoting terrorism, violent militant or separatist movements.","CategoryCode.HATE.Description":"Content that disparages particular groups or persons based on race, sex, sexuality or other characteristics.","CategoryCode.NEWS.Description":"This category includes major news outlets (BBC, CNN, etc.) as well as regional news outlets and independent media.","CategoryCode.XED.Description":"Includes contraception, abstinence, STDs, healthy sexuality, teen pregnancy, rape prevention, abortion, sexual rights, and sexual health services.","CategoryCode.PUBH.Description":"HIV, SARS, bird flu, centers for disease control, World Health Organization, etc.","CategoryCode.GMB.Description":"Online gambling sites. Includes casino games, sports betting, etc.","CategoryCode.ANON.Description":"Sites that provide tools used for anonymization, circumvention, proxy-services and encryption.","CategoryCode.DATE.Description":"Online dating services which can be used to meet people, post profiles, chat, etc.","CategoryCode.GRP.Description":"Social networking tools and platforms.","CategoryCode.LGBT.Description":"A range of gay-lesbian-bisexual-transgender queer issues. (Excluding pornography)","CategoryCode.FILE.Description":"Sites and tools used to share files, including cloud-based file storage, torrents and P2P file-sharing tools.","CategoryCode.HACK.Description":"Sites dedicated to computer security, including news and tools. Includes malicious and non-malicious content.","CategoryCode.COMT.Description":"Sites and tools for individual and group communications. Includes webmail, VoIP, instant messaging, chat and mobile messaging applications.","CategoryCode.MMED.Description":"Video, audio or photo sharing platforms.","CategoryCode.HOST.Description":"Web hosting services, blogging and other online publishing platforms.","CategoryCode.SRCH.Description":"Search engines and portals.","CategoryCode.GAME.Description":"Online games and gaming platforms, excluding gambling sites.","CategoryCode.CULTR.Description":"Content relating to entertainment, history, literature, music, film, books, satire and humour.","CategoryCode.ECON.Description":"General economic development and poverty related topics, agencies and funding opportunities.","CategoryCode.GOVT.Description":"Government-run websites, including military sites.","CategoryCode.COMM.Description":"Websites of commercial services and products.","CategoryCode.CTRL.Description":"Benign or innocuous content used as a control.","CategoryCode.IGO.Description":"Websites of intergovernmental organizations such as the United Nations.","CategoryCode.MISC.Description":"Sites that don't fit in any category. (XXX Things in here should be categorised)","Country.Heading.Overview":"Overview","Country.Heading.Websites":"Websites","Country.Heading.Apps":"Apps","Country.Heading.NetworkProperties":"Networks","Country.Overview.Heading.NwInterference":"In a nutshell","Country.Overview.NwInterference.Middleboxes.Blocked":"Middleboxes were detected on {middleboxCount} network(s)","Country.Overview.NwInterference.Middleboxes.Normal":"No middleboxes were detected on tested networks","Country.Overview.NwInterference.Middleboxes.NoData":"Not enough data available on middleboxes","Country.Overview.NwInterference.IM.Blocked":"Instant messaging apps were likely blocked","Country.Overview.NwInterference.IM.Normal":"No instant messaging apps were blocked on tested networks","Country.Overview.NwInterference.IM.NoData":"Not enough data available on instant messaging apps","Country.Overview.NwInterference.CircumventionTools.Blocked":"Circumvention tools were likely blocked","Country.Overview.NwInterference.CircumventionTools.Normal":"No circumvention tools were blocked on tested networks","Country.Overview.NwInterference.CircumventionTools.NoData":"Not enough data available on circumvention tools","Country.Overview.NwInterference.Websites.Blocked":"OONI data confirms the blocking of websites","Country.Overview.NwInterference.Websites.Normal":"The blocking of websites is not confirmed","Country.Overview.NwInterference.Websites.NoData":"Not enough data available on blocked websites","Country.Overview.Heading.TestsByClass":"Measurement Coverage","Country.Overview.Heading.TestsByClass.Description":"The graph below provides an overview of OONI Probe measurement coverage. It shows how many results have been collected from each OONI Probe test category, as well as how many networks have been covered by tests. \n\nBy looking at this graph, you can understand if there is enough data to draw meaningful conclusions. If there is not enough data and you are in the country in question, [install OONI Probe](https://ooni.org/install), run tests, and contribute data!","Country.Overview.TestsByClass.Websites":"Websites","Country.Overview.TestsByClass.InstantMessaging":"Instant Messaging","Country.Overview.TestsByClass.Performance":"Performance","Country.Overview.TestsByClass.Middleboxes":"Middleboxes","Country.Overview.TestsByClass.Circumvention":"Circumvention Tools","Country.Overview.FeaturedResearch":"Research Reports","Country.Overview.FeaturedResearch.None":"We haven't published a research report based on OONI data from this country yet. \n\nWe encourage you to use OONI data for your research!","Country.Overview.SummaryTextTemplate":"OONI Probe users in **{countryName}** have collected [**{measurementCount}** measurements]({linkToMeasurements}) from **{networkCovered}** local networks.\n\nExplore the data below to check the accessibility and/or blocking of sites and services.","Country.Overview.NoData.Title":"Let's collect more data!","Country.Overview.NoData.CallToAction":"We don’t have enough measurements for **{country}** to show a chart. If you are in {country} or know people there, tell them to run OONI Probe to collect more measurements.","Country.Overview.NoData.Button.InstallProbe":"Install OONI Probe","Country.Overview.NoData.Button.OoniRunLink":"Create OONI Run Link","Country.Meta.Title":"Internet Censorship in {countryName} - OONI Explorer","Country.Meta.Description":"OONI Probe users in {countryName} have collected {measurementCount} measurements from {networkCount} local networks. Explore the data on OONI Explorer.","Country.PeriodFilter.Label":"Show results from","Country.PeriodFilter.Option.30Days":"Last 30 Days","Country.PeriodFilter.Option.2Months":"Last 2 Months","Country.PeriodFilter.Option.3Months":"Last 3 Months","Country.PeriodFilter.Option.6Months":"Last 6 Months","Country.Websites.Description":"Check whether websites have been blocked.\n\nTesting methodology: OONI's [Web Connectivity test](https://ooni.org/nettest/web-connectivity/), designed to measure the DNS, HTTP, and TCP/IP blocking of websites. \n\nTested websites: [Citizen Lab test lists](https://github.com/citizenlab/test-lists)\n\nIf you'd like to see results on the testing of different websites, please [contribute to test lists](https://ooni.org/get-involved/contribute-test-lists/) or test the sites of your choice via the [OONI Probe mobile app](https://ooni.org/install/). \n\nPlease note that unless a block page is served, some anomalous measurements may contain [false positives](https://ooni.org/support/faq/#why-do-false-positives-occur). We therefore encourage you to examine anomalous measurements in depth and over time.","Country.Websites.Description.MoreLinkText":"","Country.Websites.Heading.BlockedByCategory":"Categories of Blocked Websites","Country.Websites.BlockedByCategory.Description":"Websites that fall under the following categories were blocked on the {selectedASN} network.","Country.Websites.TestedWebsitesCount":"URLs tested","Country.Websites.Labels.ResultsPerPage":"Results per page","Country.Websites.URLSearch.Placeholder":"Search for URL","Country.Websites.URLCharts.Legend.Label.Blocked":"Confirmed Blocked","Country.Websites.URLCharts.ExploreMoreMeasurements":"Explore more measurements","Country.Websites.URLCharts.Pagination.Previous":"Previous Page","Country.Websites.URLCharts.Pagination.Next":"Next Page","Country.Apps.Description":"Check whether instant messaging apps and circumvention tools are blocked.\n\nThe following results were collected through the use of [OONI Probe tests](https://ooni.org/nettest/) designed to measure the blocking of WhatsApp, Facebook Messenger, and Telegram. \n\nWe also share results on the testing of circumvention tools, like [Tor](https://www.torproject.org/).","Country.Apps.Label.LastTested":"Last tested","Country.Apps.Label.TestedNetworks":"tested networks","Country.Apps.Button.ShowMore":"Show More","Country.NetworkProperties.Description":"Check the speed and performance of networks.\n\nThe following results were collected through the use of [OONI Probe's performance and middlebox tests](https://ooni.org/nettest/). You can check the speed and performance of tested networks, as well as video streaming performance. \n\nYou can also learn whether middleboxes were detected in tested networks. Middleboxes are network appliances that can be used for a variety of networking purposes (such as caching), but sometimes they're used to implement internet censorship and/or surveillance.","Country.NetworkProperties.Heading.Summary":"Summary","Country.NetworkProperties.Heading.Networks":"Networks","Country.NetworkProperties.InfoBox.Label.AverageDownload":"Average Download","Country.NetworkProperties.InfoBox.Label.AverageUpload":"Average Upload","Country.NetworkProperties.InfoBox.Label.Covered":"Covered","Country.NetworkProperties.InfoBox.Label.Middleboxes":"Middleboxes detected","Country.NetworkProperties.InfoBox.Units.Mbits":"Mbit/s","Country.NetworkProperties.InfoBox.Units.Networks.Singular":"Network","Country.NetworkProperties.InfoBox.Units.Networks.Plural":"Networks","Country.NetworkProperties.InfoBox.Label.AverageStreaming":"Average Streaming","Country.NetworkProperties.InfoBox.Label.AveragePing":"Average Ping","Country.NetworkProperties.InfoBox.Units.Milliseconds":"ms","Country.NetworkProperties.InfoBox.Label.Middleboxes.Found":"Middleboxes detected","Country.NetworkProperties.InfoBox.Label.Middleboxes.NotFound":"No middleboxes detected","Country.NetworkProperties.Button.ShowMore":"Show more networks","Country.Label.NoData":"No Data Available","Search.PageTitle":"Search through millions of Internet censorship measurements","Search.Sidebar.Domain":"Domain","Search.Sidebar.Domain.Placeholder":"e.g. twitter.com or 1.1.1.1","Search.Sidebar.Domain.Error":"Please enter a valid domain name or IP address, such as twitter.com or 1.1.1.1","Search.Sidebar.Input":"Input","Search.Sidebar.Input.Placeholder":"e.g., https://fbcdn.net/robots.txt","Search.Sidebar.Input.Error":"Please enter full URL or IP address, such as https://fbcdn.net/robots.txt","Search.Sidebar.Categories":"Website Categories","Search.Sidebar.Categories.All":"Any","Search.Sidebar.Status":"Status","Search.Sidebar.TestName":"Test Name","Search.Sidebar.TestName.AllTests":"Any","Search.Sidebar.Country":"Country","Search.Sidebar.Country.AllCountries":"Any","Search.Sidebar.ASN":"ASN","Search.Sidebar.ASN.example":"e.g. AS30722","Search.Sidebar.ASN.Error":"Valid formats: AS1234, 1234","Search.Sidebar.From":"From","Search.Sidebar.Until":"Until","Search.Sidebar.HideFailed":"Hide failed measurements","Search.Sidebar.Button.FilterResults":"Filter Results","Search.FilterButton.AllResults":"All Results","Search.FilterButton.Confirmed":"Confirmed","Search.FilterButton.Anomalies":"Anomalies","Search.FilterButton.Search":"Search","Search.Filter.SortBy":"Sort by","Search.Filter.SortBy.Date":"Date","Search.WebConnectivity.Results.Blocked":"Confirmed","Search.HTTPRequests.Results.Anomaly":"","Search.HTTPRequests.Results.Blocked":"","Search.HTTPRequests.Results.Error":"","Search.HTTPRequests.Results.Reachable":"","Search.NDT.Results":"","Search.DASH.Results":"","Search.VanillaTor.Results":"","Search.BridgeReachability.Results":"","Search.LegacyTests.Results":"","Search.Results.Empty.Heading":"No Results Found","Search.Results.Empty.Description":"Please try changing the filters to get better results.","Search.Button.LoadMore":"Load more","Search.Error.Message":"This query took too long to complete. Please try adjusting the search filters or view the example queries in the [Highlights section of the homepage](/#highlights).\n\nIf you are interested in using OONI data in batch, we recommend the [ooni-data Amazon S3 bucket](https://ooni.org/post/mining-ooni-data/) or the [aggregation API](https://api.ooni.io/apidocs/#/default/get_api_v1_aggregation).\n\nWe are working on improving the performance of OONI Explorer. To track our work on this, [see the open issues on the ooni/api repository](https://github.com/ooni/api/issues?q=is%3Aissue+is%3Aopen+label%3Aoptimization).","Search.Error.Details.Label":"Server Response","Home.Banner.Title.UncoverEvidence":"Uncover evidence of internet censorship worldwide","Home.Banner.Subtitle.ExploreCensorshipEvents":"Open data collected by the global OONI community","Home.Banner.Button.Explore":"Explore","Home.Banner.Stats.Measurements":"Measurements","Home.Banner.Stats.Countries":"Countries","Home.Banner.Stats.Networks":"Networks","Home.About.SummaryText":"OONI Explorer is an open data resource on internet censorship around the world. \n\nSince 2012, millions of network measurements have been collected from more than 200 countries. OONI Explorer sheds light on internet censorship and other forms of network interference worldwide.\n\nTo contribute to this open dataset, [install OONI Probe](https://ooni.org/install/) and run tests!","Home.Websites&Apps.Title":"Blocking of Websites & Apps","Home.Websites&Apps.SummaryText":"Discover blocked websites around the world. Check whether WhatsApp, Facebook Messenger, and Telegram are blocked.","Home.Search&Filter.Title":"Search","Home.Search&Filter.SummaryText":"Explore OONI measurements with a powerful search tool. View the most recently blocked websites. Compare internet censorship across networks.","Home.NetworkProperties.Title":"Network Performance","Home.NetworkProperties.SummaryText":"Check the speed and performance of thousands of networks around the world. Explore data on video streaming performance.","Home.MonthlyStats.Title":"Monthly coverage worldwide","Home.MonthlyStats.SummaryText":"OONI Explorer hosts millions of network measurements collected from more than 200 countries since 2012. Every day, OONI Explorer is updated with new measurements!\n\nLast month, {measurementCount} OONI Probe measurements were collected from {networkCount} networks in {countryCount} countries. Explore the monthly usage of [OONI Probe](https://ooni.org/install/) through the stats below.","Home.Highlights.CTA":"We encourage you to explore OONI measurements to find more highlights!","Home.Highlights.Title":"Highlights","Home.Highlights.Description":"What can you learn from OONI Explorer? \n\nBelow we share some stories from [research reports](https://ooni.org/post/) based on OONI data.\n\nWe share these case studies to demonstrate how OONI's openly available data can be used and what types of stories can be told. \n\nWe encourage you to explore OONI data, discover more censorship cases, and to use OONI data as part of your research and/or advocacy.","Home.Highlights.Political":"Censorship during political events","Home.Highlights.Political.Description":"Internet censorship sometimes occurs in response to or in anticipation of political events, such as elections, protests, and riots. Below we share a few cases detected via OONI data and correlated with political events.","Home.Highlights.Media":"Media censorship","Home.Highlights.Media.Description":"Press freedom is threatened in countries that experience the blocking of media websites. Below we share a few cases detected through OONI data.","Home.Highlights.LGBTQI":"Blocking of LGBTQI sites","Home.Highlights.LGBTQI.Description":"Minority group sites are blocked around the world. Below we share a few cases on the blocking of LGBTQI sites.","Home.Highlights.Changes":"Censorship changes","Home.Highlights.Changes.Description":"OONI measurements have been collected on a continuous basis since 2012, enabling the identification of censorship changes around the world. Some examples include:","Home.Meta.Description":"OONI Explorer is an open data resource on Internet censorship around the world consisting of millions of measurements on network interference.","Home.Highlights.Explore":"Explore","Home.Highlights.ReadReport":"Read report","Countries.Heading.JumpToContinent":"Jump to continent","Countries.Search.NoCountriesFound":"No countries found with {searchTerm}","Countries.Search.Placeholder":"Search for countries","Countries.PageTitle":"Internet Censorship around the world","Error.404.PageNotFound":"Page Not Found","Error.404.GoBack":"Go back","Error.404.Heading":"The requested page does not exist","Error.404.Message":"We could not find the content you were looking for. Maybe try {measurmentLink} or look at {homePageLink}.","Error.404.MeasurmentLinkText":"exploring some measurement","Error.404.HomepageLinkText":"the homepage","MAT.Title":"OONI Measurement Aggregation Toolkit (MAT)","MAT.SubTitle":"Create charts based on aggregate views of real-time OONI data from around the world","MAT.JSONData":"JSON Data","MAT.CSVData":"CSV Data","MAT.Form.Label.XAxis":"X Axis","MAT.Form.Label.YAxis":"Y Axis","MAT.Form.Label.AxisOption.domain":"Domain","MAT.Form.Label.AxisOption.input":"Input","MAT.Form.Label.AxisOption.measurement_start_day":"Measurement Day","MAT.Form.Label.AxisOption.probe_cc":"Countries","MAT.Form.Label.AxisOption.category_code":"Website Categories","MAT.Form.Label.AxisOption.probe_asn":"ASN","MAT.Form.ConfirmationModal.Title":"Are you sure?","MAT.Form.ConfirmationModal.Message":"Duration too long. This can potentially slow down the page","MAT.Form.ConfirmationModal.No":"No","MAT.Form.ConfirmationModal.Button.Yes":"Yes","MAT.Form.Submit":"Show Chart","MAT.Form.All":"All","MAT.Form.AllCountries":"All Countries","MAT.Table.Header.ok_count":"OK Count","MAT.Table.Header.anomaly_count":"Anomaly Count","MAT.Table.Header.confirmed_count":"Confirmed Count","MAT.Table.Header.failure_count":"Failure Count","MAT.Table.Header.measurement_count":"Measurement Count","MAT.Table.Header.input":"URL","MAT.Table.Header.category_code":"Category Code","MAT.Table.Header.probe_cc":"Country","MAT.Table.Header.probe_asn":"ASN","MAT.Table.Header.blocking_type":"Blocking Type","MAT.Table.Header.domain":"Domain","MAT.Table.FilterPlaceholder":"Search {count} records…","MAT.Table.Search":"Search:","MAT.Table.Filters":"Filters","MAT.Charts.NoData.Title":"No Data Found","MAT.Charts.NoData.Description":"We are not able to produce a chart based on the selected filters. Please change the filters and try again.","MAT.Charts.NoData.Details":"Details:","MAT.Help.Box.Title":"Help","MAT.Help.Title":"FAQs","MAT.Help.Content":"# What is the MAT?\n\nOONI's Measurement Aggregation Toolkit (MAT) is a tool that enables you to generate your own custom charts based on **aggregate views of real-time OONI data** collected from around the world.\n\nOONI data consists of network measurements collected by [OONI Probe](https://ooni.org/install/) users around the world. \n\nThese measurements contain information about various types of **internet censorship**, such as the [blocking of websites and apps](https://ooni.org/nettest/) around the world. \n\n# Who is the MAT for?\n\nThe MAT was built for researchers, journalists, and human rights defenders interested in examining internet censorship around the world.\n\n# Why use the MAT?\n\nWhen examining cases of internet censorship, it's important to **look at many measurements at once** (\"in aggregate\") in order to answer key questions like the following:\n\n* Does the testing of a service (e.g. Facebook) present **signs of blocking every time that it is tested** in a country? This can be helpful for ruling out [false positives](https://ooni.org/support/faq/#what-are-false-positives).\n* What types of websites (e.g. human rights websites) are blocked in each country?\n* In which countries is a specific website (e.g. `bbc.com`) blocked?\n* How does the blocking of different apps (e.g. WhatsApp or Telegram) vary across countries?\n* How does the blocking of a service vary across countries and [ASNs](https://ooni.org/support/glossary/#asn)?\n* How does the blocking of a service change over time?\n\nWhen trying to answer questions like the above, we normally perform relevant data analysis (instead of inspecting measurements one by one). \n\nThe MAT incorporates our data analysis techniques, enabling you to answer such questions without any data analysis skills, and with the click of a button!\n\n# How to use the MAT?\n\nThrough the filters at the start of the page, select the parameters you care about in order to plot charts based on aggregate views of OONI data.\n\nThe MAT includes the following filters:\n\n* **Countries:** Select a country through the drop-down menu (the \"All Countries\" option will show global coverage)\n* **Test Name:** Select an [OONI Probe test](https://ooni.org/nettest/) based on which you would like to get measurements (for example, select `Web Connectivity` to view the testing of websites)\n* **Domain:** Type the domain for the website you would like to get measurements (e.g. `twitter.com`)\n* **Website categories:** Select the [website category](https://github.com/citizenlab/test-lists/blob/master/lists/00-LEGEND-new_category_codes.csv) for which you would like to get measurements (e.g. `News Media` for news media websites)\n* **ASN:** Type the [ASN](https://ooni.org/support/glossary/#asn) of the network for which you would like to get measurements (e.g. `AS30722` for Vodafone Italia)\n* **Date range:** Select the date range of the measurements by adjusting the `Since` and `Until` filters\n* **X axis:** Select the values that you would like to appear on the horizontal axis of your chart\n* **Y axis:** Select the values that you would like to appear on the vertical axis of your chart\n\nDepending on what you would like to explore, adjust the MAT filters accordingly and click `Show Chart`. \n\nFor example, if you would like to check the testing of BBC in all countries around the world:\n\n* Type `www.bbc.com` under `Domain`\n* Select `Countries` under the `Y axis`\n* Click `Show Chart`\n\nThis will plot numerous charts based on the OONI Probe testing of `www.bbc.com` worldwide.\n\n# Interpreting MAT charts\n\nThe MAT charts (and associated tables) include the following values:\n\n* **OK count:** Successful measurements (i.e. NO sign of internet censorship)\n* **Confirmed count:** Measurements from automatically **confirmed blocked websites** (e.g. a [block page](https://ooni.org/support/glossary/#block-page) was served)\n* **Anomaly count:** Measurements that provided **signs of potential blocking** (however, [false positives](https://ooni.org/support/faq/#what-are-false-positives) can occur) \n* **Failure count:** Failed experiments that should be discarded\n* **Measurement count:** Total volume of OONI measurements (pertaining to the selected country, resource, etc.)\n\nWhen trying to identify the blocking of a service (e.g. `twitter.com`), it's useful to check whether:\n\n* Measurements are annotated as `confirmed`, automatically confirming the blocking of websites\n* A large volume of measurements (in comparison to the overall measurement count) present `anomalies` (i.e. signs of potential censorship)\n\nYou can access the raw data by clicking on the bars of charts, and subsequently clicking on the relevant measurement links. \n\n# Website categories\n\n[OONI Probe](https://ooni.org/install/) users test a wide range of [websites](https://ooni.org/support/faq/#which-websites-will-i-test-for-censorship-with-ooni-probe) that fall under the following [30 standardized categories](https://github.com/citizenlab/test-lists/blob/master/lists/00-LEGEND-new_category_codes.csv).","MAT.Help.Subtitle.Categories":"Categories","MAT.CustomTooltip.ViewMeasurements":"View measurements","ReachabilityDash.Heading.CircumventionTools":"Reachability of Censorship Circumvention Tools","ReachabilityDash.CircumventionTools.Description":"The charts below display aggregate views of OONI data based on the testing of the following circumvention tools:\n\n* [Psiphon](https://ooni.org/nettest/psiphon)\n\n* [Tor](https://ooni.org/nettest/tor)\n\n* [Tor Snowflake](https://ooni.org/nettest/tor-snowflake/)\n\nPlease note that the presence of [anomalous measurements](https://ooni.org/support/faq/#what-do-you-mean-by-anomalies) is not always indicative of blocking, as [false positives](https://ooni.org/support/faq/#what-are-false-positives) can occur. Moreover, circumvention tools often have built-in circumvention techniques for evading censorship. \n\nWe therefore recommend referring to **[Tor Metrics](https://metrics.torproject.org/)** and to the **[Psiphon Data Engine](https://psix.ca/)** to view usage stats and gain a more comprehensive understanding of whether these tools work in each country.","ReachabilityDash.Form.Label.CountrySelect.AllSelected":"All countries selected","ReachabilityDash.Form.Label.CountrySelect.SearchPlaceholder":"Search","ReachabilityDash.Form.Label.CountrySelect.SelectAll":"Select All","ReachabilityDash.Form.Label.CountrySelect.SelectAllFiltered":"Select All (Filtered)","ReachabilityDash.Form.Label.CountrySelect.InputPlaceholder":"Select Countries…","ReachabilityDash.Meta.Description":"View the accessibility of censorship circumvention tools around the world through OONI data.","DateRange.Apply":"Apply","DateRange.Cancel":"Cancel","DateRange.Today":"Today","DateRange.LastWeek":"Last Week","DateRange.LastMonth":"Last Month","DateRange.LastYear":"Last Year","Highlights.Political.CubaReferendum2019.Title":"2019 Constitutional Referendum","Highlights.Political.CubaReferendum2019.Text":"Blocking of independent media","Highlights.Political.VenezuelaCrisis2019.Title":"2019 Presidential Crisis","Highlights.Political.VenezuelaCrisis2019.Text":"Blocking of Wikipedia and social media","Highlights.Political.ZimbabweProtests2019.Title":"2019 Fuel Protests","Highlights.Political.ZimbabweProtests2019.Text":"Social media blocking and internet blackouts","Highlights.Political.MaliElection2018.Title":"2018 Presidential Election","Highlights.Political.MaliElection2018.Text":"Blocking of WhatsApp and Twitter","Highlights.Political.CataloniaReferendum2017.Title":"Catalonia 2017 Independence Referendum","Highlights.Political.CataloniaReferendum2017.Text":"Blocking of sites related to the referendum","Highlights.Political.IranProtests2018.Title":"2018 Anti-government Protests","Highlights.Political.IranProtests2018.Text":"Blocking of Telegram, Instagram and Tor","Highlights.Political.EthiopiaProtests2016.Title":"2016 Wave of Protests","Highlights.Political.EthiopiaProtests2016.Text":"Blocking of news websites and social media","Highlights.Political.PakistanProtests2017.Title":"2017 Protests","Highlights.Political.PakistanProtests2017.Text":"Blocking of news websites and social media","Highlights.Media.Egypt.Title":"Pervasive media censorship","Highlights.Media.Egypt.Text":"Blocking of hundreds of media websites","Highlights.Media.Venezuela.Title":"Blocking of independent media websites","Highlights.Media.Venezuela.Text":"Venezuela's economic and political crisis","Highlights.Media.SouthSudan.Title":"Blocking of foreign-based media","Highlights.Media.SouthSudan.Text":"Media accused of hostile reporting against the government","Highlights.Media.Malaysia.Title":"Blocking of media","Highlights.Media.Malaysia.Text":"1MDB scandal","Highlights.Media.Iran.Title":"Pervasive media censorship","Highlights.Media.Iran.Text":"Blocking of at least 121 news outlets","Highlights.Lgbtqi.Indonesia.Text":"Blocking of LGBTQI sites","Highlights.Lgbtqi.Iran.Text":"Blocking of Grindr","Highlights.Lgbtqi.Ethiopia.Text":"Blocking of QueerNet","Highlights.Changes.Cuba.Text":"Cuba [used to primarily serve blank block pages](https://ooni.torproject.org/post/cuba-internet-censorship-2017/), only blocking the HTTP version of websites. Now they censor access to sites that support HTTPS by means of [IP blocking](https://ooni.org/post/cuba-referendum/).","Highlights.Changes.Venezuela.Text":"Venezuelan ISPs used to primarily block sites by means of [DNS tampering](https://ooni.torproject.org/post/venezuela-internet-censorship/). Now state-owned CANTV also implements [SNI-based filtering](https://ooni.torproject.org/post/venezuela-blocking-wikipedia-and-social-media-2019/).","Highlights.Changes.Ethiopia.Text":"Ethiopia [used to block](https://ooni.org/post/ethiopia-report/) numerous news websites, LGBTQI, political opposition, and circumvention tool sites. As part of the 2018 political reforms, most of these sites have been [unblocked](https://ooni.org/post/ethiopia-unblocking/)."}} \ No newline at end of file +window.OONITranslations = {"en":{"General.OoniExplorer":"OONI Explorer","General.OK":"OK","General.Error":"Error","General.Anomaly":"Anomaly","General.Accessible":"Accessible","General.Failed":"Failed","General.Loading":"Loading…","General.NoData":"No data","General.Apply":"Apply","General.Reset":"Reset","General.Submit":"Submit","General.Close":"Close","General.Cancel":"Cancel","General.Login":"Login","General.Logout":"Logout","General.Edit":"Edit","Login.EnterEmail":"Add your email address and click the link sent to your email to log in.\n\nWe do not store email addresses.","Login.Submitted":"Your login request has been submitted. Please check your email for a link to activate and log in to your account.","Login.LoggingIn":"Logging in...","Login.Failure":"Try logging in again","Login.Success":"Successfully logged in. Redirecting back to the measurement...","SocialButtons.CTA":"Share on Facebook or Twitter","SocialButtons.Text":"Data from OONI Explorer","Tests.WebConnectivity.Name":"Web Connectivity Test","Tests.Telegram.Name":"Telegram Test","Tests.Facebook.Name":"Facebook Messenger Test","Tests.WhatsApp.Name":"WhatsApp Test","Tests.Signal.Name":"Signal Test","Tests.HTTPInvalidReqLine.Name":"HTTP Invalid Request Line Test","Tests.HTTPHeaderManipulation.Name":"HTTP Header Field Manipulation Test","Tests.NDT.Name":"NDT Speed Test","Tests.Dash.Name":"DASH Video Streaming Test","Tests.TorVanilla.Name":"Tor (Vanilla) Test","Tests.BridgeReachability.Name":"Tor Bridge Reachability Test","Tests.TCPConnect.Name":"TCP Connect Test","Tests.DNSConsistency.Name":"DNS Consistency Test","Tests.HTTPRequests.Name":"HTTP Requests Test","Tests.Psiphon.Name":"Psiphon Test","Tests.Tor.Name":"Tor Test","Tests.RiseupVPN.Name":"Riseup VPN Test","Tests.TorSnowflake.Name":"Tor Snowflake Test","Tests.DNSCheck.Name":"DNS Check","Tests.StunReachability.Name":"STUN Reachability","Tests.URLGetter.Name":"URL Getter","Tests.Groups.Webistes.Name":"Websites","Tests.Groups.Instant Messagging.Name":"Instant Messaging","Tests.Groups.Middlebox.Name":"Middleboxes","Tests.Groups.Performance.Name":"Performance","Tests.Groups.Circumvention.Name":"Circumvention","Tests.Groups.Experimental.Name":"Experimental","Tests.Groups.Legacy.Name":"Legacy","Measurement.MetaDescription":"OONI data suggests {description} on {formattedDate}, find more open data on internet censorship on OONI Explorer.","Measurement.NotFound":"Measurement not found","Measurement.Hero.Status.Confirmed":"Confirmed Blocked","Measurement.Hero.Status.Down":"Website Down","Measurement.Hero.Status.Anomaly.DNS":"DNS","Measurement.Hero.Status.Anomaly.HTTP":"HTTP","Measurement.Hero.Status.Anomaly.TCP":"TCP/IP","Measurement.CommonSummary.Label.ASN":"Network","Measurement.CommonSummary.Label.Country":"Country","Measurement.CommonSummary.Label.DateTime":"Date & Time","Measurement.DetailsHeader.Runtime":"Runtime","Measurement.Status.Hint.Websites.Censorship":"","Measurement.Status.Hint.Websites.DNS":"DNS tampering","Measurement.Status.Hint.Websites.Error":"Error in detection","Measurement.Status.Hint.Websites.HTTPdiff":"HTTP blocking (a blockpage might be served)","Measurement.Status.Hint.Websites.HTTPfail":"HTTP blocking (HTTP requests failed)","Measurement.Status.Hint.Websites.NoCensorship":"No blocking detected","Measurement.Status.Hint.Websites.TCPBlock":"TCP/IP blocking","Measurement.Status.Hint.Websites.Unavailable":"Website down","Measurement.SummaryText.Websites.Accessible":"On {date}, {WebsiteURL} was accessible when tested on {network} in {country}.","Measurement.SummaryText.Websites.Anomaly":"On {date}, {WebsiteURL} presented signs of {reason} on {network} in {country}.\n\nThis might mean that {WebsiteURL} was blocked, but false positives can occur.\n\nPlease explore the network measurement data below.","Measurement.SummaryText.Websites.Anomaly.BlockingReason.DNS":"DNS tampering","Measurement.SummaryText.Websites.Anomaly.BlockingReason.TCP":"TCP/IP blocking","Measurement.SummaryText.Websites.Anomaly.BlockingReason.HTTP-failure":"HTTP blocking (HTTP requests failed)","Measurement.SummaryText.Websites.Anomaly.BlockingReason.HTTP-diff":"HTTP blocking (a blockpage might be served)","Measurement.SummaryText.Websites.ConfirmedBlocked":"On {date}, {WebsiteURL} was blocked on {network} in {country}.\n\nThis is confirmed because a block page was served, as illustrated through the network measurement data below.","Measurement.SummaryText.Websites.Failed":"On {date}, the test for {WebsiteURL} failed on {network} in {country}.","Measurement.SummaryText.Websites.Down":"On {date}, {WebsiteURL} was down on {network} in {country}.","Measurement.Details.Websites.Failures.Heading":"Failures","Measurement.Details.Websites.Failures.Label.HTTP":"HTTP Experiment","Measurement.Details.Websites.Failures.Label.DNS":"DNS Experiment","Measurement.Details.Websites.Failures.Label.Control":"Control","Measurement.Details.Websites.Failures.Values.Null":"null","Measurement.Details.Websites.DNSQueries.Heading":"DNS Queries","Measurement.Details.Websites.DNSQueries.Label.Resolver":"Resolver","Measurement.Details.Websites.TCP.Heading":"TCP Connections","Measurement.Details.Websites.TCP.ConnectionTo":"Connection to {destination} {connectionStatus}.","Measurement.Details.Websites.TCP.ConnectionTo.Success":"succeeded","Measurement.Details.Websites.TCP.ConnectionTo.Failed":"failed","Measurement.Details.Websites.TCP.ConnectionTo.Blocked":"was blocked","Measurement.Details.Websites.HTTP.Heading":"HTTP Requests","Measurement.Details.Websites.HTTP.Label.Response":"Response","Measurement.Details.Websites.HTTP.Request.URL":"URL","Measurement.Details.Websites.HTTP.Response.Body":"Response Body","Measurement.Details.Websites.HTTP.Response.Headers":"Response Headers","Measurement.CommonDetails.Label.MsmtID":"Report ID","Measurement.CommonDetails.Label.Platform":"Platform","Measurement.CommonDetails.Label.Software":"Software Name","Measurement.CommonDetails.Label.Engine":"Measurement Engine","Measurement.CommonDetails.Value.Unavailable":"Unavailable","Measurement.CommonDetails.RawMeasurement.Heading":"Raw Measurement Data","Measurement.CommonDetails.RawMeasurement.Download":"Download JSON","Measurement.CommonDetails.RawMeasurement.Unavailable":"Unavailable","Measurement.CommonDetails.RawMeasurement.Expand":"Expand All","Measurement.CommonDetails.Label.Resolver":"Resolver","Measurement.CommonDetails.Label.ResolverASN":"Resolver ASN","Measurement.CommonDetails.Label.ResolverIP":"Resolver IP","Measurement.CommonDetails.Label.ResolverNetworkName":"Resolver Network Name","Measurement.CommonDetails.Label.UserFeedback":"User Feedback","Measurement.Hero.Status.NDT.Title":"Results","Measurement.Status.Info.Label.Download":"Download","Measurement.Status.Info.Label.Upload":"Upload","Measurement.Status.Info.Label.Ping":"Ping","Measurement.Status.Info.Label.Server":"Server","Measurement.Status.Info.NDT.Error":"Failed Test","Measurement.Details.Performance.Heading":"Performance Details","Measurement.Details.Performance.Label.AvgPing":"Average Ping","Measurement.Details.Performance.Label.MaxPing":"Max Ping","Measurement.Details.Performance.Label.MSS":"MSS","Measurement.Details.Performance.Label.RetransmitRate":"Retransmission Rate","Measurement.Details.Performance.Label.PktLoss":"Packet Loss","Measurement.Details.Performance.Label.OutOfOrder":"Out of Order","Measurement.Details.Performance.Label.Timeouts":"Timeouts","Measurement.Hero.Status.Dash.Title":"Results","Measurement.Status.Info.Label.VideoQuality":"Video Quality","Measurement.Status.Info.Label.Bitrate":"Median Bitrate","Measurement.Status.Info.Label.Delay":"Playout Delay","Measurement.Status.Hint.Telegram.Blocked":"Telegram is likely blocked","Measurement.Status.Hint.Telegram.Reachable":"Telegram is accessible","Measurement.Status.Hint.Telegram.Failed":"The Telegram test failed","Measurement.Details.SummaryText.Telegram.Reachable":"On {date}, Telegram was reachable on {network} in {country}. \n\nOONI's Telegram test successfully connected to Telegram's endpoints and web interface (web.telegram.org).","Measurement.Details.SummaryText.Telegram.AppFailure":"On {date}, the testing of Telegram's mobile app presented signs of blocking on {network} in {country}.\n\nThis might mean that Telegram's mobile app was blocked, but [false positives](https://ooni.org/support/faq/#why-do-false-positives-occur) can occur. \n\nPlease explore the network measurement data below. Check other Telegram measurements from the same network during the same time period (if they're available).","Measurement.Details.SummaryText.Telegram.DesktopFailure":"On {date}, the testing of Telegram's web interface (web.telegram.org) presented signs of blocking on {network} in {country}.\n\nThis might mean that web.telegram.org was blocked, but [false positives](https://ooni.org/support/faq/#why-do-false-positives-occur) can occur.\n\nPlease explore the network measurement data below. Check other Telegram measurements from the same network during the same time period (if they're available).","Measurement.Details.SummaryText.Telegram.DesktopAndAppFailure":"On {date}, the testing of Telegram's mobile app and web interface (web.telegram.org) presented signs of blocking on {network} in {country}.\n\nThis might mean that both Telegram's mobile app and web interface were blocked, but [false positives](https://ooni.org/support/faq/#why-do-false-positives-occur) can occur.\n\nPlease explore the network measurement data below. Check other Telegram measurements from the same network during the same time period (if they're available).","Measurement.Details.Telegram.Endpoint.Label.Mobile":"Mobile App","Measurement.Details.Telegram.Endpoint.Label.Web":"Telegram Web","Measurement.Details.Endpoint.Status.Unknown":"Unknown","Measurement.Details.Telegram.Endpoint.Status.Heading":"Endpoint Status","Measurement.Details.Telegram.Endpoint.ConnectionTo.Failed":"Connection to {destination} failed.","Measurement.Details.Telegram.Endpoint.ConnectionTo.Successful":"Connection to {destination} was successful.","Measurement.Details.Hint.WhatsApp.Reachable":"WhatsApp is accessible","Measurement.Status.Hint.WhatsApp.Blocked":"WhatsApp is likely blocked","Measurement.Status.Hint.WhatsApp.Failed":"The WhatsApp test failed","Measurement.Details.SummaryText.WhatsApp.Reachable":"On {date}, WhatsApp was reachable on {network} in {country}. \n\nOONI's WhatsApp test successfully connected to WhatsApp's endpoints, registration service and web interface (web.whatsapp.com).","Measurement.Details.SummaryText.WhatsApp.AppFailure":"On {date}, the testing of WhatsApp's mobile app presented signs of blocking on {network} in {country}.\n\nThis might mean that WhatsApp's mobile app was blocked, but [false positives](https://ooni.org/support/faq/#why-do-false-positives-occur) can occur. \n\nPlease explore the network measurement data below. Check other WhatsApp measurements from the same network during the same time period (if they're available).","Measurement.Details.SummaryText.WhatsApp.DesktopFailure":"On {date}, the testing of WhatsApp's web interface (web.whatsapp.com) presented signs of blocking on {network} in {country}.\n\nThis might mean that web.whatsapp.com was blocked, but [false positives](https://ooni.org/support/faq/#why-do-false-positives-occur) can occur.\n\nPlease explore the network measurement data below. Check other WhatsApp measurements from the same network during the same time period (if they're available).","Measurement.Details.SummaryText.WhatsApp.DesktopAndAppFailure":"On {date}, the testing of WhatsApp's mobile app and web interface (web.whatsapp.com) presented signs of blocking on {network} in {country}.\n\nThis might mean that both WhatsApp's mobile app and web interface were blocked, but [false positives](https://ooni.org/support/faq/#why-do-false-positives-occur) can occur.\n\nPlease explore the network measurement data below. Check other WhatsApp measurements from the same network during the same time period (if they're available).","Measurement.Details.WhatsApp.Endpoint.Label.Mobile":"Mobile App","Measurement.Details.WhatsApp.Endpoint.Label.Web":"WhatsApp Web","Measurement.Details.WhatsApp.Endpoint.Label.Registration":"Registration","Measurement.Details.WhatsApp.Endpoint.Status.Heading":"Endpoint Status","Measurement.Details.WhatsApp.Endpoint.ConnectionTo.Failed":"Connection to {destination} failed.","Measurement.Details.WhatsApp.Endpoint.ConnectionTo.Successful":"Connection to {destination} was successful.","Measurement.Status.Hint.FacebookMessenger.Reachable":"Facebook Messenger is accessible","Measurement.Status.Hint.FacebookMessenger.Blocked":"Facebook Messenger is likely blocked","Measurement.Status.Hint.FacebookMessenger.Failed":"The Facebook Messenger test failed","Measurement.Details.SummaryText.FacebookMessenger.Reachable":"On {date}, Facebook Messenger was reachable on {network} in {country}.","Measurement.Details.SummaryText.FacebookMessenger.TCPFailure":"TCP connections to Facebook's endpoints failed.","Measurement.Details.SummaryText.FacebookMessenger.DNSFailure":"DNS lookups did not resolve to Facebook IP addresses.","Measurement.Details.SummaryText.FacebookMessenger.DNSSuccess":"DNS lookups resolved to Facebook IP addresses.","Measurement.Details.SummaryText.FacebookMessenger.TCPSuccess":"TCP connections to Facebook's enpoints succeeded.","Measurement.Details.FacebookMessenger.TCP.Label.Title":"TCP connections","Measurement.Details.FacebookMessenger.DNS.Label.Title":"DNS lookups","Measurement.Details.FacebookMessenger.TCPFailed":"TCP connections failed","Measurement.Details.FacebookMessenger.DNSFailed":"DNS lookups failed","Measurement.Details.FacebookMessenger.Endpoint.Status.Heading":"Endpoint Status","Measurement.Details.FacebookMessenger.Endpoint.ConnectionTo.Failed":"Connection to {destination} failed.","Measurement.Details.FacebookMessenger.Endpoint.ConnectionTo.Successful":"Connection to {destination} was successful.","Measurement.Status.Hint.Signal.Blocked":"Signal is likely blocked","Measurement.Status.Hint.Signal.Reachable":"Signal is accessible","Measurement.Status.Hint.Signal.Failed":"The Signal test failed","Measurement.Details.SummaryText.Signal.Reachable":"On {date}, [Signal](https://signal.org/) was reachable on {network} in {country}. \n\nThe [OONI Probe Signal test](https://ooni.org/nettest/signal) successfully connected to Signal's endpoints.","Measurement.Details.SummaryText.Signal.Blocked":"On {date}, the testing of the [Signal app](https://signal.org/) presented signs of blocking on {network} in {country}.\n\nThis might mean that Signal was blocked, but [false positives](https://ooni.org/support/faq/#why-do-false-positives-occur) can occur. \n\nPlease explore the network measurement data below. Check other Signal measurements from the same network during the same time period (if they're available).","Measurement.Hero.Status.HTTPHeaderManipulation.NoMiddleBoxes":"No middleboxes detected","Measurement.HTTPHeaderManipulation.NoMiddleBoxes.SummaryText":"On {date}, no network anomaly was detected on {network} in {country} when communicating with our servers.","Measurement.Hero.Status.HTTPHeaderManipulation.MiddleboxesDetected":"Network tampering","Measurement.HTTPHeaderManipulation.MiddleBoxesDetected.SummaryText":"On {date}, network traffic was manipulated when contacting our control servers. \n\nThis means that there might be a middlebox on {network} in {country}, which could be responsible for censorship and/or surveillance.","Measurement.Hero.Status.HTTPInvalidReqLine.NoMiddleBoxes":"No middleboxes detected","Measurement.HTTPInvalidReqLine.NoMiddleBoxes.SummaryText":"On {date}, no network anomaly was detected on {network} in {country} when communicating with our servers.","Measurement.Hero.Status.HTTPInvalidReqLine.MiddleboxesDetected":"Network tampering","Measurement.HTTPInvalidReqLine.MiddleboxesDetected.SummaryText":"On {date}, network traffic was manipulated when contacting our control servers. \n\nThis means that there might be a middlebox on {network} in {country}, which could be responsible for censorship and/or surveillance.","Measurement.HTTPInvalidReqLine.YouSent":"You Sent","Measurement.HTTPInvalidReqLine.YouReceived":"You Received","Measurement.Hero.Status.TorVanilla.Blocked":"Tor is likely blocked","Measurement.Hero.Status.TorVanilla.Reachable":"Tor is accessible","Measurement.Details.SummaryText.TorVanilla.Blocked":"On {date}, OONI's Vanilla Tor test did not manage to bootstrap a connection to the [Tor network](https://www.torproject.org/).\n\nThis might mean that access to the Tor network was blocked on {network} in {country}, but [false positives can occur](https://ooni.org/support/faq/#why-do-false-positives-occur).\n\nPlease explore the network measurement data below. Check other Tor measurements from the same network during the same time period (if they're available).","Measurement.Details.SummaryText.TorVanilla.Reachable":"OONI's Vanilla Tor test successfully bootstrapped a connection to the [Tor network](https://www.torproject.org/).\n\nThis means that the Tor network was reachable from {network} in {country} on {date}.","Measurement.Details.VanillaTor.Endpoint.Label.Reachability":"Reachability","Measurement.Status.Hint.Psiphon.Reachable":"Psiphon works","Measurement.Status.Hint.Psiphon.Blocked":"Psiphon is likely blocked","Measurement.Status.Hint.Psiphon.BootstrappingError":"Psiphon is likely blocked (bootstrap error)","Measurement.Details.SummaryText.Psiphon.OK":"On {date}, [Psiphon](https://psiphon.ca/) worked on {network} in {country}.\n\nThe [OONI Probe Psiphon test](https://ooni.org/nettest/psiphon/) was able to successfully bootstrap Psiphon and ensure that the app works.","Measurement.Details.SummaryText.Psiphon.Blocked":"On {date}, [Psiphon](https://psiphon.ca/) did not appear to work on {network} in {country}.\n\nWhile the [OONI Probe Psiphon test](https://ooni.org/nettest/psiphon/) was able to bootstrap Psiphon, it was unable to fetch a webpage from the internet. \n\nThis suggests that the Psiphon app may have been blocked on this network. \n\nHowever, [false positives](https://ooni.org/support/faq/#why-do-false-positives-occur) can occur. Please explore the network measurement data below and compare it with other relevant measurements (if they're available).","Measurement.Details.SummaryText.Psiphon.BootstrappingError":"On {date}, [Psiphon](https://psiphon.ca/) did not work on {network} in {country}.\n\nThe [OONI Probe Psiphon test](https://ooni.org/nettest/psiphon/) was unable to bootstrap Psiphon.\n\nThis suggests that the Psiphon app may have been blocked on this network.\n\nHowever, [false positives](https://ooni.org/support/faq/#why-do-false-positives-occur) can occur. Please explore the network measurement data below and compare it with other relevant measurements (if they're available).","Measurement.Details.Psiphon.BootstrapTime.Label":"Bootstrap Time","Measurement.Status.Hint.Tor.Reachable":"Tor works","Measurement.Status.Hint.Tor.Blocked":"Tor is likely blocked","Measurement.Status.Hint.Tor.Error":"Tor test failed","Measurement.Details.SummaryText.Tor.OK":"On {date}, [Tor](https://www.torproject.org/) worked on {network} in {country}.\n\nAs part of [OONI Probe Tor testing](https://ooni.org/nettest/tor/), all reachability measurements of selected Tor directory authorities and bridges were successful.","Measurement.Details.SummaryText.Tor.Blocked":"On {date}, [Tor](https://www.torproject.org/) did not appear to work on {network} in {country}.\n\nThe [OONI Probe Tor test](https://ooni.org/nettest/tor/) failed in performing certain measurements. More details are available through the network measurement data provided below.\n\nThis suggests that Tor may have been blocked on this network.\n\nHowever, [false positives](https://ooni.org/support/faq/#why-do-false-positives-occur) can occur.","Measurement.Details.SummaryText.Tor.Error":"On {date}, the Tor test failed on {network} in {country}.","Measurement.Details.Tor.Bridges.Label.Title":"Tor Browser Bridges","Measurement.Details.Tor.Bridges.Label.OK":"{bridgesAccessible}/{bridgesTotal} OK","Measurement.Details.Tor.DirAuth.Label.Title":"Tor Directory Authorities","Measurement.Details.Tor.DirAuth.Label.OK":"{dirAuthAccessible}/{dirAuthTotal} OK","Measurement.Details.Tor.Table.Header.Name":"Name","Measurement.Details.Tor.Table.Header.Address":"Address","Measurement.Details.Tor.Table.Header.Type":"Type","Measurement.Status.Hint.TorSnowflake.Reachable":"Tor Snowflake works","Measurement.Status.Hint.TorSnowflake.Blocked":"Tor Snowflake does not work","Measurement.Status.Hint.TorSnowflake.Error":"Tor Snowflake test failed","Measurement.Details.SummaryText.TorSnowflake.OK":"On {date}, [Tor Snowflake](https://www.torproject.org/) worked on {network} in {country}.\n\nThe [OONI Probe Tor Snowflake test](https://ooni.org/nettest/torsf/) was able to successfully bootstrap Snowflake.","Measurement.Details.SummaryText.TorSnowflake.Blocked":"On {date}, [Tor Snowflake](https://www.torproject.org/) does not work on {network} in {country}.\n\nThe [OONI Probe Tor Snowflake test](https://ooni.org/nettest/torsf/) failed to bootstrap Snowflake.","Measurement.Details.SummaryText.TorSnowflake.Error":"On {date}, the Tor Snowflake test failed on {network} in {country}.","Measurement.Details.TorSnowflake.BootstrapTime.Label":"Bootstrap Time","Measurement.Details.TorSnowflake.Error.Label":"Failure","Measurement.Metadata.TorSnowflake.Reachable":"Tor Snowflake was reachable in {country}","Measurement.Metadata.TorSnowflake.UnReachable":"Tor Snowflake was NOT reachable in {country}","Measurement.Metadata.TorSnowflake.Error":"Tor Snowflake test failed in {country}","Measurement.Status.Hint.RiseupVPN.Reachable":"RiseupVPN works","Measurement.Status.Hint.RiseupVPN.Blocked":"RiseupVPN is likely blocked","Measurement.Status.Hint.RiseupVPN.Failed":"The RiseupVPN test failed","Measurement.Details.SummaryText.RiseupVPN.OK":"On {date}, [RiseupVPN](https://riseup.net/vpn) was reachable on {network} in {country}. \n\nThe [OONI Probe RiseupVPN test](https://ooni.org/nettest/riseupvpn/) successfully connected to RiseupVPN's bootstrap servers and gateways.","Measurement.Details.SummaryText.RiseupVPN.Blocked":"On {date}, the testing of [RiseupVPN](https://riseup.net/vpn) presented signs of blocking on {network} in {country}.\n\nThis might mean that RiseupVPN was blocked, but [false positives](https://ooni.org/support/faq/#why-do-false-positives-occur) can occur. \n\nPlease explore the network measurement data below. Check other RiseupVPN measurements from the same network during the same time period (if they're available).","Measurement.Metadata.RiseupVPN.Reachable":"RiseupVPN was reachable in {country}","Measurement.Metadata.RiseupVPN.Blocked":"RiseupVPN was not reachable in {country}","Measurement.Hero.Status.Default":"Measurement Report","Measurement.Feedback.Title":"Verify the measurement","Measurement.Feedback.Description":"Please share feedback based on what you see in the measurement data.","Measurement.Feedback.ok":"It's OK","Measurement.Feedback.down":"It's down","Measurement.Feedback.down.unreachable":"It's unreachable","Measurement.Feedback.down.misconfigured":"It's misconfigured","Measurement.Feedback.blocked":"It's blocked","Measurement.Feedback.blocked.tcp":"TCP/IP blocking","Measurement.Feedback.blocked.tls":"TLS blocking","Measurement.Feedback.blocked.blockpage":"Block page","Measurement.Feedback.blocked.blockpage.http":"HTTP block page","Measurement.Feedback.blocked.blockpage.dns":"DNS block page","Measurement.Feedback.blocked.blockpage.server_side":"Server-side block page","Measurement.Feedback.blocked.blockpage.server_side.captcha":"CAPTCHA","Measurement.Feedback.blocked.dns":"DNS blocking without block page","Measurement.Feedback.blocked.dns.inconsistent":"Inconsistent DNS response","Measurement.Feedback.blocked.dns.nxdomain":"NXDOMAIN error","Measurement.Feedback.Success.Title":"Thank you!","Measurement.Feedback.Success.Description":"Your feedback will help improve OONI measurements.","Measurement.Feedback.Failure":"Something went wrong, please try again.","Measurement.Feedback.Login.Title":"Please log in to continue","Measurement.Feedback.Login.Confirmation.Title":"Login link sent","Measurement.Feedback.Login.Confirmation.Text":"Please check your email for a link to log in to your account.","Measurement.Feedback.Login.Description":"We will send a link to your email. We do not store emails.","Measurement.Feedback.ExistingFeedback":"Your previous feedback","Navbar.Search":"Search","Navbar.Countries":"Countries","Navbar.Charts.Circumvention":"Circumvention Charts","Navbar.Charts.MAT":"MAT Charts","Network.Summary.TotalMeasurements":"Total number of measurements: **{measurementsTotal}**","Network.Summary.FirstMeasurement":"Date of the first measurement: **{formattedDate}**","Network.Summary.Countries":"Network observed in countries:","Network.Summary.Country.Measurements":"({measurementsTotal} measurements)","Network.NoData.Title":"Let's collect more data!","Network.NoData.Text":"We don't have enough data for this network to show the charts. Please run OONI Probe to collect more measurements.","Footer.Text.Slogan":"Global community measuring internet censorship around the world.","Footer.Heading.About":"About","Footer.Heading.OONIProbe":"OONI Probe","Footer.Heading.Updates":"Updates","Footer.Link.About":"OONI","Footer.Link.DataPolicy":"Data Policy","Footer.Link.DataLicense":"Data License","Footer.Link.Contact":"Contact","Footer.Link.Probe":"Install","Footer.Link.Tests":"Tests","Footer.Link.Code":"Source code","Footer.Link.API":"API","Footer.Link.Blog":"Blog","Footer.Link.Twitter":"Twitter","Footer.Link.MailingList":"Mailing list","Footer.Link.Slack":"Slack","Footer.Text.Copyright":"© {currentYear} Open Observatory of Network Interference (OONI)","Footer.Text.CCommons":"Content available under a Creative Commons license.","Footer.Text.Version":"Version","CategoryCode.ALDR.Name":"Alcohol & Drugs","CategoryCode.REL.Name":"Religion","CategoryCode.PORN.Name":"Pornography","CategoryCode.PROV.Name":"Provocative Attire","CategoryCode.POLR.Name":"Political Criticism","CategoryCode.HUMR.Name":"Human Rights Issues","CategoryCode.ENV.Name":"Environment","CategoryCode.MILX.Name":"Terrorism and Militants","CategoryCode.HATE.Name":"Hate Speech","CategoryCode.NEWS.Name":"News Media","CategoryCode.XED.Name":"Sex Education","CategoryCode.PUBH.Name":"Public Health","CategoryCode.GMB.Name":"Gambling","CategoryCode.ANON.Name":"Anonymization and circumvention tools","CategoryCode.DATE.Name":"Online Dating","CategoryCode.GRP.Name":"Social Networking","CategoryCode.LGBT.Name":"LGBTQ+","CategoryCode.FILE.Name":"File-sharing","CategoryCode.HACK.Name":"Hacking Tools","CategoryCode.COMT.Name":"Communication Tools","CategoryCode.MMED.Name":"Media sharing","CategoryCode.HOST.Name":"Hosting and Blogging Platforms","CategoryCode.SRCH.Name":"Search Engines","CategoryCode.GAME.Name":"Gaming","CategoryCode.CULTR.Name":"Culture","CategoryCode.ECON.Name":"Economics","CategoryCode.GOVT.Name":"Government","CategoryCode.COMM.Name":"E-commerce","CategoryCode.CTRL.Name":"Control content","CategoryCode.IGO.Name":"Intergovernmental Organizations","CategoryCode.MISC.Name":"Miscellaneous content","CategoryCode.ALDR.Description":"Sites devoted to the use, paraphernalia, and sale of drugs and alcohol irrespective of the local legality.","CategoryCode.REL.Description":"Sites devoted to discussion of religious issues, both supportive and critical, as well as discussion of minority religious groups.","CategoryCode.PORN.Description":"Hard-core and soft-core pornography.","CategoryCode.PROV.Description":"Websites which show provocative attire and portray women in a sexual manner, wearing minimal clothing.","CategoryCode.POLR.Description":"Content that offers critical political viewpoints. Includes critical authors and bloggers, as well as oppositional political organizations. Includes pro-democracy content, anti-corruption content as well as content calling for changes in leadership, governance issues, legal reform, etc.","CategoryCode.HUMR.Description":"Sites dedicated to discussing human rights issues in various forms. Includes women's rights and rights of minority ethnic groups.","CategoryCode.ENV.Description":"Pollution, international environmental treaties, deforestation, environmental justice, disasters, etc.","CategoryCode.MILX.Description":"Sites promoting terrorism, violent militant or separatist movements.","CategoryCode.HATE.Description":"Content that disparages particular groups or persons based on race, sex, sexuality or other characteristics.","CategoryCode.NEWS.Description":"This category includes major news outlets (BBC, CNN, etc.) as well as regional news outlets and independent media.","CategoryCode.XED.Description":"Includes contraception, abstinence, STDs, healthy sexuality, teen pregnancy, rape prevention, abortion, sexual rights, and sexual health services.","CategoryCode.PUBH.Description":"HIV, SARS, bird flu, centers for disease control, World Health Organization, etc.","CategoryCode.GMB.Description":"Online gambling sites. Includes casino games, sports betting, etc.","CategoryCode.ANON.Description":"Sites that provide tools used for anonymization, circumvention, proxy-services and encryption.","CategoryCode.DATE.Description":"Online dating services which can be used to meet people, post profiles, chat, etc.","CategoryCode.GRP.Description":"Social networking tools and platforms.","CategoryCode.LGBT.Description":"A range of gay-lesbian-bisexual-transgender queer issues. (Excluding pornography)","CategoryCode.FILE.Description":"Sites and tools used to share files, including cloud-based file storage, torrents and P2P file-sharing tools.","CategoryCode.HACK.Description":"Sites dedicated to computer security, including news and tools. Includes malicious and non-malicious content.","CategoryCode.COMT.Description":"Sites and tools for individual and group communications. Includes webmail, VoIP, instant messaging, chat and mobile messaging applications.","CategoryCode.MMED.Description":"Video, audio or photo sharing platforms.","CategoryCode.HOST.Description":"Web hosting services, blogging and other online publishing platforms.","CategoryCode.SRCH.Description":"Search engines and portals.","CategoryCode.GAME.Description":"Online games and gaming platforms, excluding gambling sites.","CategoryCode.CULTR.Description":"Content relating to entertainment, history, literature, music, film, books, satire and humour.","CategoryCode.ECON.Description":"General economic development and poverty related topics, agencies and funding opportunities.","CategoryCode.GOVT.Description":"Government-run websites, including military sites.","CategoryCode.COMM.Description":"Websites of commercial services and products.","CategoryCode.CTRL.Description":"Benign or innocuous content used as a control.","CategoryCode.IGO.Description":"Websites of intergovernmental organizations such as the United Nations.","CategoryCode.MISC.Description":"Sites that don't fit in any category. (XXX Things in here should be categorised)","Country.Heading.Overview":"Overview","Country.Heading.Websites":"Websites","Country.Heading.Apps":"Apps","Country.Heading.Shutdowns":"Shutdowns","Country.Heading.NetworkProperties":"Networks","Country.Overview.Heading.NwInterference":"In a nutshell","Country.Overview.NwInterference.Middleboxes.Blocked":"Middleboxes were detected on {middleboxCount} network(s)","Country.Overview.NwInterference.Middleboxes.Normal":"No middleboxes were detected on tested networks","Country.Overview.NwInterference.Middleboxes.NoData":"Not enough data available on middleboxes","Country.Overview.NwInterference.IM.Blocked":"Instant messaging apps were likely blocked","Country.Overview.NwInterference.IM.Normal":"No instant messaging apps were blocked on tested networks","Country.Overview.NwInterference.IM.NoData":"Not enough data available on instant messaging apps","Country.Overview.NwInterference.CircumventionTools.Blocked":"Circumvention tools were likely blocked","Country.Overview.NwInterference.CircumventionTools.Normal":"No circumvention tools were blocked on tested networks","Country.Overview.NwInterference.CircumventionTools.NoData":"Not enough data available on circumvention tools","Country.Overview.NwInterference.Websites.Blocked":"OONI data confirms the blocking of websites","Country.Overview.NwInterference.Websites.Normal":"The blocking of websites is not confirmed","Country.Overview.NwInterference.Websites.NoData":"Not enough data available on blocked websites","Country.Overview.Heading.TestsByClass":"Measurement Coverage","Country.Overview.Heading.TestsByClass.Description":"The graph below provides an overview of OONI Probe measurement coverage. It shows how many results have been collected from each OONI Probe test category, as well as how many networks have been covered by tests. \n\nBy looking at this graph, you can understand if there is enough data to draw meaningful conclusions. If there is not enough data and you are in the country in question, [install OONI Probe](https://ooni.org/install), run tests, and contribute data!","Country.Overview.TestsByClass.Websites":"Websites","Country.Overview.TestsByClass.InstantMessaging":"Instant Messaging","Country.Overview.TestsByClass.Performance":"Performance","Country.Overview.TestsByClass.Middleboxes":"Middleboxes","Country.Overview.TestsByClass.Circumvention":"Circumvention Tools","Country.Overview.FeaturedResearch":"Research Reports","Country.Overview.FeaturedResearch.None":"We haven't published a research report based on OONI data from this country yet. \n\nWe encourage you to use OONI data for your research!","Country.Overview.SummaryTextTemplate":"OONI Probe users in **{countryName}** have collected [**{measurementCount}** measurements]({linkToMeasurements}) from **{networkCovered}** local networks.\n\nExplore the data below to check the accessibility and/or blocking of sites and services.","Country.Overview.NoData.Title":"Let's collect more data!","Country.Overview.NoData.CallToAction":"We don’t have enough measurements for **{country}** to show a chart. If you are in {country} or know people there, tell them to run OONI Probe to collect more measurements.","Country.Overview.NoData.Button.InstallProbe":"Install OONI Probe","Country.Overview.NoData.Button.OoniRunLink":"Create OONI Run Link","Country.Meta.Title":"Internet Censorship in {countryName} - OONI Explorer","Country.Meta.Description":"OONI Probe users in {countryName} have collected {measurementCount} measurements from {networkCount} local networks. Explore the data on OONI Explorer.","Country.PeriodFilter.Label":"Show results from","Country.PeriodFilter.Option.30Days":"Last 30 Days","Country.PeriodFilter.Option.2Months":"Last 2 Months","Country.PeriodFilter.Option.3Months":"Last 3 Months","Country.PeriodFilter.Option.6Months":"Last 6 Months","Country.Websites.Description":"Check whether websites have been blocked.\n\nTesting methodology: OONI's [Web Connectivity test](https://ooni.org/nettest/web-connectivity/), designed to measure the DNS, HTTP, and TCP/IP blocking of websites. \n\nTested websites: [Citizen Lab test lists](https://github.com/citizenlab/test-lists)\n\nIf you'd like to see results on the testing of different websites, please [contribute to test lists](https://ooni.org/get-involved/contribute-test-lists/) or test the sites of your choice via the [OONI Probe mobile app](https://ooni.org/install/). \n\nPlease note that unless a block page is served, some anomalous measurements may contain [false positives](https://ooni.org/support/faq/#why-do-false-positives-occur). We therefore encourage you to examine anomalous measurements in depth and over time.","Country.Websites.Description.MoreLinkText":"","Country.Websites.Heading.BlockedByCategory":"Categories of Blocked Websites","Country.Websites.BlockedByCategory.Description":"Websites that fall under the following categories were blocked on the {selectedASN} network.","Country.Websites.TestedWebsitesCount":"URLs tested","Country.Websites.Labels.ResultsPerPage":"Results per page","Country.Websites.URLSearch.Placeholder":"Search for URL","Country.Websites.URLCharts.Legend.Label.Blocked":"Confirmed Blocked","Country.Websites.URLCharts.ExploreMoreMeasurements":"Explore more measurements","Country.Websites.URLCharts.Pagination.Previous":"Previous Page","Country.Websites.URLCharts.Pagination.Next":"Next Page","Country.Apps.Description":"Check whether instant messaging apps and circumvention tools are blocked.\n\nThe following results were collected through the use of [OONI Probe tests](https://ooni.org/nettest/) designed to measure the blocking of WhatsApp, Facebook Messenger, Telegram and Signal. \n\nWe also share results on the testing of circumvention tools, like [Tor](https://www.torproject.org/) and [Psiphon](https://psiphon.ca/).","Country.Apps.Label.LastTested":"Last tested","Country.Apps.Label.TestedNetworks":"tested networks","Country.Apps.Button.ShowMore":"Show More","Country.Shutdowns":"Internet shutdowns","Country.Shutdowns.Description":"Monitor Internet shutdowns through public, third-party data sources. \n\nThe following graph shares data from Georgia Tech's [Internet Outage Detection and Analysis (IODA)](https://ioda.inetintel.cc.gatech.edu/) project, [Google Transparency Reports (Google traffic data)](https://transparencyreport.google.com/traffic/overview?hl=en), and [Cloudflare Radar](https://radar.cloudflare.com/).\n\nIf you notice a drop in all signals, that might serve as an indicator of an Internet shutdown. \n\nLearn more about Internet shutdowns and their impact through the [#KeepItOn campaign](https://www.accessnow.org/keepiton/).","Country.NetworkProperties.Description":"Check the speed and performance of networks.\n\nThe following results were collected through the use of [OONI Probe's performance and middlebox tests](https://ooni.org/nettest/). You can check the speed and performance of tested networks, as well as video streaming performance. \n\nYou can also learn whether middleboxes were detected in tested networks. Middleboxes are network appliances that can be used for a variety of networking purposes (such as caching), but sometimes they're used to implement internet censorship and/or surveillance.","Country.NetworkProperties.Heading.Summary":"Summary","Country.NetworkProperties.Heading.Networks":"Networks","Country.NetworkProperties.InfoBox.Label.AverageDownload":"Average Download","Country.NetworkProperties.InfoBox.Label.AverageUpload":"Average Upload","Country.NetworkProperties.InfoBox.Label.Covered":"Covered","Country.NetworkProperties.InfoBox.Label.Middleboxes":"Middleboxes detected","Country.NetworkProperties.InfoBox.Units.Mbits":"Mbit/s","Country.NetworkProperties.InfoBox.Units.Networks.Singular":"Network","Country.NetworkProperties.InfoBox.Units.Networks.Plural":"Networks","Country.NetworkProperties.InfoBox.Label.AverageStreaming":"Average Streaming","Country.NetworkProperties.InfoBox.Label.AveragePing":"Average Ping","Country.NetworkProperties.InfoBox.Units.Milliseconds":"ms","Country.NetworkProperties.InfoBox.Label.Middleboxes.Found":"Middleboxes detected","Country.NetworkProperties.InfoBox.Label.Middleboxes.NotFound":"No middleboxes detected","Country.NetworkProperties.Button.ShowMore":"Show more networks","Country.Label.NoData":"No Data Available","Search.PageTitle":"Search through millions of Internet censorship measurements","Search.Sidebar.Domain":"Domain","Search.Sidebar.Domain.Placeholder":"e.g. twitter.com or 1.1.1.1","Search.Sidebar.Domain.Error":"Please enter a valid domain name or IP address, such as twitter.com or 1.1.1.1","Search.Sidebar.Input":"Input","Search.Sidebar.Input.Placeholder":"e.g., https://twitter.com/","Search.Sidebar.Input.Error":"Please enter a full URL (e.g. https://twitter.com/) with a trailing slash (`/`) or an IP address","Search.Sidebar.Categories":"Website Categories","Search.Sidebar.Categories.All":"Any","Search.Sidebar.Status":"Status","Search.Sidebar.TestName":"Test Name","Search.Sidebar.TestName.AllTests":"Any","Search.Sidebar.Country":"Country","Search.Sidebar.Country.AllCountries":"Any","Search.Sidebar.ASN":"ASN","Search.Sidebar.ASN.example":"e.g. AS30722","Search.Sidebar.ASN.Error":"Valid formats: AS1234, 1234","Search.Sidebar.From":"From","Search.Sidebar.Until":"Until","Search.Sidebar.HideFailed":"Hide failed measurements","Search.Sidebar.Button.FilterResults":"Filter Results","Search.FilterButton.AllResults":"All Results","Search.FilterButton.Confirmed":"Confirmed","Search.FilterButton.Anomalies":"Anomalies","Search.FilterButton.Search":"Search","Search.Filter.SortBy":"Sort by","Search.Filter.SortBy.Date":"Date","Search.WebConnectivity.Results.Blocked":"Confirmed","Search.HTTPRequests.Results.Anomaly":"","Search.HTTPRequests.Results.Blocked":"","Search.HTTPRequests.Results.Error":"","Search.HTTPRequests.Results.Reachable":"","Search.NDT.Results":"","Search.DASH.Results":"","Search.VanillaTor.Results":"","Search.BridgeReachability.Results":"","Search.LegacyTests.Results":"","Search.Results.Empty.Heading":"No Results Found","Search.Results.Empty.Description":"Please try changing the filters to get better results.","Search.Button.LoadMore":"Load more","Search.Error.Message":"This query took too long to complete. Please try adjusting the search filters or view the example queries in the [Highlights section of the homepage](/#highlights).\n\nIf you are interested in using OONI data in batch, we recommend the [ooni-data Amazon S3 bucket](https://ooni.org/post/mining-ooni-data/) or the [aggregation API](https://api.ooni.io/apidocs/#/default/get_api_v1_aggregation).\n\nWe are working on improving the performance of OONI Explorer. To track our work on this, [see the open issues on the ooni/api repository](https://github.com/ooni/api/issues?q=is%3Aissue+is%3Aopen+label%3Aoptimization).","Search.Error.Details.Label":"Server Response","Home.Banner.Title.UncoverEvidence":"Uncover evidence of internet censorship worldwide","Home.Banner.Subtitle.ExploreCensorshipEvents":"Open data collected by the global OONI community","Home.Banner.Button.Explore":"Explore","Home.Banner.Stats.Measurements":"Measurements","Home.Banner.Stats.Countries":"Countries","Home.Banner.Stats.Networks":"Networks","Home.About.SummaryText":"OONI Explorer is an open data resource on internet censorship around the world. \n\nSince 2012, millions of network measurements have been collected from more than 200 countries. OONI Explorer sheds light on internet censorship and other forms of network interference worldwide.\n\nTo contribute to this open dataset, [install OONI Probe](https://ooni.org/install/) and run tests!","Home.Websites&Apps.Title":"Blocking of Websites & Apps","Home.Websites&Apps.SummaryText":"Discover blocked websites around the world. Check whether WhatsApp, Facebook Messenger, and Telegram are blocked.","Home.Search&Filter.Title":"Search","Home.Search&Filter.SummaryText":"Explore OONI measurements with a powerful search tool. View the most recently blocked websites. Compare internet censorship across networks.","Home.NetworkProperties.Title":"Network Performance","Home.NetworkProperties.SummaryText":"Check the speed and performance of thousands of networks around the world. Explore data on video streaming performance.","Home.MonthlyStats.Title":"Monthly coverage worldwide","Home.MonthlyStats.SummaryText":"OONI Explorer hosts millions of network measurements collected from more than 200 countries since 2012. Every day, OONI Explorer is updated with new measurements!\n\nLast month, {measurementCount} OONI Probe measurements were collected from {networkCount} networks in {countryCount} countries. Explore the monthly usage of [OONI Probe](https://ooni.org/install/) through the stats below.","Home.Highlights.CTA":"We encourage you to explore OONI measurements to find more highlights!","Home.Highlights.Title":"Highlights","Home.Highlights.Description":"What can you learn from OONI Explorer? \n\nBelow we share some stories from [research reports](https://ooni.org/post/) based on OONI data.\n\nWe share these case studies to demonstrate how OONI's openly available data can be used and what types of stories can be told. \n\nWe encourage you to explore OONI data, discover more censorship cases, and to use OONI data as part of your research and/or advocacy.","Home.Highlights.Political":"Censorship during political events","Home.Highlights.Political.Description":"Internet censorship sometimes occurs in response to or in anticipation of political events, such as elections, protests, and riots. Below we share a few cases detected via OONI data and correlated with political events.","Home.Highlights.Media":"Media censorship","Home.Highlights.Media.Description":"Press freedom is threatened in countries that experience the blocking of media websites. Below we share a few cases detected through OONI data.","Home.Highlights.LGBTQI":"Blocking of LGBTQI sites","Home.Highlights.LGBTQI.Description":"Minority group sites are blocked around the world. Below we share a few cases on the blocking of LGBTQI sites.","Home.Highlights.Changes":"Censorship changes","Home.Highlights.Changes.Description":"OONI measurements have been collected on a continuous basis since 2012, enabling the identification of censorship changes around the world. Some examples include:","Home.Meta.Description":"OONI Explorer is an open data resource on Internet censorship around the world consisting of millions of measurements on network interference.","Home.Highlights.Explore":"Explore","Home.Highlights.ReadReport":"Read report","Countries.Heading.JumpToContinent":"Jump to continent","Countries.Search.NoCountriesFound":"No countries found with {searchTerm}","Countries.Search.Placeholder":"Search for countries","Countries.PageTitle":"Internet Censorship around the world","Error.404.PageNotFound":"Page Not Found","Error.404.GoBack":"Go back","Error.404.Heading":"The requested page does not exist","Error.404.Message":"We could not find the content you were looking for. Maybe try {measurmentLink} or look at {homePageLink}.","Error.404.MeasurmentLinkText":"exploring some measurement","Error.404.HomepageLinkText":"the homepage","MAT.Title":"OONI Measurement Aggregation Toolkit (MAT)","MAT.SubTitle":"Create charts based on aggregate views of real-time OONI data from around the world","MAT.JSONData":"JSON Data","MAT.CSVData":"CSV Data","MAT.Form.Label.XAxis":"Columns","MAT.Form.Label.YAxis":"Rows","MAT.Form.Label.TimeGrain":"Time Granularity","MAT.Form.Label.AxisOption.domain":"Domain","MAT.Form.Label.AxisOption.input":"Input","MAT.Form.Label.AxisOption.measurement_start_day":"Measurement Day","MAT.Form.Label.AxisOption.probe_cc":"Countries","MAT.Form.Label.AxisOption.category_code":"Website Categories","MAT.Form.Label.AxisOption.probe_asn":"ASN","MAT.Form.TimeGrainOption.hour":"Hour","MAT.Form.TimeGrainOption.day":"Day","MAT.Form.TimeGrainOption.week":"Week","MAT.Form.TimeGrainOption.month":"Month","MAT.Form.ConfirmationModal.Title":"Are you sure?","MAT.Form.ConfirmationModal.Message":"Duration too long. This can potentially slow down the page","MAT.Form.ConfirmationModal.No":"No","MAT.Form.ConfirmationModal.Button.Yes":"Yes","MAT.Form.Submit":"Show Chart","MAT.Form.All":"All","MAT.Form.AllCountries":"All Countries","MAT.Table.Header.ok_count":"OK Count","MAT.Table.Header.anomaly_count":"Anomaly Count","MAT.Table.Header.confirmed_count":"Confirmed Count","MAT.Table.Header.failure_count":"Failure Count","MAT.Table.Header.measurement_count":"Measurement Count","MAT.Table.Header.input":"URL","MAT.Table.Header.category_code":"Category Code","MAT.Table.Header.probe_cc":"Country","MAT.Table.Header.probe_asn":"ASN","MAT.Table.Header.blocking_type":"Blocking Type","MAT.Table.Header.domain":"Domain","MAT.Table.FilterPlaceholder":"Search {count} records…","MAT.Table.Search":"Search:","MAT.Table.Filters":"Filters","MAT.Charts.NoData.Title":"No Data Found","MAT.Charts.NoData.Description":"We are not able to produce a chart based on the selected filters. Please change the filters and try again.","MAT.Charts.NoData.Details":"Details:","MAT.Help.Box.Title":"Help","MAT.Help.Title":"FAQs","MAT.Help.Content":"# What is the MAT?\n\nOONI's Measurement Aggregation Toolkit (MAT) is a tool that enables you to generate your own custom charts based on **aggregate views of real-time OONI data** collected from around the world.\n\nOONI data consists of network measurements collected by [OONI Probe](https://ooni.org/install/) users around the world. \n\nThese measurements contain information about various types of **internet censorship**, such as the [blocking of websites and apps](https://ooni.org/nettest/) around the world. \n\n# Who is the MAT for?\n\nThe MAT was built for researchers, journalists, and human rights defenders interested in examining internet censorship around the world.\n\n# Why use the MAT?\n\nWhen examining cases of internet censorship, it's important to **look at many measurements at once** (\"in aggregate\") in order to answer key questions like the following:\n\n* Does the testing of a service (e.g. Facebook) present **signs of blocking every time that it is tested** in a country? This can be helpful for ruling out [false positives](https://ooni.org/support/faq/#what-are-false-positives).\n* What types of websites (e.g. human rights websites) are blocked in each country?\n* In which countries is a specific website (e.g. `bbc.com`) blocked?\n* How does the blocking of different apps (e.g. WhatsApp or Telegram) vary across countries?\n* How does the blocking of a service vary across countries and [ASNs](https://ooni.org/support/glossary/#asn)?\n* How does the blocking of a service change over time?\n\nWhen trying to answer questions like the above, we normally perform relevant data analysis (instead of inspecting measurements one by one). \n\nThe MAT incorporates our data analysis techniques, enabling you to answer such questions without any data analysis skills, and with the click of a button!\n\n# How to use the MAT?\n\nThrough the filters at the start of the page, select the parameters you care about in order to plot charts based on aggregate views of OONI data.\n\nThe MAT includes the following filters:\n\n* **Countries:** Select a country through the drop-down menu (the \"All Countries\" option will show global coverage)\n* **Test Name:** Select an [OONI Probe test](https://ooni.org/nettest/) based on which you would like to get measurements (for example, select `Web Connectivity` to view the testing of websites)\n* **Domain:** Type the domain for the website you would like to get measurements (e.g. `twitter.com`)\n* **Website categories:** Select the [website category](https://github.com/citizenlab/test-lists/blob/master/lists/00-LEGEND-new_category_codes.csv) for which you would like to get measurements (e.g. `News Media` for news media websites)\n* **ASN:** Type the [ASN](https://ooni.org/support/glossary/#asn) of the network for which you would like to get measurements (e.g. `AS30722` for Vodafone Italia)\n* **Date range:** Select the date range of the measurements by adjusting the `Since` and `Until` filters\n* **Columns:** Select the values that you would like to appear on the horizontal axis of your chart\n* **Rows:** Select the values that you would like to appear on the vertical axis of your chart\n\nDepending on what you would like to explore, adjust the MAT filters accordingly and click `Show Chart`. \n\nFor example, if you would like to check the testing of BBC in all countries around the world:\n\n* Type `www.bbc.com` under `Domain`\n* Select `Countries` under the `Rows`\n* Click `Show Chart`\n\nThis will plot numerous charts based on the OONI Probe testing of `www.bbc.com` worldwide.\n\n# Interpreting MAT charts\n\nThe MAT charts (and associated tables) include the following values:\n\n* **OK count:** Successful measurements (i.e. NO sign of internet censorship)\n* **Confirmed count:** Measurements from automatically **confirmed blocked websites** (e.g. a [block page](https://ooni.org/support/glossary/#block-page) was served)\n* **Anomaly count:** Measurements that provided **signs of potential blocking** (however, [false positives](https://ooni.org/support/faq/#what-are-false-positives) can occur) \n* **Failure count:** Failed experiments that should be discarded\n* **Measurement count:** Total volume of OONI measurements (pertaining to the selected country, resource, etc.)\n\nWhen trying to identify the blocking of a service (e.g. `twitter.com`), it's useful to check whether:\n\n* Measurements are annotated as `confirmed`, automatically confirming the blocking of websites\n* A large volume of measurements (in comparison to the overall measurement count) present `anomalies` (i.e. signs of potential censorship)\n\nYou can access the raw data by clicking on the bars of charts, and subsequently clicking on the relevant measurement links. \n\n# Website categories\n\n[OONI Probe](https://ooni.org/install/) users test a wide range of [websites](https://ooni.org/support/faq/#which-websites-will-i-test-for-censorship-with-ooni-probe) that fall under the following [30 standardized categories](https://github.com/citizenlab/test-lists/blob/master/lists/00-LEGEND-new_category_codes.csv).","MAT.Help.Subtitle.Categories":"Categories","MAT.CustomTooltip.ViewMeasurements":"View measurements","ReachabilityDash.Heading.CircumventionTools":"Reachability of Censorship Circumvention Tools","ReachabilityDash.CircumventionTools.Description":"The charts below display aggregate views of OONI data based on the testing of the following circumvention tools:\n\n* [Psiphon](https://ooni.org/nettest/psiphon)\n\n* [Tor](https://ooni.org/nettest/tor)\n\n* [Tor Snowflake](https://ooni.org/nettest/tor-snowflake/)\n\nPlease note that the presence of [anomalous measurements](https://ooni.org/support/faq/#what-do-you-mean-by-anomalies) is not always indicative of blocking, as [false positives](https://ooni.org/support/faq/#what-are-false-positives) can occur. Moreover, circumvention tools often have built-in circumvention techniques for evading censorship. \n\nWe therefore recommend referring to **[Tor Metrics](https://metrics.torproject.org/)** and to the **[Psiphon Data Engine](https://psix.ca/)** to view usage stats and gain a more comprehensive understanding of whether these tools work in each country.","ReachabilityDash.Form.Label.CountrySelect.AllSelected":"All countries selected","ReachabilityDash.Form.Label.CountrySelect.SearchPlaceholder":"Search","ReachabilityDash.Form.Label.CountrySelect.SelectAll":"Select All","ReachabilityDash.Form.Label.CountrySelect.SelectAllFiltered":"Select All (Filtered)","ReachabilityDash.Form.Label.CountrySelect.InputPlaceholder":"Select Countries…","ReachabilityDash.Meta.Description":"View the accessibility of censorship circumvention tools around the world through OONI data.","DateRange.Apply":"Apply","DateRange.Cancel":"Cancel","DateRange.Today":"Today","DateRange.LastWeek":"Last Week","DateRange.LastMonth":"Last Month","DateRange.LastYear":"Last Year","ThirdPartyGraph.Label.gtr":"Google (Search)","ThirdPartyGraph.Label.merit-nt":"Telescope","ThirdPartyGraph.Label.bgp":"BGP","ThirdPartyGraph.Label.ping-slash24":"Active Probing","ThirdPartyGraph.Label.cloudflare":"Cloudflare","Highlights.Political.CubaReferendum2019.Title":"2019 Constitutional Referendum","Highlights.Political.CubaReferendum2019.Text":"Blocking of independent media","Highlights.Political.VenezuelaCrisis2019.Title":"2019 Presidential Crisis","Highlights.Political.VenezuelaCrisis2019.Text":"Blocking of Wikipedia and social media","Highlights.Political.ZimbabweProtests2019.Title":"2019 Fuel Protests","Highlights.Political.ZimbabweProtests2019.Text":"Social media blocking and internet blackouts","Highlights.Political.MaliElection2018.Title":"2018 Presidential Election","Highlights.Political.MaliElection2018.Text":"Blocking of WhatsApp and Twitter","Highlights.Political.CataloniaReferendum2017.Title":"Catalonia 2017 Independence Referendum","Highlights.Political.CataloniaReferendum2017.Text":"Blocking of sites related to the referendum","Highlights.Political.IranProtests2018.Title":"2018 Anti-government Protests","Highlights.Political.IranProtests2018.Text":"Blocking of Telegram, Instagram and Tor","Highlights.Political.EthiopiaProtests2016.Title":"2016 Wave of Protests","Highlights.Political.EthiopiaProtests2016.Text":"Blocking of news websites and social media","Highlights.Political.PakistanProtests2017.Title":"2017 Protests","Highlights.Political.PakistanProtests2017.Text":"Blocking of news websites and social media","Highlights.Media.Egypt.Title":"Pervasive media censorship","Highlights.Media.Egypt.Text":"Blocking of hundreds of media websites","Highlights.Media.Venezuela.Title":"Blocking of independent media websites","Highlights.Media.Venezuela.Text":"Venezuela's economic and political crisis","Highlights.Media.SouthSudan.Title":"Blocking of foreign-based media","Highlights.Media.SouthSudan.Text":"Media accused of hostile reporting against the government","Highlights.Media.Malaysia.Title":"Blocking of media","Highlights.Media.Malaysia.Text":"1MDB scandal","Highlights.Media.Iran.Title":"Pervasive media censorship","Highlights.Media.Iran.Text":"Blocking of at least 121 news outlets","Highlights.Lgbtqi.Indonesia.Text":"Blocking of LGBTQI sites","Highlights.Lgbtqi.Iran.Text":"Blocking of Grindr","Highlights.Lgbtqi.Ethiopia.Text":"Blocking of QueerNet","Highlights.Changes.Cuba.Text":"Cuba [used to primarily serve blank block pages](https://ooni.torproject.org/post/cuba-internet-censorship-2017/), only blocking the HTTP version of websites. Now they censor access to sites that support HTTPS by means of [IP blocking](https://ooni.org/post/cuba-referendum/).","Highlights.Changes.Venezuela.Text":"Venezuelan ISPs used to primarily block sites by means of [DNS tampering](https://ooni.torproject.org/post/venezuela-internet-censorship/). Now state-owned CANTV also implements [SNI-based filtering](https://ooni.torproject.org/post/venezuela-blocking-wikipedia-and-social-media-2019/).","Highlights.Changes.Ethiopia.Text":"Ethiopia [used to block](https://ooni.org/post/ethiopia-report/) numerous news websites, LGBTQI, political opposition, and circumvention tool sites. As part of the 2018 political reforms, most of these sites have been [unblocked](https://ooni.org/post/ethiopia-unblocking/)."}} \ No newline at end of file From 63d7a141e8d4cee6b507fa9c08a7d2bd90c55a2a Mon Sep 17 00:00:00 2001 From: Maja Komel Date: Mon, 27 Feb 2023 12:29:46 +0100 Subject: [PATCH 39/41] Fix date picker ranges --- components/DateRangePicker.js | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/components/DateRangePicker.js b/components/DateRangePicker.js index bd0af286a..60a27bcbd 100644 --- a/components/DateRangePicker.js +++ b/components/DateRangePicker.js @@ -78,21 +78,22 @@ const getDateFnsLocale = locale => { const DateRangePicker = ({handleRangeSelect, initialRange, close, ...props}) => { const intl = useIntl() + const tomorrow = addDays(new Date(), 1) const ranges = ['Today', 'LastWeek', 'LastMonth', 'LastYear'] const selectRange = (range) => { switch (range) { case 'Today': - handleRangeSelect({from: new Date(), to: new Date()}) + handleRangeSelect({from: new Date(), to: tomorrow}) break case 'LastWeek': - handleRangeSelect({from: sub(new Date(), {weeks: 1}) , to: new Date()}) + handleRangeSelect({from: sub(new Date(), {weeks: 1}) , to: tomorrow}) break case 'LastMonth': - handleRangeSelect({from: sub(new Date(), {months: 1}) , to: new Date()}) + handleRangeSelect({from: sub(new Date(), {months: 1}) , to: tomorrow}) break case 'LastYear': - handleRangeSelect({from: sub(new Date(), {years: 1}) , to: new Date()}) + handleRangeSelect({from: sub(new Date(), {years: 1}) , to: tomorrow}) break } } @@ -125,8 +126,6 @@ const DateRangePicker = ({handleRangeSelect, initialRange, close, ...props}) => setRange(range) } - const tomorrow = addDays(new Date(), 1) - return ( close()}> From d22492835c5226340030aa80360a0a6f3e62f62a Mon Sep 17 00:00:00 2001 From: Maja Komel Date: Mon, 27 Feb 2023 12:30:38 +0100 Subject: [PATCH 40/41] Adjust MAT confirmation modal logic --- components/aggregation/mat/Form.js | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/components/aggregation/mat/Form.js b/components/aggregation/mat/Form.js index 32a10e357..12fc83843 100644 --- a/components/aggregation/mat/Form.js +++ b/components/aggregation/mat/Form.js @@ -17,8 +17,8 @@ import DateRangePicker from '../../DateRangePicker' import { ConfirmationModal } from './ConfirmationModal' import { TestNameOptions } from '../../TestNameOptions' -const THRESHOLD_IN_MONTHS = 12 -const MAX_RANGE_DAYS = 31 +const DAY_GRAIN_THRESHOLD_IN_MONTHS = 12 +const WEEK_GRAIN_THRESHOLD_IN_MONTHS = 36 export const StyledLabel = styled(Label).attrs({ my: 2, @@ -175,10 +175,15 @@ export const Form = ({ onSubmit, testNames, query }) => { const maybeWarnBeforeSubmit = useCallback((e) => { e.preventDefault() - const [since, until] = getValues(['since', 'until']) - const isDurationMoreThanThresold = (dayjs(until).diff(dayjs(since), 'month')) > THRESHOLD_IN_MONTHS + const [since, until, timeGrain] = getValues(['since', 'until', 'time_grain']) + const shouldShowConfirmationModal = () => { + if (timeGrain === 'month') return false + const diff = (dayjs(until).diff(dayjs(since), 'month')) + if (timeGrain === 'week') return diff > WEEK_GRAIN_THRESHOLD_IN_MONTHS + return diff > DAY_GRAIN_THRESHOLD_IN_MONTHS + } - if (isDurationMoreThanThresold) { + if (shouldShowConfirmationModal()) { setShowConfirmation(true) } else { // Otherwise just continue with submission without interruption @@ -296,7 +301,6 @@ export const Form = ({ onSubmit, testNames, query }) => { { showDatePicker && setShowDatePicker(false)} From 2e4d5d7de7ee19a5e2a6533749c6850711cb05db Mon Sep 17 00:00:00 2001 From: Maja Komel Date: Wed, 8 Mar 2023 10:34:08 +0100 Subject: [PATCH 41/41] User feedback mechanism with login (#790) --- .env.development | 3 +- .env.production | 1 + .env.test | 9 +- .github/workflows/main.yml | 9 +- components/Flag.js | 6 +- components/Layout.js | 21 +- components/NavBar.js | 20 +- components/login/LoginForm.js | 107 +++ components/measurement/AccessPointStatus.js | 2 +- components/measurement/CommonDetails.js | 12 +- components/measurement/CommonSummary.js | 99 +- components/measurement/FeedbackBox.js | 288 ++++++ components/measurement/Hero.js | 12 +- components/measurement/StatusInfo.js | 4 +- components/measurement/nettests/Tor.js | 5 +- components/search/Radio.js | 22 +- components/vendor/SpinLoader.js | 6 +- components/withIntl.js | 2 +- cypress.config.js | 2 + cypress/e2e/home.e2e.cy.js | 2 +- cypress/e2e/measurement.e2e.cy.js | 54 ++ cypress/e2e/search.e2e.cy.js | 6 +- cypress/plugins/email-account.js | 69 ++ cypress/plugins/index.js | 14 +- cypress/support/commands.js | 4 +- hooks/useUser.js | 114 +++ jsconfig.json | 3 +- lib/api.js | 48 +- package.json | 15 +- pages/_app.js | 1 + pages/chart/mat.js | 1 - pages/country/[countryCode].js | 1 - pages/login.js | 95 ++ pages/measurement/[[...report_id]].js | 207 ++-- pages/network/[asn].js | 1 - pages/search.js | 1 - public/static/lang/en.json | 1 + yarn.lock | 994 ++++++++++++++++++-- 38 files changed, 1957 insertions(+), 304 deletions(-) create mode 100644 components/login/LoginForm.js create mode 100644 components/measurement/FeedbackBox.js create mode 100644 cypress/plugins/email-account.js create mode 100644 hooks/useUser.js create mode 100644 pages/login.js diff --git a/.env.development b/.env.development index 754719543..d930c3dc5 100644 --- a/.env.development +++ b/.env.development @@ -2,7 +2,8 @@ # To override locally, make a copy called `.env.development.local` # Refer: https://nextjs.org/docs/basic-features/environment-variables -NEXT_PUBLIC_OONI_API=https://ams-pg-test.ooni.org +NEXT_PUBLIC_OONI_API=https://api.ooni.io +NEXT_PUBLIC_USER_FEEDBACK_API=https://ams-pg-test.ooni.org NEXT_PUBLIC_EXPLORER_URL=http://localhost:3100 RUN_GIT_COMMIT_SHA_SHORT=yarn --silent git:getCommitSHA:short diff --git a/.env.production b/.env.production index aa64090ca..a6892c033 100644 --- a/.env.production +++ b/.env.production @@ -3,5 +3,6 @@ # Refer: https://nextjs.org/docs/basic-features/environment-variables NEXT_PUBLIC_OONI_API=https://api.ooni.io +NEXT_PUBLIC_USER_FEEDBACK_API=https://api.ooni.io NEXT_PUBLIC_SENTRY_DSN=https://49af7fff247c445b9a7c98ee21ddfd2f@o155150.ingest.sentry.io/1427510 NEXT_PUBLIC_EXPLORER_URL=https://explorer.ooni.org diff --git a/.env.test b/.env.test index 8a566942a..d83d6bf27 100644 --- a/.env.test +++ b/.env.test @@ -3,9 +3,6 @@ # Refer: https://nextjs.org/docs/basic-features/environment-variables NEXT_PUBLIC_OONI_API=https://api.ooni.io -NEXT_PUBLIC_EXPLORER_URL=https://explorer-test.ooni.io - -RUN_GIT_COMMIT_SHA_SHORT=yarn --silent git:getCommitSHA:short -RUN_GIT_COMMIT_SHA=yarn --silent git:getCommitSHA -RUN_GIT_COMMIT_REF=yarn --silent git:getCommitRef -RUN_GIT_COMMIT_TAGS=yarn --silent git:getReleasesAndTags \ No newline at end of file +NEXT_PUBLIC_USER_FEEDBACK_API=https://ams-pg-test.ooni.org +NEXT_PUBLIC_EXPLORER_URL=https://explorer.test.ooni.org +NEXT_PUBLIC_IS_TEST_ENV=1 diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 78639490b..c53e7827f 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -11,18 +11,19 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout - uses: actions/checkout@v2 + uses: actions/checkout@v3 - name: Cypress run - uses: cypress-io/github-action@v4.1.0 + uses: cypress-io/github-action@v5 with: build: npm run build start: npm start wait-on: 'http://localhost:3100' - browser: chrome + env: + NODE_ENV: 'test' - name: Upload screenshots on failure - uses: actions/upload-artifact@v2 + uses: actions/upload-artifact@v3 if: always() with: name: cypress-screenshots diff --git a/components/Flag.js b/components/Flag.js index f651899e9..f2e094dcb 100644 --- a/components/Flag.js +++ b/components/Flag.js @@ -34,9 +34,9 @@ const FlagContainer = styled.div` border-radius: 50%; /* padding-left: 3px; */ /* padding-top: 3px; */ - width: ${props => props.size + 6}px; - height: ${props => props.size + 6}px; - border: ${props => props.border ? '3px solid white' : 'none'}; + width: ${props => props.size + 2}px; + height: ${props => props.size + 2}px; + border: ${props => props.border ? '1px solid white' : 'none'}; ` export const Flag = ({countryCode, size, border}) => { diff --git a/components/Layout.js b/components/Layout.js index f0c07fa1a..c1b225766 100644 --- a/components/Layout.js +++ b/components/Layout.js @@ -8,7 +8,7 @@ import Header from './Header' import Footer from './Footer' import { useIntl } from 'react-intl' import { getDirection } from 'components/withIntl' -// import FeedbackButton from '../components/FeedbackFloat' +import { UserProvider } from 'hooks/useUser' theme.maxWidth = 1024 @@ -61,22 +61,23 @@ const Layout = ({ children, disableFooter = false }) => { return ( - -
    -
    -
    - { children } + + +
    +
    +
    + { children } +
    + {!disableFooter &&
    }
    - {!disableFooter &&
    } -
    - {/* */} + ) } Layout.propTypes = { - children: PropTypes.array.isRequired, + children: PropTypes.object.isRequired, disableFooter: PropTypes.bool } diff --git a/components/NavBar.js b/components/NavBar.js index 6198d7a0d..8254bc4a7 100644 --- a/components/NavBar.js +++ b/components/NavBar.js @@ -3,11 +3,8 @@ import { useRouter, withRouter } from 'next/router' import NLink from 'next/link' import styled from 'styled-components' import { FormattedMessage, useIntl } from 'react-intl' - import { getLocalisedLanguageName } from 'utils/i18nCountries' - import ExplorerLogo from 'ooni-components/components/svgs/logos/Explorer-HorizontalMonochromeInverted.svg' - import { Link, Flex, @@ -15,6 +12,7 @@ import { Container, Select, } from 'ooni-components' +import useUser from 'hooks/useUser' const StyledNavItem = styled.a` text-decoration: none; @@ -83,11 +81,17 @@ export const NavBar = ({color}) => { const { locale } = useIntl() const router = useRouter() const { pathname, asPath, query } = router + const { user, logout } = useUser() const handleLocaleChange = (event) => { router.push({ pathname, query }, asPath, { locale: event.target.value }) } + const logoutUser = (e) => { + e.preventDefault() + logout() + } + return ( @@ -116,6 +120,16 @@ export const NavBar = ({color}) => { ))} */} + {user?.logged_in && + + + + + + + + + } diff --git a/components/login/LoginForm.js b/components/login/LoginForm.js new file mode 100644 index 000000000..92ab39f7f --- /dev/null +++ b/components/login/LoginForm.js @@ -0,0 +1,107 @@ +import React, { useState, useCallback, useEffect } from 'react' +import { useForm, Controller } from 'react-hook-form' +import { Flex, Box, Input, Button, Modal, Text, Heading, Container } from 'ooni-components' +import styled from 'styled-components' +import NLink from 'next/link' + +import { registerUser } from '/lib/api' +import { useRouter } from 'next/router' +import { FormattedMessage } from 'react-intl' +import SpinLoader from 'components/vendor/SpinLoader' + +const StyledError = styled.small` + color: ${props => props.theme.colors.red5}; +` + +const StyledInputContainer = styled(Box).attrs({ + width: '100%', + mt: 3, +})` + position: relative; + & ${StyledError} { + position: absolute; + top: -10px; + right: 0px; + } +` + +export const LoginForm = ({ onLogin, redirectTo }) => { + const router = useRouter() + const [submitting, setSubmitting] = useState(false) + const [loginError, setError] = useState(null) + + const { handleSubmit, control, formState, reset } = useForm({ + mode: 'onTouched', + defaultValues: { email_address: '' } + }) + + const { errors, isValid, isDirty } = formState + + const onSubmit = useCallback((data) => { + const { email_address } = data + const registerApi = async (email_address) => { + try { + await registerUser(email_address, redirectTo) + if (typeof onLogin === 'function') { + onLogin() + } + } catch (e) { + setError(e.message) + // Reset form to mark `isDirty` as false + reset({}, { keepValues: true }) + } finally { + setSubmitting(false) + } + } + setSubmitting(true) + registerApi(email_address) + }, [onLogin, reset, redirectTo]) + + useEffect(() => { + // Remove previous errors when form becomes dirty again + if (isDirty) { + setError(null) + } + }, [isDirty]) + + return ( + + + + ( + + )} + rules={{ + pattern: { + value: /^[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}$/i, + }, + required: true, + }} + name='email_address' + control={control} + /> + {errors?.email_address?.message} + + {loginError && + + {loginError} + + } + + {!submitting ? + : + + } + + + + ) +} + +export default LoginForm diff --git a/components/measurement/AccessPointStatus.js b/components/measurement/AccessPointStatus.js index 76a6e6099..969e10171 100644 --- a/components/measurement/AccessPointStatus.js +++ b/components/measurement/AccessPointStatus.js @@ -36,7 +36,7 @@ const AccessPointStatus = ({ icon, label, ok, content, color, ...props}) => { } AccessPointStatus.propTypes = { - icon: PropTypes.element.isRequired, + icon: PropTypes.element, label: PropTypes.oneOfType([ PropTypes.string, PropTypes.element diff --git a/components/measurement/CommonDetails.js b/components/measurement/CommonDetails.js index cdb2d49e5..ef23c9af7 100644 --- a/components/measurement/CommonDetails.js +++ b/components/measurement/CommonDetails.js @@ -47,7 +47,8 @@ JsonViewer.propTypes = { const CommonDetails = ({ measurement, - reportId + reportId, + userFeedbackItems =[] }) => { const { software_name, @@ -143,6 +144,15 @@ const CommonDetails = ({ bg={theme.colors.gray2} /> + {/* User Feedback */} + {!!userFeedbackItems.length && + + } + items={userFeedbackItems} + /> + + } {/* Raw Measurement */} ( - - - {link ? {content} : content} - - - {label} - - -) - -SummaryItemBox.propTypes = { - label: PropTypes.string, - content: PropTypes.node -} - const CommonSummary = ({ color, measurement_start_time, probe_asn, probe_cc, - country + networkName, + country, + hero, + onVerifyClick }) => { const intl = useIntl() const startTime = measurement_start_time const network = probe_asn const countryCode = probe_cc - - const countryBlock = - - - - - {country} - - - const formattedDate = new Intl.DateTimeFormat(intl.locale, { dateStyle: 'long', timeStyle: 'long', timeZone: 'UTC' }).format(new Date(startTime)) return ( <> - + - - {/**/} - - - + + + {formattedDate} + + + + {/* + + {'Share'.toUpperCase()} + */} + + + {intl.formatMessage({id: 'Measurement.CommonSummary.Verify'}).toUpperCase()} + + + + + {hero} + + + + + + {network} + {networkName} + + + + + + + + + + + + {country} + + + + @@ -99,6 +97,7 @@ CommonSummary.propTypes = { measurement_start_time: PropTypes.string.isRequired, probe_asn: PropTypes.string.isRequired, probe_cc: PropTypes.string.isRequired, + networkName: PropTypes.string, country: PropTypes.string.isRequired, color: PropTypes.string.isRequired } diff --git a/components/measurement/FeedbackBox.js b/components/measurement/FeedbackBox.js new file mode 100644 index 000000000..3de18659e --- /dev/null +++ b/components/measurement/FeedbackBox.js @@ -0,0 +1,288 @@ +import React, { useEffect, useMemo, useRef, useState } from 'react' +import { Controller, useForm } from 'react-hook-form' +import { FormattedMessage, useIntl } from 'react-intl' +import { Box, Button, Flex, Link, Text, theme } from 'ooni-components' +import { GrClose } from 'react-icons/gr' +import { RadioGroup, RadioButton } from 'components/search/Radio' +import useStateMachine from '@cassiozen/usestatemachine' +import SpinLoader from 'components/vendor/SpinLoader' +import { submitFeedback, getAPI } from 'lib/api' +import LoginForm from 'components/login/LoginForm' +import styled from 'styled-components' + +const StyledCloseIcon = styled(GrClose)` +position: absolute; +top: 16px; +right: 16px; +font-size: 16px; +cursor: pointer; +` + +const StyledError = styled.small` + color: ${props => props.theme.colors.red5}; +` + +const blockedValues = [ + { top: 'ok' }, + { top: 'down', + sub: [ + { top: 'down.unreachable' }, + { top: 'down.misconfigured' }, + ] + }, + { top: 'blocked', + sub: [ + { + top: 'blocked.blockpage', + sub: [ + 'blocked.blockpage.http', + 'blocked.blockpage.dns', + 'blocked.blockpage.server_side', + 'blocked.blockpage.server_side.captcha' + ] + }, + { top: 'blocked.dns', + sub: ['blocked.dns.inconsistent', 'blocked.dns.nxdomain'] + }, + { top: 'blocked.tcp' }, + { top: 'blocked.tls' } + ] + }, +] + +const radioLevels = ['firstLevelRadio', 'secondLevelRadio', 'thirdLevelRadio'] + +const FeedbackBox = ({user, report_id, setShowModal, previousFeedback, mutateUserFeedback}) => { + const intl = useIntl() + const [error, setError] = useState(null) + + const redirectTo = typeof window !== 'undefined' && window.location.href + + const defaultValues = useMemo(() => { + if (!previousFeedback) return {} + const feedbackLevels = previousFeedback.split('.') + return feedbackLevels.reduce((acc, current, i) => { + return {...acc, ...{[radioLevels[i]]: feedbackLevels.slice(0, i+1).join('.')}} + }, {}) + }, [previousFeedback]) + + const { handleSubmit, control, watch, setValue, getValues } = useForm({defaultValues}) + + const [state, send] = useStateMachine({ + initial: 'initial', + states: { + initial: { + effect({ send, setContext, event, context }) { + if (!user?.logged_in) send('LOGIN') + if (!previousFeedback) send('FEEDBACK') + }, + on: { + LOGIN: 'login', + FEEDBACK: 'feedback' + }, + }, + login: { + on: { + LOGIN_SUCCESS: 'loginSuccess', + } + }, + loginSuccess: {}, + feedback: { + on: { + CANCEL: 'initial', + SUBMIT: 'submit' + }, + effect() { + return () => setError(null) + }, + }, + submit: { + effect({ send, setContext, event, context }) { + const { firstLevelRadio, secondLevelRadio, thirdLevelRadio } = getValues() + const feedbackParams = { + measurement_uid: report_id, + status: thirdLevelRadio || secondLevelRadio || firstLevelRadio + } + submitFeedback(feedbackParams) + .then(() => { + mutateUserFeedback() + send('SUCCESS') + }) + .catch((response) => { + setError(response?.message || 'unknown') + }).finally(() => send('FEEDBACK')) + }, + on: { + SUCCESS: 'success', + FEEDBACK: 'feedback' + } + }, + success: {}, + }, + }) + + + const initialLoadFirst = useRef(false) + const initialLoadSecond = useRef(false) + const firstLevelRadio = watch(radioLevels[0]) + const secondLevelRadio = watch(radioLevels[1]) + + useEffect(() => { + if (initialLoadFirst.current) return setValue(radioLevels[1], null) + initialLoadFirst.current = true + }, [firstLevelRadio]) + useEffect(() => { + if (initialLoadSecond.current) return setValue(radioLevels[2], null) + initialLoadSecond.current = true + }, [secondLevelRadio]) + + const submitEnabled = useMemo(() => !!firstLevelRadio, [firstLevelRadio]) + + return ( + <> + + setShowModal(false)} /> + <> + {state.value === 'initial' && + <> + + + + + + + + + + + } + {state.value === 'login' && + <> + + + + + {send('LOGIN_SUCCESS')}} redirectTo={redirectTo} /> + + } + {state.value === 'loginSuccess' && + <> + + + + + + + + + } + {state.value === 'feedback' && +
    e.preventDefault()}> + + + + ( + + {blockedValues.map(({top, sub}) => ( + <> + + {sub && firstLevelRadio === top && ( + ( + + {sub.map(({top, sub}) => ( + <> + + {sub && secondLevelRadio === top && ( + ( + + {sub.map(subVal => ( + ) + )} + + )} + />)} + + ))} + + )} + />)} + + ))} + + )} + /> + + + + {error && + + + Error: {error} + + } + + + + } + {state.value === 'submit' && + + } + {state.value === 'success' && + <> + + + + + + + } + +
    + + ) +} + +export default FeedbackBox \ No newline at end of file diff --git a/components/measurement/Hero.js b/components/measurement/Hero.js index 57ff5d343..4310734f1 100644 --- a/components/measurement/Hero.js +++ b/components/measurement/Hero.js @@ -8,11 +8,10 @@ import styled from 'styled-components' import { FormattedMessage } from 'react-intl' const HeroContainer = styled(Box)` - background-color: ${props => props.color}; color: white; ` -const Hero = ({ status, color, icon, label, info }) => { +const Hero = ({ status, icon, label, info }) => { let computedLabel = '' if (status) { switch (status) { @@ -46,15 +45,15 @@ const Hero = ({ status, color, icon, label, info }) => { } return ( - + - + - {icon} {label} + {icon} {label} {info && - + {info} } @@ -65,7 +64,6 @@ const Hero = ({ status, color, icon, label, info }) => { Hero.propTypes = { status: PropTypes.string, - color: PropTypes.string, icon: PropTypes.node, label: PropTypes.string, info: PropTypes.node diff --git a/components/measurement/StatusInfo.js b/components/measurement/StatusInfo.js index 4fb7758cd..eb341a75f 100644 --- a/components/measurement/StatusInfo.js +++ b/components/measurement/StatusInfo.js @@ -7,11 +7,11 @@ import { const StatusInfo = ({ title, message}) => ( - + {title} - {message} + {message} ) diff --git a/components/measurement/nettests/Tor.js b/components/measurement/nettests/Tor.js index 5cde51a60..2cb7c74f4 100644 --- a/components/measurement/nettests/Tor.js +++ b/components/measurement/nettests/Tor.js @@ -72,7 +72,10 @@ const NameCell = ({ children }) => { } NameCell.propTypes = { - children: PropTypes.element.isRequired + children: PropTypes.oneOfType([ + PropTypes.string, + PropTypes.element + ]).isRequired } const Table = ({ columns, data }) => { diff --git a/components/search/Radio.js b/components/search/Radio.js index 674c47ca7..117a26273 100644 --- a/components/search/Radio.js +++ b/components/search/Radio.js @@ -21,17 +21,21 @@ export const RadioGroup = ({ ...props }) => { + const iterateOverChildren = (children) => { + return React.Children.map(children, (child) => { + if (!React.isValidElement(child)) return + + return React.cloneElement(child, { + ...child.props, + checked: child.props.value === value, + onChange: (e) => { onChange(e.target.value) }, + children: iterateOverChildren(child.props.children)}) + }) + } + return ( - {React.Children.map(children, child => ( - !(child.type.name === RadioButton.name) - ? child - : React.cloneElement(child, { - name: name, - checked: child.props.value === value, - onChange: (e) => { onChange(e.target.value) }, - }) - ))} + {iterateOverChildren(children)} ) } diff --git a/components/vendor/SpinLoader.js b/components/vendor/SpinLoader.js index 4ad343293..43ac01d67 100644 --- a/components/vendor/SpinLoader.js +++ b/components/vendor/SpinLoader.js @@ -25,7 +25,7 @@ const Spin = styled.div` border-radius: 50%; font-size: ${props => `${props.size}px`}; height: 11em; - margin: 50px auto; + margin: ${props => props.margin}; position: relative; text-indent: -9999em; transform: translateZ(0); @@ -66,13 +66,15 @@ SpinLoader.propTypes = { color: PropTypes.string, duration: PropTypes.number, size: PropTypes.number, + margin: PropTypes.string, } SpinLoader.defaultProps = { background: '#fff', color: theme.colors.blue5, duration: 1.4, - size: 5 + size: 5, + margin: '50px auto', } export default SpinLoader diff --git a/components/withIntl.js b/components/withIntl.js index 67ff6bc5e..929fe765c 100644 --- a/components/withIntl.js +++ b/components/withIntl.js @@ -1,5 +1,5 @@ /* global require */ -import React, { useEffect, useState, useMemo, createContext } from 'react' +import React, { useMemo } from 'react' import { IntlProvider } from 'react-intl' import { useRouter } from 'next/router' diff --git a/cypress.config.js b/cypress.config.js index f4f51e8de..8d61f4b31 100644 --- a/cypress.config.js +++ b/cypress.config.js @@ -2,6 +2,7 @@ const { defineConfig } = require('cypress') module.exports = defineConfig({ video: false, + chromeWebSecurity: false, e2e: { // We've imported your old cypress plugins here. // You may want to clean this up later by importing these. @@ -9,5 +10,6 @@ module.exports = defineConfig({ return require('./cypress/plugins/index.js')(on, config) }, baseUrl: 'http://localhost:3100', + testIsolation: false, }, }) diff --git a/cypress/e2e/home.e2e.cy.js b/cypress/e2e/home.e2e.cy.js index f97842463..fb0daefde 100644 --- a/cypress/e2e/home.e2e.cy.js +++ b/cypress/e2e/home.e2e.cy.js @@ -12,6 +12,6 @@ describe('Home Page Tests', () => { it('explore button works', () => { // Check if explore button cy.get('button').contains('Explore').click() - cy.url().should('include', '/chart/mat') + cy.location('pathname', { timeout: 20000 }).should('include', '/chart/mat') }) }) diff --git a/cypress/e2e/measurement.e2e.cy.js b/cypress/e2e/measurement.e2e.cy.js index 71336044b..0118aa52e 100644 --- a/cypress/e2e/measurement.e2e.cy.js +++ b/cypress/e2e/measurement.e2e.cy.js @@ -1,3 +1,5 @@ +const { recurse } = require('cypress-recurse') + describe('Measurement Page Tests', () => { const normalColor = 'rgb(47, 158, 68)' @@ -303,4 +305,56 @@ describe('Measurement Page Tests', () => { }) }) + describe('User Feedback', () => { + let userEmail + + before(() => { + cy.clearLocalStorage() + cy.intercept('GET', 'https://ams-pg-test.ooni.org/api/v1/user_login*').as('userLogin') + // get and check the test email only once before the tests + cy.task('getUserEmail').then((email) => { + expect(email).to.be.a('string') + userEmail = email + }) + }) + + it('can login and submit feedback', () => { + cy.visit('/measurement/20230307T142506Z_webconnectivity_US_20115_n1_PXV9h44BLL9pXFhs?input=https%3A%2F%2Fwww.instagram.com%2F') + cy.findByText('VERIFY').click() + + cy.findByRole('textbox').click().type(userEmail) + cy.findByText('Login').click() + cy.findByText('Login link sent') + + recurse( + () => cy.task('getLastEmail'), // Cypress commands to retry + Cypress._.isObject, // keep retrying until the task returns an object + { + timeout: 60000, // retry up to 1 minute + delay: 5000, // wait 5 seconds between attempts + }, + ).then(({ loginLink }) => { + cy.visit(loginLink) + }) + + cy.url().should('contain', '/login') + cy.wait('@userLogin') + + cy.visit('/measurement/20230307T142506Z_webconnectivity_US_20115_n1_PXV9h44BLL9pXFhs?input=https%3A%2F%2Fwww.instagram.com%2F') + cy.findByText('VERIFY').click() + + cy.get('body').then(($body) => { + if ($body.text().includes('Your previous feedback')) { + cy.findByText('Edit').click() + } + }) + + cy.get('form').findByText('It\'s blocked').click() + cy.get('form').findByText('Block page').click() + cy.get('form').findByText('CAPTCHA').click() + cy.get('form').findByText('Submit').click() + + cy.findByText('Thank you!') + }) + }) }) diff --git a/cypress/e2e/search.e2e.cy.js b/cypress/e2e/search.e2e.cy.js index 1826fb551..e30e6d6c2 100644 --- a/cypress/e2e/search.e2e.cy.js +++ b/cypress/e2e/search.e2e.cy.js @@ -29,16 +29,16 @@ describe('Search Page Tests', () => { }) it('fetches more results when "Load More" button is clicked', () => { - cy.intercept('/api/v1/*').as('searchAPI') + cy.intercept('/api/v1/measurements*').as('searchAPI') cy.get('[data-test-id="load-more-button"]').click() cy.wait('@searchAPI') cy.get('[data-test-id="results-list"]', { timeout: 10000 }).children('a').should('have.length', 100) }) it('results loaded by "Load More" button are valid', () => { - cy.intercept('/api/v1/*').as('searchAPI') + cy.intercept('/api/v1/measurements*').as('searchAPI') cy.get('[data-test-id="results-list"] > a:nth-child(51)').click() - cy.url().should('include', '/measurement/') + cy.location('pathname', { timeout: 20000 }).should('include', '/measurement/') cy.go('back') cy.wait('@searchAPI') }) diff --git a/cypress/plugins/email-account.js b/cypress/plugins/email-account.js new file mode 100644 index 000000000..2c5d9796c --- /dev/null +++ b/cypress/plugins/email-account.js @@ -0,0 +1,69 @@ +const nodemailer = require('nodemailer') +const imaps = require('imap-simple') +const simpleParser = require('mailparser').simpleParser +const { JSDOM } = require('jsdom') + +const makeEmailAccount = async () => { + // Generate a new Ethereal email inbox account + const testAccount = await nodemailer.createTestAccount() + + const emailConfig = { + imap: { + user: testAccount.user, + password: testAccount.pass, + host: 'imap.ethereal.email', + port: 993, + tls: true, + authTimeout: 10000, + }, + } + console.log('created new email account %s', testAccount.user) + console.log('the password is %s', testAccount.pass) + + const userEmail = { + email: testAccount.user, + + async getLastEmail() { + try { + const connection = await imaps.connect(emailConfig) + + await connection.openBox('INBOX') + const searchCriteria = ['1:50'] + const fetchOptions = { + bodies: [''], + } + const messages = await connection.search(searchCriteria, fetchOptions) + connection.end() + + if (!messages.length) { + console.log('cannot find any emails') + return null + } else { + console.log('there are %d messages', messages.length) + // get the last email + const mail = await simpleParser( + messages[messages.length - 1].parts[0].body, + ) + + const dom = new JSDOM(mail.html) + const link = dom.window.document.querySelector('a').href + const url = new URL(link) + const path = url.pathname + url.search + + return { + loginLink: path + } + return {} + } + } catch (e) { + console.error(e) + return null + } + }, + } + + return userEmail +} + +module.exports = makeEmailAccount + diff --git a/cypress/plugins/index.js b/cypress/plugins/index.js index aa9918d21..d6cb14c4d 100644 --- a/cypress/plugins/index.js +++ b/cypress/plugins/index.js @@ -1,4 +1,5 @@ /// +const makeEmailAccount = require('./email-account') // *********************************************************** // This example plugins/index.js can be used to load plugins // @@ -15,7 +16,18 @@ /** * @type {Cypress.PluginConfig} */ -module.exports = (on, config) => { +module.exports = async (on, config) => { // `on` is used to hook into various events Cypress emits // `config` is the resolved Cypress config + const emailAccount = await makeEmailAccount() + + on('task', { + getUserEmail() { + return emailAccount.email + }, + + getLastEmail() { + return emailAccount.getLastEmail() + }, + }) } diff --git a/cypress/support/commands.js b/cypress/support/commands.js index d9d238a6d..2a6a214f8 100644 --- a/cypress/support/commands.js +++ b/cypress/support/commands.js @@ -1,4 +1,6 @@ /* global Cypress, cy */ +import '@testing-library/cypress/add-commands' + // *********************************************** // This example commands.js shows you how to // create various custom commands and overwrite @@ -25,6 +27,6 @@ // -- This will overwrite an existing command -- // Cypress.Commands.overwrite("visit", (originalFn, url, options) => { ... }) Cypress.Commands.add('heroHasColor', (color) => { - cy.get('[data-test-id="hero"]') + cy.get('[data-test-id="common-summary"]') .should('have.css', 'background-color', color) }) diff --git a/hooks/useUser.js b/hooks/useUser.js new file mode 100644 index 000000000..71f80954c --- /dev/null +++ b/hooks/useUser.js @@ -0,0 +1,114 @@ +import { useRouter } from 'next/router' +import { useEffect, useState, useCallback, useContext, createContext, useMemo } from 'react' +import useSWR from 'swr' + +import { apiEndpoints, loginUser, refreshToken, getAPI } from '/lib/api' + +const TWELVE_HOURS = 1000 * 60 * 60 * 12 +const TEN_MINUTES = 1000 * 60 * 10 + +const UserContext = createContext({}) + +export const UserProvider = ({children}) => { + const router = useRouter() + const { token } = router.query + const [user, setUser] = useState() + const [error, setError] = useState() + const [loading, setLoading] = useState(false) + const [loadingInitial, setLoadingInitial] = useState(true) + + const getUser = () => { + return getAPI(apiEndpoints.ACCOUNT_METADATA) + .then((user) => setUser(user)) + .catch(() => setUser(undefined)) + .finally(() => setLoadingInitial(false)) + } + + const afterLogin = useCallback((redirectTo) => { + const { pathname, searchParams } = new URL(redirectTo) + setTimeout(() => { + router.push({ pathname, query: Object.fromEntries([...searchParams]) }) + }, 3000) + }, [router]) + + useEffect(() => { + if (token && router.pathname === '/login') { + loginUser(token) + .then((data) => { + getUser() + if (data?.redirect_to) afterLogin(data.redirect_to) + }).catch((e)=> { + console.log(e) + setError(e.message) + }) + } else { + setError(null) + } + }, [afterLogin, token, router.pathname]) + + // periodically check if the token need to be refreshed and request a + // new one if needed + useEffect(() => { + const interval = setInterval(() => { + const tokenCreatedAt = JSON.parse(localStorage.getItem('bearer'))?.created_at + if (tokenCreatedAt) { + const tokenExpiry = tokenCreatedAt + TWELVE_HOURS + const now = Date.now() + if (now > tokenExpiry) { + refreshToken().catch((e) => { + if (e?.response?.status === 401) { + localStorage.removeItem('bearer') + getUser() + } + }) + } + } + }, TEN_MINUTES) + + return () => clearInterval(interval) + }, []) + + useEffect(() => { + getUser() + }, []) + + function login(email, password) { + setLoading(true) + loginUser(token) + .then((data) => { + setUser(data) + if (data?.redirect_to) afterLogin(data.redirect_to) + }).catch((e)=> { + console.log(e) + setError(error) + }).finally(() => setLoading(false)) + } + + function logout() { + localStorage.removeItem('bearer') + getUser() + } + + const memoedValue = useMemo( + () => ({ + user, + loading, + error, + login, + logout, + }), + [user, loading, error] + ) + + return ( + + {children} + + ) +} + +const useUser = () => { + return useContext(UserContext) +} + +export default useUser \ No newline at end of file diff --git a/jsconfig.json b/jsconfig.json index 41d8c4cb2..60689b8c1 100644 --- a/jsconfig.json +++ b/jsconfig.json @@ -5,5 +5,6 @@ "exclude": [ "node_modules", ".next" - ] + ], + "include": ["node_modules/cypress", "./cypress/**/*.js"] } \ No newline at end of file diff --git a/lib/api.js b/lib/api.js index f20c25d3f..129675595 100644 --- a/lib/api.js +++ b/lib/api.js @@ -2,23 +2,27 @@ import Axios from 'axios' export const apiEndpoints = { ACCOUNT_METADATA: '/api/_/account_metadata', + TOKEN_REFRESH: '/api/v1/user_refresh_token', USER_REGISTER: '/api/v1/user_register', USER_LOGIN: '/api/v1/user_login', USER_LOGOUT: '/api/v1/user_logout', FEEDBACK_SUBMIT: '/api/_/measurement_feedback', } -const axios = Axios.create({ - baseURL: process.env.NEXT_PUBLIC_OONI_API, - withCredentials: true -}) +const getBearerToken = () => { + return typeof localStorage !== 'undefined' ? JSON.parse(localStorage.getItem('bearer'))?.token : '' +} + +const axios = Axios.create({baseURL: process.env.NEXT_PUBLIC_USER_FEEDBACK_API}) export const getAPI = async (endpoint, params = {}, config = {}) => { + const bearerToken = getBearerToken() return await axios.request({ method: config.method ?? 'GET', url: endpoint, params: params, - ...config + ...config, + ...(bearerToken && { headers: { Authorization: `Bearer ${bearerToken}` } }) }) .then(res => res.data) .catch(e => { @@ -34,7 +38,9 @@ const postAPI = async (endpoint, params, config) => { } export const registerUser = async (email_address, redirectUrl = 'https://explorer.ooni.org' ) => { - const redirectTo = process.env.NODE_ENV === 'development' ? + // current testing setup does not enable us to check process.env.NODE_ENV (it's set to production + // in headless mode), therefore custom NEXT_PUBLIC_IS_TEST_ENV is used + const redirectTo = process.env.NODE_ENV === 'development' || process.env.NEXT_PUBLIC_IS_TEST_ENV ? 'https://explorer.test.ooni.org' : redirectUrl @@ -49,18 +55,40 @@ export const submitFeedback = (feedback) => { return postAPI(apiEndpoints.FEEDBACK_SUBMIT, feedback) } -export const loginUser = async (token) => { - return await getAPI(apiEndpoints.USER_LOGIN, { k: token }) +export const loginUser = (token) => { + return axios.get(apiEndpoints.USER_LOGIN, { params: { k: token } }) + .then(({ data }) => { + localStorage.setItem('bearer', JSON.stringify({ token: data?.bearer, created_at: Date.now() })) + return data + }) +} + +export const refreshToken = () => { + return getAPI(apiEndpoints.TOKEN_REFRESH).then(( data ) => { + localStorage.setItem('bearer', JSON.stringify({ token: data.bearer, created_at: Date.now() })) + }) } export const fetcher = async (url) => { try { - const res = await axios.get(url) - return res.data.rules ?? res.data + const res = await getAPI(url) + return res } catch (e) { const error = new Error(e?.response?.data?.error ?? e.message) error.info = e?.response?.statusText error.status = e?.response?.status throw error } +} + +export const customErrorRetry = (error, key, config, revalidate, opts) => { + // This overrides the default exponential backoff algorithm + // Instead it uses the `errorRetryInterval` and `errorRetryCount` configuration to + // limit the retries + const maxRetryCount = config.errorRetryCount + if (maxRetryCount !== undefined && opts.retryCount > maxRetryCount) return + // Never retry on 4xx errors + if (Math.floor(error.status / 100) === 4) return + + setTimeout(revalidate, config.errorRetryInterval, opts) } \ No newline at end of file diff --git a/package.json b/package.json index 8d6a199b9..e0fab3857 100644 --- a/package.json +++ b/package.json @@ -6,6 +6,7 @@ "main": "index.js", "repository": "https://github.com/ooni/explorer", "dependencies": { + "@cassiozen/usestatemachine": "^1.0.1", "@datapunt/matomo-tracker-react": "^0.5.1", "@fontsource/fira-sans": "^4.5.9", "@nivo/bar": "^0.80.0", @@ -64,14 +65,20 @@ }, "devDependencies": { "@svgr/webpack": "^6.5.0", + "@testing-library/cypress": "^9.0.0", "@welldone-software/why-did-you-render": "^7.0.1", - "cypress": "^10.6.0", + "cypress": "^12.6.0", + "cypress-recurse": "^1.27.0", "eslint": "^8.22.0", "eslint-config-next": "^12.2.5", "eslint-plugin-cypress": "^2.12.1", "glob": "^8.0.3", + "imap-simple": "^5.1.0", + "jsdom": "^21.1.0", + "mailparser": "^3.6.3", "mustache": "^4.2.0", - "start-server-and-test": "^1.14.0" + "nodemailer": "^6.9.1", + "start-server-and-test": "^2.0.0" }, "scripts": { "dev": "next dev -p 3100", @@ -80,7 +87,9 @@ "build": "next build", "export": "next export", "lint": "next lint", - "test:e2e": "yarn build && start-server-and-test start http://localhost:3100 cypress:run", + "start:testServer": "NODE_ENV=test yarn start", + "build:test": "NODE_ENV=test yarn build", + "test:e2e": "yarn build:test && start-server-and-test start:testServer http://localhost:3100 cypress:run", "cypress:run": "cypress run", "test": "yarn run test:e2e", "script:build-translations": "node ./scripts/build-translations.js", diff --git a/pages/_app.js b/pages/_app.js index 05eeb51c9..a0601e481 100644 --- a/pages/_app.js +++ b/pages/_app.js @@ -8,6 +8,7 @@ import 'regenerator-runtime/runtime' import NProgress from 'nprogress' import { useRouter } from 'next/router' import '@fontsource/fira-sans' +import '@fontsource/fira-sans/300.css' import '../public/static/nprogress.css' import Layout from '../components/Layout' diff --git a/pages/chart/mat.js b/pages/chart/mat.js index c87e1f4ef..1c8b21662 100644 --- a/pages/chart/mat.js +++ b/pages/chart/mat.js @@ -13,7 +13,6 @@ import { import useSWR from 'swr' import { FormattedMessage, useIntl } from 'react-intl' -import Layout from 'components/Layout' import NavBar from 'components/NavBar' import { MATContextProvider } from 'components/aggregation/mat/MATContext' import { StackedBarChart } from 'components/aggregation/mat/StackedBarChart' diff --git a/pages/country/[countryCode].js b/pages/country/[countryCode].js index b6a77d7b6..6e2e1bdfd 100644 --- a/pages/country/[countryCode].js +++ b/pages/country/[countryCode].js @@ -16,7 +16,6 @@ import dayjs from 'services/dayjs' import Form from 'components/network/Form' import NavBar from 'components/NavBar' import Flag from 'components/Flag' -import Layout from 'components/Layout' import PageNavMenu from 'components/country/PageNavMenu' import Overview from 'components/country/Overview' import WebsitesSection from 'components/country/Websites' diff --git a/pages/login.js b/pages/login.js new file mode 100644 index 000000000..b941f0db5 --- /dev/null +++ b/pages/login.js @@ -0,0 +1,95 @@ +import React, { useCallback, useEffect, useState } from 'react' +import { Box, Flex, Heading, Text, Link, Container } from 'ooni-components' +import { useRouter } from 'next/router' +import NLink from 'next/link' +import Head from 'next/head' + +import NavBar from 'components/NavBar' +import LoginForm from 'components/login/LoginForm' +import { mutate } from 'swr' +import SpinLoader from 'components/vendor/SpinLoader' +import useUser from 'hooks/useUser' +import { FormattedMessage, useIntl } from 'react-intl' + +const Login = () => { + const intl = useIntl() + const router = useRouter() + const { token } = router.query + + const [submitted, setSubmitted] = useState(false) + + const redirectTo = typeof window !== 'undefined' && window.location.origin + + const { user, loading, error } = useUser() + + // If user is already logged in, redirect to home page + useEffect(() => { + if (!loading && user && !token) { + router.replace('/') + } + }, [user, loading, router, token]) + + return ( + <> + + {intl.formatMessage({id: 'General.Login'})} + + + + + + + + + + + {/* Before logging In */} + {!token && !submitted && + <> + + + + + setSubmitted(true)} redirectTo={redirectTo} /> + + + } + {!token && submitted && + + + + } + + {/* While logging In */} + {token && !user && !error && + <> + + + + + + } + + {/* After loggin in */} + {user && !error && token && + <> + + + + + } + + {/* Errors */} + {error && + + {error} + + + } + + + + ) +} + +export default Login diff --git a/pages/measurement/[[...report_id]].js b/pages/measurement/[[...report_id]].js index 07ab3c77a..0f3f9fc0b 100644 --- a/pages/measurement/[[...report_id]].js +++ b/pages/measurement/[[...report_id]].js @@ -1,23 +1,30 @@ /* global process */ -import React from 'react' +import React, { useMemo, useState } from 'react' import PropTypes from 'prop-types' import Head from 'next/head' import axios from 'axios' -import { Container, theme } from 'ooni-components' -import { getLocalisedRegionName } from '../../utils/i18nCountries' - -import Hero from '../../components/measurement/Hero' -import CommonSummary from '../../components/measurement/CommonSummary' -import DetailsHeader from '../../components/measurement/DetailsHeader' -import SummaryText from '../../components/measurement/SummaryText' -import CommonDetails from '../../components/measurement/CommonDetails' -import MeasurementContainer from '../../components/measurement/MeasurementContainer' -import MeasurementNotFound from '../../components/measurement/MeasurementNotFound' -import HeadMetadata from '../../components/measurement/HeadMetadata' - -import NavBar from '../../components/NavBar' +import { Container, theme, Flex, Text } from 'ooni-components' +import { getLocalisedRegionName } from '/utils/i18nCountries' +import NLink from 'next/link' +import { FormattedMessage } from 'react-intl' +import useSWR from 'swr' + +import Hero from 'components/measurement/Hero' +import CommonSummary from 'components/measurement/CommonSummary' +import DetailsHeader from 'components/measurement/DetailsHeader' +import SummaryText from 'components/measurement/SummaryText' +import CommonDetails from 'components/measurement/CommonDetails' +import MeasurementContainer from 'components/measurement/MeasurementContainer' +import MeasurementNotFound from 'components/measurement/MeasurementNotFound' +import HeadMetadata from 'components/measurement/HeadMetadata' +import FeedbackBox from 'components/measurement/FeedbackBox' + +import NavBar from 'components/NavBar' import ErrorPage from '../_error' import { useIntl } from 'react-intl' +import useUser from 'hooks/useUser' +import { DetailsBoxTable } from 'components/measurement/DetailsBox' +import { fetcher } from '/lib/api' const pageColors = { default: theme.colors.base, @@ -30,7 +37,8 @@ const pageColors = { export async function getServerSideProps({ query }) { let initialProps = { - error: null + error: null, + userFeedback: null } // Get `report_id` using optional catch all dynamic route of Next.js @@ -113,6 +121,20 @@ const Measurement = ({ }) => { const intl = useIntl() const country = getLocalisedRegionName(probe_cc, intl.locale) + + const { user, setSubmitted } = useUser() + const [showModal, setShowModal] = useState(false) + + const {data: userFeedback, error: userFeedbackError, mutate: mutateUserFeedback} = useSWR(`/api/_/measurement_feedback/${report_id}`, fetcher) + + const userFeedbackItems = useMemo(() => { + return userFeedback ? + Object.entries(userFeedback.summary).map(([key, value]) => ( + {label: intl.formatMessage({id: `Measurement.Feedback.${key}`}), value} + )) : + [] + }, [userFeedback]) + // Add the 'AS' prefix to probe_asn when API chooses to send just the number probe_asn = typeof probe_asn === 'number' ? `AS${probe_asn}` : probe_asn if (error) { @@ -121,6 +143,11 @@ const Measurement = ({ ) } + const hideModal = () => { + setShowModal(false) + setSubmitted(false) + } + return ( <> @@ -129,84 +156,92 @@ const Measurement = ({ {notFound ? ( ): ( - { - const color = failure === true ? pageColors['error'] : pageColors[status] - const info = scores?.msg ?? statusInfo - return ( - <> - {headMetadata && - - } - - - - - - - {summaryText && - + { + const color = failure === true ? pageColors['error'] : pageColors[status] + const info = scores?.msg ?? statusInfo + return ( + <> + {headMetadata && + } - {details} - + {showModal && + + } + } + onVerifyClick={() => setShowModal(true)} /> - - - ) - }} /> + + + {summaryText && + + } + {details} + + + + ) + } + } /> + )} ) diff --git a/pages/network/[asn].js b/pages/network/[asn].js index 4d9697b1d..ff68ca587 100644 --- a/pages/network/[asn].js +++ b/pages/network/[asn].js @@ -5,7 +5,6 @@ import { Container, Heading, Box, Text, Link } from 'ooni-components' import { useIntl } from 'react-intl' import NLink from 'next/link' import dayjs from 'services/dayjs' -import Layout from 'components/Layout' import NavBar from 'components/NavBar' import { MetaTags } from 'components/dashboard/MetaTags' import Form from 'components/network/Form' diff --git a/pages/search.js b/pages/search.js index cd5846105..783102523 100644 --- a/pages/search.js +++ b/pages/search.js @@ -15,7 +15,6 @@ import { FormattedMessage, useIntl } from 'react-intl' import dayjs from 'services/dayjs' import NavBar from '../components/NavBar' -import Layout from '../components/Layout' import ResultsList from '../components/search/ResultsList' import FilterSidebar, { queryToFilterMap } from '../components/search/FilterSidebar' diff --git a/public/static/lang/en.json b/public/static/lang/en.json index 85a9efc92..b6674e03f 100644 --- a/public/static/lang/en.json +++ b/public/static/lang/en.json @@ -60,6 +60,7 @@ "Measurement.CommonSummary.Label.ASN": "Network", "Measurement.CommonSummary.Label.Country": "Country", "Measurement.CommonSummary.Label.DateTime": "Date & Time", + "Measurement.CommonSummary.Verify": "Verify", "Measurement.DetailsHeader.Runtime": "Runtime", "Measurement.Status.Hint.Websites.Censorship": "", "Measurement.Status.Hint.Websites.DNS": "DNS tampering", diff --git a/yarn.lock b/yarn.lock index 1c6a4f84a..37ef1a5a2 100644 --- a/yarn.lock +++ b/yarn.lock @@ -17,6 +17,13 @@ dependencies: "@babel/highlight" "^7.0.0" +"@babel/code-frame@^7.10.4", "@babel/code-frame@^7.18.6": + version "7.18.6" + resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.18.6.tgz#3b25d38c89600baa2dcc219edfa88a74eb2c427a" + integrity sha512-TDCmlK5eOvH+eH7cdAFlNXeVJqWIQ7gW9tY1GJIpUtFb6CmjVyq2VM3u71bOyR8CRihcCgMUYoDNyLXao3+70Q== + dependencies: + "@babel/highlight" "^7.18.6" + "@babel/code-frame@^7.12.13": version "7.12.13" resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.12.13.tgz#dcfc826beef65e75c50e21d3837d7d95798dd658" @@ -24,13 +31,6 @@ dependencies: "@babel/highlight" "^7.12.13" -"@babel/code-frame@^7.18.6": - version "7.18.6" - resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.18.6.tgz#3b25d38c89600baa2dcc219edfa88a74eb2c427a" - integrity sha512-TDCmlK5eOvH+eH7cdAFlNXeVJqWIQ7gW9tY1GJIpUtFb6CmjVyq2VM3u71bOyR8CRihcCgMUYoDNyLXao3+70Q== - dependencies: - "@babel/highlight" "^7.18.6" - "@babel/compat-data@^7.17.7", "@babel/compat-data@^7.19.3", "@babel/compat-data@^7.19.4": version "7.19.4" resolved "https://registry.yarnpkg.com/@babel/compat-data/-/compat-data-7.19.4.tgz#95c86de137bf0317f3a570e1b6e996b427299747" @@ -1234,6 +1234,13 @@ dependencies: regenerator-runtime "^0.13.4" +"@babel/runtime@^7.12.5", "@babel/runtime@^7.14.6": + version "7.21.0" + resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.21.0.tgz#5b55c9d394e5fcf304909a8b00c07dc217b56673" + integrity sha512-xwII0//EObnq89Ji5AKYQaRYiW/nZ3llSv29d49IuxPhKbtJoLP+9QUUZ4nVragQVtaVGeZrpB+ZtG/Pdy/POw== + dependencies: + regenerator-runtime "^0.13.11" + "@babel/runtime@^7.18.3": version "7.18.6" resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.18.6.tgz#6a1ef59f838debd670421f8c7f2cbb8da9751580" @@ -1357,6 +1364,11 @@ "@babel/helper-validator-identifier" "^7.19.1" to-fast-properties "^2.0.0" +"@cassiozen/usestatemachine@^1.0.1": + version "1.0.1" + resolved "https://registry.yarnpkg.com/@cassiozen/usestatemachine/-/usestatemachine-1.0.1.tgz#0f06a1372771b34e0ffac2c5cee2a177d2309173" + integrity sha512-KWik9Kj+h/Cle/8/4LTF6cWcUslrS3Us9du3JlyneMduiGYx5ZxkdkA+YOS1NxlyMp7Lztb+1cChiTOSh3AntQ== + "@cypress/request@^2.88.10": version "2.88.10" resolved "https://registry.yarnpkg.com/@cypress/request/-/request-2.88.10.tgz#b66d76b07f860d3a4b8d7a0604d020c662752cce" @@ -1606,9 +1618,9 @@ tslib "2.4.0" "@hapi/hoek@^9.0.0": - version "9.2.1" - resolved "https://registry.yarnpkg.com/@hapi/hoek/-/hoek-9.2.1.tgz#9551142a1980503752536b5050fd99f4a7f13b17" - integrity sha512-gfta+H8aziZsm8pZa0vj04KO6biEiisppNgA1kbJvFrrWu9Vm7eaUEy76DIxsuTaWvti5fkJVhllWc6ZTE+Mdw== + version "9.3.0" + resolved "https://registry.yarnpkg.com/@hapi/hoek/-/hoek-9.3.0.tgz#8368869dcb735be2e7f5cb7647de78e167a251fb" + integrity sha512-/c6rf4UJlmHlC9b5BaNvzAcFv7HZ2QHaV0D4/HNlBdvFnvQq8RI4kYdhyPCl7Xj+oWvTWQ8ujhqS53LIgAe6KQ== "@hapi/topo@^5.0.0": version "5.1.0" @@ -1969,6 +1981,14 @@ resolved "https://registry.yarnpkg.com/@rushstack/eslint-patch/-/eslint-patch-1.1.4.tgz#0c8b74c50f29ee44f423f7416829c0bf8bb5eb27" integrity sha512-LwzQKA4vzIct1zNZzBmRKI9QuNpLgTQMEjsQLf3BXuGYb3QPTP4Yjf6mkdX+X1mYttZ808QpOwAzZjv28kq7DA== +"@selderee/plugin-htmlparser2@^0.10.0": + version "0.10.0" + resolved "https://registry.yarnpkg.com/@selderee/plugin-htmlparser2/-/plugin-htmlparser2-0.10.0.tgz#8a304d18df907e086f3cfc71ea0ced52d6524430" + integrity sha512-gW69MEamZ4wk1OsOq1nG1jcyhXIQcnrsX5JwixVw/9xaiav8TCyjESAruu1Rz9yyInhgBXxkNwMeygKnN2uxNA== + dependencies: + domhandler "^5.0.3" + selderee "^0.10.0" + "@sentry/browser@7.11.1": version "7.11.1" resolved "https://registry.yarnpkg.com/@sentry/browser/-/browser-7.11.1.tgz#377d417e833ef54c78a93ef720a742bda5022625" @@ -2095,16 +2115,16 @@ "@sentry/cli" "^1.74.4" "@sideway/address@^4.1.3": - version "4.1.3" - resolved "https://registry.yarnpkg.com/@sideway/address/-/address-4.1.3.tgz#d93cce5d45c5daec92ad76db492cc2ee3c64ab27" - integrity sha512-8ncEUtmnTsMmL7z1YPB47kPUq7LpKWJNFPsRzHiIajGC5uXlWGn+AmkYPcHNl8S4tcEGx+cnORnNYaw2wvL+LQ== + version "4.1.4" + resolved "https://registry.yarnpkg.com/@sideway/address/-/address-4.1.4.tgz#03dccebc6ea47fdc226f7d3d1ad512955d4783f0" + integrity sha512-7vwq+rOHVWjyXxVlR76Agnvhy8I9rpzjosTESvmhNeXOXdZZB15Fl+TI9x1SiHZH5Jv2wTGduSxFDIaq0m3DUw== dependencies: "@hapi/hoek" "^9.0.0" -"@sideway/formula@^3.0.0": - version "3.0.0" - resolved "https://registry.yarnpkg.com/@sideway/formula/-/formula-3.0.0.tgz#fe158aee32e6bd5de85044be615bc08478a0a13c" - integrity sha512-vHe7wZ4NOXVfkoRb8T5otiENVlT7a3IAiw7H5M2+GO+9CDgcVUUsX1zalAztCmwyOr2RUTGJdgB+ZvSVqmdHmg== +"@sideway/formula@^3.0.1": + version "3.0.1" + resolved "https://registry.yarnpkg.com/@sideway/formula/-/formula-3.0.1.tgz#80fcbcbaf7ce031e0ef2dd29b1bfc7c3f583611f" + integrity sha512-/poHZJJVjx3L+zVD6g9KgHfYnb443oi7wLu/XKojDviHy6HOEOA6z1Trk5aR1dGcmPenJEgb2sK2I80LeS3MIg== "@sideway/pinpoint@^2.0.0": version "2.0.0" @@ -2323,11 +2343,43 @@ dependencies: tslib "^2.4.0" +"@testing-library/cypress@^9.0.0": + version "9.0.0" + resolved "https://registry.yarnpkg.com/@testing-library/cypress/-/cypress-9.0.0.tgz#3facad49c4654a99bbd138f83f33b415d2d6f097" + integrity sha512-c1XiCGeHGGTWn0LAU12sFUfoX3qfId5gcSE2yHode+vsyHDWraxDPALjVnHd4/Fa3j4KBcc5k++Ccy6A9qnkMA== + dependencies: + "@babel/runtime" "^7.14.6" + "@testing-library/dom" "^8.1.0" + +"@testing-library/dom@^8.1.0": + version "8.20.0" + resolved "https://registry.yarnpkg.com/@testing-library/dom/-/dom-8.20.0.tgz#914aa862cef0f5e89b98cc48e3445c4c921010f6" + integrity sha512-d9ULIT+a4EXLX3UU8FBjauG9NnsZHkHztXoIcTsOKoOw030fyjheN9svkTULjJxtYag9DZz5Jz5qkWZDPxTFwA== + dependencies: + "@babel/code-frame" "^7.10.4" + "@babel/runtime" "^7.12.5" + "@types/aria-query" "^5.0.1" + aria-query "^5.0.0" + chalk "^4.1.0" + dom-accessibility-api "^0.5.9" + lz-string "^1.4.4" + pretty-format "^27.0.2" + +"@tootallnate/once@2": + version "2.0.0" + resolved "https://registry.yarnpkg.com/@tootallnate/once/-/once-2.0.0.tgz#f544a148d3ab35801c1f633a7441fd87c2e484bf" + integrity sha512-XCuKFP5PS55gnMVu3dty8KPatLqUoy/ZYzDzAGCQ8JNFCkLXzmI7vNHCR+XpbZaMWQK/vQubr7PkYq8g470J/A== + "@trysound/sax@0.2.0": version "0.2.0" resolved "https://registry.yarnpkg.com/@trysound/sax/-/sax-0.2.0.tgz#cccaab758af56761eb7bf37af6f03f326dd798ad" integrity sha512-L7z9BgrNEcYyUYtF+HaEfiS5ebkh9jXqbszz7pC0hRBPaatV0XjSD3+eHrpqFemQfgwiFF0QPIarnIihIDn7OA== +"@types/aria-query@^5.0.1": + version "5.0.1" + resolved "https://registry.yarnpkg.com/@types/aria-query/-/aria-query-5.0.1.tgz#3286741fb8f1e1580ac28784add4c7a1d49bdfbc" + integrity sha512-XTIieEY+gvJ39ChLcB4If5zHtPxt3Syj5rgZR+e1ctpmK8NjPf0zFqsz4JpLJT0xla9GFDKjy8Cpu331nrmE1Q== + "@types/hoist-non-react-statics@^3.3.1": version "3.3.1" resolved "https://registry.yarnpkg.com/@types/hoist-non-react-statics/-/hoist-non-react-statics-3.3.1.tgz#1124aafe5118cb591977aeb1ceaaed1070eb039f" @@ -2451,16 +2503,39 @@ dependencies: lodash "^4" +abab@^2.0.6: + version "2.0.6" + resolved "https://registry.yarnpkg.com/abab/-/abab-2.0.6.tgz#41b80f2c871d19686216b82309231cfd3cb3d291" + integrity sha512-j2afSsaIENvHZN2B8GOpF566vZ5WVk5opAiMTvWgaQT8DkbOqsTfvNAvHoRGU2zzP8cPoqys+xHTRDWW8L+/BA== + abbrev@1: version "1.1.1" resolved "https://registry.yarnpkg.com/abbrev/-/abbrev-1.1.1.tgz#f8f2c887ad10bf67f634f005b6987fed3179aac8" integrity sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q== +acorn-globals@^7.0.0: + version "7.0.1" + resolved "https://registry.yarnpkg.com/acorn-globals/-/acorn-globals-7.0.1.tgz#0dbf05c44fa7c94332914c02066d5beff62c40c3" + integrity sha512-umOSDSDrfHbTNPuNpC2NSnnA3LUrqpevPb4T9jRx4MagXNS0rs+gwiTcAvqCRmsD6utzsrzNt+ebm00SNWiC3Q== + dependencies: + acorn "^8.1.0" + acorn-walk "^8.0.2" + acorn-jsx@^5.3.2: version "5.3.2" resolved "https://registry.yarnpkg.com/acorn-jsx/-/acorn-jsx-5.3.2.tgz#7ed5bb55908b3b2f1bc55c6af1653bada7f07937" integrity sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ== +acorn-walk@^8.0.2: + version "8.2.0" + resolved "https://registry.yarnpkg.com/acorn-walk/-/acorn-walk-8.2.0.tgz#741210f2e2426454508853a2f44d0ab83b7f69c1" + integrity sha512-k+iyHEuPgSw6SbuDpGQM+06HQUa04DZ3o+F6CSzXMvvI5KMvnaEqXe+YVe555R9nn6GPt404fos4wcgpw12SDA== + +acorn@^8.1.0, acorn@^8.8.1: + version "8.8.2" + resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.8.2.tgz#1b2f25db02af965399b9776b0c2c391276d37c4a" + integrity sha512-xjIYgE8HBrkpd/sJqOGNspf8uHG+NOHGOw6a/Urj8taM2EXfdNAH2oFcPeIFfsv3+kz/mJrS5VuMqbNLjCa2vw== + acorn@^8.7.1: version "8.7.1" resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.7.1.tgz#0197122c843d1bf6d0a5e83220a788f278f63c30" @@ -2471,6 +2546,13 @@ acorn@^8.8.0: resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.8.0.tgz#88c0187620435c7f6015803f5539dae05a9dbea8" integrity sha512-QOxyigPVrpZ2GXT+PFyZTl6TtOFc5egxHIP9IlQ+RbupQuX4RkT/Bee4/kQuC02Xkzg84JcT7oLYtDIQxp+v7w== +agent-base@6: + version "6.0.2" + resolved "https://registry.yarnpkg.com/agent-base/-/agent-base-6.0.2.tgz#49fff58577cfee3f37176feab4c22e00f86d7f77" + integrity sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ== + dependencies: + debug "4" + agent-base@^4.3.0: version "4.3.0" resolved "https://registry.yarnpkg.com/agent-base/-/agent-base-4.3.0.tgz#8165f01c436009bccad0b1d122f05ed770efc6ee" @@ -2552,6 +2634,11 @@ ansi-styles@^4.0.0, ansi-styles@^4.1.0: dependencies: color-convert "^2.0.1" +ansi-styles@^5.0.0: + version "5.2.0" + resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-5.2.0.tgz#07449690ad45777d1924ac2abb2fc8895dba836b" + integrity sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA== + aproba@^1.0.3: version "1.2.0" resolved "https://registry.yarnpkg.com/aproba/-/aproba-1.2.0.tgz#6802e6264efd18c790a1b0d517f0f2627bf2c94a" @@ -2570,6 +2657,11 @@ are-we-there-yet@~1.1.2: delegates "^1.0.0" readable-stream "^2.0.6" +arg@^5.0.2: + version "5.0.2" + resolved "https://registry.yarnpkg.com/arg/-/arg-5.0.2.tgz#c81433cc427c92c4dcf4865142dbca6f15acd59c" + integrity sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg== + argparse@^1.0.7: version "1.0.10" resolved "https://registry.yarnpkg.com/argparse/-/argparse-1.0.10.tgz#bcd6791ea5ae09725e17e5ad988134cd40b3d911" @@ -2585,6 +2677,13 @@ aria-query@^4.2.2: "@babel/runtime" "^7.10.2" "@babel/runtime-corejs3" "^7.10.2" +aria-query@^5.0.0: + version "5.1.3" + resolved "https://registry.yarnpkg.com/aria-query/-/aria-query-5.1.3.tgz#19db27cd101152773631396f7a95a3b58c22c35e" + integrity sha512-R5iJ5lkuHybztUfuOAznmboyjWq8O6sqNqtK7CLOqdydi54VNbORp49mb14KbWgG1QD3JFO9hJdZ+y4KutfdOQ== + dependencies: + deep-equal "^2.0.5" + arr-diff@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/arr-diff/-/arr-diff-4.0.0.tgz#d6461074febfec71e7e15235761a329a5dc7c520" @@ -2739,6 +2838,11 @@ atob@^2.1.2: resolved "https://registry.yarnpkg.com/atob/-/atob-2.1.2.tgz#6d9517eb9e030d2436666651e86bd9f6f13533c9" integrity sha512-Wm6ukoaOGJi/73p/cl2GvLjTI5JM1k/O14isD73YML8StrH/7/lRFgmg8nICZgD3bZZvjwCGxtMOD3wWNAu8cg== +available-typed-arrays@^1.0.5: + version "1.0.5" + resolved "https://registry.yarnpkg.com/available-typed-arrays/-/available-typed-arrays-1.0.5.tgz#92f95616501069d07d10edb2fc37d3e1c65123b7" + integrity sha512-DMD0KiN46eipeziST1LPP/STfDU0sufISXmjSgvVsoU2tqxctQeASejWcfNtxYKqETM1UxQ8sp2OrSBWpHY6sw== + aws-sign2@~0.7.0: version "0.7.0" resolved "https://registry.yarnpkg.com/aws-sign2/-/aws-sign2-0.7.0.tgz#b46e890934a9591f2d2f6f86d7e6a9f1b3fe76a8" @@ -2754,13 +2858,6 @@ axe-core@^4.4.2: resolved "https://registry.yarnpkg.com/axe-core/-/axe-core-4.4.2.tgz#dcf7fb6dea866166c3eab33d68208afe4d5f670c" integrity sha512-LVAaGp/wkkgYJcjmHsoKx4juT1aQvJyPcW09MLCjVTh3V2cc6PnyempiLMNH5iMdfIX/zdbjUx2KDjMLCTdPeA== -axios@^0.21.1: - version "0.21.4" - resolved "https://registry.yarnpkg.com/axios/-/axios-0.21.4.tgz#c67b90dc0568e5c1cf2b0b858c43ba28e2eda575" - integrity sha512-ut5vewkiu8jjGBdqpM44XxjuCjq9LAKeHVmoVfHVzy8eHgxxq8SbAVQNovDA8mVi05kP0Ea/n/UzcSHcTJQfNg== - dependencies: - follow-redirects "^1.14.0" - axios@^0.27.2: version "0.27.2" resolved "https://registry.yarnpkg.com/axios/-/axios-0.27.2.tgz#207658cc8621606e586c85db4b41a750e756d972" @@ -3385,6 +3482,23 @@ csso@^4.2.0: dependencies: css-tree "^1.1.2" +cssom@^0.5.0: + version "0.5.0" + resolved "https://registry.yarnpkg.com/cssom/-/cssom-0.5.0.tgz#d254fa92cd8b6fbd83811b9fbaed34663cc17c36" + integrity sha512-iKuQcq+NdHqlAcwUY0o/HL69XQrUaQdMjmStJ8JFmUaiiQErlhrmuigkg/CU4E2J0IyUKUrMAgl36TvN67MqTw== + +cssom@~0.3.6: + version "0.3.8" + resolved "https://registry.yarnpkg.com/cssom/-/cssom-0.3.8.tgz#9f1276f5b2b463f2114d3f2c75250af8c1a36f4a" + integrity sha512-b0tGHbfegbhPJpxpiBPU2sCkigAqtM9O121le6bbOlgyV+NyGyCmVfJ6QW9eRjz8CpNfWEOYBIMIGRYkLwsIYg== + +cssstyle@^2.3.0: + version "2.3.0" + resolved "https://registry.yarnpkg.com/cssstyle/-/cssstyle-2.3.0.tgz#ff665a0ddbdc31864b09647f34163443d90b0852" + integrity sha512-AZL67abkUzIuvcHqk7c09cezpGNcxUxU4Ioi/05xHk4DQeTkWmGYftIE6ctU6AEt+Gn4n1lDStOtj7FKycP71A== + dependencies: + cssom "~0.3.6" + csstype@^2.2.0: version "2.6.9" resolved "https://registry.yarnpkg.com/csstype/-/csstype-2.6.9.tgz#05141d0cd557a56b8891394c1911c40c8a98d098" @@ -3400,10 +3514,17 @@ csstype@^3.0.2: resolved "https://registry.yarnpkg.com/csstype/-/csstype-3.1.0.tgz#4ddcac3718d787cf9df0d1b7d15033925c8f29f2" integrity sha512-uX1KG+x9h5hIJsaKR9xHUeUraxf8IODOwq9JLNPq6BwB04a/xgpq3rcx47l5BZu5zBPlgD342tdke3Hom/nJRA== -cypress@^10.6.0: - version "10.6.0" - resolved "https://registry.yarnpkg.com/cypress/-/cypress-10.6.0.tgz#13f46867febf2c3715874ed5dce9c2e946b175fe" - integrity sha512-6sOpHjostp8gcLO34p6r/Ci342lBs8S5z9/eb3ZCQ22w2cIhMWGUoGKkosabPBfKcvRS9BE4UxybBtlIs8gTQA== +cypress-recurse@^1.27.0: + version "1.27.0" + resolved "https://registry.yarnpkg.com/cypress-recurse/-/cypress-recurse-1.27.0.tgz#0c61e809c5f7740a7e907714614c49c72dcb5c1f" + integrity sha512-BCD83UqaxlD+JiqZn1PvIhHRXasgfCt57vLC1Fcyifvxh4QklELRcYUJV3MdhKamMkmajaErLfnCNbZ8VJ5SIg== + dependencies: + humanize-duration "^3.27.3" + +cypress@^12.6.0: + version "12.6.0" + resolved "https://registry.yarnpkg.com/cypress/-/cypress-12.6.0.tgz#d71a82639756173c0682b3d467eb9f0523460e91" + integrity sha512-WdHSVaS1lumSd5XpVTslZd8ui9GIGphrzvXq9+3DtVhqjRZC5M70gu5SW/Y/SLPq3D1wiXGZoHC6HJ7ESVE2lw== dependencies: "@cypress/request" "^2.88.10" "@cypress/xvfb" "^1.2.4" @@ -3422,9 +3543,9 @@ cypress@^10.6.0: commander "^5.1.0" common-tags "^1.8.0" dayjs "^1.10.4" - debug "^4.3.2" + debug "^4.3.4" enquirer "^2.3.6" - eventemitter2 "^6.4.3" + eventemitter2 "6.4.7" execa "4.1.0" executable "^4.1.1" extract-zip "2.0.1" @@ -3623,6 +3744,15 @@ dashdash@^1.12.0: dependencies: assert-plus "^1.0.0" +data-urls@^3.0.2: + version "3.0.2" + resolved "https://registry.yarnpkg.com/data-urls/-/data-urls-3.0.2.tgz#9cf24a477ae22bcef5cd5f6f0bfbc1d2d3be9143" + integrity sha512-Jy/tj3ldjZJo63sVAvg6LHt2mHvl4V6AgRAmNDtLdm7faqtsx+aJG42rsyCo9JCoRVKwPFzKlIPx3DIibwSIaQ== + dependencies: + abab "^2.0.6" + whatwg-mimetype "^3.0.0" + whatwg-url "^11.0.0" + date-fns@^2.29.1: version "2.29.1" resolved "https://registry.yarnpkg.com/date-fns/-/date-fns-2.29.1.tgz#9667c2615525e552b5135a3116b95b1961456e60" @@ -3638,10 +3768,10 @@ dayjs@^1.11.5: resolved "https://registry.yarnpkg.com/dayjs/-/dayjs-1.11.5.tgz#00e8cc627f231f9499c19b38af49f56dc0ac5e93" integrity sha512-CAdX5Q3YW3Gclyo5Vpqkgpj8fSdLQcRuzfX6mC6Phy0nfJ0eGYOeS7m4mt2plDWLAtA4TqTakvbboHvUxfe4iA== -debug@4.3.2: - version "4.3.2" - resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.2.tgz#f0a49c18ac8779e31d4a0c6029dfb76873c7428b" - integrity sha512-mOp8wKcvj7XxC78zLgw/ZA+6TSgkoE2C/ienthhRD298T7UNwAg9diBpLRxC0mOezLl4B0xV7M0cCO6P/O0Xhw== +debug@4, debug@4.3.4, debug@^4.3.4: + version "4.3.4" + resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.4.tgz#1319f6579357f2338d3337d2cdd4914bb5dcc865" + integrity sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ== dependencies: ms "2.1.2" @@ -3687,18 +3817,39 @@ debug@^4.3.2: dependencies: ms "2.1.2" -debug@^4.3.4: - version "4.3.4" - resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.4.tgz#1319f6579357f2338d3337d2cdd4914bb5dcc865" - integrity sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ== - dependencies: - ms "2.1.2" +decimal.js@^10.4.2: + version "10.4.3" + resolved "https://registry.yarnpkg.com/decimal.js/-/decimal.js-10.4.3.tgz#1044092884d245d1b7f65725fa4ad4c6f781cc23" + integrity sha512-VBBaLc1MgL5XpzgIP7ny5Z6Nx3UrRkIViUkPUdtl9aya5amy3De1gsUUSB1g3+3sExYNjCAsAznmukyxCb1GRA== decode-uri-component@^0.2.0: version "0.2.2" resolved "https://registry.yarnpkg.com/decode-uri-component/-/decode-uri-component-0.2.2.tgz#e69dbe25d37941171dd540e024c444cd5188e1e9" integrity sha512-FqUYQ+8o158GyGTrMFJms9qh3CqTKvAqgqsTnkLI8sKu0028orqBhxNMFkFen0zGyg6epACD32pjVk58ngIErQ== +deep-equal@^2.0.5: + version "2.2.0" + resolved "https://registry.yarnpkg.com/deep-equal/-/deep-equal-2.2.0.tgz#5caeace9c781028b9ff459f33b779346637c43e6" + integrity sha512-RdpzE0Hv4lhowpIUKKMJfeH6C1pXdtT1/it80ubgWqwI3qpuxUBpC1S4hnHg+zjnuOoDkzUtUCEEkG+XG5l3Mw== + dependencies: + call-bind "^1.0.2" + es-get-iterator "^1.1.2" + get-intrinsic "^1.1.3" + is-arguments "^1.1.1" + is-array-buffer "^3.0.1" + is-date-object "^1.0.5" + is-regex "^1.1.4" + is-shared-array-buffer "^1.0.2" + isarray "^2.0.5" + object-is "^1.1.5" + object-keys "^1.1.1" + object.assign "^4.1.4" + regexp.prototype.flags "^1.4.3" + side-channel "^1.0.4" + which-boxed-primitive "^1.0.2" + which-collection "^1.0.1" + which-typed-array "^1.1.9" + deep-extend@^0.6.0: version "0.6.0" resolved "https://registry.yarnpkg.com/deep-extend/-/deep-extend-0.6.0.tgz#c4fa7c95404a17a9c3e8ca7e1537312b736330ac" @@ -3709,6 +3860,11 @@ deep-is@^0.1.3: resolved "https://registry.yarnpkg.com/deep-is/-/deep-is-0.1.3.tgz#b369d6fb5dbc13eecf524f91b070feedc357cf34" integrity sha1-s2nW+128E+7PUk+RsHD+7cNXzzQ= +deep-is@~0.1.3: + version "0.1.4" + resolved "https://registry.yarnpkg.com/deep-is/-/deep-is-0.1.4.tgz#a6f2dce612fadd2ef1f519b73551f17e85199831" + integrity sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ== + deepmerge@^4.2.2: version "4.2.2" resolved "https://registry.yarnpkg.com/deepmerge/-/deepmerge-4.2.2.tgz#44d2ea3679b8f4d4ffba33f03d865fc1e7bf4955" @@ -3794,6 +3950,11 @@ document.contains@^1.0.1: dependencies: define-properties "^1.1.3" +dom-accessibility-api@^0.5.9: + version "0.5.16" + resolved "https://registry.yarnpkg.com/dom-accessibility-api/-/dom-accessibility-api-0.5.16.tgz#5a7429e6066eb3664d911e33fb0e45de8eb08453" + integrity sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg== + dom-serializer@^1.0.1: version "1.4.1" resolved "https://registry.yarnpkg.com/dom-serializer/-/dom-serializer-1.4.1.tgz#de5d41b1aea290215dc45a6dae8adcf1d32e2d30" @@ -3803,11 +3964,27 @@ dom-serializer@^1.0.1: domhandler "^4.2.0" entities "^2.0.0" -domelementtype@^2.0.1, domelementtype@^2.2.0: +dom-serializer@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/dom-serializer/-/dom-serializer-2.0.0.tgz#e41b802e1eedf9f6cae183ce5e622d789d7d8e53" + integrity sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg== + dependencies: + domelementtype "^2.3.0" + domhandler "^5.0.2" + entities "^4.2.0" + +domelementtype@^2.0.1, domelementtype@^2.2.0, domelementtype@^2.3.0: version "2.3.0" resolved "https://registry.yarnpkg.com/domelementtype/-/domelementtype-2.3.0.tgz#5c45e8e869952626331d7aab326d01daf65d589d" integrity sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw== +domexception@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/domexception/-/domexception-4.0.0.tgz#4ad1be56ccadc86fc76d033353999a8037d03673" + integrity sha512-A2is4PLG+eeSfoTMA95/s4pvAoSo2mKtiM5jlHkAVewmiO8ISFTFKZjH7UAM1Atli/OT/7JHOrJRJiMKUZKYBw== + dependencies: + webidl-conversions "^7.0.0" + domhandler@^4.2.0, domhandler@^4.3.1: version "4.3.1" resolved "https://registry.yarnpkg.com/domhandler/-/domhandler-4.3.1.tgz#8d792033416f59d68bc03a5aa7b018c1ca89279c" @@ -3815,6 +3992,13 @@ domhandler@^4.2.0, domhandler@^4.3.1: dependencies: domelementtype "^2.2.0" +domhandler@^5.0.1, domhandler@^5.0.2, domhandler@^5.0.3: + version "5.0.3" + resolved "https://registry.yarnpkg.com/domhandler/-/domhandler-5.0.3.tgz#cc385f7f751f1d1fc650c21374804254538c7d31" + integrity sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w== + dependencies: + domelementtype "^2.3.0" + domutils@^2.8.0: version "2.8.0" resolved "https://registry.yarnpkg.com/domutils/-/domutils-2.8.0.tgz#4437def5db6e2d1f5d6ee859bd95ca7d02048135" @@ -3824,10 +4008,19 @@ domutils@^2.8.0: domelementtype "^2.2.0" domhandler "^4.2.0" +domutils@^3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/domutils/-/domutils-3.0.1.tgz#696b3875238338cb186b6c0612bd4901c89a4f1c" + integrity sha512-z08c1l761iKhDFtfXO04C7kTdPBLi41zwOZl00WS8b5eiaebNpY00HKbztwBq+e3vyqWNwWF3mP9YLUeqIrF+Q== + dependencies: + dom-serializer "^2.0.0" + domelementtype "^2.3.0" + domhandler "^5.0.1" + duplexer@~0.1.1: - version "0.1.1" - resolved "https://registry.yarnpkg.com/duplexer/-/duplexer-0.1.1.tgz#ace6ff808c1ce66b57d1ebf97977acb02334cfc1" - integrity sha1-rOb/gIwc5mtX0ev5eXessCM0z8E= + version "0.1.2" + resolved "https://registry.yarnpkg.com/duplexer/-/duplexer-0.1.2.tgz#3abe43aef3835f8ae077d136ddce0f276b0400e6" + integrity sha512-jtD6YG370ZCIi/9GTaJKQxWTZD045+4R4hTk/x1UyoqadyJ9x9CgSi1RlVDQF8U2sxLLSnFkCaMihqljHIWgMg== ecc-jsbn@~0.1.1: version "0.1.2" @@ -3866,6 +4059,11 @@ emotion-theming@^10.0.27: "@emotion/weak-memoize" "0.2.5" hoist-non-react-statics "^3.3.0" +encoding-japanese@2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/encoding-japanese/-/encoding-japanese-2.0.0.tgz#fa0226e5469e7b5b69a04fea7d5481bd1fa56936" + integrity sha512-++P0RhebUC8MJAwJOsT93dT+5oc5oPImp1HubZpAuCZ5kTLnhuuBhKHj2jJeO/Gj93idPBWmIuQ9QWMe5rX3pQ== + end-of-stream@^1.1.0: version "1.4.1" resolved "https://registry.yarnpkg.com/end-of-stream/-/end-of-stream-1.4.1.tgz#ed29634d19baba463b6ce6b80a37213eab71ec43" @@ -3885,7 +4083,7 @@ entities@^2.0.0: resolved "https://registry.yarnpkg.com/entities/-/entities-2.2.0.tgz#098dc90ebb83d8dffa089d55256b351d34c4da55" integrity sha512-p92if5Nz619I0w+akJrLZH0MX0Pb5DX39XOwQTtXSdQQOaYH03S1uIQp4mhOZtAXrxq4ViO67YTiLBo2638o9A== -entities@^4.3.0: +entities@^4.2.0, entities@^4.3.0, entities@^4.4.0: version "4.4.0" resolved "https://registry.yarnpkg.com/entities/-/entities-4.4.0.tgz#97bdaba170339446495e653cfd2db78962900174" integrity sha512-oYp7156SP8LkeGD0GF85ad1X9Ai79WtRsZ2gxJqtBuzH+98YUV6jkHEKlZkMbcrjJjIVJNIDP/3WL9wQkoPbWA== @@ -4004,6 +4202,21 @@ es-abstract@^1.19.4: string.prototype.trimstart "^1.0.4" unbox-primitive "^1.0.1" +es-get-iterator@^1.1.2: + version "1.1.3" + resolved "https://registry.yarnpkg.com/es-get-iterator/-/es-get-iterator-1.1.3.tgz#3ef87523c5d464d41084b2c3c9c214f1199763d6" + integrity sha512-sPZmqHBe6JIiTfN5q2pEi//TwxmAFHwj/XEuYjTuse78i8KxaqMTTzxPoFKuzRpDpTJ+0NAbpfenkmH2rePtuw== + dependencies: + call-bind "^1.0.2" + get-intrinsic "^1.1.3" + has-symbols "^1.0.3" + is-arguments "^1.1.1" + is-map "^2.0.2" + is-set "^2.0.2" + is-string "^1.0.7" + isarray "^2.0.5" + stop-iteration-iterator "^1.0.0" + es-shim-unscopables@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/es-shim-unscopables/-/es-shim-unscopables-1.0.0.tgz#702e632193201e3edf8713635d083d378e510241" @@ -4047,6 +4260,18 @@ escape-string-regexp@^4.0.0: resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz#14ba83a5d373e3d311e5afca29cf5bfad965bf34" integrity sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA== +escodegen@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/escodegen/-/escodegen-2.0.0.tgz#5e32b12833e8aa8fa35e1bf0befa89380484c7dd" + integrity sha512-mmHKys/C8BFUGI+MAWNcSYoORYLMdPzjrknd2Vc+bUsjN5bXcr8EhrNB+UTqfL1y3I9c4fw2ihgtMPQLBRiQxw== + dependencies: + esprima "^4.0.1" + estraverse "^5.2.0" + esutils "^2.0.2" + optionator "^0.8.1" + optionalDependencies: + source-map "~0.6.1" + eslint-config-next@^12.2.5: version "12.2.5" resolved "https://registry.yarnpkg.com/eslint-config-next/-/eslint-config-next-12.2.5.tgz#76ce83f18cc02f6f42ed407a127f83db54fabd3c" @@ -4247,7 +4472,7 @@ espree@^9.3.3: acorn-jsx "^5.3.2" eslint-visitor-keys "^3.3.0" -esprima@^4.0.0, esprima@~4.0.0: +esprima@^4.0.0, esprima@^4.0.1, esprima@~4.0.0: version "4.0.1" resolved "https://registry.yarnpkg.com/esprima/-/esprima-4.0.1.tgz#13b04cdb3e6c5d19df91ab6987a8695619b0aa71" integrity sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A== @@ -4284,7 +4509,7 @@ esutils@^2.0.2: event-stream@=3.3.4: version "3.3.4" resolved "https://registry.yarnpkg.com/event-stream/-/event-stream-3.3.4.tgz#4ab4c9a0f5a54db9338b4c34d86bfce8f4b35571" - integrity sha1-SrTJoPWlTbkzi0w02Gv86PSzVXE= + integrity sha512-QHpkERcGsR0T7Qm3HNJSyXKEEj8AHNxkY3PK8TS2KJvQ7NiSHe3DDpwVKKtoYprL/AreyzFBeIkBIWChAqn60g== dependencies: duplexer "~0.1.1" from "~0" @@ -4294,10 +4519,10 @@ event-stream@=3.3.4: stream-combiner "~0.0.4" through "~2.3.1" -eventemitter2@^6.4.3: - version "6.4.5" - resolved "https://registry.yarnpkg.com/eventemitter2/-/eventemitter2-6.4.5.tgz#97380f758ae24ac15df8353e0cc27f8b95644655" - integrity sha512-bXE7Dyc1i6oQElDG0jMRZJrRAn9QR2xyyFGmBdZleNmyQX0FqGYmhZIrIrpPfm/w//LTo4tVQGOGQcGCb5q9uw== +eventemitter2@6.4.7: + version "6.4.7" + resolved "https://registry.yarnpkg.com/eventemitter2/-/eventemitter2-6.4.7.tgz#a7f6c4d7abf28a14c1ef3442f21cb306a054271d" + integrity sha512-tYUSVOGeQPKt/eC1ABfhHy5Xd96N3oIijJvN3O9+TsC28T5V9yX9oEfEK5faP0EFSNVOG97qtAS68GBrQB2hDg== execa@4.1.0: version "4.1.0" @@ -4430,7 +4655,7 @@ fast-json-stable-stringify@^2.0.0: resolved "https://registry.yarnpkg.com/fast-json-stable-stringify/-/fast-json-stable-stringify-2.0.0.tgz#d5142c0caee6b1189f87d3a76111064f86c8bbf2" integrity sha1-1RQsDK7msRifh9OnYREGT4bIu/I= -fast-levenshtein@^2.0.6: +fast-levenshtein@^2.0.6, fast-levenshtein@~2.0.6: version "2.0.6" resolved "https://registry.yarnpkg.com/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz#3d8a5c66883a16a30ca8643e851f19baa7797917" integrity sha1-PYpcZog6FqMMqGQ+hR8Zuqd5eRc= @@ -4572,16 +4797,18 @@ flux@^4.0.1: fbemitter "^3.0.0" fbjs "^3.0.1" -follow-redirects@^1.14.0: - version "1.14.9" - resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.14.9.tgz#dd4ea157de7bfaf9ea9b3fbd85aa16951f78d8d7" - integrity sha512-MQDfihBQYMcyy5dhRDJUHcw7lb2Pv/TuE6xP1vyraLukNDHKbDxDNaOE3NbCAdKQApno+GPRyo1YAp89yCjK4w== - follow-redirects@^1.14.9: version "1.15.1" resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.15.1.tgz#0ca6a452306c9b276e4d3127483e29575e207ad5" integrity sha512-yLAMQs+k0b2m7cVxpS1VKJVvoz7SS9Td1zss3XRwXj+ZDH00RJgnuLx7E44wx02kQLrdM3aOOy+FpzS7+8OizA== +for-each@^0.3.3: + version "0.3.3" + resolved "https://registry.yarnpkg.com/for-each/-/for-each-0.3.3.tgz#69b447e88a0a5d32c3e7084f3f1710034b21376e" + integrity sha512-jqYfLp7mo9vIyQf8ykW2v7A+2N4QjeCeI5+Dz9XraiO1ign81wjiH7Fb9vSOWvQfNtmSa4H2RoQTrrXivdUZmw== + dependencies: + is-callable "^1.1.3" + for-in@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/for-in/-/for-in-1.0.2.tgz#81068d295a8142ec0ac726c6e2200c30fb6d5e80" @@ -4620,7 +4847,7 @@ fragment-cache@^0.2.1: from@~0: version "0.1.7" resolved "https://registry.yarnpkg.com/from/-/from-0.1.7.tgz#83c60afc58b9c56997007ed1a768b3ab303a44fe" - integrity sha1-g8YK/Fi5xWmXAH7Rp2izqzA6RP4= + integrity sha512-twe20eF1OxVxp/ML/kq2p1uc6KvFK/+vs8WjEbeKmV2He22MKm7YF2ANIt+EOqhJ5L3K/SuuPhk0hWQDjOM23g== fs-extra@^9.1.0: version "9.1.0" @@ -4705,6 +4932,15 @@ get-intrinsic@^1.0.2, get-intrinsic@^1.1.0, get-intrinsic@^1.1.1: has "^1.0.3" has-symbols "^1.0.1" +get-intrinsic@^1.1.3, get-intrinsic@^1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/get-intrinsic/-/get-intrinsic-1.2.0.tgz#7ad1dc0535f3a2904bba075772763e5051f6d05f" + integrity sha512-L049y6nFOuom5wGyRc3/gdTLO94dySVKRACj1RmJZBQXlbTMhtNIgkWkUHq+jYmZvKf14EW1EoJnnjbmoHij0Q== + dependencies: + function-bind "^1.1.1" + has "^1.0.3" + has-symbols "^1.0.3" + get-stream@^5.0.0: version "5.1.0" resolved "https://registry.yarnpkg.com/get-stream/-/get-stream-5.1.0.tgz#01203cdc92597f9b909067c3e656cc1f4d3c4dc9" @@ -4860,6 +5096,13 @@ globby@^11.1.0: merge2 "^1.4.1" slash "^3.0.0" +gopd@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/gopd/-/gopd-1.0.1.tgz#29ff76de69dac7489b7c0918a5788e56477c332c" + integrity sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA== + dependencies: + get-intrinsic "^1.1.3" + graceful-fs@^4.1.11, graceful-fs@^4.2.4: version "4.2.10" resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.10.tgz#147d3a006da4ca3ce14728c7aefc287c367d7a6c" @@ -4967,6 +5210,11 @@ has@^1.0.3: dependencies: function-bind "^1.1.1" +he@1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/he/-/he-1.2.0.tgz#84ae65fa7eafb165fddb61566ae14baf05664f0f" + integrity sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw== + hoist-non-react-statics@3.0.1, hoist-non-react-statics@^3.0.0, hoist-non-react-statics@^3.3.0, hoist-non-react-statics@^3.3.2: version "3.0.1" resolved "https://registry.yarnpkg.com/hoist-non-react-statics/-/hoist-non-react-statics-3.0.1.tgz#fba3e7df0210eb9447757ca1a7cb607162f0a364" @@ -4974,6 +5222,43 @@ hoist-non-react-statics@3.0.1, hoist-non-react-statics@^3.0.0, hoist-non-react-s dependencies: react-is "^16.3.2" +html-encoding-sniffer@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/html-encoding-sniffer/-/html-encoding-sniffer-3.0.0.tgz#2cb1a8cf0db52414776e5b2a7a04d5dd98158de9" + integrity sha512-oWv4T4yJ52iKrufjnyZPkrN0CH3QnrUqdB6In1g5Fe1mia8GmF36gnfNySxoZtxD5+NmYw1EElVXiBk93UeskA== + dependencies: + whatwg-encoding "^2.0.0" + +html-to-text@9.0.3: + version "9.0.3" + resolved "https://registry.yarnpkg.com/html-to-text/-/html-to-text-9.0.3.tgz#331368f32fcb270c59dbd3a7fdb32813d2a490bc" + integrity sha512-hxDF1kVCF2uw4VUJ3vr2doc91pXf2D5ngKcNviSitNkhP9OMOaJkDrFIFL6RMvko7NisWTEiqGpQ9LAxcVok1w== + dependencies: + "@selderee/plugin-htmlparser2" "^0.10.0" + deepmerge "^4.2.2" + dom-serializer "^2.0.0" + htmlparser2 "^8.0.1" + selderee "^0.10.0" + +htmlparser2@^8.0.1: + version "8.0.1" + resolved "https://registry.yarnpkg.com/htmlparser2/-/htmlparser2-8.0.1.tgz#abaa985474fcefe269bc761a779b544d7196d010" + integrity sha512-4lVbmc1diZC7GUJQtRQ5yBAeUCL1exyMwmForWkRLnwyzWBFxN633SALPMGYaWZvKe9j1pRZJpauvmxENSp/EA== + dependencies: + domelementtype "^2.3.0" + domhandler "^5.0.2" + domutils "^3.0.1" + entities "^4.3.0" + +http-proxy-agent@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/http-proxy-agent/-/http-proxy-agent-5.0.0.tgz#5129800203520d434f142bc78ff3c170800f2b43" + integrity sha512-n2hY8YdoRE1i7r6M0w9DIw5GgZN0G25P8zLCRQ8rjXtTU3vsNFBI/vWK/UIeE6g5MUUz6avwAPXmL6Fy9D/90w== + dependencies: + "@tootallnate/once" "2" + agent-base "6" + debug "4" + http-signature@~1.3.6: version "1.3.6" resolved "https://registry.yarnpkg.com/http-signature/-/http-signature-1.3.6.tgz#cb6fbfdf86d1c974f343be94e87f7fc128662cf9" @@ -4983,7 +5268,7 @@ http-signature@~1.3.6: jsprim "^2.0.2" sshpk "^1.14.1" -https-proxy-agent@^2.2.3, https-proxy-agent@^5.0.0: +https-proxy-agent@^2.2.3, https-proxy-agent@^5.0.0, https-proxy-agent@^5.0.1: version "2.2.4" resolved "https://registry.yarnpkg.com/https-proxy-agent/-/https-proxy-agent-2.2.4.tgz#4ee7a737abd92678a293d9b34a1af4d0d08c787b" integrity sha512-OmvfoQ53WLjtA9HeYP9RNrWMJzzAz1JGaSFr1nijg0PVR1JaD/xbJq1mdEIIlxGpXp9eSe/O2LgU9DJmTPd0Eg== @@ -5001,12 +5286,24 @@ human-signals@^2.1.0: resolved "https://registry.yarnpkg.com/human-signals/-/human-signals-2.1.0.tgz#dc91fcba42e4d06e4abaed33b3e7a3c02f514ea0" integrity sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw== +humanize-duration@^3.27.3: + version "3.28.0" + resolved "https://registry.yarnpkg.com/humanize-duration/-/humanize-duration-3.28.0.tgz#f79770c0bec34d3bfd4899338cc40643bc04df72" + integrity sha512-jMAxraOOmHuPbffLVDKkEKi/NeG8dMqP8lGRd6Tbf7JgAeG33jjgPWDbXXU7ypCI0o+oNKJFgbSB9FKVdWNI2A== + iconv-lite@0.4.15: version "0.4.15" resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.15.tgz#fe265a218ac6a57cfe854927e9d04c19825eddeb" integrity sha1-/iZaIYrGpXz+hUkn6dBMGYJe3es= -iconv-lite@^0.4.4: +iconv-lite@0.6.3: + version "0.6.3" + resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.6.3.tgz#a52f80bf38da1952eb5c681790719871a1a72501" + integrity sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw== + dependencies: + safer-buffer ">= 2.1.2 < 3.0.0" + +iconv-lite@^0.4.4, iconv-lite@~0.4.13: version "0.4.24" resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.24.tgz#2022b4b25fbddc21d2f524974a474aafe733908b" integrity sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA== @@ -5030,6 +5327,26 @@ ignore@^5.2.0: resolved "https://registry.yarnpkg.com/ignore/-/ignore-5.2.0.tgz#6d3bac8fa7fe0d45d9f9be7bac2fc279577e345a" integrity sha512-CmxgYGiEPCLhfLnpPp1MoRmifwEIOgjcHXxOBjv7mY96c+eWScsOP9c112ZyLdWHi0FxHjI+4uVhKYp/gcdRmQ== +imap-simple@^5.1.0: + version "5.1.0" + resolved "https://registry.yarnpkg.com/imap-simple/-/imap-simple-5.1.0.tgz#7d8936eb1c4774f21cc38c358888fbf82c2fb57a" + integrity sha512-FLZm1v38C5ekN46l/9X5gBRNMQNVc5TSLYQ3Hsq3xBLvKwt1i5fcuShyth8MYMPuvId1R46oaPNrH92hFGHr/g== + dependencies: + iconv-lite "~0.4.13" + imap "^0.8.18" + nodeify "^1.0.0" + quoted-printable "^1.0.0" + utf8 "^2.1.1" + uuencode "0.0.4" + +imap@^0.8.18: + version "0.8.19" + resolved "https://registry.yarnpkg.com/imap/-/imap-0.8.19.tgz#3678873934ab09cea6ba48741f284da2af59d8d5" + integrity sha512-z5DxEA1uRnZG73UcPA4ES5NSCGnPuuouUx43OPX7KZx1yzq3N8/vx2mtXEShT5inxB3pRgnfG1hijfu7XN2YMw== + dependencies: + readable-stream "1.1.x" + utf7 ">=1.0.2" + immediate@~3.0.5: version "3.0.6" resolved "https://registry.yarnpkg.com/immediate/-/immediate-3.0.6.tgz#9db1dbd0faf8de6fbe0f5dd5e56bb606280de69b" @@ -5069,7 +5386,7 @@ inflight@^1.0.4: once "^1.3.0" wrappy "1" -inherits@2, inherits@~2.0.3: +inherits@2, inherits@~2.0.1, inherits@~2.0.3: version "2.0.4" resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c" integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ== @@ -5093,6 +5410,15 @@ internal-slot@^1.0.3: has "^1.0.3" side-channel "^1.0.4" +internal-slot@^1.0.4: + version "1.0.5" + resolved "https://registry.yarnpkg.com/internal-slot/-/internal-slot-1.0.5.tgz#f2a2ee21f668f8627a4667f309dc0f4fb6674986" + integrity sha512-Y+R5hJrzs52QCG2laLn4udYVnxsfny9CpOhNhUvk/SSSVyF6T27FzRbF0sroPidSu3X8oEAkOn2K804mjpt6UQ== + dependencies: + get-intrinsic "^1.2.0" + has "^1.0.3" + side-channel "^1.0.4" + internmap@^1.0.0: version "1.0.1" resolved "https://registry.yarnpkg.com/internmap/-/internmap-1.0.1.tgz#0017cc8a3b99605f0302f2b198d272e015e5df95" @@ -5122,6 +5448,23 @@ is-accessor-descriptor@^1.0.0: dependencies: kind-of "^6.0.0" +is-arguments@^1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/is-arguments/-/is-arguments-1.1.1.tgz#15b3f88fda01f2a97fec84ca761a560f123efa9b" + integrity sha512-8Q7EARjzEnKpt/PCD7e1cgUS0a6X8u5tdSiMqXhojOdoV9TsMsiO+9VLC5vAmO8N7/GmXn7yjR8qnA6bVAEzfA== + dependencies: + call-bind "^1.0.2" + has-tostringtag "^1.0.0" + +is-array-buffer@^3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/is-array-buffer/-/is-array-buffer-3.0.1.tgz#deb1db4fcae48308d54ef2442706c0393997052a" + integrity sha512-ASfLknmY8Xa2XtB4wmbz13Wu202baeA18cJBCeCy0wXUHZF0IPyVEXqKEcd+t2fNSLLL1vC6k7lxZEojNbISXQ== + dependencies: + call-bind "^1.0.2" + get-intrinsic "^1.1.3" + is-typed-array "^1.1.10" + is-arrayish@^0.2.1: version "0.2.1" resolved "https://registry.yarnpkg.com/is-arrayish/-/is-arrayish-0.2.1.tgz#77c99840527aa8ecb1a8ba697b80645a7a926a9d" @@ -5152,6 +5495,11 @@ is-buffer@^1.1.5: resolved "https://registry.yarnpkg.com/is-buffer/-/is-buffer-1.1.6.tgz#efaa2ea9daa0d7ab2ea13a97b2b8ad51fefbe8be" integrity sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w== +is-callable@^1.1.3: + version "1.2.7" + resolved "https://registry.yarnpkg.com/is-callable/-/is-callable-1.2.7.tgz#3bc2a85ea742d9e36205dcacdd72ca1fdc51b055" + integrity sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA== + is-callable@^1.1.4, is-callable@^1.2.4: version "1.2.4" resolved "https://registry.yarnpkg.com/is-callable/-/is-callable-1.2.4.tgz#47301d58dd0259407865547853df6d61fe471945" @@ -5192,7 +5540,7 @@ is-data-descriptor@^1.0.0: dependencies: kind-of "^6.0.0" -is-date-object@^1.0.1: +is-date-object@^1.0.1, is-date-object@^1.0.5: version "1.0.5" resolved "https://registry.yarnpkg.com/is-date-object/-/is-date-object-1.0.5.tgz#0841d5536e724c25597bf6ea62e1bd38298df31f" integrity sha512-9YQaSxsAiSwcvS33MBk3wTCVnWK+HhF8VZR2jRxehM16QcVOdHqPn4VPHmRK4lSr38n9JriurInLcP90xsYNfQ== @@ -5266,6 +5614,11 @@ is-installed-globally@~0.4.0: global-dirs "^3.0.0" is-path-inside "^3.0.2" +is-map@^2.0.1, is-map@^2.0.2: + version "2.0.2" + resolved "https://registry.yarnpkg.com/is-map/-/is-map-2.0.2.tgz#00922db8c9bf73e81b7a335827bc2a43f2b91127" + integrity sha512-cOZFQQozTha1f4MxLFzlgKYPTyj26picdZTx82hbc/Xf4K/tZOOXSCkMvU4pKioRXGDLJRn0GM7Upe7kR721yg== + is-negative-zero@^2.0.1, is-negative-zero@^2.0.2: version "2.0.2" resolved "https://registry.yarnpkg.com/is-negative-zero/-/is-negative-zero-2.0.2.tgz#7bf6f03a28003b8b3965de3ac26f664d765f3150" @@ -5302,6 +5655,16 @@ is-plain-object@^2.0.3, is-plain-object@^2.0.4: dependencies: isobject "^3.0.1" +is-potential-custom-element-name@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.1.tgz#171ed6f19e3ac554394edf78caa05784a45bebb5" + integrity sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ== + +is-promise@~1, is-promise@~1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/is-promise/-/is-promise-1.0.1.tgz#31573761c057e33c2e91aab9e96da08cefbe76e5" + integrity sha512-mjWH5XxnhMA8cFnDchr6qRP9S/kLntKuEfIYku+PaN1CnS8v+OG9O/BKpRCVRJvpIkgAZm0Pf5Is3iSSOILlcg== + is-regex@^1.1.0, is-regex@^1.1.4: version "1.1.4" resolved "https://registry.yarnpkg.com/is-regex/-/is-regex-1.1.4.tgz#eef5663cd59fa4c0ae339505323df6854bb15958" @@ -5310,6 +5673,11 @@ is-regex@^1.1.0, is-regex@^1.1.4: call-bind "^1.0.2" has-tostringtag "^1.0.0" +is-set@^2.0.1, is-set@^2.0.2: + version "2.0.2" + resolved "https://registry.yarnpkg.com/is-set/-/is-set-2.0.2.tgz#90755fa4c2562dc1c5d4024760d6119b94ca18ec" + integrity sha512-+2cnTEZeY5z/iXGbLhPrOAaK/Mau5k5eXq9j14CpRTftq0pAJu2MwVRSZhyZWBzx3o6X795Lz6Bpb6R0GKf37g== + is-shared-array-buffer@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/is-shared-array-buffer/-/is-shared-array-buffer-1.0.1.tgz#97b0c85fbdacb59c9c446fe653b82cf2b5b7cfe6" @@ -5346,11 +5714,27 @@ is-symbol@^1.0.2, is-symbol@^1.0.3: dependencies: has-symbols "^1.0.2" +is-typed-array@^1.1.10: + version "1.1.10" + resolved "https://registry.yarnpkg.com/is-typed-array/-/is-typed-array-1.1.10.tgz#36a5b5cb4189b575d1a3e4b08536bfb485801e3f" + integrity sha512-PJqgEHiWZvMpaFZ3uTc8kHPM4+4ADTlDniuQL7cU/UDA0Ql7F70yGfHph3cLNe+c9toaigv+DFzTJKhc2CtO6A== + dependencies: + available-typed-arrays "^1.0.5" + call-bind "^1.0.2" + for-each "^0.3.3" + gopd "^1.0.1" + has-tostringtag "^1.0.0" + is-typedarray@~1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/is-typedarray/-/is-typedarray-1.0.0.tgz#e479c80858df0c1b11ddda6940f96011fcda4a9a" integrity sha1-5HnICFjfDBsR3dppQPlgEfzaSpo= +is-weakmap@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/is-weakmap/-/is-weakmap-2.0.1.tgz#5008b59bdc43b698201d18f62b37b2ca243e8cf2" + integrity sha512-NSBR4kH5oVj1Uwvv970ruUkCV7O1mzgVFO4/rev2cLRda9Tm9HrL70ZPut4rOHgY0FNrUu9BCbXA2sdQ+x0chA== + is-weakref@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/is-weakref/-/is-weakref-1.0.1.tgz#842dba4ec17fa9ac9850df2d6efbc1737274f2a2" @@ -5365,16 +5749,34 @@ is-weakref@^1.0.2: dependencies: call-bind "^1.0.2" +is-weakset@^2.0.1: + version "2.0.2" + resolved "https://registry.yarnpkg.com/is-weakset/-/is-weakset-2.0.2.tgz#4569d67a747a1ce5a994dfd4ef6dcea76e7c0a1d" + integrity sha512-t2yVvttHkQktwnNNmBQ98AhENLdPUTDTE21uPqAQ0ARwQfGeQKRVS0NNurH7bTf7RrvcVn1OOge45CnBeHCSmg== + dependencies: + call-bind "^1.0.2" + get-intrinsic "^1.1.1" + is-windows@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/is-windows/-/is-windows-1.0.2.tgz#d1850eb9791ecd18e6182ce12a30f396634bb19d" integrity sha512-eXK1UInq2bPmjyX6e3VHIzMLobc4J94i4AWn+Hpq3OU5KkrRC96OAcR3PRJ/pGu6m8TRnBHP9dkXQVsT/COVIA== +isarray@0.0.1: + version "0.0.1" + resolved "https://registry.yarnpkg.com/isarray/-/isarray-0.0.1.tgz#8a18acfca9a8f4177e09abfc6038939b05d1eedf" + integrity sha512-D2S+3GLxWH+uhrNEcoh/fnmYeP8E8/zHl644d/jdA0g2uyXvy3sb0qxotE+ne0LtccHknQzWwZEzhak7oJ0COQ== + isarray@1.0.0, isarray@~1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/isarray/-/isarray-1.0.0.tgz#bb935d48582cba168c06834957a54a3e07124f11" integrity sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE= +isarray@^2.0.5: + version "2.0.5" + resolved "https://registry.yarnpkg.com/isarray/-/isarray-2.0.5.tgz#8af1e4c1221244cc62459faf38940d4e644a5723" + integrity sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw== + isexe@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/isexe/-/isexe-2.0.0.tgz#e8fbf374dc556ff8947a10dcb0572d633f2cfa10" @@ -5397,15 +5799,15 @@ isstream@0.1.2, isstream@~0.1.2: resolved "https://registry.yarnpkg.com/isstream/-/isstream-0.1.2.tgz#47e63f7af55afa6f92e1500e690eb8b8529c099a" integrity sha1-R+Y/evVa+m+S4VAOaQ64uFKcCZo= -joi@^17.4.0: - version "17.6.0" - resolved "https://registry.yarnpkg.com/joi/-/joi-17.6.0.tgz#0bb54f2f006c09a96e75ce687957bd04290054b2" - integrity sha512-OX5dG6DTbcr/kbMFj0KGYxuew69HPcAE3K/sZpEV2nP6e/j/C0HV+HNiBPCASxdx5T7DMoa0s8UeHWMnb6n2zw== +joi@^17.7.0: + version "17.8.3" + resolved "https://registry.yarnpkg.com/joi/-/joi-17.8.3.tgz#d772fe27a87a5cda21aace5cf11eee8671ca7e6f" + integrity sha512-q5Fn6Tj/jR8PfrLrx4fpGH4v9qM6o+vDUfD4/3vxxyg34OmKcNqYZ1qn2mpLza96S8tL0p0rIw2gOZX+/cTg9w== dependencies: "@hapi/hoek" "^9.0.0" "@hapi/topo" "^5.0.0" "@sideway/address" "^4.1.3" - "@sideway/formula" "^3.0.0" + "@sideway/formula" "^3.0.1" "@sideway/pinpoint" "^2.0.0" "js-tokens@^3.0.0 || ^4.0.0", js-tokens@^4.0.0: @@ -5451,6 +5853,38 @@ jscodeshift@^0.13.1: temp "^0.8.4" write-file-atomic "^2.3.0" +jsdom@^21.1.0: + version "21.1.0" + resolved "https://registry.yarnpkg.com/jsdom/-/jsdom-21.1.0.tgz#d56ba4a84ed478260d83bd53dc181775f2d8e6ef" + integrity sha512-m0lzlP7qOtthD918nenK3hdItSd2I+V3W9IrBcB36sqDwG+KnUs66IF5GY7laGWUnlM9vTsD0W1QwSEBYWWcJg== + dependencies: + abab "^2.0.6" + acorn "^8.8.1" + acorn-globals "^7.0.0" + cssom "^0.5.0" + cssstyle "^2.3.0" + data-urls "^3.0.2" + decimal.js "^10.4.2" + domexception "^4.0.0" + escodegen "^2.0.0" + form-data "^4.0.0" + html-encoding-sniffer "^3.0.0" + http-proxy-agent "^5.0.0" + https-proxy-agent "^5.0.1" + is-potential-custom-element-name "^1.0.1" + nwsapi "^2.2.2" + parse5 "^7.1.1" + saxes "^6.0.0" + symbol-tree "^3.2.4" + tough-cookie "^4.1.2" + w3c-xmlserializer "^4.0.0" + webidl-conversions "^7.0.0" + whatwg-encoding "^2.0.0" + whatwg-mimetype "^3.0.0" + whatwg-url "^11.0.0" + ws "^8.11.0" + xml-name-validator "^4.0.0" + jsesc@^2.5.1: version "2.5.1" resolved "https://registry.yarnpkg.com/jsesc/-/jsesc-2.5.1.tgz#e421a2a8e20d6b0819df28908f782526b96dd1fe" @@ -5574,6 +6008,11 @@ lazy-ass@1.6.0, lazy-ass@^1.6.0: resolved "https://registry.yarnpkg.com/lazy-ass/-/lazy-ass-1.6.0.tgz#7999655e8646c17f089fdd187d150d3324d54513" integrity sha1-eZllXoZGwX8In90YfRUNMyTVRRM= +leac@^0.6.0: + version "0.6.0" + resolved "https://registry.yarnpkg.com/leac/-/leac-0.6.0.tgz#dcf136e382e666bd2475f44a1096061b70dc0912" + integrity sha512-y+SqErxb8h7nE/fiEX07jsbuhrpO9lL8eca7/Y1nuWV2moNlXhyd59iDGcRf6moVyDMbmTNzL40SUyrFU/yDpg== + levn@^0.4.1: version "0.4.1" resolved "https://registry.yarnpkg.com/levn/-/levn-0.4.1.tgz#ae4562c007473b932a6200d403268dd2fffc6ade" @@ -5582,6 +6021,34 @@ levn@^0.4.1: prelude-ls "^1.2.1" type-check "~0.4.0" +levn@~0.3.0: + version "0.3.0" + resolved "https://registry.yarnpkg.com/levn/-/levn-0.3.0.tgz#3b09924edf9f083c0490fdd4c0bc4421e04764ee" + integrity sha512-0OO4y2iOHix2W6ujICbKIaEQXvFQHue65vUG3pb5EUomzPI90z9hsA1VsO/dbIIpC53J8gxM9Q4Oho0jrCM/yA== + dependencies: + prelude-ls "~1.1.2" + type-check "~0.3.2" + +libbase64@1.2.1: + version "1.2.1" + resolved "https://registry.yarnpkg.com/libbase64/-/libbase64-1.2.1.tgz#fb93bf4cb6d730f29b92155b6408d1bd2176a8c8" + integrity sha512-l+nePcPbIG1fNlqMzrh68MLkX/gTxk/+vdvAb388Ssi7UuUN31MI44w4Yf33mM3Cm4xDfw48mdf3rkdHszLNew== + +libmime@5.2.0: + version "5.2.0" + resolved "https://registry.yarnpkg.com/libmime/-/libmime-5.2.0.tgz#c4ed5cbd2d9fdd27534543a68bb8d17c658d51d8" + integrity sha512-X2U5Wx0YmK0rXFbk67ASMeqYIkZ6E5vY7pNWRKtnNzqjvdYYG8xtPDpCnuUEnPU9vlgNev+JoSrcaKSUaNvfsw== + dependencies: + encoding-japanese "2.0.0" + iconv-lite "0.6.3" + libbase64 "1.2.1" + libqp "2.0.1" + +libqp@2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/libqp/-/libqp-2.0.1.tgz#b8fed76cc1ea6c9ceff8888169e4e0de70cd5cf2" + integrity sha512-Ka0eC5LkF3IPNQHJmYBWljJsw0UvM6j+QdKRbWyCdTmYwvIDE6a7bCm0UkTAL/K+3KXK5qXT/ClcInU01OpdLg== + lie@3.1.1: version "3.1.1" resolved "https://registry.yarnpkg.com/lie/-/lie-3.1.1.tgz#9a436b2cc7746ca59de7a41fa469b3efb76bd87e" @@ -5594,6 +6061,13 @@ lines-and-columns@^1.1.6: resolved "https://registry.yarnpkg.com/lines-and-columns/-/lines-and-columns-1.1.6.tgz#1c00c743b433cd0a4e80758f7b64a57440d9ff00" integrity sha1-HADHQ7QzzQpOgHWPe2SldEDZ/wA= +linkify-it@4.0.1: + version "4.0.1" + resolved "https://registry.yarnpkg.com/linkify-it/-/linkify-it-4.0.1.tgz#01f1d5e508190d06669982ba31a7d9f56a5751ec" + integrity sha512-C7bfi1UZmoj8+PQx22XyeXCuBlokoyWQL5pWSP+EI6nzRylyThouddufc2c1NDIcP9k5agmN9fLpA7VNJfIiqw== + dependencies: + uc.micro "^1.0.1" + listr2@^3.8.3: version "3.14.0" resolved "https://registry.yarnpkg.com/listr2/-/listr2-3.14.0.tgz#23101cc62e1375fd5836b248276d1d2b51fdbe9e" @@ -5704,6 +6178,11 @@ lru_map@^0.3.3: resolved "https://registry.yarnpkg.com/lru_map/-/lru_map-0.3.3.tgz#b5c8351b9464cbd750335a79650a0ec0e56118dd" integrity sha1-tcg1G5Rky9dQM1p5ZQoOwOVhGN0= +lz-string@^1.4.4: + version "1.4.4" + resolved "https://registry.yarnpkg.com/lz-string/-/lz-string-1.4.4.tgz#c0d8eaf36059f705796e1e344811cf4c498d3a26" + integrity sha512-0ckx7ZHRPqb0oUm8zNr+90mtf9DQB60H1wMCjBtfi62Kl3a7JbHob6gA2bC+xRvZoOL+1hzUK8jeuEIQE8svEQ== + magic-string@^0.19.0: version "0.19.1" resolved "https://registry.yarnpkg.com/magic-string/-/magic-string-0.19.1.tgz#14d768013caf2ec8fdea16a49af82fc377e75201" @@ -5711,6 +6190,30 @@ magic-string@^0.19.0: dependencies: vlq "^0.2.1" +mailparser@^3.6.3: + version "3.6.3" + resolved "https://registry.yarnpkg.com/mailparser/-/mailparser-3.6.3.tgz#7edcfd9af7931e8a724e97880756477a9ea80f88" + integrity sha512-Yi6poKSsZsmjEcUexv3H4w4+TIeyN9u3+TCdC43VK7fe4rUOGDJ3wL4kMhNLiTOScCA1Rpzldv1hcf6g1MLtZQ== + dependencies: + encoding-japanese "2.0.0" + he "1.2.0" + html-to-text "9.0.3" + iconv-lite "0.6.3" + libmime "5.2.0" + linkify-it "4.0.1" + mailsplit "5.4.0" + nodemailer "6.8.0" + tlds "1.236.0" + +mailsplit@5.4.0: + version "5.4.0" + resolved "https://registry.yarnpkg.com/mailsplit/-/mailsplit-5.4.0.tgz#9f4692fadd9013e9ce632147d996931d2abac6ba" + integrity sha512-wnYxX5D5qymGIPYLwnp6h8n1+6P6vz/MJn5AzGjZ8pwICWssL+CCQjWBIToOVHASmATot4ktvlLo6CyLfOXWYA== + dependencies: + libbase64 "1.2.1" + libmime "5.2.0" + libqp "2.0.1" + make-dir@^2.0.0, make-dir@^2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/make-dir/-/make-dir-2.1.0.tgz#5f0310e18b8be898cc07009295a30ae41e91e6f5" @@ -5727,7 +6230,7 @@ map-cache@^0.2.2: map-stream@~0.1.0: version "0.1.0" resolved "https://registry.yarnpkg.com/map-stream/-/map-stream-0.1.0.tgz#e56aa94c4c8055a16404a0674b78f215f7c8e194" - integrity sha1-5WqpTEyAVaFkBKBnS3jyFffI4ZQ= + integrity sha512-CkYQrPYZfWnu/DAmVCpTSX/xHpKZ80eKh2lAkyA6AJTef6bW+6JpbQZN5rofum7da+SyN1bi5ctTm+lTfcCW3g== map-visit@^1.0.0: version "1.0.0" @@ -5836,7 +6339,7 @@ minimatch@^5.0.1: dependencies: brace-expansion "^2.0.1" -minimist@1.2.0, minimist@^1.2.0, minimist@^1.2.5, minimist@^1.2.6: +minimist@1.2.0, minimist@^1.2.0, minimist@^1.2.6, minimist@^1.2.7: version "1.2.7" resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.7.tgz#daa1c4d91f507390437c6a8bc01078e7000c4d18" integrity sha512-bzfL1YUZsP41gmu/qjrEk0Q6i2ix/cVeAhbCbqH9u3zYutS1cLg00qhrD0M2MVdCcx4Sc0UpP2eBWo9rotpq6g== @@ -5998,6 +6501,24 @@ node-releases@^2.0.6: resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-2.0.6.tgz#8a7088c63a55e493845683ebf3c828d8c51c5503" integrity sha512-PiVXnNuFm5+iYkLBNeq5211hvO38y63T0i2KKh2KnUs3RpzJ+JtODFjkD8yjLwnDkTYF1eKXheUwdssR+NRZdg== +nodeify@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/nodeify/-/nodeify-1.0.1.tgz#64ab69a7bdbaf03ce107b4f0335c87c0b9e91b1d" + integrity sha512-n7C2NyEze8GCo/z73KdbjRsBiLbv6eBn1FxwYKQ23IqGo7pQY3mhQan61Sv7eEDJCiyUjTVrVkXTzJCo1dW7Aw== + dependencies: + is-promise "~1.0.0" + promise "~1.3.0" + +nodemailer@6.8.0: + version "6.8.0" + resolved "https://registry.yarnpkg.com/nodemailer/-/nodemailer-6.8.0.tgz#804bcc5256ee5523bc914506ee59f8de8f0b1cd5" + integrity sha512-EjYvSmHzekz6VNkNd12aUqAco+bOkRe3Of5jVhltqKhEsjw/y0PYPJfp83+s9Wzh1dspYAkUW/YNQ350NATbSQ== + +nodemailer@^6.9.1: + version "6.9.1" + resolved "https://registry.yarnpkg.com/nodemailer/-/nodemailer-6.9.1.tgz#8249d928a43ed85fec17b13d2870c8f758a126ed" + integrity sha512-qHw7dOiU5UKNnQpXktdgQ1d3OFgRAekuvbJLcdG5dnEo/GtcTHRYM7+UfJARdOFU9WUQO8OiIamgWPmiSFHYAA== + nopt@^4.0.1: version "4.0.1" resolved "https://registry.yarnpkg.com/nopt/-/nopt-4.0.1.tgz#d0d4685afd5415193c8c7505602d0d17cd64474d" @@ -6053,6 +6574,11 @@ number-is-nan@^1.0.0: resolved "https://registry.yarnpkg.com/number-is-nan/-/number-is-nan-1.0.1.tgz#097b602b53422a522c1afb8790318336941a011d" integrity sha1-CXtgK1NCKlIsGvuHkDGDNpQaAR0= +nwsapi@^2.2.2: + version "2.2.2" + resolved "https://registry.yarnpkg.com/nwsapi/-/nwsapi-2.2.2.tgz#e5418863e7905df67d51ec95938d67bf801f0bb0" + integrity sha512-90yv+6538zuvUMnN+zCr8LuV6bPFdq50304114vJYJ8RDyK8D5O9Phpbd6SZWgI7PwzmmfN1upeOJlvybDSgCw== + object-assign@^4.1.0, object-assign@^4.1.1: version "4.1.1" resolved "https://registry.yarnpkg.com/object-assign/-/object-assign-4.1.1.tgz#2109adc7965887cfc05cbbd442cac8bfbb360863" @@ -6077,7 +6603,7 @@ object-inspect@^1.12.0, object-inspect@^1.9.0: resolved "https://registry.yarnpkg.com/object-inspect/-/object-inspect-1.12.0.tgz#6e2c120e868fd1fd18cb4f18c31741d0d6e776f0" integrity sha512-Ho2z80bVIvJloH+YzRmpZVQe87+qASmBUKZDWgx9cu+KDrX2ZDH/3tMy+gXbZETVGs2M8YdxObOh7XAtim9Y0g== -object-is@^1.1.2: +object-is@^1.1.2, object-is@^1.1.5: version "1.1.5" resolved "https://registry.yarnpkg.com/object-is/-/object-is-1.1.5.tgz#b9deeaa5fc7f1846a0faecdceec138e5778f53ac" integrity sha512-3cyDsyHgtmi7I7DfSSI2LDp6SK2lwvtbg0p0R1e0RvTqF5ceGx+K2dfSjm1bKDMVCFEDAQvy+o8c6a7VujOddw== @@ -6107,6 +6633,16 @@ object.assign@^4.1.0, object.assign@^4.1.2: has-symbols "^1.0.1" object-keys "^1.1.1" +object.assign@^4.1.4: + version "4.1.4" + resolved "https://registry.yarnpkg.com/object.assign/-/object.assign-4.1.4.tgz#9673c7c7c351ab8c4d0b516f4343ebf4dfb7799f" + integrity sha512-1mxKf0e58bvyjSCtKYY4sRe9itRk3PJpquJOjeIkz885CczcI4IvJJDLPS72oowuSh+pBxUFROpX+TU++hxhZQ== + dependencies: + call-bind "^1.0.2" + define-properties "^1.1.4" + has-symbols "^1.0.3" + object-keys "^1.1.1" + object.entries@^1.1.2, object.entries@^1.1.5: version "1.1.5" resolved "https://registry.yarnpkg.com/object.entries/-/object.entries-1.1.5.tgz#e1acdd17c4de2cd96d5a08487cfb9db84d881861" @@ -6191,6 +6727,18 @@ ooni-components@^0.4.7: optionalDependencies: fsevents "1.2.9" +optionator@^0.8.1: + version "0.8.3" + resolved "https://registry.yarnpkg.com/optionator/-/optionator-0.8.3.tgz#84fa1d036fe9d3c7e21d99884b601167ec8fb495" + integrity sha512-+IW9pACdk3XWmmTXG8m3upGUJst5XRGzxMRjXzAuJ1XnIFNvfhjjIuYkDvysnPQ7qzqVzLt78BCruntqRhWQbA== + dependencies: + deep-is "~0.1.3" + fast-levenshtein "~2.0.6" + levn "~0.3.0" + prelude-ls "~1.1.2" + type-check "~0.3.2" + word-wrap "~1.2.3" + optionator@^0.9.1: version "0.9.1" resolved "https://registry.yarnpkg.com/optionator/-/optionator-0.9.1.tgz#4f236a6373dae0566a6d43e1326674f50c291499" @@ -6315,6 +6863,21 @@ parse-ms@^3.0.0: resolved "https://registry.yarnpkg.com/parse-ms/-/parse-ms-3.0.0.tgz#3ea24a934913345fcc3656deda72df921da3a70e" integrity sha512-Tpb8Z7r7XbbtBTrM9UhpkzzaMrqA2VXMT3YChzYltwV3P3pM6t8wl7TvpMnSTosz1aQAdVib7kdoys7vYOPerw== +parse5@^7.1.1: + version "7.1.2" + resolved "https://registry.yarnpkg.com/parse5/-/parse5-7.1.2.tgz#0736bebbfd77793823240a23b7fc5e010b7f8e32" + integrity sha512-Czj1WaSVpaoj0wbhMzLmWD69anp2WH7FXMB9n1Sy8/ZFF9jolSQVMu1Ij5WIyGmcBmhk7EOndpO4mIpihVqAXw== + dependencies: + entities "^4.4.0" + +parseley@^0.11.0: + version "0.11.0" + resolved "https://registry.yarnpkg.com/parseley/-/parseley-0.11.0.tgz#1ff817c829a02fcc214c9cc0d96b126d772ee814" + integrity sha512-VfcwXlBWgTF+unPcr7yu3HSSA6QUdDaDnrHcytVfj5Z8azAyKBDrYnSIfeSxlrEayndNcLmrXzg+Vxbo6DWRXQ== + dependencies: + leac "^0.6.0" + peberminta "^0.8.0" + pascalcase@^0.1.1: version "0.1.1" resolved "https://registry.yarnpkg.com/pascalcase/-/pascalcase-0.1.1.tgz#b363e55e8006ca6fe21784d2db22bd15d7917f14" @@ -6353,10 +6916,15 @@ path-type@^4.0.0: pause-stream@0.0.11: version "0.0.11" resolved "https://registry.yarnpkg.com/pause-stream/-/pause-stream-0.0.11.tgz#fe5a34b0cbce12b5aa6a2b403ee2e73b602f1445" - integrity sha1-/lo0sMvOErWqaitAPuLnO2AvFEU= + integrity sha512-e3FBlXLmN/D1S+zHzanP4E/4Z60oFAa3O051qt1pxa7DEJWKAyil6upYVXCWadEnuoqa4Pkc9oUx9zsxYeRv8A== dependencies: through "~2.3" +peberminta@^0.8.0: + version "0.8.0" + resolved "https://registry.yarnpkg.com/peberminta/-/peberminta-0.8.0.tgz#acf7b105f3d13c8ac28cad81f2f5fe4698507590" + integrity sha512-YYEs+eauIjDH5nUEGi18EohWE0nV2QbGTqmxQcqgZ/0g+laPCQmuIqq7EBLVi9uim9zMgfJv0QBZEnQ3uHw/Tw== + pend@~1.2.0: version "1.2.0" resolved "https://registry.yarnpkg.com/pend/-/pend-1.2.0.tgz#7a57eb550a6783f9115331fcf4663d5c8e007a50" @@ -6428,11 +6996,25 @@ prelude-ls@^1.2.1: resolved "https://registry.yarnpkg.com/prelude-ls/-/prelude-ls-1.2.1.tgz#debc6489d7a6e6b0e7611888cec880337d316396" integrity sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g== +prelude-ls@~1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/prelude-ls/-/prelude-ls-1.1.2.tgz#21932a549f5e52ffd9a827f570e04be62a97da54" + integrity sha512-ESF23V4SKG6lVSGZgYNpbsiaAkdab6ZgOxe52p7+Kid3W3u3bxR4Vfd/o21dmN7jSt0IwgZ4v5MUd26FEtXE9w== + pretty-bytes@^5.6.0: version "5.6.0" resolved "https://registry.yarnpkg.com/pretty-bytes/-/pretty-bytes-5.6.0.tgz#356256f643804773c82f64723fe78c92c62beaeb" integrity sha512-FFw039TmrBqFK8ma/7OL3sDz/VytdtJr044/QUJtH0wK9lb9jLq9tJyIxUwtQJHwar2BqtiA4iCWSwo9JLkzFg== +pretty-format@^27.0.2: + version "27.5.1" + resolved "https://registry.yarnpkg.com/pretty-format/-/pretty-format-27.5.1.tgz#2181879fdea51a7a5851fb39d920faa63f01d88e" + integrity sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ== + dependencies: + ansi-regex "^5.0.1" + ansi-styles "^5.0.0" + react-is "^17.0.1" + pretty-ms@^8.0.0: version "8.0.0" resolved "https://registry.yarnpkg.com/pretty-ms/-/pretty-ms-8.0.0.tgz#a35563b2a02df01e595538f86d7de54ca23194a3" @@ -6457,6 +7039,13 @@ promise@^7.1.1: dependencies: asap "~2.0.3" +promise@~1.3.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/promise/-/promise-1.3.0.tgz#e5cc9a4c8278e4664ffedc01c7da84842b040175" + integrity sha512-R9WrbTF3EPkVtWjp7B7umQGVndpsi+rsDAfrR4xAALQpFLa/+2OriecLhawxzvii2gd9+DZFwROWDuUUaqS5yA== + dependencies: + is-promise "~1" + prop-types-exact@^1.2.0: version "1.2.0" resolved "https://registry.yarnpkg.com/prop-types-exact/-/prop-types-exact-1.2.0.tgz#825d6be46094663848237e3925a98c6e944e9869" @@ -6506,6 +7095,11 @@ psl@^1.1.28: resolved "https://registry.yarnpkg.com/psl/-/psl-1.7.0.tgz#f1c4c47a8ef97167dea5d6bbf4816d736e884a3c" integrity sha512-5NsSEDv8zY70ScRnOTn7bK7eanl2MvFrOrS/R6x+dBt5g1ghnj9Zv90kO8GwT8gxcu2ANyFprnFYB85IogIJOQ== +psl@^1.1.33: + version "1.9.0" + resolved "https://registry.yarnpkg.com/psl/-/psl-1.9.0.tgz#d0df2a137f00794565fcaf3b2c00cd09f8d5a5a7" + integrity sha512-E/ZsdU4HLs/68gYzgGTkMicWTLPdAftJLfJFlLUAAKZGkStNU72sZjT66SnMDVOfOWY/YAoiD7Jxa9iHvngcag== + pump@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/pump/-/pump-3.0.0.tgz#b4a2116815bde2f4e1ea602354e8c75565107a64" @@ -6529,11 +7123,23 @@ qs@~6.5.2: resolved "https://registry.yarnpkg.com/qs/-/qs-6.5.3.tgz#3aeeffc91967ef6e35c0e488ef46fb296ab76aad" integrity sha512-qxXIEh4pCGfHICj1mAJQ2/2XVZkjCDTcEgfoSQxc/fYivUZxTkk7L3bDBJSoNrEzXI17oUO5Dp07ktqE5KzczA== +querystringify@^2.1.1: + version "2.2.0" + resolved "https://registry.yarnpkg.com/querystringify/-/querystringify-2.2.0.tgz#3345941b4153cb9d082d8eee4cda2016a9aef7f6" + integrity sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ== + queue-microtask@^1.2.2: version "1.2.3" resolved "https://registry.yarnpkg.com/queue-microtask/-/queue-microtask-1.2.3.tgz#4929228bbc724dfac43e0efb058caf7b6cfb6243" integrity sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A== +quoted-printable@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/quoted-printable/-/quoted-printable-1.0.1.tgz#9eebf5eb3d11eef022b264fd2d2b6b2bb3b84cc3" + integrity sha512-cihC68OcGiQOjGiXuo5Jk6XHANTHl1K4JLk/xlEJRTIXfy19Sg6XzB95XonYgr+1rB88bCpr7WZE7D7AlZow4g== + dependencies: + utf8 "^2.1.0" + raf@^3.3.0: version "3.4.1" resolved "https://registry.yarnpkg.com/raf/-/raf-3.4.1.tgz#0742e99a4a6552f445d73e3ee0328af0ff1ede39" @@ -6650,6 +7256,11 @@ react-is@^16.3.2, react-is@^16.8.1: resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.12.0.tgz#2cc0fe0fba742d97fd527c42a13bec4eeb06241c" integrity sha512-rPCkf/mWBtKc97aLL9/txD8DZdemK0vkA3JMLShjlJB3Pj3s+lpf1KaBzMfQrAmhMQB0n1cU/SUGgKKBCe837Q== +react-is@^17.0.1: + version "17.0.2" + resolved "https://registry.yarnpkg.com/react-is/-/react-is-17.0.2.tgz#e691d4a8e9c789365655539ab372762b0efb54f0" + integrity sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w== + react-json-view@^1.21.3: version "1.21.3" resolved "https://registry.yarnpkg.com/react-json-view/-/react-json-view-1.21.3.tgz#f184209ee8f1bf374fb0c41b0813cff54549c475" @@ -6747,6 +7358,16 @@ react@17.0.2: loose-envify "^1.1.0" object-assign "^4.1.1" +readable-stream@1.1.x: + version "1.1.14" + resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-1.1.14.tgz#7cf4c54ef648e3813084c636dd2079e166c081d9" + integrity sha512-+MeVjFf4L44XUkhM1eYbD8fyEsxcV81pqMSR5gblfcLCHfZvbrqy4/qYHE+/R5HoBUT11WV5O08Cr1n3YXkWVQ== + dependencies: + core-util-is "~1.0.0" + inherits "~2.0.1" + isarray "0.0.1" + string_decoder "~0.10.x" + readable-stream@^2.0.6: version "2.3.6" resolved "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.6.tgz#b11c27d88b8ff1fbe070643cf94b0c79ae1b0aaf" @@ -6805,6 +7426,11 @@ regenerate@^1.4.2: resolved "https://registry.yarnpkg.com/regenerate/-/regenerate-1.4.2.tgz#b9346d8827e8f5a32f7ba29637d398b69014848a" integrity sha512-zrceR/XhGYU/d/opr2EKO7aRHUeiBI8qjtfHqADTwZd6Szfy16la6kqD0MIUs5z5hx6AaKa+PixpPrR289+I0A== +regenerator-runtime@^0.13.11: + version "0.13.11" + resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.13.11.tgz#f6dca3e7ceec20590d07ada785636a90cdca17f9" + integrity sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg== + regenerator-runtime@^0.13.4: version "0.13.9" resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.13.9.tgz#8925742a98ffd90814988d7566ad30ca3b263b52" @@ -6880,6 +7506,11 @@ request-progress@^3.0.0: dependencies: throttleit "^1.0.0" +requires-port@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/requires-port/-/requires-port-1.0.0.tgz#925d2601d39ac485e091cf0da5c6e694dc3dcaff" + integrity sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ== + resolve-from@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/resolve-from/-/resolve-from-4.0.0.tgz#4abcd852ad32dd7baabfe9b40e00a36db5f392e6" @@ -6974,13 +7605,20 @@ run-parallel@^1.1.9: dependencies: queue-microtask "^1.2.2" -rxjs@^7.1.0, rxjs@^7.5.1: +rxjs@^7.5.1: version "7.5.2" resolved "https://registry.yarnpkg.com/rxjs/-/rxjs-7.5.2.tgz#11e4a3a1dfad85dbf7fb6e33cbba17668497490b" integrity sha512-PwDt186XaL3QN5qXj/H9DGyHhP3/RYYgZZwqBv9Tv8rsAaiwFH1IsJJlcgD37J7UW5a6O67qX0KWKS3/pu0m4w== dependencies: tslib "^2.1.0" +rxjs@^7.8.0: + version "7.8.0" + resolved "https://registry.yarnpkg.com/rxjs/-/rxjs-7.8.0.tgz#90a938862a82888ff4c7359811a595e14e1e09a4" + integrity sha512-F2+gxDshqmIub1KdvZkaEfGDwLNpPvk9Fs6LD/MyQxNgMds/WH9OdDDXOmxUZpME+iSK3rQCctkL0DYyytUqMg== + dependencies: + tslib "^2.1.0" + safe-buffer@^5.0.1, safe-buffer@^5.1.2, safe-buffer@^5.2.1: version "5.2.1" resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.2.1.tgz#1eaf9fa9bdb1fdd4ec75f58f9cdb4e6b7827eec6" @@ -6998,7 +7636,7 @@ safe-regex@^1.1.0: dependencies: ret "~0.1.10" -"safer-buffer@>= 2.1.2 < 3", safer-buffer@^2.0.2, safer-buffer@^2.1.0, safer-buffer@~2.1.0: +"safer-buffer@>= 2.1.2 < 3", "safer-buffer@>= 2.1.2 < 3.0.0", safer-buffer@^2.0.2, safer-buffer@^2.1.0, safer-buffer@~2.1.0: version "2.1.2" resolved "https://registry.yarnpkg.com/safer-buffer/-/safer-buffer-2.1.2.tgz#44fa161b0187b9549dd84bb91802f9bd8385cd6a" integrity sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg== @@ -7008,6 +7646,13 @@ sax@^1.2.4: resolved "https://registry.yarnpkg.com/sax/-/sax-1.2.4.tgz#2816234e2378bddc4e5354fab5caa895df7100d9" integrity sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw== +saxes@^6.0.0: + version "6.0.0" + resolved "https://registry.yarnpkg.com/saxes/-/saxes-6.0.0.tgz#fe5b4a4768df4f14a201b1ba6a65c1f3d9988cc5" + integrity sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA== + dependencies: + xmlchars "^2.2.0" + scheduler@^0.17.0: version "0.17.0" resolved "https://registry.yarnpkg.com/scheduler/-/scheduler-0.17.0.tgz#7c9c673e4ec781fac853927916d1c426b6f3ddfe" @@ -7024,6 +7669,13 @@ scheduler@^0.20.2: loose-envify "^1.1.0" object-assign "^4.1.1" +selderee@^0.10.0: + version "0.10.0" + resolved "https://registry.yarnpkg.com/selderee/-/selderee-0.10.0.tgz#ec83d6044d9026668dc9bd2561acfde99a4e3a1c" + integrity sha512-DEL/RW/f4qLw/NrVg97xKaEBC8IpzIG2fvxnzCp3Z4yk4jQ3MXom+Imav9wApjxX2dfS3eW7x0DXafJr85i39A== + dependencies: + parseley "^0.11.0" + semver@^5.3.0: version "5.5.1" resolved "https://registry.yarnpkg.com/semver/-/semver-5.5.1.tgz#7dfdd8814bdb7cabc7be0fb1d734cfb66c940477" @@ -7046,6 +7698,11 @@ semver@^7.3.2, semver@^7.3.7: dependencies: lru-cache "^6.0.0" +semver@~5.3.0: + version "5.3.0" + resolved "https://registry.yarnpkg.com/semver/-/semver-5.3.0.tgz#9b2ce5d3de02d17c6012ad326aa6b4d0cf54f94f" + integrity sha512-mfmm3/H9+67MCVix1h+IXTpDwL6710LyHuk7+cWC9T1mE0qz4iHhh6r4hU2wrIT9iTsAAC2XQRvfblL028cpLw== + set-blocking@~2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/set-blocking/-/set-blocking-2.0.0.tgz#045f9782d011ae9a6803ddd382b24392b3d890f7" @@ -7105,9 +7762,9 @@ signal-exit@^3.0.0, signal-exit@^3.0.2: integrity sha1-tf3AjxKH6hF4Yo5BXiUTK3NkbG0= signal-exit@^3.0.3: - version "3.0.6" - resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-3.0.6.tgz#24e630c4b0f03fea446a2bd299e62b4a6ca8d0af" - integrity sha512-sDl4qMFpijcGw22U5w63KmD3cZJfBuFlVNbVMKje2keoKML7X2UzWbc4XrmEbDwg0NXJc3yv4/ox7b+JWb57kQ== + version "3.0.7" + resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-3.0.7.tgz#a9a1767f8af84155114eaabd73f99273c8f59ad9" + integrity sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ== slash@^3.0.0: version "3.0.0" @@ -7211,7 +7868,7 @@ split-string@^3.0.1, split-string@^3.0.2: split@0.3: version "0.3.3" resolved "https://registry.yarnpkg.com/split/-/split-0.3.3.tgz#cd0eea5e63a211dfff7eb0f091c4133e2d0dd28f" - integrity sha1-zQ7qXmOiEd//frDwkcQTPi0N0o8= + integrity sha512-wD2AeVmxXRBoX44wAycgjVpMhvbwdI2aZjCkvfNcH1YqHQvJVa1duWc73OyVGJUc05fhFaTZeQ/PYsrmyH0JVA== dependencies: through "2" @@ -7240,18 +7897,19 @@ stable@^0.1.8: resolved "https://registry.yarnpkg.com/stable/-/stable-0.1.8.tgz#836eb3c8382fe2936feaf544631017ce7d47a3cf" integrity sha512-ji9qxRnOVfcuLDySj9qzhGSEFVobyt1kIOSkj1qZzYLzq7Tos/oUUWvotUPQLlrsidqsK6tBH89Bc9kL5zHA6w== -start-server-and-test@^1.14.0: - version "1.14.0" - resolved "https://registry.yarnpkg.com/start-server-and-test/-/start-server-and-test-1.14.0.tgz#c57f04f73eac15dd51733b551d775b40837fdde3" - integrity sha512-on5ELuxO2K0t8EmNj9MtVlFqwBMxfWOhu4U7uZD1xccVpFlOQKR93CSe0u98iQzfNxRyaNTb/CdadbNllplTsw== +start-server-and-test@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/start-server-and-test/-/start-server-and-test-2.0.0.tgz#0644809d63036a8a001efb70582f3e37ebfdd33d" + integrity sha512-UqKLw0mJbfrsG1jcRLTUlvuRi9sjNuUiDOLI42r7R5fA9dsFoywAy9DoLXNYys9B886E4RCKb+qM1Gzu96h7DQ== dependencies: + arg "^5.0.2" bluebird "3.7.2" check-more-types "2.24.0" - debug "4.3.2" + debug "4.3.4" execa "5.1.1" lazy-ass "1.6.0" ps-tree "1.2.0" - wait-on "6.0.0" + wait-on "7.0.1" static-extend@^0.1.1: version "0.1.2" @@ -7261,10 +7919,17 @@ static-extend@^0.1.1: define-property "^0.2.5" object-copy "^0.1.0" +stop-iteration-iterator@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/stop-iteration-iterator/-/stop-iteration-iterator-1.0.0.tgz#6a60be0b4ee757d1ed5254858ec66b10c49285e4" + integrity sha512-iCGQj+0l0HOdZ2AEeBADlsRC+vsnDsZsbdSiH1yNSjcfKM7fdpCMfqAL/dwF5BLiw/XhRft/Wax6zQbhq2BcjQ== + dependencies: + internal-slot "^1.0.4" + stream-combiner@~0.0.4: version "0.0.4" resolved "https://registry.yarnpkg.com/stream-combiner/-/stream-combiner-0.0.4.tgz#4d5e433c185261dde623ca3f44c586bcf5c4ad14" - integrity sha1-TV5DPBhSYd3mI8o/RMWGvPXErRQ= + integrity sha512-rT00SPnTVyRsaSz5zgSPma/aHSOic5U1prhYdRy5HS2kTZviFpmDgzilbtsJsxiroqACmayynDN/9VzIbX5DOw== dependencies: duplexer "~0.1.1" @@ -7351,6 +8016,11 @@ string.prototype.trimstart@^1.0.5: define-properties "^1.1.4" es-abstract "^1.19.5" +string_decoder@~0.10.x: + version "0.10.31" + resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-0.10.31.tgz#62e203bc41766c6c28c9fc84301dab1c5310fa94" + integrity sha512-ev2QzSzWPYmy9GuqfIVildA4OdcGLeFZQrq5ys6RtiuF+RQQiZWr8TZNyAcuVXyQRYfEO+MsoB/1BuQVhOJuoQ== + string_decoder@~1.1.1: version "1.1.1" resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.1.1.tgz#9cf1611ba62685d7030ae9e4ba34149c3af03fc8" @@ -7495,6 +8165,11 @@ swr@^1.3.0: resolved "https://registry.yarnpkg.com/swr/-/swr-1.3.0.tgz#c6531866a35b4db37b38b72c45a63171faf9f4e8" integrity sha512-dkghQrOl2ORX9HYrMDtPa7LTVHJjCTeZoB1dqTbnnEDlSvN8JEKpYIYurDfvbQFUUS8Cg8PceFVZNkW0KNNYPw== +symbol-tree@^3.2.4: + version "3.2.4" + resolved "https://registry.yarnpkg.com/symbol-tree/-/symbol-tree-3.2.4.tgz#430637d248ba77e078883951fb9aa0eed7c63fa2" + integrity sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw== + tar@^4: version "4.4.19" resolved "https://registry.yarnpkg.com/tar/-/tar-4.4.19.tgz#2e4d7263df26f2b914dee10c825ab132123742f3" @@ -7530,6 +8205,11 @@ through@2, through@^2.3.8, through@~2.3, through@~2.3.1: resolved "https://registry.npmjs.org/through/-/through-2.3.8.tgz#0dd4c9ffaabc357960b1b724115d7e0e86a2e1f5" integrity sha1-DdTJ/6q8NXlgsbckEV1+Doai4fU= +tlds@1.236.0: + version "1.236.0" + resolved "https://registry.yarnpkg.com/tlds/-/tlds-1.236.0.tgz#a118eebe33261c577e3a3025144faeabb7dd813c" + integrity sha512-oP2PZ3KeGlgpHgsEfrtva3/K9kzsJUNliQSbCfrJ7JMCWFoCdtG+9YMq/g2AnADQ1v5tVlbtvKJZ4KLpy/P6MA== + tmp@~0.2.1: version "0.2.1" resolved "https://registry.yarnpkg.com/tmp/-/tmp-0.2.1.tgz#8457fc3037dcf4719c251367a1af6500ee1ccf14" @@ -7574,6 +8254,16 @@ to-regex@^3.0.1, to-regex@^3.0.2: regex-not "^1.0.2" safe-regex "^1.1.0" +tough-cookie@^4.1.2: + version "4.1.2" + resolved "https://registry.yarnpkg.com/tough-cookie/-/tough-cookie-4.1.2.tgz#e53e84b85f24e0b65dd526f46628db6c85f6b874" + integrity sha512-G9fqXWoYFZgTc2z8Q5zaHy/vJMjm+WV0AkAeHxVCQiEB1b+dGvWzFW6QV07cY5jQ5gRkeid2qIkzkxUnmoQZUQ== + dependencies: + psl "^1.1.33" + punycode "^2.1.1" + universalify "^0.2.0" + url-parse "^1.5.3" + tough-cookie@~2.5.0: version "2.5.0" resolved "https://registry.yarnpkg.com/tough-cookie/-/tough-cookie-2.5.0.tgz#cd9fb2a0aa1d5a12b473bd9fb96fa3dcff65ade2" @@ -7582,6 +8272,13 @@ tough-cookie@~2.5.0: psl "^1.1.28" punycode "^2.1.1" +tr46@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/tr46/-/tr46-3.0.0.tgz#555c4e297a950617e8eeddef633c87d4d9d6cbf9" + integrity sha512-l7FvfAHlcmulp8kr+flpQZmVwtu7nfRV7NZujtN0OqES8EL4O4e0qqzL0DC5gAvx/ZC/9lk6rhcUwYvkBnBnYA== + dependencies: + punycode "^2.1.1" + tr46@~0.0.3: version "0.0.3" resolved "https://registry.yarnpkg.com/tr46/-/tr46-0.0.3.tgz#8184fd347dac9cdc185992f3a6622e14b9d9ab6a" @@ -7638,6 +8335,13 @@ type-check@^0.4.0, type-check@~0.4.0: dependencies: prelude-ls "^1.2.1" +type-check@~0.3.2: + version "0.3.2" + resolved "https://registry.yarnpkg.com/type-check/-/type-check-0.3.2.tgz#5884cab512cf1d355e3fb784f30804b2b520db72" + integrity sha512-ZCmOJdvOWDBYJlzAoFkC+Q0+bUyEOS1ltgp1MGU03fqHG+dbi9tBFU2Rd9QKiDZFAYrhPh2JUf7rZRIuHRKtOg== + dependencies: + prelude-ls "~1.1.2" + type-fest@^0.20.2: version "0.20.2" resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.20.2.tgz#1bf207f4b28f91583666cb5fbd327887301cd5f4" @@ -7653,6 +8357,11 @@ ua-parser-js@^0.7.30: resolved "https://registry.yarnpkg.com/ua-parser-js/-/ua-parser-js-0.7.33.tgz#1d04acb4ccef9293df6f70f2c3d22f3030d8b532" integrity sha512-s8ax/CeZdK9R/56Sui0WM6y9OFREJarMRHqLB2EwkovemBxNQ+Bqu8GAsUnVcXKgphb++ghr/B2BZx4mahujPw== +uc.micro@^1.0.1: + version "1.0.6" + resolved "https://registry.yarnpkg.com/uc.micro/-/uc.micro-1.0.6.tgz#9c411a802a409a91fc6cf74081baba34b24499ac" + integrity sha512-8Y75pvTYkLJW2hWQHXxoqRgV7qb9B+9vFEtidML+7koHUFapnVJAZ6cKs+Qjz5Aw3aZWHMC6u0wJE3At+nSGwA== + unbox-primitive@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/unbox-primitive/-/unbox-primitive-1.0.1.tgz#085e215625ec3162574dc8859abee78a59b14471" @@ -7706,6 +8415,11 @@ union-value@^1.0.0: is-extendable "^0.1.1" set-value "^2.0.1" +universalify@^0.2.0: + version "0.2.0" + resolved "https://registry.yarnpkg.com/universalify/-/universalify-0.2.0.tgz#6451760566fa857534745ab1dde952d1b1761be0" + integrity sha512-CJ1QgKmNg3CwvAv/kOFmtnEN05f0D/cn9QntgNOQlQF9dgvVTHj3t+8JPdjqawCHk7V/KA+fbUqzZ9XWhcqPUg== + universalify@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/universalify/-/universalify-2.0.0.tgz#75a4984efedc4b08975c5aeb73f530d02df25717" @@ -7757,6 +8471,14 @@ urix@^0.1.0: resolved "https://registry.yarnpkg.com/urix/-/urix-0.1.0.tgz#da937f7a62e21fec1fd18d49b35c2935067a6c72" integrity sha512-Am1ousAhSLBeB9cG/7k7r2R0zj50uDRlZHPGbazid5s9rlF1F/QKYObEKSIunSjIOkJZqwRRLpvewjEkM7pSqg== +url-parse@^1.5.3: + version "1.5.10" + resolved "https://registry.yarnpkg.com/url-parse/-/url-parse-1.5.10.tgz#9d3c2f736c1d75dd3bd2be507dcc111f1e2ea9c1" + integrity sha512-WypcfiRhfeUP9vvF0j6rw0J3hrWrw6iZv3+22h6iRMJ/8z1Tj6XfLP4DsUix5MhMPnXpiHDoKyoZ/bdCkwBCiQ== + dependencies: + querystringify "^2.1.1" + requires-port "^1.0.0" + use-clipboard-copy@^0.2.0: version "0.2.0" resolved "https://registry.yarnpkg.com/use-clipboard-copy/-/use-clipboard-copy-0.2.0.tgz#e1f31f2b21e369bc79b5d7b358e2c8aece6ef264" @@ -7791,11 +8513,28 @@ use@^3.1.0: resolved "https://registry.yarnpkg.com/use/-/use-3.1.1.tgz#d50c8cac79a19fbc20f2911f56eb973f4e10070f" integrity sha512-cwESVXlO3url9YWlFW/TA9cshCEhtu7IKJ/p5soJ/gGpj7vbvFrAY/eIioQ6Dw23KjZhYgiIo8HOs1nQ2vr/oQ== +utf7@>=1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/utf7/-/utf7-1.0.2.tgz#955f490aae653ba220b9456a0a8776c199360991" + integrity sha512-qQrPtYLLLl12NF4DrM9CvfkxkYI97xOb5dsnGZHE3teFr0tWiEZ9UdgMPczv24vl708cYMpe6mGXGHrotIp3Bw== + dependencies: + semver "~5.3.0" + +utf8@^2.1.0, utf8@^2.1.1: + version "2.1.2" + resolved "https://registry.yarnpkg.com/utf8/-/utf8-2.1.2.tgz#1fa0d9270e9be850d9b05027f63519bf46457d96" + integrity sha512-QXo+O/QkLP/x1nyi54uQiG0XrODxdysuQvE5dtVqv7F5K2Qb6FsN+qbr6KhF5wQ20tfcV3VQp0/2x1e1MRSPWg== + util-deprecate@~1.0.1: version "1.0.2" resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf" integrity sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8= +uuencode@0.0.4: + version "0.0.4" + resolved "https://registry.yarnpkg.com/uuencode/-/uuencode-0.0.4.tgz#c8d50370885663879385ab37e333c7e8e3b0218c" + integrity sha512-yEEhCuCi5wRV7Z5ZVf9iV2gWMvUZqKJhAs1ecFdKJ0qzbyaVelmsE3QjYAamehfp9FKLiZbKldd+jklG3O0LfA== + uuid@^8.3.2: version "8.3.2" resolved "https://registry.yarnpkg.com/uuid/-/uuid-8.3.2.tgz#80d5b5ced271bb9af6c445f21a1a04c606cefbe2" @@ -8139,22 +8878,54 @@ vlq@^0.2.1: resolved "https://registry.yarnpkg.com/vlq/-/vlq-0.2.3.tgz#8f3e4328cf63b1540c0d67e1b2778386f8975b26" integrity sha512-DRibZL6DsNhIgYQ+wNdWDL2SL3bKPlVrRiBqV5yuMm++op8W4kGFtaQfCs4KEJn0wBZcHVHJ3eoywX8983k1ow== -wait-on@6.0.0: - version "6.0.0" - resolved "https://registry.yarnpkg.com/wait-on/-/wait-on-6.0.0.tgz#7e9bf8e3d7fe2daecbb7a570ac8ca41e9311c7e7" - integrity sha512-tnUJr9p5r+bEYXPUdRseolmz5XqJTTj98JgOsfBn7Oz2dxfE2g3zw1jE+Mo8lopM3j3et/Mq1yW7kKX6qw7RVw== +w3c-xmlserializer@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/w3c-xmlserializer/-/w3c-xmlserializer-4.0.0.tgz#aebdc84920d806222936e3cdce408e32488a3073" + integrity sha512-d+BFHzbiCx6zGfz0HyQ6Rg69w9k19nviJspaj4yNscGjrHu94sVP+aRm75yEbCh+r2/yR+7q6hux9LVtbuTGBw== dependencies: - axios "^0.21.1" - joi "^17.4.0" + xml-name-validator "^4.0.0" + +wait-on@7.0.1: + version "7.0.1" + resolved "https://registry.yarnpkg.com/wait-on/-/wait-on-7.0.1.tgz#5cff9f8427e94f4deacbc2762e6b0a489b19eae9" + integrity sha512-9AnJE9qTjRQOlTZIldAaf/da2eW0eSRSgcqq85mXQja/DW3MriHxkpODDSUEg+Gri/rKEcXUZHe+cevvYItaog== + dependencies: + axios "^0.27.2" + joi "^17.7.0" lodash "^4.17.21" - minimist "^1.2.5" - rxjs "^7.1.0" + minimist "^1.2.7" + rxjs "^7.8.0" webidl-conversions@^3.0.0: version "3.0.1" resolved "https://registry.yarnpkg.com/webidl-conversions/-/webidl-conversions-3.0.1.tgz#24534275e2a7bc6be7bc86611cc16ae0a5654871" integrity sha1-JFNCdeKnvGvnvIZhHMFq4KVlSHE= +webidl-conversions@^7.0.0: + version "7.0.0" + resolved "https://registry.yarnpkg.com/webidl-conversions/-/webidl-conversions-7.0.0.tgz#256b4e1882be7debbf01d05f0aa2039778ea080a" + integrity sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g== + +whatwg-encoding@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/whatwg-encoding/-/whatwg-encoding-2.0.0.tgz#e7635f597fd87020858626805a2729fa7698ac53" + integrity sha512-p41ogyeMUrw3jWclHWTQg1k05DSVXPLcVxRTYsXUk+ZooOCZLcoYgPZ/HL/D/N+uQPOtcp1me1WhBEaX02mhWg== + dependencies: + iconv-lite "0.6.3" + +whatwg-mimetype@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/whatwg-mimetype/-/whatwg-mimetype-3.0.0.tgz#5fa1a7623867ff1af6ca3dc72ad6b8a4208beba7" + integrity sha512-nt+N2dzIutVRxARx1nghPKGv1xHikU7HKdfafKkLNLindmPU/ch3U31NOCGGA/dmPcmb1VlofO0vnKAcsm0o/Q== + +whatwg-url@^11.0.0: + version "11.0.0" + resolved "https://registry.yarnpkg.com/whatwg-url/-/whatwg-url-11.0.0.tgz#0a849eebb5faf2119b901bb76fd795c2848d4018" + integrity sha512-RKT8HExMpoYx4igMiVMY83lN6UeITKJlBQ+vR/8ZJ8OCdSiN3RwCq+9gH0+Xzj0+5IrM6i4j/6LuvzbZIQgEcQ== + dependencies: + tr46 "^3.0.0" + webidl-conversions "^7.0.0" + whatwg-url@^5.0.0: version "5.0.0" resolved "https://registry.yarnpkg.com/whatwg-url/-/whatwg-url-5.0.0.tgz#966454e8765462e37644d3626f6742ce8b70965d" @@ -8174,6 +8945,28 @@ which-boxed-primitive@^1.0.2: is-string "^1.0.5" is-symbol "^1.0.3" +which-collection@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/which-collection/-/which-collection-1.0.1.tgz#70eab71ebbbd2aefaf32f917082fc62cdcb70906" + integrity sha512-W8xeTUwaln8i3K/cY1nGXzdnVZlidBcagyNFtBdD5kxnb4TvGKR7FfSIS3mYpwWS1QUCutfKz8IY8RjftB0+1A== + dependencies: + is-map "^2.0.1" + is-set "^2.0.1" + is-weakmap "^2.0.1" + is-weakset "^2.0.1" + +which-typed-array@^1.1.9: + version "1.1.9" + resolved "https://registry.yarnpkg.com/which-typed-array/-/which-typed-array-1.1.9.tgz#307cf898025848cf995e795e8423c7f337efbde6" + integrity sha512-w9c4xkx6mPidwp7180ckYWfMmvxpjlZuIudNtDf4N/tTAUB8VJbX25qZoAsrtGuYNnGw3pa0AXgbGKRB8/EceA== + dependencies: + available-typed-arrays "^1.0.5" + call-bind "^1.0.2" + for-each "^0.3.3" + gopd "^1.0.1" + has-tostringtag "^1.0.0" + is-typed-array "^1.1.10" + which@^2.0.1, which@^2.0.2: version "2.0.2" resolved "https://registry.yarnpkg.com/which/-/which-2.0.2.tgz#7c6a8dd0a636a0327e10b59c9286eee93f3f51b1" @@ -8188,7 +8981,7 @@ wide-align@^1.1.0: dependencies: string-width "^1.0.2 || 2" -word-wrap@^1.2.3: +word-wrap@^1.2.3, word-wrap@~1.2.3: version "1.2.3" resolved "https://registry.yarnpkg.com/word-wrap/-/word-wrap-1.2.3.tgz#610636f6b1f703891bd34771ccb17fb93b47079c" integrity sha512-Hz/mrNwitNRh/HUAtM/VT/5VH+ygD6DV7mYKZAtHOrbs8U7lvPS6xf7EJKMF0uW1KJCl0H701g3ZGus+muE5vQ== @@ -8225,6 +9018,21 @@ write-file-atomic@^2.3.0: imurmurhash "^0.1.4" signal-exit "^3.0.2" +ws@^8.11.0: + version "8.12.1" + resolved "https://registry.yarnpkg.com/ws/-/ws-8.12.1.tgz#c51e583d79140b5e42e39be48c934131942d4a8f" + integrity sha512-1qo+M9Ba+xNhPB+YTWUlK6M17brTut5EXbcBaMRN5pH5dFrXz7lzz1ChFSUq3bOUl8yEvSenhHmYUNJxFzdJew== + +xml-name-validator@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/xml-name-validator/-/xml-name-validator-4.0.0.tgz#79a006e2e63149a8600f15430f0a4725d1524835" + integrity sha512-ICP2e+jsHvAj2E2lIHxa5tjXRlKDJo4IdvPvCXbXQGdzSfmSpNVyIKMvoZHjDY9DP0zV17iI85o90vRFXNccRw== + +xmlchars@^2.2.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/xmlchars/-/xmlchars-2.2.0.tgz#060fe1bcb7f9c76fe2a17db86a9bc3ab894210cb" + integrity sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw== + yallist@^3.0.0, yallist@^3.1.1: version "3.1.1" resolved "https://registry.yarnpkg.com/yallist/-/yallist-3.1.1.tgz#dbb7daf9bfd8bac9ab45ebf602b8cbad0d5d08fd"