diff --git a/.babelrc b/.babelrc new file mode 100644 index 00000000..63c84127 --- /dev/null +++ b/.babelrc @@ -0,0 +1,6 @@ +{ + "presets": [ + "next/babel" + ], + "plugins": ["@lingui/babel-plugin-lingui-macro"] +} diff --git a/.gitignore b/.gitignore index f65d00b8..2e48b01b 100644 --- a/.gitignore +++ b/.gitignore @@ -42,3 +42,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 00000000..02840dd6 --- /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 1fd06d66..89c32198 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 60a3d7ae..3814f6d7 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" }, @@ -24,6 +26,8 @@ "@emotion/styled": "^11.13.0", "@fontsource-variable/inter": "^5.2.5", "@giscus/react": "^3.0.0", + "@lingui/core": "^5.0.0-next.2", + "@lingui/react": "^5.0.0-next.2", "@mdx-js/loader": "^3.0.1", "@mdx-js/react": "^3.0.1", "@next/mdx": "^14.2.14", @@ -46,6 +50,9 @@ "swr": "^2.2.5" }, "devDependencies": { + "@lingui/babel-plugin-lingui-macro": "^5.0.0-next.2", + "@lingui/cli": "^5.0.0-next.2", + "@lingui/loader": "^5.0.0-next.2", "@types/react": "^18.3.11", "eslint": "^9.12.0", "eslint-config-next": "14.2.14", diff --git a/src/components/array-libraries.js b/src/components/array-libraries.js index 7451543b..c2ecaa9e 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 c1ba57ce..b87123f8 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 e87a78e6..e15c037a 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 15b6ea11..6e1df945 100644 --- a/src/components/donate.js +++ b/src/components/donate.js @@ -7,16 +7,20 @@ import { Text, } from '@chakra-ui/react' +import { LanguageSwitcher } from '@/components/language-switcher' + 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 7f91c819..1e51cee8 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 ae5aaf65..ef0c104f 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 5fb7f0be..fa64ece0 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 { VercelCallout } from '@/components/vercel' -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 5c96b9c8..a4343fd6 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 00000000..371a1577 --- /dev/null +++ b/src/components/language-switcher.js @@ -0,0 +1,74 @@ +'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 { t } = useLingui() + 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({ target: { value: storedLocale } }) + } + }, []) + return ( + + + {getLocaleLabel(i18n.locale)} + + + {getLocales().map((object) => ( + + + {object.label} + + + ))} + + + ) +} diff --git a/src/components/layout.js b/src/components/layout.js index aecd295d..1f177a1b 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,11 +15,19 @@ export const Layout = ({ url = 'https://xarray.dev', enableBanner = false, }) => { - const bannerTitle = 'Check out the new blog post on DataTree!' + /** + * This macro hook is needed to get `t` which + * is bound to i18n from React.Context + */ + const { t } = useLingui() + const router = useRouter() + const { pathname, asPath, query } = router + + const bannerTitle = t`Check out the new blog post on DataTree!` const bannerDescription = '' const bannerChildren = ( - Xarray x NASA: xarray.DataTree for hierarchical data structures + {t`Xarray x NASA: xarray.DataTree for hierarchical data structures`} ) @@ -69,7 +79,6 @@ export const Layout = ({ )} {children}
-