diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index d66eb55c6e..6066585853 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -80,7 +80,7 @@ jobs: - name: Upload Assets (develop - R2) if: github.base_ref == 'develop' - uses: wei/rclone@v1 + uses: andreiio/rclone-action@v1 env: RCLONE_CONFIG_R2_TYPE: s3 RCLONE_CONFIG_R2_PROVIDER: Cloudflare @@ -107,7 +107,7 @@ jobs: - name: Upload Assets (production - R2) if: github.base_ref == 'master' - uses: wei/rclone@v1 + uses: andreiio/rclone-action@v1 env: RCLONE_CONFIG_R2_TYPE: s3 RCLONE_CONFIG_R2_PROVIDER: Cloudflare diff --git a/next.config.js b/next.config.js index 1a290191c0..bbb454dda3 100644 --- a/next.config.js +++ b/next.config.js @@ -60,12 +60,6 @@ const nextConfig = { reactStrictMode: true, compress: false, poweredByHeader: false, - i18n: { - locales: ['zh-Hant', 'zh-Hans', 'en', '__defaultLocale'], - // FIXME: Disable Next.js auto detection and prefixing since we have a fallback strategy based on user request and browser perference in `` - defaultLocale: '__defaultLocale', - localeDetection: false, - }, } const withBundleAnalyzer = require('@next/bundle-analyzer')({ diff --git a/package-lock.json b/package-lock.json index 19738d8d24..76cd65ac9e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "matters-web", - "version": "4.23.1", + "version": "4.24.0", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "matters-web", - "version": "4.23.1", + "version": "4.24.0", "license": "Apache-2.0", "dependencies": { "@apollo/react-common": "^3.1.3", diff --git a/package.json b/package.json index 3b8b795ca0..ac604fa959 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "matters-web", - "version": "4.23.1", + "version": "4.24.0", "description": "codebase of Matters' website", "sideEffects": false, "author": "Matters ", diff --git a/src/common/enums/csp.ts b/src/common/enums/csp.ts index ac608aa324..61f2f3f120 100644 --- a/src/common/enums/csp.ts +++ b/src/common/enums/csp.ts @@ -67,11 +67,8 @@ const IMG_SRC = [ site_domain_tld_old ), - // get server hostname, for img-cache redirected url - // NEXT_PUBLIC_API_HOSTNAME as string, - process.env.NEXT_PUBLIC_API_URL - ? new URL(process.env.NEXT_PUBLIC_API_URL).hostname - : undefined, + // Alchemy NFT CDN + 'nft-cdn.alchemy.com', // for some old articles were using this s3 urls directly 'matters-server-production.s3-ap-southeast-1.amazonaws.com', diff --git a/src/common/enums/lang.ts b/src/common/enums/lang.ts index b4ae33c054..a1c6a158bd 100644 --- a/src/common/enums/lang.ts +++ b/src/common/enums/lang.ts @@ -35,5 +35,3 @@ export const CONTENT_LANG_TEXT_MAP = { la: 'Latin', }, } - -export const DEFAULT_LOCALE = '__defaultLocale' diff --git a/src/common/utils/language.ts b/src/common/utils/language.ts index fb41db7c75..39abf233eb 100644 --- a/src/common/utils/language.ts +++ b/src/common/utils/language.ts @@ -25,12 +25,16 @@ export const toLocale = (lang: string) => { lang = lang.toLowerCase() // zh_hans - if (['zh-cn', 'zh-hans', 'zh_hans'].indexOf(lang) >= 0) { + if (['zh-cn', 'zh_cn', 'zh-hans', 'zh_hans'].indexOf(lang) >= 0) { return 'zh-Hans' } // zh_hant - if (['zh', 'zh-tw', 'zh-hk', 'zh-hant', 'zh_hant'].indexOf(lang) >= 0) { + if ( + ['zh', 'zh_tw', 'zh-tw', 'zh_hk', 'zh-hk', 'zh-hant', 'zh_hant'].indexOf( + lang + ) >= 0 + ) { return 'zh-Hant' } @@ -41,3 +45,24 @@ export const toLocale = (lang: string) => { return '' } + +export const toOGLanguage = (lang: string) => { + lang = lang.toLowerCase() + + // zh_hans + if (['zh-cn', 'zh-hans', 'zh_hans'].indexOf(lang) >= 0) { + return 'zh_CN' + } + + // zh_hant + if (['zh', 'zh-tw', 'zh-hk', 'zh-hant', 'zh_hant'].indexOf(lang) >= 0) { + return 'zh_TW' + } + + // en + if (['en', 'en-us', 'en-au', 'en-za', 'en-gb'].indexOf(lang) >= 0) { + return 'en' + } + + return '' +} diff --git a/src/common/utils/withApollo.ts b/src/common/utils/withApollo.ts index 348e9a0bd1..8a2f8e3ea1 100644 --- a/src/common/utils/withApollo.ts +++ b/src/common/utils/withApollo.ts @@ -83,6 +83,7 @@ const httpLink = ({ host, headers }: { host: string; headers: any }) => { headers: { ...headers, host: hostname, + 'Apollo-Require-Preflight': 'true', }, fetchOptions: { agent, diff --git a/src/components/Avatar/styles.module.css b/src/components/Avatar/styles.module.css index 0b7dac7796..3d1fa18470 100644 --- a/src/components/Avatar/styles.module.css +++ b/src/components/Avatar/styles.module.css @@ -164,7 +164,7 @@ .ring { &.civicLiker { - background-image: var(--avatar-ring-civic-ciker); + background-image: var(--avatar-ring-civic-liker); } &.architect { diff --git a/src/components/Context/Language/LanguageContext.tsx b/src/components/Context/Language/LanguageContext.tsx index c212254d99..27ff7b0b32 100644 --- a/src/components/Context/Language/LanguageContext.tsx +++ b/src/components/Context/Language/LanguageContext.tsx @@ -1,15 +1,15 @@ import gql from 'graphql-tag' -import { useRouter } from 'next/router' import { createContext, useContext, useState } from 'react' -import { ADD_TOAST, COOKIE_LANGUAGE, DEFAULT_LOCALE } from '~/common/enums' +import { ADD_TOAST, COOKIE_LANGUAGE } from '~/common/enums' import { getCookie, getIsomorphicCookie, setCookies, + toLocale, toUserLanguage, } from '~/common/utils' -import { Translate, useMutation, ViewerContext } from '~/components' +import { Translate, useMutation, useRoute, ViewerContext } from '~/components' import { UpdateLanguageMutation, UserLanguage } from '~/gql/graphql' const UPDATE_VIEWER_LANGUAGE = gql` @@ -49,11 +49,7 @@ export const LanguageProvider = ({ const viewerLang = viewer.isAuthed ? viewer.settings.language : '' // read from URL subpath - const router = useRouter() - const routerLang = - router.locale && router.locale !== DEFAULT_LOCALE - ? toUserLanguage(router.locale) - : '' + const { routerLang } = useRoute() // read from cookie (both server-side and client-side) let cookieLang = getIsomorphicCookie(headers?.cookie, COOKIE_LANGUAGE) @@ -85,8 +81,16 @@ export const LanguageProvider = ({ const setLang = async (language: UserLanguage) => { // update local cookie setCookies({ [COOKIE_LANGUAGE]: language }) + // update local cache setLocalLang(language) + + // update attribute + const html = document.querySelector('html') + if (html) { + html.setAttribute('lang', toLocale(language)) + } + // anonymous if (!viewer.isAuthed) { return diff --git a/src/components/Dialogs/ShareDialog/Content.tsx b/src/components/Dialogs/ShareDialog/Content.tsx index 61b5ae8596..206921d7ae 100644 --- a/src/components/Dialogs/ShareDialog/Content.tsx +++ b/src/components/Dialogs/ShareDialog/Content.tsx @@ -1,7 +1,6 @@ import classNames from 'classnames' import { TextId } from '~/common/enums' -import { toLocale } from '~/common/utils' import { Dialog, ShareButtons, Translate } from '~/components' import Copy from './Copy' @@ -31,9 +30,7 @@ const ShareDialogContent: React.FC = ({ footerButtons, }) => { const url = new URL(shareLink) - const pathnames = url.pathname.split('/') - const showTranslation = toLocale(pathnames[1]) !== '' - if (showTranslation) { + if (url.searchParams.get('locale')) { description = ( = (props) => { return ( diff --git a/src/components/Forms/PaymentForm/SubscribeCircle/CardPayment.tsx b/src/components/Forms/PaymentForm/SubscribeCircle/CardPayment.tsx index 154871f9dc..432a453a37 100644 --- a/src/components/Forms/PaymentForm/SubscribeCircle/CardPayment.tsx +++ b/src/components/Forms/PaymentForm/SubscribeCircle/CardPayment.tsx @@ -22,6 +22,7 @@ import { DigestRichCirclePrivateFragment, DigestRichCirclePublicFragment, SubscribeCircleMutation, + UserLanguage, } from '~/gql/graphql' import StripeCheckout from '../StripeCheckout' @@ -180,7 +181,7 @@ const CardPayment: React.FC = (props) => { return ( diff --git a/src/components/Head/index.tsx b/src/components/Head/index.tsx index 121d5012fb..3464c918b0 100644 --- a/src/components/Head/index.tsx +++ b/src/components/Head/index.tsx @@ -6,7 +6,12 @@ import IMAGE_FAVICON_16 from '@/public/static/favicon-16x16.png' import IMAGE_FAVICON_32 from '@/public/static/favicon-32x32.png' import IMAGE_FAVICON_64 from '@/public/static/favicon-64x64.png' import IMAGE_INTRO from '@/public/static/images/intro.jpg' -import { toLocale, translate, TranslateArgs } from '~/common/utils' +import { + toLocale, + toOGLanguage, + translate, + TranslateArgs, +} from '~/common/utils' import { LanguageContext, useRoute } from '~/components' import { UserLanguage } from '~/gql/graphql' @@ -58,8 +63,8 @@ export const Head: React.FC = (props) => { const i18nUrl = (language: string) => { return props.path - ? `https://${siteDomain}/${language}${props.path}` - : `https://${siteDomain}/${language}${router.asPath || '/'}` + ? `https://${siteDomain}/${props.path}?locale=${language}` + : `https://${siteDomain}/${router.asPath || '/'}?locale=${language}` } if (props.jsonLdData && !props.jsonLdData.description) { @@ -134,6 +139,7 @@ export const Head: React.FC = (props) => { key="og:description" content={head.description} /> + { @@ -59,5 +62,17 @@ export const useRoute = () => { router.replace({ query }) } - return { isInPath, isPathStartWith, getQuery, setQuery, replaceQuery, router } + // i18n + const locale = getQuery('locale') + const routerLang = toUserLanguage(locale) as UserLanguage + + return { + isInPath, + isPathStartWith, + getQuery, + setQuery, + replaceQuery, + router, + routerLang, + } } diff --git a/src/components/Layout/SideNav/Items.tsx b/src/components/Layout/SideNav/Items.tsx new file mode 100644 index 0000000000..25bd41faee --- /dev/null +++ b/src/components/Layout/SideNav/Items.tsx @@ -0,0 +1,60 @@ +import { FormattedMessage } from 'react-intl' + +import { PATHS } from '~/common/enums' +import { toPath } from '~/common/utils' +import { + IconNavHome32, + IconNavHomeActive32, + IconNavSearch32, + IconNavSearchActive32, + useRoute, +} from '~/components' + +import NavListItem from './NavListItem' + +export const NavListItemHome = ({ isMdUp }: { isMdUp: boolean }) => { + const { isInPath } = useRoute() + const isInHome = isInPath('HOME') + + return ( + } + icon={} + activeIcon={} + active={isInHome} + href={PATHS.HOME} + isMdUp={isMdUp} + /> + ) +} + +export const NavListItemSearch = ({ isMdUp }: { isMdUp: boolean }) => { + const { isInPath, router } = useRoute() + const isInSearch = isInPath('SEARCH') + + return ( + } + icon={} + activeIcon={} + active={isInSearch} + href={PATHS.SEARCH} + onClick={(e) => { + e?.preventDefault() + + const path = toPath({ + page: 'search', + }) + + if (isInSearch) { + router.replace(path.href) + } else { + router.push(path.href) + } + + return false + }} + isMdUp={isMdUp} + /> + ) +} diff --git a/src/components/Layout/SideNav/Logo.tsx b/src/components/Layout/SideNav/Logo.tsx new file mode 100644 index 0000000000..1f1f1125d4 --- /dev/null +++ b/src/components/Layout/SideNav/Logo.tsx @@ -0,0 +1,33 @@ +import Link from 'next/link' +import { useIntl } from 'react-intl' + +import { PATHS } from '~/common/enums' +import { IconLogo, IconLogoGraph, Media } from '~/components' + +import styles from './styles.module.css' + +const Logo = () => { + const intl = useIntl() + + return ( +
+ + + + + + + + + + +
+ ) +} + +export default Logo diff --git a/src/components/Layout/SideNav/index.tsx b/src/components/Layout/SideNav/index.tsx index 7c12c64aec..13b8ecde9c 100644 --- a/src/components/Layout/SideNav/index.tsx +++ b/src/components/Layout/SideNav/index.tsx @@ -1,22 +1,14 @@ import { VisuallyHidden } from '@reach/visually-hidden' -import Link from 'next/link' import { useContext } from 'react' import FocusLock from 'react-focus-lock' -import { FormattedMessage, useIntl } from 'react-intl' +import { FormattedMessage } from 'react-intl' import { PATHS, Z_INDEX } from '~/common/enums' -import { toPath } from '~/common/utils' import { Dropdown, hidePopperOnClick, - IconLogo, - IconLogoGraph, - IconNavHome32, - IconNavHomeActive32, IconNavMe32, IconNavMeActive32, - IconNavSearch32, - IconNavSearchActive32, Media, Menu, UniversalAuthButton, @@ -27,45 +19,21 @@ import { import NavMenu from '../NavMenu' import UnreadIcon from '../UnreadIcon' +import { NavListItemHome, NavListItemSearch } from './Items' +import Logo from './Logo' import NavListItem from './NavListItem' import styles from './styles.module.css' -const Logo = () => { - const intl = useIntl() - - return ( -
- - - - - - - - - - -
- ) -} - const SideNavMenu = ({ isMdUp }: { isMdUp: boolean }) => { - const { router, isInPath, isPathStartWith, getQuery } = useRoute() + const { isInPath, isPathStartWith, getQuery } = useRoute() const viewer = useContext(ViewerContext) const name = getQuery('name') const viewerUserName = viewer.userName || '' const viewerCircle = viewer.ownCircles && viewer.ownCircles[0] - const isInHome = isInPath('HOME') const isInFollow = isInPath('FOLLOW') const isInNotification = isInPath('ME_NOTIFICATIONS') - const isInSearch = isInPath('SEARCH') const isMyProfile = isPathStartWith('/@', true) && name === viewerUserName const isMyCircle = isPathStartWith('/~', true) && name === viewerCircle?.name @@ -74,14 +42,7 @@ const SideNavMenu = ({ isMdUp }: { isMdUp: boolean }) => { return (