From 2eb37112d826903a05869e4a38f948f348094fc2 Mon Sep 17 00:00:00 2001 From: Maja Komel Date: Thu, 22 Dec 2022 16:19:27 +0100 Subject: [PATCH] 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"