diff --git a/.github/workflows/get-translation-progress.yml b/.github/workflows/get-translation-progress.yml new file mode 100644 index 00000000000..a476785c909 --- /dev/null +++ b/.github/workflows/get-translation-progress.yml @@ -0,0 +1,64 @@ +name: Update Crowdin translation progression + +on: + schedule: + - cron: "20 16 * * FRI" + workflow_dispatch: + +jobs: + create_pr: + runs-on: ubuntu-latest + steps: + - name: Check out code + uses: actions/checkout@v3 + + - name: Set up Node.js + uses: actions/setup-node@v3 + with: + node-version: 18 + + - name: Install dependencies + run: yarn install + + - name: Install ts-node + run: yarn global add ts-node + + - name: Set up git + run: | + git config --global user.email "actions@github.com" + git config --global user.name "GitHub Action" + + - name: Generate timestamp and readable date + id: date + run: | + echo "TIMESTAMP=$(date +'%Y%m%d%H%M%S')" >> $GITHUB_ENV + echo "READABLE_DATE=$(date +'%B %-d')" >> $GITHUB_ENV + + - name: Fetch latest dev and create new branch + run: | + git fetch origin dev + git checkout -b "automated-update-${{ env.TIMESTAMP }}" origin/dev + + - name: Run script + run: npx ts-node -O '{"module":"commonjs"}' ./src/scripts/crowdin/getTranslationProgress.ts + env: + CROWDIN_API_KEY: ${{ secrets.CROWDIN_API_KEY }} + CROWDIN_PROJECT_ID: ${{ secrets.CROWDIN_PROJECT_ID }} + + - name: Commit and push + run: | + git add -A + git commit -m "Update Crowdin translation progress" + git push origin "automated-update-${{ env.TIMESTAMP }}" + + - name: Create PR body + run: | + echo "This PR was automatically created to update Crowdin translation progress." > pr_body.txt + echo "This workflows runs every Friday at 16:20 (UTC)." >> pr_body.txt + echo "" >> pr_body.txt + echo "Thank you to everyone contributing to translate ethereum.org ❤️" >> pr_body.txt + + - name: Create Pull Request + run: | + gh auth login --with-token <<< ${{ secrets.GITHUB_TOKEN }} + gh pr create --base dev --head "automated-update-${{ env.TIMESTAMP }}" --title "Update translation progress from Crowdin - ${{ env.READABLE_DATE }}" --body-file pr_body.txt diff --git a/i18n.config.json b/i18n.config.json index 06b6c673ced..f6b9d006b66 100644 --- a/i18n.config.json +++ b/i18n.config.json @@ -297,7 +297,7 @@ }, { "code": "ne-np", - "crowdinCode": "ne-np", + "crowdinCode": "ne-NP", "name": "Nepali", "localName": "नेपाली", "langDir": "ltr", @@ -457,7 +457,7 @@ }, { "code": "ur", - "crowdinCode": "ur", + "crowdinCode": "ur-IN", "name": "Urdu", "localName": "اردو", "langDir": "rtl", diff --git a/src/components/LanguagePicker/MenuItem.tsx b/src/components/LanguagePicker/MenuItem.tsx new file mode 100644 index 00000000000..65d660c64b0 --- /dev/null +++ b/src/components/LanguagePicker/MenuItem.tsx @@ -0,0 +1,127 @@ +import { useRouter } from "next/router" +import { useTranslation } from "next-i18next" +import { BsCheck } from "react-icons/bs" +import { + Badge, + Box, + Flex, + forwardRef, + Icon, + MenuItem as ChakraMenuItem, + type MenuItemProps as ChakraMenuItemProps, + Text, +} from "@chakra-ui/react" + +import type { LocaleDisplayInfo } from "@/lib/types" + +import { BaseLink } from "@/components/Link" + +import ProgressBar from "./ProgressBar" + +type ItemProps = ChakraMenuItemProps & { + displayInfo: LocaleDisplayInfo +} + +const MenuItem = forwardRef(({ displayInfo, ...props }: ItemProps, ref) => { + const { + localeOption, + sourceName, + targetName, + approvalProgress, + wordsApproved, + isBrowserDefault, + } = displayInfo + const { t } = useTranslation("page-languages") + const { asPath, locale } = useRouter() + const isCurrent = localeOption === locale + + const getProgressInfo = (approvalProgress: number, wordsApproved: number) => { + const percentage = new Intl.NumberFormat(locale!, { + style: "percent", + }).format(approvalProgress / 100) + const progress = + approvalProgress === 0 ? "<" + percentage.replace("0", "1") : percentage + const words = new Intl.NumberFormat(locale!).format(wordsApproved) + return { progress, words } + } + + const { progress, words } = getProgressInfo(approvalProgress, wordsApproved) + + return ( + { + e.target.scrollIntoView({ block: "nearest" }) + }} + scrollMarginY="8" + _hover={{ bg: "primary.lowContrast", textDecoration: "none" }} + _focus={{ bg: "primary.lowContrast" }} + sx={{ + p: { + textDecoration: "none", + overflow: "hidden", + textOverflow: "ellipsis", + whiteSpace: "nowrap", + }, + }} + href={asPath} + locale={localeOption} + {...props} + > + + + + + {targetName} + + {isBrowserDefault && ( + + {t("page-languages-browser-default")} + + )} + + + {sourceName} + + + {isCurrent && } + + + {progress} {t("page-languages-translated")} • {words}{" "} + {t("page-languages-words")} + + + + ) +}) + +export default MenuItem diff --git a/src/components/LanguagePicker/NoResultsCallout.tsx b/src/components/LanguagePicker/NoResultsCallout.tsx new file mode 100644 index 00000000000..e45916839de --- /dev/null +++ b/src/components/LanguagePicker/NoResultsCallout.tsx @@ -0,0 +1,33 @@ +import { useTranslation } from "next-i18next" +import { FormHelperText, forwardRef, Text } from "@chakra-ui/react" + +import { BaseLink } from "@/components/Link" + +import MenuItem from "./MenuItem" + +type NoResultsCalloutProps = { onClose: () => void } + +const NoResultsCallout = forwardRef( + ({ onClose }: NoResultsCalloutProps, ref) => { + const { t } = useTranslation("page-languages") + return ( + + + {t("page-languages-want-more-header")} + + {t("page-languages-want-more-paragraph")}{" "} + + {t("page-languages-want-more-link")} + + + ) + } +) + +export default NoResultsCallout diff --git a/src/components/LanguagePicker/ProgressBar.tsx b/src/components/LanguagePicker/ProgressBar.tsx new file mode 100644 index 00000000000..06bb008923f --- /dev/null +++ b/src/components/LanguagePicker/ProgressBar.tsx @@ -0,0 +1,19 @@ +import { Progress, ProgressProps } from "@chakra-ui/react" + +type ProgressBarProps = Pick + +const ProgressBar = ({ value }: ProgressBarProps) => ( + +) + +export default ProgressBar diff --git a/src/components/LanguagePicker/index.tsx b/src/components/LanguagePicker/index.tsx new file mode 100644 index 00000000000..367d9a488be --- /dev/null +++ b/src/components/LanguagePicker/index.tsx @@ -0,0 +1,226 @@ +import { + Box, + Flex, + FormControl, + FormLabel, + Input, + InputGroup, + InputRightElement, + Kbd, + Menu, + MenuList, + type MenuListProps, + type MenuProps, + Text, + type UseDisclosureReturn, + useEventListener, +} from "@chakra-ui/react" + +import { Button } from "@/components/Buttons" +import { BaseLink } from "@/components/Link" + +import MenuItem from "./MenuItem" +import NoResultsCallout from "./NoResultsCallout" +import { useLanguagePicker } from "./useLanguagePicker" + +type LanguagePickerProps = Omit & { + children: React.ReactNode + placement?: MenuProps["placement"] + handleClose?: () => void + menuState?: UseDisclosureReturn +} + +const LanguagePicker = ({ + children, + placement, + handleClose, + menuState, + ...props +}: LanguagePickerProps) => { + const { t, refs, disclosure, filterValue, setFilterValue, filteredNames } = + useLanguagePicker(handleClose, menuState) + const { inputRef, firstItemRef, noResultsRef, footerRef } = refs + const { onClose } = disclosure + + /** + * Adds a keydown event listener to focus filter input (\). + * @param {string} event - The keydown event. + */ + useEventListener("keydown", (e) => { + if (e.key !== "\\") return + e.preventDefault() + inputRef.current?.focus() + }) + + return ( + + {children} + { + if (e.key === "Tab" || e.key === "\\") { + e.preventDefault() + ;(e.shiftKey ? inputRef : footerRef).current?.focus() + } + }} + {...props} + > + {/* Mobile Close bar */} + + + + + {/* Main Language selection menu */} + + + + {t("page-languages-filter-label")}{" "} + + ({filteredNames.length} {t("common:languages")}) + + + + setFilterValue(e.target.value)} + onBlur={(e) => { + if (e.relatedTarget?.tagName.toLowerCase() === "div") { + e.currentTarget.focus() + } + }} + ref={inputRef} + h="8" + mt="1" + mb="2" + bg="background.base" + color="body.base" + onKeyDown={(e) => { + // Navigate to first result on enter + if (e.key === "Enter") { + e.preventDefault() + firstItemRef.current?.click() + } + // If Tab/ArrowDown, focus on first item if available, NoResults link otherwise + if (e.key === "Tab" || e.key === "ArrowDown") { + e.preventDefault() + ;(filteredNames.length === 0 + ? noResultsRef + : firstItemRef + ).current?.focus() + e.stopPropagation() + } + }} + /> + + + \ + + + + + {filteredNames.map((displayInfo, index) => ( + { + if (e.key !== "\\") return + e.preventDefault() + inputRef.current?.focus() + }} + onClick={() => + onClose({ + eventAction: "Locale chosen", + eventName: displayInfo.localeOption, + }) + } + /> + ))} + + {filteredNames.length === 0 && ( + + onClose({ + eventAction: "Translation program link (no results)", + eventName: "/contributing/translation-program", + }) + } + /> + )} + + + + {/* Footer callout */} + + + {t("page-languages-recruit-community")}{" "} + + onClose({ + eventAction: "Translation program link (menu footer)", + eventName: "/contributing/translation-program", + }) + } + > + {t("common:learn-more")} + + + + + + ) +} + +export default LanguagePicker diff --git a/src/components/LanguagePicker/useLanguagePicker.tsx b/src/components/LanguagePicker/useLanguagePicker.tsx new file mode 100644 index 00000000000..78c3df6926d --- /dev/null +++ b/src/components/LanguagePicker/useLanguagePicker.tsx @@ -0,0 +1,187 @@ +import { useEffect, useRef, useState } from "react" +import { useRouter } from "next/router" +import { useTranslation } from "next-i18next" +import { useDisclosure, type UseDisclosureReturn } from "@chakra-ui/react" + +import type { + I18nLocale, + Lang, + LocaleDisplayInfo, + ProjectProgressData, +} from "@/lib/types" + +import { MatomoEventOptions, trackCustomEvent } from "@/lib/utils/matomo" +import { languages } from "@/lib/utils/translations" + +import progressData from "@/data/translationProgress.json" + +import { DEFAULT_LOCALE } from "@/lib/constants" + +const data = progressData as ProjectProgressData[] + +export const useLanguagePicker = ( + handleClose?: () => void, + menuState?: UseDisclosureReturn +) => { + const { t } = useTranslation("page-languages") + const { locale, locales } = useRouter() + const refs = { + inputRef: useRef(null), + firstItemRef: useRef(null), + noResultsRef: useRef(null), + footerRef: useRef(null), + } + const [filterValue, setFilterValue] = useState("") + + const [filteredNames, setFilteredNames] = useState([]) + + // perform all the filtering and mapping when the filter value change + useEffect(() => { + // Get the preferred languages for the users browser + const navLangs = typeof navigator !== "undefined" ? navigator.languages : [] + + // For each browser preference, reduce to the most specific match found in `locales` array + const allBrowserLocales: Lang[] = navLangs + .map( + (navLang) => + locales?.reduce((acc, cur) => { + if (cur.toLowerCase() === navLang.toLowerCase()) return cur + if ( + navLang.toLowerCase().startsWith(cur.toLowerCase()) && + acc !== navLang + ) + return cur + return acc + }, "") as Lang + ) + .filter((i) => !!i) // Remove those without matches + + // Remove duplicate matches + const browserLocales = Array.from(new Set(allBrowserLocales)) + + const localeToDisplayInfo = (localeOption: Lang): LocaleDisplayInfo => { + const i18nItem: I18nLocale = languages[localeOption] + const englishName = i18nItem.name + + // Get "source" display name (Language choice displayed in language of current locale) + const intlSource = new Intl.DisplayNames([locale!], { + type: "language", + }).of(localeOption) + // For languages that do not have an Intl display name, use English name as fallback + const fallbackSource = + intlSource !== localeOption ? intlSource : englishName + const i18nKey = "language-" + localeOption.toLowerCase() + const i18nSource = t(i18nKey) + const sourceName = i18nSource === i18nKey ? fallbackSource : i18nSource + + // Get "target" display name (Language choice displayed in that language) + const fallbackTarget = new Intl.DisplayNames([localeOption], { + type: "language", + }).of(localeOption) + const i18nConfigTarget = i18nItem.localName + const targetName = i18nConfigTarget || fallbackTarget + + if (!sourceName || !targetName) { + throw new Error( + "Missing language display name, locale: " + localeOption + ) + } + + // English will not have a dataItem + const dataItem = data.find( + ({ languageId }) => + i18nItem.crowdinCode.toLowerCase() === languageId.toLowerCase() + ) + + const approvalProgress = + localeOption === DEFAULT_LOCALE ? 100 : dataItem?.approvalProgress || 0 + + if (data.length === 0) + throw new Error( + "Missing translation progress data; check GitHub action" + ) + + const totalWords = data[0].words.total + + const wordsApproved = + localeOption === DEFAULT_LOCALE + ? totalWords || 0 + : dataItem?.words.approved || 0 + + const isBrowserDefault = browserLocales.includes(localeOption) + + return { + localeOption, + approvalProgress, + sourceName, + targetName, + englishName, + wordsApproved, + isBrowserDefault, + } + } + + const displayNames: LocaleDisplayInfo[] = + (locales as Lang[])?.map(localeToDisplayInfo).sort((a, b) => { + const indexA = browserLocales.indexOf(a.localeOption as Lang) + const indexB = browserLocales.indexOf(b.localeOption as Lang) + if (indexA >= 0 && indexB >= 0) return indexA - indexB + if (indexA >= 0) return -1 + if (indexB >= 0) return 1 + return b.approvalProgress - a.approvalProgress + }) || [] + + setFilteredNames( + displayNames.filter( + ({ localeOption, sourceName, targetName, englishName }) => + (localeOption + sourceName + targetName + englishName) + .toLowerCase() + .includes(filterValue.toLowerCase()) + ) + ) + }, [filterValue, locale, locales, t]) + + const { isOpen, ...menu } = useDisclosure() + + const eventBase: Pick = { + eventCategory: `Language picker`, + eventAction: "Open or close language picker", + } + + const onOpen = () => { + menu.onOpen() + menuState?.onOpen() + trackCustomEvent({ + ...eventBase, + eventName: "Opened", + } as MatomoEventOptions) + } + + /** + * When closing the menu, track whether this is following a link, or simply closing the menu + * @param customMatomoEvent Optional custom event property overrides + */ + const onClose = ( + customMatomoEvent?: Required> & + Partial + ): void => { + setFilterValue("") + handleClose && handleClose() + menu.onClose() + menuState?.onClose() + trackCustomEvent( + (customMatomoEvent + ? { ...eventBase, ...customMatomoEvent } + : { ...eventBase, eventName: "Closed" }) satisfies MatomoEventOptions + ) + } + + return { + t, + refs, + disclosure: { isOpen, onOpen, onClose }, + filterValue, + setFilterValue, + filteredNames, + } +} diff --git a/src/components/Nav/Mobile.tsx b/src/components/Nav/Mobile.tsx index 2d32a8c43d3..266e473f00a 100644 --- a/src/components/Nav/Mobile.tsx +++ b/src/components/Nav/Mobile.tsx @@ -1,22 +1,32 @@ import React, { Fragment, ReactNode, RefObject } from "react" import { motion } from "framer-motion" import { useTranslation } from "next-i18next" -import { MdBrightness2, MdLanguage, MdSearch, MdWbSunny } from "react-icons/md" +import { IconType } from "react-icons" +import { BsTranslate } from "react-icons/bs" +import { MdBrightness2, MdSearch, MdWbSunny } from "react-icons/md" import { Box, ButtonProps, Drawer, DrawerBody, + DrawerCloseButton, DrawerContent, DrawerFooter, DrawerOverlay, Flex, forwardRef, + Grid, Icon, + IconButton, List, ListItem, + MenuButton, + Text, + useColorModeValue, } from "@chakra-ui/react" +import LanguagePicker from "@/components/LanguagePicker" + import type { ChildOnlyProp } from "../../lib/types" import { Button } from "../Buttons" import { BaseLink } from "../Link" @@ -70,8 +80,25 @@ const FooterItem = forwardRef((props, ref) => ( /> )) +type FooterButtonProps = ButtonProps & { + icon: IconType +} + +const FooterButton = ({ icon, ...props }: FooterButtonProps) => ( + + } + {...props} + /> + ) +} + +type CloseButtonProps = ButtonProps & { + onToggle: () => void +} + +const CloseButton = ({ onToggle, ...props }: CloseButtonProps) => { + const { t } = useTranslation("common") + return ( + + + + + + + } + {...props} + /> + ) +} + +export interface IProps extends ButtonProps { + isMenuOpen: boolean + isDarkTheme: boolean + toggleMenu: () => void + toggleTheme: () => void + toggleSearch: () => void + linkSections: ISections + fromPageParameter: string + drawerContainerRef: RefObject +} + +const MobileNavMenu: React.FC = ({ + isMenuOpen, + isDarkTheme, + toggleMenu: onToggle, + toggleTheme, + toggleSearch, + linkSections, + fromPageParameter, + drawerContainerRef, + ...props +}) => { + const { t } = useTranslation("common") + + const ThemeIcon = useColorModeValue(MdBrightness2, MdWbSunny) + const themeLabelKey = useColorModeValue("dark-mode", "light-mode") + + return ( + <> + + + {t("close")} + {Object.keys(linkSections).map((sectionKey, idx) => { @@ -204,7 +276,7 @@ const MobileNavMenu: React.FC = ({ {item.text} {item.items.map((item, idx) => ( - + = ({ ))} ) : ( - + = ({ ) : ( - + = ({ py={0} mt="auto" > - { - // Workaround to ensure the input for the search modal can have focus - toggleMenu() - toggleSearch() - }} - > - - {t("search")} - - - - - {t(isDarkTheme ? "light-mode" : "dark-mode")} - - - - + { + // Workaround to ensure the input for the search modal can have focus + onToggle() + toggleSearch() }} > - - {t("languages")} - - + {t("search")} + + + {t(themeLabelKey)} + + + + {t("languages")} + + + diff --git a/src/components/Nav/index.tsx b/src/components/Nav/index.tsx index 5374671b252..99f23767f03 100644 --- a/src/components/Nav/index.tsx +++ b/src/components/Nav/index.tsx @@ -1,13 +1,25 @@ -import React, { FC, useRef } from "react" +import { FC, useRef } from "react" import { useRouter } from "next/router" import { useTranslation } from "next-i18next" -import { MdBrightness2, MdLanguage, MdWbSunny } from "react-icons/md" -import { Box, Flex, HStack, Icon, useDisclosure } from "@chakra-ui/react" +import { BsTranslate } from "react-icons/bs" +import { MdBrightness2, MdWbSunny } from "react-icons/md" +import { + Box, + Button, + Flex, + HStack, + Icon, + MenuButton, + Text, + useDisclosure, + useEventListener, +} from "@chakra-ui/react" -import { ButtonLink, IconButton } from "../Buttons" -import { EthHomeIcon } from "../icons" -import { BaseLink } from "../Link" -import Search from "../Search" +import { IconButton } from "@/components/Buttons" +import { EthHomeIcon } from "@/components/icons" +import LanguagePicker from "@/components/LanguagePicker" +import { BaseLink } from "@/components/Link" +import Search from "@/components/Search" import Menu from "./Menu" import MobileNavMenu from "./Mobile" @@ -21,7 +33,6 @@ export interface IProps { const Nav: FC = ({ path }) => { const { ednLinks, - fromPageParameter, isDarkTheme, shouldShowSubNav, toggleColorMode, @@ -32,6 +43,23 @@ const Nav: FC = ({ path }) => { const { t } = useTranslation("common") const searchModalDisclosure = useDisclosure() const navWrapperRef = useRef(null) + const languagePickerState = useDisclosure() + const languagePickerRef = useRef(null) + /** + * Adds a keydown event listener to toggle color mode (ctrl|cmd + \) + * or open the language picker (\). + * @param {string} event - The keydown event. + */ + useEventListener("keydown", (e) => { + if (e.key !== "\\") return + e.preventDefault() + if (e.metaKey || e.ctrlKey) { + toggleColorMode() + } else { + if (languagePickerState.isOpen) return + languagePickerRef.current?.click() + } + }) return ( @@ -82,6 +110,7 @@ const Nav: FC = ({ path }) => { toggleSearch={searchModalDisclosure.onOpen} drawerContainerRef={navWrapperRef} /> + {/* Desktop */} = ({ path }) => { color: "primary.hover", }} onClick={toggleColorMode} - > - } - variant="ghost" - isSecondary - px={1.5} - _hover={{ - color: "primary.hover", - "& svg": { - transform: "rotate(10deg)", - transition: "transform 0.5s", - }, - }} + /> + + {/* Locale-picker menu */} + - {t("languages")} {locale!.toUpperCase()} - + + + + {t("common:languages")}  + + {locale!.toUpperCase()} + + diff --git a/src/data/translationProgress.json b/src/data/translationProgress.json new file mode 100644 index 00000000000..7b8f39ddf89 --- /dev/null +++ b/src/data/translationProgress.json @@ -0,0 +1,1549 @@ +[ + { + "languageId": "af", + "words": { + "total": 336489, + "translated": 2654, + "preTranslateAppliedTo": 134, + "approved": 0 + }, + "phrases": { + "total": 24685, + "translated": 474, + "preTranslateAppliedTo": 36, + "approved": 0 + }, + "translationProgress": 0, + "approvalProgress": 0 + }, + { + "languageId": "am", + "words": { + "total": 336489, + "translated": 34590, + "preTranslateAppliedTo": 3747, + "approved": 9634 + }, + "phrases": { + "total": 24685, + "translated": 3447, + "preTranslateAppliedTo": 362, + "approved": 993 + }, + "translationProgress": 10, + "approvalProgress": 2 + }, + { + "languageId": "ar", + "words": { + "total": 336489, + "translated": 101830, + "preTranslateAppliedTo": 35259, + "approved": 40466 + }, + "phrases": { + "total": 24685, + "translated": 8511, + "preTranslateAppliedTo": 3243, + "approved": 3373 + }, + "translationProgress": 30, + "approvalProgress": 12 + }, + { + "languageId": "az", + "words": { + "total": 336489, + "translated": 29092, + "preTranslateAppliedTo": 1761, + "approved": 18707 + }, + "phrases": { + "total": 24685, + "translated": 2701, + "preTranslateAppliedTo": 310, + "approved": 1852 + }, + "translationProgress": 8, + "approvalProgress": 5 + }, + { + "languageId": "be", + "words": { + "total": 336489, + "translated": 7372, + "preTranslateAppliedTo": 726, + "approved": 5879 + }, + "phrases": { + "total": 24685, + "translated": 822, + "preTranslateAppliedTo": 94, + "approved": 603 + }, + "translationProgress": 2, + "approvalProgress": 1 + }, + { + "languageId": "bg", + "words": { + "total": 336489, + "translated": 36624, + "preTranslateAppliedTo": 12272, + "approved": 14767 + }, + "phrases": { + "total": 24685, + "translated": 3737, + "preTranslateAppliedTo": 1422, + "approved": 1531 + }, + "translationProgress": 10, + "approvalProgress": 4 + }, + { + "languageId": "bi", + "words": { + "total": 336489, + "translated": 180, + "preTranslateAppliedTo": 20, + "approved": 0 + }, + "phrases": { + "total": 24685, + "translated": 115, + "preTranslateAppliedTo": 17, + "approved": 0 + }, + "translationProgress": 0, + "approvalProgress": 0 + }, + { + "languageId": "bn", + "words": { + "total": 336489, + "translated": 44527, + "preTranslateAppliedTo": 4114, + "approved": 36246 + }, + "phrases": { + "total": 24685, + "translated": 3853, + "preTranslateAppliedTo": 532, + "approved": 3027 + }, + "translationProgress": 13, + "approvalProgress": 10 + }, + { + "languageId": "br-FR", + "words": { + "total": 336489, + "translated": 192, + "preTranslateAppliedTo": 148, + "approved": 82 + }, + "phrases": { + "total": 24685, + "translated": 29, + "preTranslateAppliedTo": 21, + "approved": 7 + }, + "translationProgress": 0, + "approvalProgress": 0 + }, + { + "languageId": "bs", + "words": { + "total": 336489, + "translated": 12036, + "preTranslateAppliedTo": 713, + "approved": 5879 + }, + "phrases": { + "total": 24685, + "translated": 1160, + "preTranslateAppliedTo": 68, + "approved": 603 + }, + "translationProgress": 3, + "approvalProgress": 1 + }, + { + "languageId": "ca", + "words": { + "total": 336489, + "translated": 46990, + "preTranslateAppliedTo": 14452, + "approved": 19695 + }, + "phrases": { + "total": 24685, + "translated": 4571, + "preTranslateAppliedTo": 1601, + "approved": 2056 + }, + "translationProgress": 13, + "approvalProgress": 5 + }, + { + "languageId": "cs", + "words": { + "total": 336489, + "translated": 62786, + "preTranslateAppliedTo": 6867, + "approved": 26678 + }, + "phrases": { + "total": 24685, + "translated": 5544, + "preTranslateAppliedTo": 1004, + "approved": 2427 + }, + "translationProgress": 18, + "approvalProgress": 7 + }, + { + "languageId": "da", + "words": { + "total": 336489, + "translated": 15857, + "preTranslateAppliedTo": 2817, + "approved": 1466 + }, + "phrases": { + "total": 24685, + "translated": 1668, + "preTranslateAppliedTo": 488, + "approved": 263 + }, + "translationProgress": 4, + "approvalProgress": 0 + }, + { + "languageId": "de", + "words": { + "total": 336489, + "translated": 237654, + "preTranslateAppliedTo": 43906, + "approved": 163737 + }, + "phrases": { + "total": 24685, + "translated": 18419, + "preTranslateAppliedTo": 4130, + "approved": 13313 + }, + "translationProgress": 70, + "approvalProgress": 48 + }, + { + "languageId": "el", + "words": { + "total": 336489, + "translated": 102635, + "preTranslateAppliedTo": 18079, + "approved": 102345 + }, + "phrases": { + "total": 24685, + "translated": 10333, + "preTranslateAppliedTo": 2113, + "approved": 10314 + }, + "translationProgress": 30, + "approvalProgress": 30 + }, + { + "languageId": "eo", + "words": { + "total": 336489, + "translated": 824, + "preTranslateAppliedTo": 268, + "approved": 169 + }, + "phrases": { + "total": 24685, + "translated": 77, + "preTranslateAppliedTo": 27, + "approved": 16 + }, + "translationProgress": 0, + "approvalProgress": 0 + }, + { + "languageId": "es-EM", + "words": { + "total": 336489, + "translated": 331819, + "preTranslateAppliedTo": 51273, + "approved": 296693 + }, + "phrases": { + "total": 24685, + "translated": 24558, + "preTranslateAppliedTo": 4649, + "approved": 21958 + }, + "translationProgress": 98, + "approvalProgress": 88 + }, + { + "languageId": "et", + "words": { + "total": 336489, + "translated": 1014, + "preTranslateAppliedTo": 245, + "approved": 75 + }, + "phrases": { + "total": 24685, + "translated": 149, + "preTranslateAppliedTo": 46, + "approved": 12 + }, + "translationProgress": 0, + "approvalProgress": 0 + }, + { + "languageId": "eu", + "words": { + "total": 336489, + "translated": 768, + "preTranslateAppliedTo": 217, + "approved": 36 + }, + "phrases": { + "total": 24685, + "translated": 83, + "preTranslateAppliedTo": 38, + "approved": 4 + }, + "translationProgress": 0, + "approvalProgress": 0 + }, + { + "languageId": "fa", + "words": { + "total": 336489, + "translated": 149845, + "preTranslateAppliedTo": 28269, + "approved": 96744 + }, + "phrases": { + "total": 24685, + "translated": 12201, + "preTranslateAppliedTo": 2782, + "approved": 7769 + }, + "translationProgress": 44, + "approvalProgress": 28 + }, + { + "languageId": "fa-AF", + "words": { + "total": 336489, + "translated": 193, + "preTranslateAppliedTo": 37, + "approved": 186 + }, + "phrases": { + "total": 24685, + "translated": 22, + "preTranslateAppliedTo": 6, + "approved": 17 + }, + "translationProgress": 0, + "approvalProgress": 0 + }, + { + "languageId": "fi", + "words": { + "total": 336489, + "translated": 45286, + "preTranslateAppliedTo": 11063, + "approved": 22594 + }, + "phrases": { + "total": 24685, + "translated": 4157, + "preTranslateAppliedTo": 1096, + "approved": 2136 + }, + "translationProgress": 13, + "approvalProgress": 6 + }, + { + "languageId": "fil", + "words": { + "total": 336489, + "translated": 63679, + "preTranslateAppliedTo": 5142, + "approved": 54718 + }, + "phrases": { + "total": 24685, + "translated": 5343, + "preTranslateAppliedTo": 656, + "approved": 4539 + }, + "translationProgress": 18, + "approvalProgress": 16 + }, + { + "languageId": "fr", + "words": { + "total": 336489, + "translated": 336489, + "preTranslateAppliedTo": 54154, + "approved": 336420 + }, + "phrases": { + "total": 24685, + "translated": 24685, + "preTranslateAppliedTo": 4849, + "approved": 24674 + }, + "translationProgress": 100, + "approvalProgress": 99 + }, + { + "languageId": "gi", + "words": { + "total": 336489, + "translated": 4, + "preTranslateAppliedTo": 4, + "approved": 0 + }, + "phrases": { + "total": 24685, + "translated": 2, + "preTranslateAppliedTo": 2, + "approved": 0 + }, + "translationProgress": 0, + "approvalProgress": 0 + }, + { + "languageId": "gl", + "words": { + "total": 336489, + "translated": 8308, + "preTranslateAppliedTo": 1290, + "approved": 1062 + }, + "phrases": { + "total": 24685, + "translated": 1042, + "preTranslateAppliedTo": 238, + "approved": 165 + }, + "translationProgress": 2, + "approvalProgress": 0 + }, + { + "languageId": "gu-IN", + "words": { + "total": 336489, + "translated": 3066, + "preTranslateAppliedTo": 1043, + "approved": 1300 + }, + "phrases": { + "total": 24685, + "translated": 551, + "preTranslateAppliedTo": 251, + "approved": 235 + }, + "translationProgress": 0, + "approvalProgress": 0 + }, + { + "languageId": "ha", + "words": { + "total": 336489, + "translated": 524, + "preTranslateAppliedTo": 114, + "approved": 4 + }, + "phrases": { + "total": 24685, + "translated": 54, + "preTranslateAppliedTo": 18, + "approved": 2 + }, + "translationProgress": 0, + "approvalProgress": 0 + }, + { + "languageId": "he", + "words": { + "total": 336489, + "translated": 7207, + "preTranslateAppliedTo": 1109, + "approved": 1222 + }, + "phrases": { + "total": 24685, + "translated": 1041, + "preTranslateAppliedTo": 268, + "approved": 203 + }, + "translationProgress": 2, + "approvalProgress": 0 + }, + { + "languageId": "hi", + "words": { + "total": 336489, + "translated": 75996, + "preTranslateAppliedTo": 8937, + "approved": 57736 + }, + "phrases": { + "total": 24685, + "translated": 6278, + "preTranslateAppliedTo": 975, + "approved": 4820 + }, + "translationProgress": 22, + "approvalProgress": 17 + }, + { + "languageId": "hr", + "words": { + "total": 336489, + "translated": 28058, + "preTranslateAppliedTo": 9317, + "approved": 13546 + }, + "phrases": { + "total": 24685, + "translated": 3014, + "preTranslateAppliedTo": 1040, + "approved": 1399 + }, + "translationProgress": 8, + "approvalProgress": 4 + }, + { + "languageId": "hu", + "words": { + "total": 336489, + "translated": 218207, + "preTranslateAppliedTo": 18587, + "approved": 148555 + }, + "phrases": { + "total": 24685, + "translated": 16143, + "preTranslateAppliedTo": 1952, + "approved": 11899 + }, + "translationProgress": 64, + "approvalProgress": 44 + }, + { + "languageId": "hy-AM", + "words": { + "total": 336489, + "translated": 10508, + "preTranslateAppliedTo": 1290, + "approved": 9634 + }, + "phrases": { + "total": 24685, + "translated": 1094, + "preTranslateAppliedTo": 243, + "approved": 993 + }, + "translationProgress": 3, + "approvalProgress": 2 + }, + { + "languageId": "id", + "words": { + "total": 336489, + "translated": 280227, + "preTranslateAppliedTo": 37675, + "approved": 156274 + }, + "phrases": { + "total": 24685, + "translated": 21146, + "preTranslateAppliedTo": 3636, + "approved": 12116 + }, + "translationProgress": 83, + "approvalProgress": 46 + }, + { + "languageId": "ig", + "words": { + "total": 336489, + "translated": 31195, + "preTranslateAppliedTo": 1678, + "approved": 23475 + }, + "phrases": { + "total": 24685, + "translated": 2954, + "preTranslateAppliedTo": 299, + "approved": 2278 + }, + "translationProgress": 9, + "approvalProgress": 6 + }, + { + "languageId": "it", + "words": { + "total": 336489, + "translated": 336489, + "preTranslateAppliedTo": 57137, + "approved": 336174 + }, + "phrases": { + "total": 24685, + "translated": 24685, + "preTranslateAppliedTo": 5157, + "approved": 24656 + }, + "translationProgress": 100, + "approvalProgress": 99 + }, + { + "languageId": "ja", + "words": { + "total": 336489, + "translated": 316663, + "preTranslateAppliedTo": 48606, + "approved": 284629 + }, + "phrases": { + "total": 24685, + "translated": 23404, + "preTranslateAppliedTo": 4431, + "approved": 20925 + }, + "translationProgress": 94, + "approvalProgress": 84 + }, + { + "languageId": "ka", + "words": { + "total": 336489, + "translated": 15234, + "preTranslateAppliedTo": 2130, + "approved": 1449 + }, + "phrases": { + "total": 24685, + "translated": 1936, + "preTranslateAppliedTo": 387, + "approved": 253 + }, + "translationProgress": 4, + "approvalProgress": 0 + }, + { + "languageId": "kk", + "words": { + "total": 336489, + "translated": 2027, + "preTranslateAppliedTo": 1138, + "approved": 1155 + }, + "phrases": { + "total": 24685, + "translated": 427, + "preTranslateAppliedTo": 225, + "approved": 185 + }, + "translationProgress": 0, + "approvalProgress": 0 + }, + { + "languageId": "km", + "words": { + "total": 336489, + "translated": 16940, + "preTranslateAppliedTo": 1646, + "approved": 15713 + }, + "phrases": { + "total": 24685, + "translated": 1769, + "preTranslateAppliedTo": 327, + "approved": 1551 + }, + "translationProgress": 5, + "approvalProgress": 4 + }, + { + "languageId": "kn", + "words": { + "total": 336489, + "translated": 44325, + "preTranslateAppliedTo": 1474, + "approved": 26051 + }, + "phrases": { + "total": 24685, + "translated": 3595, + "preTranslateAppliedTo": 144, + "approved": 2343 + }, + "translationProgress": 13, + "approvalProgress": 7 + }, + { + "languageId": "ko", + "words": { + "total": 336489, + "translated": 108091, + "preTranslateAppliedTo": 19220, + "approved": 51562 + }, + "phrases": { + "total": 24685, + "translated": 8998, + "preTranslateAppliedTo": 2057, + "approved": 3874 + }, + "translationProgress": 32, + "approvalProgress": 15 + }, + { + "languageId": "ku", + "words": { + "total": 336489, + "translated": 897, + "preTranslateAppliedTo": 70, + "approved": 0 + }, + "phrases": { + "total": 24685, + "translated": 116, + "preTranslateAppliedTo": 40, + "approved": 0 + }, + "translationProgress": 0, + "approvalProgress": 0 + }, + { + "languageId": "ky", + "words": { + "total": 336489, + "translated": 456, + "preTranslateAppliedTo": 129, + "approved": 12 + }, + "phrases": { + "total": 24685, + "translated": 129, + "preTranslateAppliedTo": 55, + "approved": 7 + }, + "translationProgress": 0, + "approvalProgress": 0 + }, + { + "languageId": "lb", + "words": { + "total": 336489, + "translated": 257, + "preTranslateAppliedTo": 81, + "approved": 0 + }, + "phrases": { + "total": 24685, + "translated": 29, + "preTranslateAppliedTo": 11, + "approved": 0 + }, + "translationProgress": 0, + "approvalProgress": 0 + }, + { + "languageId": "lt", + "words": { + "total": 336489, + "translated": 4171, + "preTranslateAppliedTo": 1794, + "approved": 1567 + }, + "phrases": { + "total": 24685, + "translated": 790, + "preTranslateAppliedTo": 398, + "approved": 257 + }, + "translationProgress": 1, + "approvalProgress": 0 + }, + { + "languageId": "mai", + "words": { + "total": 336489, + "translated": 1, + "preTranslateAppliedTo": 1, + "approved": 0 + }, + "phrases": { + "total": 24685, + "translated": 1, + "preTranslateAppliedTo": 1, + "approved": 0 + }, + "translationProgress": 0, + "approvalProgress": 0 + }, + { + "languageId": "mk", + "words": { + "total": 336489, + "translated": 422, + "preTranslateAppliedTo": 245, + "approved": 88 + }, + "phrases": { + "total": 24685, + "translated": 164, + "preTranslateAppliedTo": 88, + "approved": 16 + }, + "translationProgress": 0, + "approvalProgress": 0 + }, + { + "languageId": "ml-IN", + "words": { + "total": 336489, + "translated": 18507, + "preTranslateAppliedTo": 6285, + "approved": 11452 + }, + "phrases": { + "total": 24685, + "translated": 2119, + "preTranslateAppliedTo": 750, + "approved": 1281 + }, + "translationProgress": 5, + "approvalProgress": 3 + }, + { + "languageId": "mn", + "words": { + "total": 336489, + "translated": 142, + "preTranslateAppliedTo": 131, + "approved": 64 + }, + "phrases": { + "total": 24685, + "translated": 19, + "preTranslateAppliedTo": 17, + "approved": 4 + }, + "translationProgress": 0, + "approvalProgress": 0 + }, + { + "languageId": "mr", + "words": { + "total": 336489, + "translated": 33873, + "preTranslateAppliedTo": 1592, + "approved": 26062 + }, + "phrases": { + "total": 24685, + "translated": 2914, + "preTranslateAppliedTo": 269, + "approved": 2346 + }, + "translationProgress": 10, + "approvalProgress": 7 + }, + { + "languageId": "ms", + "words": { + "total": 336489, + "translated": 74157, + "preTranslateAppliedTo": 4802, + "approved": 37271 + }, + "phrases": { + "total": 24685, + "translated": 6231, + "preTranslateAppliedTo": 619, + "approved": 2879 + }, + "translationProgress": 22, + "approvalProgress": 11 + }, + { + "languageId": "my", + "words": { + "total": 336489, + "translated": 1568, + "preTranslateAppliedTo": 914, + "approved": 706 + }, + "phrases": { + "total": 24685, + "translated": 188, + "preTranslateAppliedTo": 99, + "approved": 58 + }, + "translationProgress": 0, + "approvalProgress": 0 + }, + { + "languageId": "ne-NP", + "words": { + "total": 336489, + "translated": 1887, + "preTranslateAppliedTo": 200, + "approved": 1434 + }, + "phrases": { + "total": 24685, + "translated": 317, + "preTranslateAppliedTo": 45, + "approved": 248 + }, + "translationProgress": 0, + "approvalProgress": 0 + }, + { + "languageId": "nl", + "words": { + "total": 336489, + "translated": 71400, + "preTranslateAppliedTo": 17112, + "approved": 37568 + }, + "phrases": { + "total": 24685, + "translated": 6480, + "preTranslateAppliedTo": 1791, + "approved": 3380 + }, + "translationProgress": 21, + "approvalProgress": 11 + }, + { + "languageId": "no", + "words": { + "total": 336489, + "translated": 6743, + "preTranslateAppliedTo": 1939, + "approved": 1717 + }, + "phrases": { + "total": 24685, + "translated": 1319, + "preTranslateAppliedTo": 414, + "approved": 306 + }, + "translationProgress": 2, + "approvalProgress": 0 + }, + { + "languageId": "or", + "words": { + "total": 336489, + "translated": 146, + "preTranslateAppliedTo": 42, + "approved": 0 + }, + "phrases": { + "total": 24685, + "translated": 79, + "preTranslateAppliedTo": 31, + "approved": 0 + }, + "translationProgress": 0, + "approvalProgress": 0 + }, + { + "languageId": "pa-IN", + "words": { + "total": 336489, + "translated": 3977, + "preTranslateAppliedTo": 458, + "approved": 6 + }, + "phrases": { + "total": 24685, + "translated": 365, + "preTranslateAppliedTo": 44, + "approved": 2 + }, + "translationProgress": 1, + "approvalProgress": 0 + }, + { + "languageId": "pcm", + "words": { + "total": 336489, + "translated": 28626, + "preTranslateAppliedTo": 2714, + "approved": 17267 + }, + "phrases": { + "total": 24685, + "translated": 2637, + "preTranslateAppliedTo": 350, + "approved": 1686 + }, + "translationProgress": 8, + "approvalProgress": 5 + }, + { + "languageId": "pl", + "words": { + "total": 336489, + "translated": 158045, + "preTranslateAppliedTo": 23871, + "approved": 94469 + }, + "phrases": { + "total": 24685, + "translated": 12909, + "preTranslateAppliedTo": 2437, + "approved": 7963 + }, + "translationProgress": 46, + "approvalProgress": 28 + }, + { + "languageId": "pt-BR", + "words": { + "total": 336489, + "translated": 326075, + "preTranslateAppliedTo": 53214, + "approved": 319354 + }, + "phrases": { + "total": 24685, + "translated": 24198, + "preTranslateAppliedTo": 4787, + "approved": 23630 + }, + "translationProgress": 96, + "approvalProgress": 94 + }, + { + "languageId": "pt-PT", + "words": { + "total": 336489, + "translated": 39477, + "preTranslateAppliedTo": 4918, + "approved": 26172 + }, + "phrases": { + "total": 24685, + "translated": 3712, + "preTranslateAppliedTo": 775, + "approved": 2376 + }, + "translationProgress": 11, + "approvalProgress": 7 + }, + { + "languageId": "ro", + "words": { + "total": 336489, + "translated": 103193, + "preTranslateAppliedTo": 28227, + "approved": 78311 + }, + "phrases": { + "total": 24685, + "translated": 9188, + "preTranslateAppliedTo": 2632, + "approved": 6983 + }, + "translationProgress": 30, + "approvalProgress": 23 + }, + { + "languageId": "ru", + "words": { + "total": 336489, + "translated": 172802, + "preTranslateAppliedTo": 35985, + "approved": 96860 + }, + "phrases": { + "total": 24685, + "translated": 14092, + "preTranslateAppliedTo": 3599, + "approved": 7842 + }, + "translationProgress": 51, + "approvalProgress": 28 + }, + { + "languageId": "sat", + "words": { + "total": 336489, + "translated": 69, + "preTranslateAppliedTo": 66, + "approved": 57 + }, + "phrases": { + "total": 24685, + "translated": 26, + "preTranslateAppliedTo": 25, + "approved": 20 + }, + "translationProgress": 0, + "approvalProgress": 0 + }, + { + "languageId": "si-LK", + "words": { + "total": 336489, + "translated": 978, + "preTranslateAppliedTo": 886, + "approved": 706 + }, + "phrases": { + "total": 24685, + "translated": 134, + "preTranslateAppliedTo": 98, + "approved": 58 + }, + "translationProgress": 0, + "approvalProgress": 0 + }, + { + "languageId": "sk", + "words": { + "total": 336489, + "translated": 14738, + "preTranslateAppliedTo": 2629, + "approved": 6377 + }, + "phrases": { + "total": 24685, + "translated": 1683, + "preTranslateAppliedTo": 439, + "approved": 700 + }, + "translationProgress": 4, + "approvalProgress": 1 + }, + { + "languageId": "sl", + "words": { + "total": 336489, + "translated": 54938, + "preTranslateAppliedTo": 20007, + "approved": 26540 + }, + "phrases": { + "total": 24685, + "translated": 5175, + "preTranslateAppliedTo": 2068, + "approved": 2537 + }, + "translationProgress": 16, + "approvalProgress": 7 + }, + { + "languageId": "sn", + "words": { + "total": 336489, + "translated": 557, + "preTranslateAppliedTo": 557, + "approved": 465 + }, + "phrases": { + "total": 24685, + "translated": 53, + "preTranslateAppliedTo": 53, + "approved": 40 + }, + "translationProgress": 0, + "approvalProgress": 0 + }, + { + "languageId": "so", + "words": { + "total": 336489, + "translated": 1238, + "preTranslateAppliedTo": 797, + "approved": 493 + }, + "phrases": { + "total": 24685, + "translated": 252, + "preTranslateAppliedTo": 156, + "approved": 42 + }, + "translationProgress": 0, + "approvalProgress": 0 + }, + { + "languageId": "sq", + "words": { + "total": 336489, + "translated": 8532, + "preTranslateAppliedTo": 6014, + "approved": 693 + }, + "phrases": { + "total": 24685, + "translated": 1115, + "preTranslateAppliedTo": 741, + "approved": 58 + }, + "translationProgress": 2, + "approvalProgress": 0 + }, + { + "languageId": "sr-CS", + "words": { + "total": 336489, + "translated": 41464, + "preTranslateAppliedTo": 3636, + "approved": 26313 + }, + "phrases": { + "total": 24685, + "translated": 3837, + "preTranslateAppliedTo": 504, + "approved": 2374 + }, + "translationProgress": 12, + "approvalProgress": 7 + }, + { + "languageId": "sv-SE", + "words": { + "total": 336489, + "translated": 28083, + "preTranslateAppliedTo": 8024, + "approved": 10006 + }, + "phrases": { + "total": 24685, + "translated": 3150, + "preTranslateAppliedTo": 1096, + "approved": 1087 + }, + "translationProgress": 8, + "approvalProgress": 2 + }, + { + "languageId": "sw", + "words": { + "total": 336489, + "translated": 24971, + "preTranslateAppliedTo": 6832, + "approved": 16569 + }, + "phrases": { + "total": 24685, + "translated": 2729, + "preTranslateAppliedTo": 883, + "approved": 1784 + }, + "translationProgress": 7, + "approvalProgress": 4 + }, + { + "languageId": "ta", + "words": { + "total": 336489, + "translated": 8030, + "preTranslateAppliedTo": 1738, + "approved": 1453 + }, + "phrases": { + "total": 24685, + "translated": 1041, + "preTranslateAppliedTo": 335, + "approved": 255 + }, + "translationProgress": 2, + "approvalProgress": 0 + }, + { + "languageId": "te", + "words": { + "total": 336489, + "translated": 13832, + "preTranslateAppliedTo": 1291, + "approved": 694 + }, + "phrases": { + "total": 24685, + "translated": 1401, + "preTranslateAppliedTo": 153, + "approved": 59 + }, + "translationProgress": 4, + "approvalProgress": 0 + }, + { + "languageId": "tg", + "words": { + "total": 336489, + "translated": 169, + "preTranslateAppliedTo": 87, + "approved": 0 + }, + "phrases": { + "total": 24685, + "translated": 52, + "preTranslateAppliedTo": 44, + "approved": 0 + }, + "translationProgress": 0, + "approvalProgress": 0 + }, + { + "languageId": "th", + "words": { + "total": 336489, + "translated": 12941, + "preTranslateAppliedTo": 2660, + "approved": 5951 + }, + "phrases": { + "total": 24685, + "translated": 1728, + "preTranslateAppliedTo": 498, + "approved": 630 + }, + "translationProgress": 3, + "approvalProgress": 1 + }, + { + "languageId": "ti", + "words": { + "total": 336489, + "translated": 160, + "preTranslateAppliedTo": 14, + "approved": 0 + }, + "phrases": { + "total": 24685, + "translated": 17, + "preTranslateAppliedTo": 1, + "approved": 0 + }, + "translationProgress": 0, + "approvalProgress": 0 + }, + { + "languageId": "tk", + "words": { + "total": 336489, + "translated": 6361, + "preTranslateAppliedTo": 739, + "approved": 5881 + }, + "phrases": { + "total": 24685, + "translated": 709, + "preTranslateAppliedTo": 131, + "approved": 604 + }, + "translationProgress": 1, + "approvalProgress": 1 + }, + { + "languageId": "tl", + "words": { + "total": 336489, + "translated": 2844, + "preTranslateAppliedTo": 811, + "approved": 86 + }, + "phrases": { + "total": 24685, + "translated": 264, + "preTranslateAppliedTo": 93, + "approved": 8 + }, + "translationProgress": 0, + "approvalProgress": 0 + }, + { + "languageId": "tr", + "words": { + "total": 336489, + "translated": 326807, + "preTranslateAppliedTo": 44723, + "approved": 321705 + }, + "phrases": { + "total": 24685, + "translated": 24288, + "preTranslateAppliedTo": 4221, + "approved": 23859 + }, + "translationProgress": 97, + "approvalProgress": 95 + }, + { + "languageId": "uk", + "words": { + "total": 336489, + "translated": 191008, + "preTranslateAppliedTo": 34741, + "approved": 64755 + }, + "phrases": { + "total": 24685, + "translated": 15442, + "preTranslateAppliedTo": 3316, + "approved": 5426 + }, + "translationProgress": 56, + "approvalProgress": 19 + }, + { + "languageId": "ur-IN", + "words": { + "total": 336489, + "translated": 1998, + "preTranslateAppliedTo": 367, + "approved": 1214 + }, + "phrases": { + "total": 24685, + "translated": 437, + "preTranslateAppliedTo": 162, + "approved": 200 + }, + "translationProgress": 0, + "approvalProgress": 0 + }, + { + "languageId": "ur-PK", + "words": { + "total": 336489, + "translated": 2766, + "preTranslateAppliedTo": 1441, + "approved": 725 + }, + "phrases": { + "total": 24685, + "translated": 451, + "preTranslateAppliedTo": 191, + "approved": 60 + }, + "translationProgress": 0, + "approvalProgress": 0 + }, + { + "languageId": "uz", + "words": { + "total": 336489, + "translated": 22487, + "preTranslateAppliedTo": 4383, + "approved": 1878 + }, + "phrases": { + "total": 24685, + "translated": 2310, + "preTranslateAppliedTo": 640, + "approved": 339 + }, + "translationProgress": 6, + "approvalProgress": 0 + }, + { + "languageId": "vi", + "words": { + "total": 336489, + "translated": 62946, + "preTranslateAppliedTo": 12751, + "approved": 16174 + }, + "phrases": { + "total": 24685, + "translated": 5744, + "preTranslateAppliedTo": 1399, + "approved": 1635 + }, + "translationProgress": 18, + "approvalProgress": 4 + }, + { + "languageId": "yo", + "words": { + "total": 336489, + "translated": 3820, + "preTranslateAppliedTo": 930, + "approved": 687 + }, + "phrases": { + "total": 24685, + "translated": 494, + "preTranslateAppliedTo": 117, + "approved": 55 + }, + "translationProgress": 1, + "approvalProgress": 0 + }, + { + "languageId": "zh-CN", + "words": { + "total": 336489, + "translated": 323069, + "preTranslateAppliedTo": 56826, + "approved": 305017 + }, + "phrases": { + "total": 24685, + "translated": 23991, + "preTranslateAppliedTo": 5128, + "approved": 22626 + }, + "translationProgress": 96, + "approvalProgress": 90 + }, + { + "languageId": "zh-TW", + "words": { + "total": 336489, + "translated": 214786, + "preTranslateAppliedTo": 37224, + "approved": 111257 + }, + "phrases": { + "total": 24685, + "translated": 17351, + "preTranslateAppliedTo": 3689, + "approved": 8893 + }, + "translationProgress": 63, + "approvalProgress": 33 + }, + { + "languageId": "zu", + "words": { + "total": 336489, + "translated": 164, + "preTranslateAppliedTo": 164, + "approved": 109 + }, + "phrases": { + "total": 24685, + "translated": 17, + "preTranslateAppliedTo": 17, + "approved": 9 + }, + "translationProgress": 0, + "approvalProgress": 0 + } +] \ No newline at end of file diff --git a/src/intl/en/page-languages.json b/src/intl/en/page-languages.json index 53d97878c77..9fd6feea227 100644 --- a/src/intl/en/page-languages.json +++ b/src/intl/en/page-languages.json @@ -11,7 +11,12 @@ "page-languages-want-more-header": "Want to see ethereum.org in a different language?", "page-languages-want-more-link": "Translation Program", "page-languages-want-more-paragraph": "ethereum.org translators are always translating pages in as many languages as possible. To see what they're working on right now or to sign up to join them, read about our", - "page-languages-filter-placeholder": "Filter", + "page-languages-filter-label": "Filter list", + "page-languages-filter-placeholder": "Type to filter", + "page-languages-browser-default": "Browser default", + "page-languages-translated": "translated", + "page-languages-words": "words", + "page-languages-recruit-community": "Help us translate ethereum.org.", "langauge-am": "Amharic", "language-ar": "Arabic", "language-az": "Azerbaijani", diff --git a/src/lib/types.ts b/src/lib/types.ts index 94d474b89d9..18d437e134d 100644 --- a/src/lib/types.ts +++ b/src/lib/types.ts @@ -190,6 +190,99 @@ export type LocaleContributions = { data: FileContributorData[] } +// Crowdin translation progress +type Language = { + id: string + name: string + editorCode: string + twoLettersCode: string + threeLettersCode: string + locale: string + androidCode: string + osxCode: string + osxLocale: string + pluralCategoryNames: string[] + pluralRules: string + pluralExamples: string[] + textDirection: string + dialectOf: unknown +} + +type CountSummary = { + total: number + translated: number + preTranslateAppliedTo: number + approved: number +} + +export type ProjectProgressData = { + languageId: string, + language?: Language, + words: CountSummary, + phrases: CountSummary, + translationProgress: number + approvalProgress: number +} + +export type LocaleDisplayInfo = { + localeOption: string + sourceName: string + targetName: string + englishName: string + approvalProgress: number + wordsApproved: number + isBrowserDefault?: boolean +} + +type TranslatedStats = { + tmMatch: number + default: number + total: number +} + +export type AllTimeData = { + name: string + url: string + unit: string + dateRange: { + from: string + to: string + } + currency: string + mode: string + totalCosts: number + totalTMSavings: number + totalPreTranslated: number + data: Array<{ + user: { + id: number + username: string + fullName: string + userRole: string + avatarUrl: string + preTranslated: number + totalCosts: number + } + languages: Array<{ + language: { + id: string + name: string + userRole: string + tmSavings: number + preTranslate: number + totalCosts: number + } + translated: TranslatedStats + targetTranslated: TranslatedStats + translatedByMt: TranslatedStats + approved: TranslatedStats + translationCosts: TranslatedStats + approvalCosts: TranslatedStats + }> + }> +} + + // GitHub contributors export type Commit = { commit: { @@ -228,8 +321,8 @@ export type ToCNodeEntry = { export type TocNodeType = | ToCNodeEntry | { - items: TocNodeType[] - } + items: TocNodeType[] + } export type ToCItem = { title: string @@ -295,12 +388,12 @@ export type TimestampedData = { export type MetricDataValue = | { - error: string - } + error: string + } | { - data: Data - value: Value - } + data: Data + value: Value + } export type EtherscanNodeResponse = { result: { @@ -358,51 +451,3 @@ export type CommunityConference = { startDate: string endDate: string } - -type TranslatedStats = { - tmMatch: number - default: number - total: number -} - -export type AllTimeData = { - name: string - url: string - unit: string - dateRange: { - from: string - to: string - } - currency: string - mode: string - totalCosts: number - totalTMSavings: number - totalPreTranslated: number - data: Array<{ - user: { - id: number - username: string - fullName: string - userRole: string - avatarUrl: string - preTranslated: number - totalCosts: number - } - languages: Array<{ - language: { - id: string - name: string - userRole: string - tmSavings: number - preTranslate: number - totalCosts: number - } - translated: TranslatedStats - targetTranslated: TranslatedStats - translatedByMt: TranslatedStats - approved: TranslatedStats - translationCosts: TranslatedStats - approvalCosts: TranslatedStats - }> - }> -} diff --git a/src/lib/utils/translations.ts b/src/lib/utils/translations.ts index f9179a68c55..8c20d52edeb 100644 --- a/src/lib/utils/translations.ts +++ b/src/lib/utils/translations.ts @@ -31,7 +31,7 @@ export const getRequiredNamespacesForPage = ( path: string, layout?: string | undefined ) => { - const baseNamespaces = ["common"] + const baseNamespaces = ["common", "page-languages"] const requiredNamespacesForPath = getRequiredNamespacesForPath(path) const requiredNamespacesForLayout = getRequiredNamespacesForLayout(layout) @@ -61,10 +61,6 @@ const getRequiredNamespacesForPath = (path: string) => { if (path === "/contributing/translation-program/contributors") { primaryNamespace = "page-contributing-translation-program-contributors" - requiredNamespaces = [ - ...requiredNamespaces, - "page-languages", - ] } if (path.startsWith("/community")) { @@ -135,10 +131,6 @@ const getRequiredNamespacesForPath = (path: string) => { primaryNamespace = "page-get-eth" } - if (path.startsWith("/languages")) { - primaryNamespace = "page-languages" - } - if (path.startsWith("/roadmap/vision")) { primaryNamespace = "page-roadmap-vision" requiredNamespaces = [ diff --git a/src/pages/assets.tsx b/src/pages/assets.tsx index bf05cc2fbe5..85f713fe9bb 100644 --- a/src/pages/assets.tsx +++ b/src/pages/assets.tsx @@ -120,7 +120,7 @@ const H3 = (props: ChildOnlyProp) => ( export const getStaticProps = (async ({ locale }) => { const requiredNamespaces = getRequiredNamespacesForPage("assets") - const contentNotTranslated = !existsNamespace(locale!, requiredNamespaces[1]) + const contentNotTranslated = !existsNamespace(locale!, requiredNamespaces[2]) const lastDeployDate = getLastDeployDate() diff --git a/src/pages/bug-bounty.tsx b/src/pages/bug-bounty.tsx index 3a2bd9de162..ea3b02820ce 100644 --- a/src/pages/bug-bounty.tsx +++ b/src/pages/bug-bounty.tsx @@ -328,7 +328,7 @@ const sortBountyHuntersFn = (a: BountyHuntersArg, b: BountyHuntersArg) => { export const getStaticProps = (async ({ locale }) => { const requiredNamespaces = getRequiredNamespacesForPage("bug-bounty") - const contentNotTranslated = !existsNamespace(locale!, requiredNamespaces[1]) + const contentNotTranslated = !existsNamespace(locale!, requiredNamespaces[2]) const lastDeployDate = getLastDeployDate() diff --git a/src/pages/community.tsx b/src/pages/community.tsx index 4a3530cd39e..f79f4cb9147 100644 --- a/src/pages/community.tsx +++ b/src/pages/community.tsx @@ -44,7 +44,7 @@ import whatIsEthereumImg from "@/public/what-is-ethereum.png" export const getStaticProps = (async ({ locale }) => { const requiredNamespaces = getRequiredNamespacesForPage("/community") - const contentNotTranslated = !existsNamespace(locale!, requiredNamespaces[1]) + const contentNotTranslated = !existsNamespace(locale!, requiredNamespaces[2]) const lastDeployDate = getLastDeployDate() diff --git a/src/pages/contributing/translation-program/acknowledgements.tsx b/src/pages/contributing/translation-program/acknowledgements.tsx index 3b36816a4b0..f05b85a9452 100644 --- a/src/pages/contributing/translation-program/acknowledgements.tsx +++ b/src/pages/contributing/translation-program/acknowledgements.tsx @@ -53,7 +53,7 @@ export const getStaticProps = (async ({ locale }) => { "/contributing/translation-program/acknowledgements" ) - const contentNotTranslated = !existsNamespace(locale!, requiredNamespaces[1]) + const contentNotTranslated = !existsNamespace(locale!, requiredNamespaces[2]) return { props: { diff --git a/src/pages/contributing/translation-program/contributors.tsx b/src/pages/contributing/translation-program/contributors.tsx index c300cc2d1d0..6b3b22e8409 100644 --- a/src/pages/contributing/translation-program/contributors.tsx +++ b/src/pages/contributing/translation-program/contributors.tsx @@ -42,7 +42,7 @@ export const getStaticProps = (async ({ locale }) => { "/contributing/translation-program/contributors" ) - const contentNotTranslated = !existsNamespace(locale!, requiredNamespaces[1]) + const contentNotTranslated = !existsNamespace(locale!, requiredNamespaces[2]) return { props: { diff --git a/src/pages/dapps.tsx b/src/pages/dapps.tsx index 3f9ae06589c..05e8891e955 100644 --- a/src/pages/dapps.tsx +++ b/src/pages/dapps.tsx @@ -434,7 +434,7 @@ interface Categories { export const getStaticProps = (async ({ locale }) => { const requiredNamespaces = getRequiredNamespacesForPage("/dapps") - const contentNotTranslated = !existsNamespace(locale!, requiredNamespaces[1]) + const contentNotTranslated = !existsNamespace(locale!, requiredNamespaces[2]) const lastDeployDate = getLastDeployDate() diff --git a/src/pages/developers/index.tsx b/src/pages/developers/index.tsx index 7f9624d8846..fe021304180 100644 --- a/src/pages/developers/index.tsx +++ b/src/pages/developers/index.tsx @@ -139,7 +139,7 @@ const StyledCallout = chakra(Callout, { export const getStaticProps = (async ({ locale }) => { const requiredNamespaces = getRequiredNamespacesForPage("/developers") - const contentNotTranslated = !existsNamespace(locale!, requiredNamespaces[1]) + const contentNotTranslated = !existsNamespace(locale!, requiredNamespaces[2]) const lastDeployDate = getLastDeployDate() diff --git a/src/pages/developers/learning-tools.tsx b/src/pages/developers/learning-tools.tsx index b1fc5557892..145a44a5b6c 100644 --- a/src/pages/developers/learning-tools.tsx +++ b/src/pages/developers/learning-tools.tsx @@ -122,7 +122,7 @@ export const getStaticProps = (async ({ locale }) => { "/developers/learning-tools" ) - const contentNotTranslated = !existsNamespace(locale!, requiredNamespaces[1]) + const contentNotTranslated = !existsNamespace(locale!, requiredNamespaces[2]) const lastDeployDate = getLastDeployDate() diff --git a/src/pages/developers/local-environment.tsx b/src/pages/developers/local-environment.tsx index 4e235a973d5..9e681a55466 100644 --- a/src/pages/developers/local-environment.tsx +++ b/src/pages/developers/local-environment.tsx @@ -65,7 +65,7 @@ export const getStaticProps = (async ({ locale }) => { "/developers/local-environment" ) - const contentNotTranslated = !existsNamespace(locale!, requiredNamespaces[1]) + const contentNotTranslated = !existsNamespace(locale!, requiredNamespaces[2]) const frameworksListData = await cachedFetchLocalEnvironmentFrameworkData() diff --git a/src/pages/developers/tutorials.tsx b/src/pages/developers/tutorials.tsx index fafe8ab3bc2..bba63a2f74e 100644 --- a/src/pages/developers/tutorials.tsx +++ b/src/pages/developers/tutorials.tsx @@ -84,7 +84,7 @@ export const getStaticProps = (async ({ locale }) => { "/developers/tutorials" ) - const contentNotTranslated = !existsNamespace(locale!, requiredNamespaces[1]) + const contentNotTranslated = !existsNamespace(locale!, requiredNamespaces[2]) const lastDeployDate = getLastDeployDate() diff --git a/src/pages/eth.tsx b/src/pages/eth.tsx index e00920b1a2d..364a2523545 100644 --- a/src/pages/eth.tsx +++ b/src/pages/eth.tsx @@ -266,7 +266,7 @@ const CentralActionCard = (props: ComponentProps) => ( export const getStaticProps = (async ({ locale }) => { const requiredNamespaces = getRequiredNamespacesForPage("/eth") - const contentNotTranslated = !existsNamespace(locale!, requiredNamespaces[1]) + const contentNotTranslated = !existsNamespace(locale!, requiredNamespaces[2]) const lastDeployDate = getLastDeployDate() diff --git a/src/pages/gas.tsx b/src/pages/gas.tsx index d6f7bcdc742..593f75ffbfe 100644 --- a/src/pages/gas.tsx +++ b/src/pages/gas.tsx @@ -100,7 +100,7 @@ const H3 = (props: HeadingProps) => ( export const getStaticProps = (async ({ locale }) => { const requiredNamespaces = getRequiredNamespacesForPage("/gas") - const contentNotTranslated = !existsNamespace(locale!, requiredNamespaces[1]) + const contentNotTranslated = !existsNamespace(locale!, requiredNamespaces[2]) const lastDeployDate = getLastDeployDate() diff --git a/src/pages/get-eth.tsx b/src/pages/get-eth.tsx index 85bc5dd915a..4bf759cfcd6 100644 --- a/src/pages/get-eth.tsx +++ b/src/pages/get-eth.tsx @@ -109,7 +109,7 @@ type Props = BasePageProps & { export const getStaticProps = (async ({ locale }) => { const requiredNamespaces = getRequiredNamespacesForPage("get-eth") - const contentNotTranslated = !existsNamespace(locale!, requiredNamespaces[1]) + const contentNotTranslated = !existsNamespace(locale!, requiredNamespaces[2]) const lastDataUpdateDate = getLastModifiedDateByPath( "src/data/exchangesByCountry.ts" diff --git a/src/pages/layer-2.tsx b/src/pages/layer-2.tsx index 23c1b508ce9..591e3e6d555 100644 --- a/src/pages/layer-2.tsx +++ b/src/pages/layer-2.tsx @@ -115,7 +115,7 @@ export const getStaticProps = (async ({ locale }) => { const requiredNamespaces = getRequiredNamespacesForPage("/layer-2") - const contentNotTranslated = !existsNamespace(locale!, requiredNamespaces[1]) + const contentNotTranslated = !existsNamespace(locale!, requiredNamespaces[2]) return { props: { diff --git a/src/pages/learn.tsx b/src/pages/learn.tsx index ce2f03c6040..740ed2d0af7 100644 --- a/src/pages/learn.tsx +++ b/src/pages/learn.tsx @@ -131,7 +131,7 @@ const H3 = ({ children, ...props }: HeadingProps) => ( export const getStaticProps = (async ({ locale }) => { const requiredNamespaces = getRequiredNamespacesForPage("/learn") - const contentNotTranslated = !existsNamespace(locale!, requiredNamespaces[1]) + const contentNotTranslated = !existsNamespace(locale!, requiredNamespaces[2]) const lastDeployDate = getLastDeployDate() diff --git a/src/pages/quizzes.tsx b/src/pages/quizzes.tsx index 6c82d41b60c..0a0b5abe285 100644 --- a/src/pages/quizzes.tsx +++ b/src/pages/quizzes.tsx @@ -40,7 +40,7 @@ const handleGHAdd = () => export const getStaticProps = (async ({ locale }) => { const requiredNamespaces = getRequiredNamespacesForPage("/quizzes") - const contentNotTranslated = !existsNamespace(locale!, requiredNamespaces[1]) + const contentNotTranslated = !existsNamespace(locale!, requiredNamespaces[2]) const lastDeployDate = getLastDeployDate() diff --git a/src/pages/roadmap/vision.tsx b/src/pages/roadmap/vision.tsx index 18e3c952228..589f9e177bb 100644 --- a/src/pages/roadmap/vision.tsx +++ b/src/pages/roadmap/vision.tsx @@ -119,7 +119,7 @@ const TrilemmaContent = (props: ChildOnlyProp) => ( export const getStaticProps = (async ({ locale }) => { const requiredNamespaces = getRequiredNamespacesForPage("/roadmap/vision") - const contentNotTranslated = !existsNamespace(locale!, requiredNamespaces[1]) + const contentNotTranslated = !existsNamespace(locale!, requiredNamespaces[2]) const lastDeployDate = getLastDeployDate() diff --git a/src/pages/run-a-node.tsx b/src/pages/run-a-node.tsx index cce35ee3dc3..a496dd726cc 100644 --- a/src/pages/run-a-node.tsx +++ b/src/pages/run-a-node.tsx @@ -331,7 +331,7 @@ type RunANodeCard = { export const getStaticProps = (async ({ locale }) => { const requiredNamespaces = getRequiredNamespacesForPage("/run-a-node") - const contentNotTranslated = !existsNamespace(locale!, requiredNamespaces[1]) + const contentNotTranslated = !existsNamespace(locale!, requiredNamespaces[2]) const lastDeployDate = getLastDeployDate() diff --git a/src/pages/stablecoins.tsx b/src/pages/stablecoins.tsx index f7e28079fbe..4da7217170d 100644 --- a/src/pages/stablecoins.tsx +++ b/src/pages/stablecoins.tsx @@ -94,7 +94,7 @@ export const getStaticProps = (async ({ locale }) => { const requiredNamespaces = getRequiredNamespacesForPage("/stablecoins") - const contentNotTranslated = !existsNamespace(locale!, requiredNamespaces[1]) + const contentNotTranslated = !existsNamespace(locale!, requiredNamespaces[2]) let marketsHasError = false let markets: Market[] = [] diff --git a/src/pages/staking/deposit-contract.tsx b/src/pages/staking/deposit-contract.tsx index c5a1ea19f81..6f47fa100d2 100644 --- a/src/pages/staking/deposit-contract.tsx +++ b/src/pages/staking/deposit-contract.tsx @@ -214,7 +214,7 @@ export const getStaticProps = (async ({ locale }) => { "/staking/deposit-contract" ) - const contentNotTranslated = !existsNamespace(locale!, requiredNamespaces[1]) + const contentNotTranslated = !existsNamespace(locale!, requiredNamespaces[2]) const lastDeployDate = getLastDeployDate() diff --git a/src/pages/staking/index.tsx b/src/pages/staking/index.tsx index 385d4e7bb29..19ef9dc2014 100644 --- a/src/pages/staking/index.tsx +++ b/src/pages/staking/index.tsx @@ -212,7 +212,7 @@ export const getStaticProps = (async ({ locale }) => { const requiredNamespaces = getRequiredNamespacesForPage("/staking") - const contentNotTranslated = !existsNamespace(locale!, requiredNamespaces[1]) + const contentNotTranslated = !existsNamespace(locale!, requiredNamespaces[2]) const data = await cachedFetchBeaconchainData() diff --git a/src/pages/wallets/find-wallet.tsx b/src/pages/wallets/find-wallet.tsx index a2b3ae51390..9191c340482 100644 --- a/src/pages/wallets/find-wallet.tsx +++ b/src/pages/wallets/find-wallet.tsx @@ -95,7 +95,7 @@ export const getStaticProps = (async ({ locale }) => { "/wallets/find-wallet" ) - const contentNotTranslated = !existsNamespace(locale!, requiredNamespaces[1]) + const contentNotTranslated = !existsNamespace(locale!, requiredNamespaces[2]) return { props: { diff --git a/src/pages/wallets/index.tsx b/src/pages/wallets/index.tsx index 30dc8f5b923..15d68c8e932 100644 --- a/src/pages/wallets/index.tsx +++ b/src/pages/wallets/index.tsx @@ -140,7 +140,7 @@ export const getStaticProps = (async ({ locale }) => { const requiredNamespaces = getRequiredNamespacesForPage("/wallets") - const contentNotTranslated = !existsNamespace(locale!, requiredNamespaces[1]) + const contentNotTranslated = !existsNamespace(locale!, requiredNamespaces[2]) return { props: { diff --git a/src/pages/what-is-ethereum.tsx b/src/pages/what-is-ethereum.tsx index 96398faab72..6dab2a5b1e2 100644 --- a/src/pages/what-is-ethereum.tsx +++ b/src/pages/what-is-ethereum.tsx @@ -195,7 +195,7 @@ export const getStaticProps = (async ({ locale }) => { const requiredNamespaces = getRequiredNamespacesForPage("/what-is-ethereum") - const contentNotTranslated = !existsNamespace(locale!, requiredNamespaces[1]) + const contentNotTranslated = !existsNamespace(locale!, requiredNamespaces[2]) const data = await cachedFetchTxCount() diff --git a/src/scripts/crowdin/getTranslationProgress.ts b/src/scripts/crowdin/getTranslationProgress.ts new file mode 100644 index 00000000000..bb654f6ce33 --- /dev/null +++ b/src/scripts/crowdin/getTranslationProgress.ts @@ -0,0 +1,30 @@ +import fs from 'fs' + +import type { ProjectProgressData } from "../../lib/types" + +import crowdin from "./api-client/crowdinClient" + +import "dotenv/config" + +async function main() { + const projectId = Number(process.env.CROWDIN_PROJECT_ID) || 363359 + + try { + const response = await crowdin.translationStatusApi.getProjectProgress(projectId, { + limit: 200, + }) + + if (!response) throw new Error("Error fetching Crowdin translation progress. Check your environment variables for a working API key.") + + const progress = response.data.map(({ data }) => ({ ...data, language: undefined } satisfies ProjectProgressData)) + + fs.writeFileSync("src/data/translationProgress.json", JSON.stringify(progress, null, 2)) + + } catch (error: unknown) { + console.error((error as Error).message) + } +} + +main() + +export default main