Skip to content

[refactor] isolate Internationalization data with MobX-i18n 0.7 & React context #79

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
May 7, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .env
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ NEXT_PUBLIC_SITE_SUMMARY = 全行业信息化转型专家
NEXT_PUBLIC_LOGO = /idea2app.svg

NEXT_PUBLIC_CACHE_HOST = https://cache.idea2.app
CACHE_REPOSITORY = idea2app/OWS-cache

NEXT_PUBLIC_SENTRY_DSN = https://03e5d951172f411a04c1bab44022e22b@o4506471366852608.ingest.sentry.io/4506484563705856
SENTRY_ORG = idea2app
Expand Down
33 changes: 17 additions & 16 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -68,20 +68,20 @@ You can check out [the Next.js GitHub repository][27] - your feedback and contri

| name | file | description |
| :----------------------: | :----------: | :---------------------: |
| `CRAWLER_TOKEN` | `.env.local` | Web hooks authorization |
| `SENTRY_AUTH_TOKEN` | `.env.local` | [Official document][28] |
| `SENTRY_ORG` | `.env` | [Official document][29] |
| `SENTRY_PROJECT` | `.env` | [Official document][29] |
| `NEXT_PUBLIC_SENTRY_DSN` | `.env` | [Official document][30] |
| `GITHUB_TOKEN` | `.env.local` | [Official document][31] |
| `LARK_APP_ID` | `.env.local` | [Official document][32] |
| `LARK_APP_SECRET` | `.env.local` | [Official document][32] |
| `JWT_SECRET` | `.env.local` | [API authorization][28] |
| `SENTRY_AUTH_TOKEN` | `.env.local` | [Official document][29] |
| `SENTRY_ORG` | `.env` | [Official document][30] |
| `SENTRY_PROJECT` | `.env` | [Official document][30] |
| `NEXT_PUBLIC_SENTRY_DSN` | `.env` | [Official document][31] |
| `GITHUB_TOKEN` | `.env.local` | [Official document][32] |
| `LARK_APP_ID` | `.env.local` | [Official document][33] |
| `LARK_APP_SECRET` | `.env.local` | [Official document][33] |

### Vercel

The easiest way to deploy your Next.js app is to use the [Vercel Platform][13] from the creators of Next.js.

Check out our [Next.js deployment documentation][33] for more details.
Check out our [Next.js deployment documentation][34] for more details.

### Docker

Expand Down Expand Up @@ -110,16 +110,17 @@ pnpm container
[18]: https://github.com/new?template_name=idea2app.github.io&template_owner=idea2app
[19]: https://github.com/idea2app/idea2app.github.io/blob/34a68d5c3a21665c5971edff5aa7c208647d1566/.github/workflows/main.yml#L9-L11
[20]: https://github.com/idea2app/idea2app.github.io/settings/secrets/actions
[21]: https://github.com/kaiyuanshe/kaiyuanshe.github.io/blob/bb4675a56bf1d6b207231313da5ed0af7cf0ebd6/.github/workflows/pull-request.yml#L32-L56
[21]: https://github.com/idea2app/Lark-Next-Bootstrap-ts/blob/363e023e5dd472c8ea53ec96eac25ec5122e667b/.github/workflows/Lark-notification.yml#L39
[22]: https://github.com/idea2app/idea2app.github.io/issues/new/choose
[23]: https://github.com/idea2app/idea2app.github.io/projects
[24]: https://nextjs.org/docs/api-routes/introduction
[25]: https://nextjs.org/docs
[26]: https://nextjs.org/learn
[27]: https://github.com/vercel/next.js/
[28]: https://docs.sentry.io/platforms/javascript/guides/nextjs/manual-setup/#use-configuration-files-for-source-map-upload
[29]: https://docs.sentry.io/platforms/javascript/guides/nextjs/manual-setup/#use-environment-variables
[30]: https://docs.sentry.io/platforms/javascript/guides/nextjs/manual-setup/#create-initialization-config-files
[31]: https://docs.github.com/en/rest/authentication/authenticating-to-the-rest-api#authenticating-with-a-personal-access-token
[32]: https://open.larksuite.com/document/server-docs/getting-started/api-access-token/app-access-token-development-guide#95c7f5f5
[33]: https://nextjs.org/docs/deployment
[28]: https://github.com/auth0/node-jsonwebtoken?tab=readme-ov-file#jwtsignpayload-secretorprivatekey-options-callback
[29]: https://docs.sentry.io/platforms/javascript/guides/nextjs/manual-setup/#use-configuration-files-for-source-map-upload
[30]: https://docs.sentry.io/platforms/javascript/guides/nextjs/manual-setup/#use-environment-variables
[31]: https://docs.sentry.io/platforms/javascript/guides/nextjs/manual-setup/#create-initialization-config-files
[32]: https://docs.github.com/en/rest/authentication/authenticating-to-the-rest-api#authenticating-with-a-personal-access-token
[33]: https://open.larksuite.com/document/server-docs/getting-started/api-access-token/app-access-token-development-guide#95c7f5f5
[34]: https://nextjs.org/docs/deployment
72 changes: 38 additions & 34 deletions components/Git/Card.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,9 @@ import { Button, Chip } from '@mui/material';
import { GitRepository } from 'mobx-github';
import { observer } from 'mobx-react';
import Link from 'next/link';
import { FC } from 'react';
import { FC, useContext } from 'react';

import { i18n } from '../../models/Translation';
import { I18nContext } from '../../models/Translation';

export interface GitCardProps
extends Pick<GitRepository, 'full_name' | 'html_url' | 'languages'>,
Expand All @@ -13,39 +13,43 @@ export interface GitCardProps
}

export const GitCard: FC<GitCardProps> = observer(
({ className = '', full_name, html_url, topics = [], description, homepage }) => (
<li
className={`${className} grid grid-cols-1 grid-rows-10 gap-2 rounded-2xl border p-4 elevation-1 hover:elevation-8 dark:border-0`}
>
<h2 className="row-span-2 text-lg">
<a target="_blank" href={html_url} rel="noreferrer">
{full_name}
</a>
</h2>
({ className = '', full_name, html_url, topics = [], description, homepage }) => {
const { t } = useContext(I18nContext);

<nav className="row-span-3 flex flex-row flex-wrap gap-2">
{topics.map(topic => (
<Chip
key={topic}
size="small"
component="a"
target="_blank"
href={`https://github.com/topics/${topic}`}
label={topic}
/>
))}
</nav>
return (
<li
className={`${className} elevation-1 hover:elevation-8 grid grid-cols-1 grid-rows-10 gap-2 rounded-2xl border p-4 dark:border-0`}
>
<h2 className="row-span-2 text-lg">
<a target="_blank" href={html_url} rel="noreferrer">
{full_name}
</a>
</h2>

<p className="row-span-3 text-sm">{description}</p>
<nav className="row-span-3 flex flex-row flex-wrap gap-2">
{topics.map(topic => (
<Chip
key={topic}
size="small"
component="a"
target="_blank"
href={`https://github.com/topics/${topic}`}
label={topic}
/>
))}
</nav>

<Button
className="row-span-2 place-self-center"
component={Link}
target="_blank"
href={homepage ?? html_url}
>
{i18n.t('home_page')}
</Button>
</li>
),
<p className="row-span-3 text-sm">{description}</p>

<Button
className="row-span-2 place-self-center"
component={Link}
target="_blank"
href={homepage ?? html_url}
>
{t('home_page')}
</Button>
</li>
);
},
);
42 changes: 24 additions & 18 deletions components/Layout/MainNavigator.tsx
Original file line number Diff line number Diff line change
@@ -1,40 +1,51 @@
import { AppBar, Drawer, IconButton, Menu, MenuItem, PopoverProps, Toolbar } from '@mui/material';
import { observable } from 'mobx';
import { computed, observable } from 'mobx';
import { observer } from 'mobx-react';
import { ObservedComponent, observePropsState } from 'mobx-react-helper';
import Link from 'next/link';
import { Component } from 'react';

import { i18n, LanguageName, t } from '../../models/Translation';
import { i18n, I18nContext, LanguageName } from '../../models/Translation';
import { SymbolIcon } from '../Icon';
import { ColorModeIconDropdown } from './ColorModeDropdown';
import { BrandLogo, GithubIcon } from './Svg';

export const mainNavLinks = () => [
{ title: t('latest_projects'), href: '/project' },
{ title: 'GitHub-reward', href: '/project/reward/issue', target: '_top' },
{ title: t('member'), href: '/member' },
{ title: t('open_source_project'), href: '/open-source' },
];
export interface MainNavigator extends ObservedComponent<{}, typeof i18n> {}

@observer
@observePropsState
export class MainNavigator extends Component {
static contextType = I18nContext;

@observable accessor menuExpand = false;
@observable accessor menuAnchor: PopoverProps['anchorEl'] = null;

@computed
get links() {
const { t } = this.observedContext!;

return [
{ title: t('latest_projects'), href: '/project' },
{ title: 'GitHub-reward', href: '/project/reward/issue', target: '_top' },
{ title: t('member'), href: '/member' },
{ title: t('open_source_project'), href: '/open-source' },
];
}

switchI18n = (key: string) => {
i18n.changeLanguage(key as keyof typeof LanguageName);
this.observedContext!.loadLanguages(key as keyof typeof LanguageName);
this.menuAnchor = null;
};

renderLinks = () =>
mainNavLinks().map(({ title, href, target }) => (
this.links.map(({ title, href, target }) => (
<Link key={title} className="py-1" href={href} target={target}>
{title}
</Link>
));

renderI18nSwitch = () => {
const { currentLanguage } = i18n,
const { currentLanguage } = this.observedContext!,
{ menuAnchor } = this;

return (
Expand All @@ -50,12 +61,7 @@ export class MainNavigator extends Component {
<Menu
anchorEl={menuAnchor}
id="i18n-menu"
slotProps={{
paper: {
variant: 'outlined',
sx: { my: '4px' },
},
}}
slotProps={{ paper: { variant: 'outlined', sx: { my: '4px' } } }}
open={Boolean(menuAnchor)}
onClose={() => (this.menuAnchor = null)}
>
Expand Down Expand Up @@ -89,7 +95,7 @@ export class MainNavigator extends Component {
variant="temporary"
anchor="top"
ModalProps={{ keepMounted: true }}
PaperProps={{ className: 'w-full bg-transparent shadow-none bg-none' }}
slotProps={{ paper: { className: 'w-full bg-transparent shadow-none bg-none' } }}
open={this.menuExpand}
onClose={() => (this.menuExpand = false)}
>
Expand Down
38 changes: 21 additions & 17 deletions components/Layout/ScrollListPage.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { DataObject, Filter, ListModel } from 'mobx-restful';
import { FC, HTMLAttributes } from 'react';
import { FC, HTMLAttributes, useContext } from 'react';

import { i18n } from '../../models/Translation';
import { I18nContext } from '../../models/Translation';
import { PageHead } from '../PageHead';
import { ScrollList } from '../ScrollList';

Expand All @@ -27,19 +27,23 @@ export const ScrollListPage = <D extends DataObject, F extends Filter<D> = Filte
header,
Layout,
...rest
}: ScrollListPageProps<D, F>) => (
<div className={`container mx-auto max-w-(--breakpoint-xl) px-4 pt-16 pb-6 ${className}`}>
<PageHead title={title} />
<h1 className="my-8 text-4xl">{header}</h1>
}: ScrollListPageProps<D, F>) => {
const i18n = useContext(I18nContext);

{scrollList ? (
<ScrollList
translator={i18n}
renderList={allItems => <Layout defaultData={allItems} />}
{...rest}
/>
) : (
children
)}
</div>
);
return (
<div className={`container mx-auto max-w-(--breakpoint-xl) px-4 pt-16 pb-6 ${className}`}>
<PageHead title={title} />
<h1 className="my-8 text-4xl">{header}</h1>

{scrollList ? (
<ScrollList
translator={i18n}
renderList={allItems => <Layout defaultData={allItems} />}
{...rest}
/>
) : (
children
)}
</div>
);
};
38 changes: 21 additions & 17 deletions components/Layout/Section.tsx
Original file line number Diff line number Diff line change
@@ -1,30 +1,34 @@
import { Button } from '@mui/material';
import { observer } from 'mobx-react';
import Link from 'next/link';
import { FC, PropsWithChildren } from 'react';
import { FC, PropsWithChildren, useContext } from 'react';

import { t } from '../../models/Translation';
import { I18nContext } from '../../models/Translation';

export type SectionProps = PropsWithChildren<
Partial<Record<'id' | 'title' | 'link' | 'className', string>>
>;

export const Section: FC<SectionProps> = observer(
({ id, title, children, link, className = '' }) => (
<section className={`mx-auto flex max-w-(--breakpoint-xl) flex-col gap-6 py-8 ${className}`}>
<h2 className="text-center" id={id}>
{title}
</h2>
({ id, title, children, link, className = '' }) => {
const { t } = useContext(I18nContext);

{children}
return (
<section className={`mx-auto flex max-w-(--breakpoint-xl) flex-col gap-6 py-8 ${className}`}>
<h2 className="text-center" id={id}>
{title}
</h2>

{link && (
<footer className="text-center">
<Button component={Link} href={link} aria-label={`load more ${title}`}>
{t('load_more')}
</Button>
</footer>
)}
</section>
),
{children}

{link && (
<footer className="text-center">
<Button component={Link} href={link} aria-label={`load more ${title}`}>
{t('load_more')}
</Button>
</footer>
)}
</section>
);
},
);
13 changes: 8 additions & 5 deletions components/NotFoundCard.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
/* eslint-disable @next/next/no-sync-scripts */
import { ErrorProps } from 'next/error';
import { FC } from 'react';
import { FC, useContext } from 'react';

import { i18n } from '../models/Translation';
import { I18nContext } from '../models/Translation';

export const NotFoundCard: FC<ErrorProps> = ({ title }) =>
i18n.currentLanguage.startsWith('zh') ? (
export const NotFoundCard: FC<ErrorProps> = ({ title }) => {
const { currentLanguage } = useContext(I18nContext);

return currentLanguage.startsWith('zh') ? (
<script
src="//cdn.dnpw.org/404/v1.min.js"
// @ts-expect-error https://www.dnpw.org/cn/pa-notfound.html
Expand All @@ -15,7 +17,8 @@ export const NotFoundCard: FC<ErrorProps> = ({ title }) =>
/>
) : (
<iframe
className="w-100 vh-100 border-0"
className="vh-100 w-100 border-0"
src="https://notfound-static.fwebservices.be/en/404?key=66abb751ed312"
/>
);
};
6 changes: 3 additions & 3 deletions models/Base.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,14 @@ import MIME from 'mime';
import { githubClient } from 'mobx-github';
import { TableCellValue, TableCellMedia, TableCellAttachment } from 'mobx-lark';

import { API_Host, GithubToken, isServer } from './configuration';
import { API_Host, GITHUB_TOKEN, isServer } from './configuration';

if (!isServer()) githubClient.baseURI = `${API_Host}/api/GitHub/`;

githubClient.use(({ request }, next) => {
if (GithubToken)
if (GITHUB_TOKEN)
request.headers = {
Authorization: `Bearer ${GithubToken}`,
Authorization: `Bearer ${GITHUB_TOKEN}`,
...request.headers,
};
return next();
Expand Down
Loading