diff --git a/.babelrc b/.babelrc new file mode 100644 index 000000000..63c841270 --- /dev/null +++ b/.babelrc @@ -0,0 +1,6 @@ +{ + "presets": [ + "next/babel" + ], + "plugins": ["@lingui/babel-plugin-lingui-macro"] +} diff --git a/.gitignore b/.gitignore index 3a08ba6c4..47ff8eb05 100644 --- a/.gitignore +++ b/.gitignore @@ -47,3 +47,12 @@ yarn-error.log* .yarn .vscode/ + +# Lingui +src/locales/**/*.mo +src/locales/**/*.js + +# RSS +atom.xml +rss.json +rss.xml diff --git a/lingui.config.js b/lingui.config.js new file mode 100644 index 000000000..02840dd6f --- /dev/null +++ b/lingui.config.js @@ -0,0 +1,17 @@ +const nextConfig = require('./next.config') + +/** @type {import('@lingui/conf').LinguiConfig} */ +module.exports = { + locales: nextConfig.i18n.locales, + pseudoLocale: 'pseudo', + sourceLocale: nextConfig.i18n.defaultLocale, + fallbackLocales: { + default: 'en', + }, + catalogs: [ + { + path: 'src/locales/{locale}/messages', + include: ['src/'], + }, + ], +} diff --git a/next.config.mjs b/next.config.mjs index 1fd06d66a..89c321987 100644 --- a/next.config.mjs +++ b/next.config.mjs @@ -18,4 +18,10 @@ export default withMDX({ images: { domains: ["raw.githubusercontent.com", "numpy.org", "dask.org", "chainer.org", ], }, + i18n: { + // These are all the locales you want to support in + // your application + locales: ["en", "es", "pt"], + defaultLocale: "en", + }, }) diff --git a/package.json b/package.json index 6d198be52..4a3462bfb 100644 --- a/package.json +++ b/package.json @@ -4,8 +4,10 @@ "private": true, "scripts": { "build-cards": "node build-cards.js", - "dev": "next dev", - "build": "next build", + "extract": "lingui extract", + "compile": "lingui compile", + "dev": "yarn compile && next dev", + "build": "yarn extract && yarn compile && next build", "start": "next start", "lint": "next lint" }, @@ -16,42 +18,48 @@ "author": "Xarray Developers & Contributors", "license": "Apache 2.0", "dependencies": { - "@chakra-ui/icons": "^2.2.1", - "@chakra-ui/next-js": "^2.3.1", - "@chakra-ui/react": "^2.9.1", + "@chakra-ui/icons": "^2.2.4", + "@chakra-ui/next-js": "^2.4.2", + "@chakra-ui/react": "^2.10.9", "@chakra-ui/system": "^2.6.2", - "@emotion/react": "^11.13.3", - "@emotion/styled": "^11.13.0", - "@fontsource-variable/inter": "^5.2.5", - "@giscus/react": "^3.0.0", - "@mdx-js/loader": "^3.0.1", - "@mdx-js/react": "^3.0.1", - "@next/mdx": "^14.2.14", - "apexcharts": "3.54.0", + "@emotion/react": "^11.14.0", + "@emotion/styled": "^11.14.1", + "@fontsource-variable/inter": "^5.2.8", + "@giscus/react": "^3.1.0", + "@lingui/babel-plugin-lingui-macro": "^5.6.0", + "@lingui/core": "^5.6.0", + "@lingui/react": "^5.6.0", + "@mdx-js/loader": "^3.1.1", + "@mdx-js/react": "^3.1.1", + "@next/mdx": "^14.2.33", "d3": "^7.9.0", - "date-fns": "^3.0.0", + "date-fns": "^3.6.0", "date-fns-tz": "^3.2.0", "feed": "^5.1.0", - "framer-motion": "^11.9.0", - "glob": "^11.0.3", + "framer-motion": "^11.18.2", + "glob": "^11.1.0", "gray-matter": "^4.0.3", - "isomorphic-dompurify": "^2.16.0", - "next": "^14.2.30", + "isomorphic-dompurify": "^2.32.0", + "next": "^14.2.33", "next-mdx-remote": "^5.0.0", "react": "^18.3.1", - "react-apexcharts": "^1.4.1", + "react-apexcharts": "^1.8.0", "react-dom": "^18.3.1", - "react-icons": "^5.3.0", - "react-syntax-highlighter": "^15.5.0", + "react-icons": "^5.5.0", + "react-syntax-highlighter": "^15.6.6", "rehype-slug": "^6.0.0", - "swr": "^2.2.5" + "swr": "^2.3.6", + "yarn": "^1.22.22" }, "devDependencies": { - "@types/react": "^18.3.11", - "eslint": "^9.25.1", + "@lingui/cli": "^5.6.0", + "@lingui/loader": "^5.6.0", + "@types/react": "^18.3.26", + "apexcharts": "^5.3.6", + "eslint": "^9.39.1", "eslint-config-next": "15.3.3", - "playwright": "^1.47.2", - "typescript": ">=5.9.2", - "webpack": "^5.101.0" + "playwright": "^1.56.1", + "typescript": "^5.9.3", + "webpack": "^5.102.1" } } diff --git a/src/components/array-libraries.js b/src/components/array-libraries.js index 7451543b8..c2ecaa9ed 100644 --- a/src/components/array-libraries.js +++ b/src/components/array-libraries.js @@ -3,9 +3,10 @@ import React from 'react' import { IoIosGlobe, IoLogoGithub } from 'react-icons/io' import { Image } from '@/components/mdx' -import { Libraries as data } from '@/data/array-libraries' +import { getLibraries } from '@/data/array-libraries' import { SocialLink } from '@/components/social-link' +import { useLingui } from '@lingui/react/macro' const Library = ({ name, description, repo, url, logo }) => { return ( @@ -41,12 +42,13 @@ const Library = ({ name, description, repo, url, logo }) => { } export const ArrayLibraries = () => { + const { t } = useLingui() + let data = getLibraries() const libraries = React.useMemo(() => data, []) return ( - Xarray supports multiple array backends, allowing users to choose array - types that work best for their application. + {t`Xarray supports multiple array backends, allowing users to choose array types that work best for their application.`} { + const { t } = useLingui() return ( - Xarray Issue Tracker + {t`Xarray Issue Tracker`} {' '} } /> } query={ 'https://xarray-datasette.fly.dev/github.json?_shape=array&&sql=select%0D%0A++id%2C%0D%0A++number%2C%0D%0A++state%2C%0D%0A++created_at%2C%0D%0A++closed_at%2C%0D%0A++julianday%28closed_at%29+-+julianday%28created_at%29+as+age_in_days%0D%0Afrom%0D%0A++issues+as+data%0D%0Awhere%0D%0A++type+%3D+%27issue%27%0D%0A++and+state+%3D+%27closed%27%0D%0Aorder+by%0D%0A++id' diff --git a/src/components/dashboard/project-metrics.js b/src/components/dashboard/project-metrics.js index c1ba57ced..b87123f86 100644 --- a/src/components/dashboard/project-metrics.js +++ b/src/components/dashboard/project-metrics.js @@ -5,6 +5,7 @@ import { fetcher } from '@/lib/data-fetching' import { Box, Container, SimpleGrid, Spinner } from '@chakra-ui/react' import { BsPeople, BsPerson } from 'react-icons/bs' import { GoBook, GoPackage, GoStar, GoTag } from 'react-icons/go' +import { useLingui } from '@lingui/react/macro' import useSWR from 'swr' export const ProjectMetrics = () => { @@ -12,8 +13,8 @@ export const ProjectMetrics = () => { 'https://raw.githubusercontent.com/andersy005/xarray-datasette/a73704d803350a2ec059bec1b4cce601cd9efdd9/data/docs-monthly-views.json', fetcher, ) - - if (error) return
failed to load data
+ const { t } = useLingui() + if (error) return
{t`failed to load data`}
if (!data) return ( { {' '} - Xarray Project Metrics + {t`Xarray Project Metrics`} } link={'https://docs.xarray.dev/en/stable/team.html'} /> { /> } query={ 'https://xarray-datasette.fly.dev/github/_analyze_tables_/stars,user.json?_shape=array' @@ -64,20 +65,20 @@ export const ProjectMetrics = () => { /> } link={'https://github.com/pydata/xarray/network/dependents'} /> } /> { 'https://pydata-datasette.fly.dev/open_pulls_and_issues.json?_shape=array&&sql=select%0D%0A++time%2C%0D%0A++open_issues%2C%0D%0A++open_pull_requests%0D%0Afrom%0D%0A++open_pulls_and_issues%0D%0Awhere%0D%0A++project+%3D+%27pydata%2Fxarray%27%0D%0Aorder+by%0D%0A++time', fetcher, ) - - if (error) return
failed to load data
+ const { t } = useLingui() + if (error) return
{t`failed to load data`}
if (!data) return ( { return ( - This is a timeline of how many open issues and pull requests Xarray has - on Github over time from {new Date(start).toLocaleDateString()} to{' '} + {t`This is a timeline of how many open issues and pull requests Xarray has on Github over time from ${new Date(start).toLocaleDateString()} to `} {new Date(end).toLocaleDateString()}.
@@ -46,7 +46,7 @@ export const TimelinePlotContainer = () => { - Pull Requests + {t`Pull Requests`} Issues diff --git a/src/components/dashboard/timeseries-agg-stats-card.js b/src/components/dashboard/timeseries-agg-stats-card.js index e87a78e6b..e15c037a6 100644 --- a/src/components/dashboard/timeseries-agg-stats-card.js +++ b/src/components/dashboard/timeseries-agg-stats-card.js @@ -4,10 +4,12 @@ import { Spinner, Text } from '@chakra-ui/react' import * as d3 from 'd3' import { isWithinInterval, lastDayOfMonth, startOfMonth } from 'date-fns' import useSWR from 'swr' +import { useLingui } from '@lingui/react/macro' export const TimeseriesAggStatsCard = ({ query, title, icon }) => { + const { t } = useLingui() let { data, error } = useSWR(query, fetcher) - if (error) return failed to load + if (error) return {t`failed to load`} if (!data) return ( { const change = { type: diffPercentage < 0 ? 'increase' : 'decrease', - value: `${d3.format('.2f')(Math.abs(diffPercentage))}% since last month`, + value: + `${d3.format('.2f')(Math.abs(diffPercentage))}% ` + t`since last month`, } return ( { icon={icon} stat={ result <= 2 - ? `${d3.format('.1f')(result * 24)} hours` - : `${d3.format('.1f')(result)} days` + ? `${d3.format('.1f')(result * 24)} ` + t`hours` + : `${d3.format('.1f')(result)} ` + t`days` } diff={change} /> diff --git a/src/components/donate.js b/src/components/donate.js index 15b6ea116..f567f923d 100644 --- a/src/components/donate.js +++ b/src/components/donate.js @@ -10,13 +10,15 @@ import { import { Heading, Image, Link } from '@/components/mdx' import { BiDonateHeart } from 'react-icons/bi' +import { useLingui } from '@lingui/react/macro' export const Donate = () => { + const { t } = useLingui() return ( - Donate + {t`Donate`} { position={'relative'} > - Xarray is a Sponsored Project of NumFOCUS, a{' '} + {t`Xarray is a NumFOCUS Sponsored Project, a `} { > 501(c)(3) nonprofit charity {' '} - in the United States. NumFOCUS provides Xarray with fiscal, legal, + {t`in the United States. NumFOCUS provides Xarray with fiscal, legal, and administrative support to help ensure the health and - sustainability of the project. Visit{' '} + sustainability of the project. For more information, visit `} { > numfocus.org {' '} - for more information.

- If you like Xarray and want to support our mission, please - consider making a donation to support our efforts. + {t`If you like Xarray and want to support our mission, please consider making a donation to support our efforts.`}
diff --git a/src/components/ecosystem.js b/src/components/ecosystem.js index 7f91c8190..1e51cee89 100644 --- a/src/components/ecosystem.js +++ b/src/components/ecosystem.js @@ -16,18 +16,21 @@ import { Heading, Link } from '@/components/mdx' import { ScientificDomains } from '@/components/scientific-domains' import { IoLogoGithub } from 'react-icons/io5' +import { useLingui } from '@lingui/react/macro' + import useSWR from 'swr' const fetcher = (...args) => fetch(...args).then((res) => res.json()) const GitHubStats = () => { + const { t } = useLingui() const { data, error } = useSWR( 'https://xarray-datasette.fly.dev/github/_analyze_tables_/stars,user.json?_shape=array', fetcher, ) - if (error) return
failed to load
- if (!data) return
loading...
+ if (error) return
{t`failed to load`}
+ if (!data) return
{t`loading...`}
return ( @@ -42,23 +45,22 @@ const GitHubStats = () => { {data[0].total_rows.toLocaleString(undefined, { minimumFractionDigits: 0, })}{' '} - Stars + {t`Stars`} ) } export const Ecosystem = () => { + const { t } = useLingui() return ( - Ecosystem + {t`Ecosystem`} - Xarray is part of the larger scientific Python ecosystem. It is built - on top of NumPy, Pandas, and Dask and supports a wide range of domain - specific scientific applications. + {t`Xarray is part of the larger scientific Python ecosystem. It is built on top of NumPy, Pandas, and Dask and supports a wide range of domain specific scientific applications.`} { > - Scientific Domains + {t`Scientific Domains`} - Array Libraries + {t`Array Libraries`} diff --git a/src/components/features.js b/src/components/features.js index ae5aaf65b..ef0c104fa 100644 --- a/src/components/features.js +++ b/src/components/features.js @@ -10,22 +10,22 @@ import { import React from 'react' import { Heading } from '@/components/mdx' -import { Features as data } from '@/data/features' +import { getFeatures } from '@/data/features' import { CheckIcon } from '@chakra-ui/icons' +import { useLingui } from '@lingui/react/macro' export const Features = () => { + const { t } = useLingui() + let data = getFeatures() const features = React.useMemo(() => data, []) return ( - Key Features & Capabilities + {t`Key Features & Capabilities`} - Xarray provides data models for working with labeled arrays and - datasets. Its toolkit includes a broad set of domain-agnostic - functions for advanced analytics and visualization with these data - structures. + {t`Xarray provides data models for working with labeled arrays and datasets. Its toolkit includes a broad set of domain-agnostic functions for advanced analytics and visualization with these data structures.`} diff --git a/src/components/footer.js b/src/components/footer.js index b1d41bced..6b18f54f8 100644 --- a/src/components/footer.js +++ b/src/components/footer.js @@ -15,8 +15,9 @@ import { getRootURL } from '@/lib/seo-utils' import { GitSHA } from '@/components/git-sha' import { Image, Link } from '@/components/mdx' import { NetlifyCallout } from '@/components/netlify' -import { footerItems } from '@/data/footer-items' +import { getFooterItems } from '@/data/footer-items' import { FaGithub, FaRss, FaTwitter, FaYoutube } from 'react-icons/fa' +import { useLingui } from '@lingui/react/macro' const SocialButton = ({ children, label, href }) => { return ( @@ -51,6 +52,9 @@ const ListHeader = ({ children }) => { } export const Footer = () => { + let { t } = useLingui() + let footerItems = getFooterItems() + const currentYear = new Date().getFullYear() return ( { - Β© {new Date().getFullYear()}, Xarray core developers. Apache 2.0 - Licensed. + {t`Β© ${currentYear}, Xarray core developers. Apache 2.0Licensed.`} @@ -123,7 +126,7 @@ export const Footer = () => { })} - Resources + {t`Resources`} {footerItems.resources.map((item) => { return ( { })} - Community + {t`Community`} {footerItems.community.map((item) => { return ( { - const navItems = React.useMemo(() => menuItems, []) +import { LanguageSwitcher } from './language-switcher' +export const Header = () => { + let navItems = getMenuItems() const { isOpen, onToggle } = useDisclosure() const { colorMode, toggleColorMode } = useColorMode() @@ -93,6 +94,7 @@ export const Header = () => { navItems={navItems} display={{ base: 'none', md: 'flex' }} /> + diff --git a/src/components/hero-banner.js b/src/components/hero-banner.js index 5c96b9c86..a4343fd6a 100644 --- a/src/components/hero-banner.js +++ b/src/components/hero-banner.js @@ -2,7 +2,10 @@ import { Box, Button, Container, Heading, Stack, Text } from '@chakra-ui/react' import { Image, Link } from '@/components/mdx' +import { useLingui } from '@lingui/react/macro' + export const HeroBanner = () => { + const { t } = useLingui() return ( @@ -35,20 +38,14 @@ export const HeroBanner = () => {
- N-D labeled arrays and datasets in Python + {t`N-D labeled arrays and datasets in Python`} - Xarray is an open source project and Python - package that introduces labels in the form of dimensions, - coordinates, and attributes on top of raw NumPy-like arrays, which - allows for more intuitive, more concise, and less error-prone user - experience. + {t`Xarray is an open source project and Python package that introduces labels in the form of dimensions, coordinates, and attributes on top of raw NumPy-like arrays, which allows for more intuitive, more concise, and less error-prone user experience.`}

- Xarray includes a large and growing library of domain-agnostic - functions for advanced analytics and visualization with these data - structures. + {t`Xarray includes a large and growing library of domain-agnostic functions for advanced analytics and visualization with these data structures.`}
@@ -70,7 +67,7 @@ export const HeroBanner = () => { colorScheme={'blue'} href='https://docs.xarray.dev/en/stable/getting-started-guide/quick-overview.html' > - Get Started + {t`Get Started`} diff --git a/src/components/language-switcher.js b/src/components/language-switcher.js new file mode 100644 index 000000000..cd70bf289 --- /dev/null +++ b/src/components/language-switcher.js @@ -0,0 +1,73 @@ +'use client' +import { useLingui } from '@lingui/react/macro' +import { i18n } from '@lingui/core' +import { useRouter, usePathname } from 'next/navigation' +import { useEffect } from 'react' +import { + Menu, + MenuButton, + MenuList, + MenuItem, + Link, + Button, +} from '@chakra-ui/react' + +function getLocales() { + const { t } = useLingui() + return [ + { locale: 'en', label: t`English` }, + { locale: 'es', label: t`Spanish` }, + { locale: 'pt', label: t`Portuguese` }, + ] +} + +function getLocaleLabel(locale) { + const locales = getLocales() + const localeObject = locales.find((object) => object.locale === locale) + return localeObject ? localeObject.label : locale +} + +export const LanguageSwitcher = () => { + const router = useRouter() + const pathname = usePathname() + + async function changeLocale() { + const localeString = i18n.locale + localStorage.setItem('locale', localeString) + const catalog = await import(`../locales/${localeString}/messages.js`) + i18n.load(localeString, catalog.messages) + i18n.activate(localeString) + } + + useEffect(() => { + const storedLocale = localStorage.getItem('locale') + if (storedLocale && storedLocale !== router.locale) { + changeLocale() + } + }, []) + return ( + + + {getLocaleLabel(i18n.locale)} + + + {getLocales().map((object) => ( + + + {object.label} + + + ))} + + + ) +} diff --git a/src/components/layout.js b/src/components/layout.js index aa216cfd0..6f578773a 100644 --- a/src/components/layout.js +++ b/src/components/layout.js @@ -4,6 +4,8 @@ import { Header } from '@/components/header' import { Link } from '@/components/mdx' import { Box, Flex } from '@chakra-ui/react' import Head from 'next/head' +import { useLingui } from '@lingui/react/macro' +import { useRouter } from 'next/router' export const Layout = ({ title, @@ -13,7 +15,14 @@ export const Layout = ({ url = 'https://xarray.dev', enableBanner = false, }) => { - const bannerTitle = 'Check out the latest blog post:' + /** + * This macro hook is needed to get `t` which + * is bound to i18n from React.Context + */ + const { t } = useLingui() + const router = useRouter() + + const bannerTitle = t`Check out the latest blog post:` // The first link will be the main description for the banner const bannerDescription = ( @@ -60,7 +69,7 @@ export const Layout = ({ rel='icon' type='image/png' sizes='96x96' - href='/Xarray-assets/Icon/Xarray_Icon_final.svg' + href='/Xarray-assets/Icon/Xarray_Icon_Final.svg' /> @@ -82,7 +91,6 @@ export const Layout = ({ )} {children}
-