feat(pass): localize pass page copy

This commit is contained in:
Tommaso Casaburi
2026-04-17 15:11:15 +07:00
parent b81bcd8c55
commit d8944e3c7c
38 changed files with 1449 additions and 150 deletions
+34 -3
View File
@@ -1,17 +1,48 @@
import * as React from 'react';
import { createElement } from 'react';
import { Fragment, cloneElement, createElement, isValidElement } from 'react';
import { createRoot, type Root } from 'react-dom/client';
import { MemoryRouter } from 'react-router-dom';
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
import en from '../../../../public/translations/en/default.json';
import Pass from '../pass';
(globalThis as { IS_REACT_ACT_ENVIRONMENT?: boolean }).IS_REACT_ACT_ENVIRONMENT = true;
const act = (React as { act?: (cb: () => void | Promise<void>) => void | Promise<void> }).act as (cb: () => void | Promise<void>) => void | Promise<void>;
const renderTransTemplate = (template: string, components?: Record<string, React.ReactElement>, keyPrefix = 'trans'): React.ReactNode => {
const match = template.match(/<(\w+)>(.*?)<\/\1>/s);
if (!match || match.index === undefined) {
return template;
}
const before = template.slice(0, match.index);
const [, componentName, inner] = match;
const after = template.slice(match.index + match[0].length);
const component = components?.[componentName];
return createElement(
Fragment,
{},
before,
component && isValidElement(component)
? cloneElement(component, { key: `${keyPrefix}-${componentName}` }, renderTransTemplate(inner, components, `${keyPrefix}-${componentName}`))
: inner,
renderTransTemplate(after, components, `${keyPrefix}-after`),
);
};
vi.mock('react-router-hash-link', () => ({
HashLink: ({ children, to }: { children: React.ReactNode; to: string }) => createElement('a', { href: to }, children),
}));
vi.mock('react-i18next', () => ({
Trans: ({ components, i18nKey }: { components?: Record<string, React.ReactElement>; i18nKey: string }) =>
createElement(Fragment, {}, renderTransTemplate((en as Record<string, string>)[i18nKey] ?? i18nKey, components)),
useTranslation: () => ({
t: (key: string) => (en as Record<string, string>)[key] ?? key,
}),
}));
vi.mock('../../home', () => ({
Footer: () => createElement('div', { 'data-testid': 'footer' }, 'footer'),
HomeLogo: () => createElement('div', { 'data-testid': 'home-logo' }, 'home-logo'),
@@ -47,7 +78,7 @@ describe('Pass', () => {
expect(container.querySelector('[data-testid="home-logo"]')).toBeTruthy();
expect(container.querySelector('[data-testid="footer"]')).toBeTruthy();
expect(container.textContent).toContain('Support 5chan with 5chan Pass');
expect(container.textContent).toContain((en as Record<string, string>).pass_heading);
expect(container.textContent).toContain('not available yet');
expect(container.textContent).toContain('crypto only');
expect(container.textContent).toContain('directory voting');
@@ -65,6 +96,6 @@ describe('Pass', () => {
expect(container.querySelector('a[href="https://github.com/bitsocialnet/mintpass"]')?.textContent).toBe('MintPass');
expect(container.querySelector('a[href="https://bitsocial.net"]')?.textContent).toBe('Bitsocial');
expect(window.scrollTo).toHaveBeenCalledWith(0, 0);
expect(document.title).toBe('Pass - 5chan');
expect(document.title).toBe((en as Record<string, string>).pass_document_title);
});
});
+5
View File
@@ -131,6 +131,11 @@
border-bottom: solid #006 1px;
}
.rightBox dd:last-of-type {
padding-bottom: 0;
border-bottom: none;
}
@media (max-width: 640px) {
.content {
min-width: 0;
+115 -112
View File
@@ -1,93 +1,89 @@
import { useEffect } from 'react';
import { Trans, useTranslation } from 'react-i18next';
import { HashLink } from 'react-router-hash-link';
import { Link } from 'react-router-dom';
import { Footer, HomeLogo } from '../home';
import styles from './pass.module.css';
const overviewQuestionLinks = [
{ id: 'whatpass', key: 'pass_question_what_is' },
{ id: 'available', key: 'pass_question_available' },
{ id: 'whatdoes', key: 'pass_question_what_does' },
{ id: 'whatnot', key: 'pass_question_what_not' },
{ id: 'vip', key: 'pass_question_vip' },
{ id: 'challenge', key: 'pass_question_challenge' },
{ id: 'payment', key: 'pass_question_payment' },
{ id: 'pricing', key: 'pass_question_pricing' },
{ id: 'nft', key: 'pass_question_nft' },
{ id: 'proceeds', key: 'pass_question_proceeds' },
] as const;
const faqQuestionLinks = [
{ id: 'needpass', key: 'pass_question_need' },
{ id: 'fiat', key: 'pass_question_fiat' },
{ id: 'vpn', key: 'pass_question_vpn' },
{ id: 'bypass', key: 'pass_question_bypass' },
{ id: 'privileges', key: 'pass_question_privileges' },
] as const;
const Pass = () => {
const { t } = useTranslation();
useEffect(() => {
window.scrollTo(0, 0);
document.title = 'Pass - 5chan';
}, []);
useEffect(() => {
document.title = t('pass_document_title');
}, [t]);
return (
<div className={styles.wrapper}>
<div className={styles.content}>
<HomeLogo />
<div className={`${styles.box} ${styles.infoBox}`}>
<div className={styles.boxBar}>
<h2>Support 5chan with 5chan Pass</h2>
<h2>{t('pass_heading')}</h2>
</div>
<div className={styles.boxContent}>
5chan Pass is a planned paid supporter pass for 5chan. It is <strong>not available yet</strong>. When it launches, 5chan Pass holders will be able to vote on
directory pages, post on <Link to='/vip'>/vip/</Link>, and bypass typing a CAPTCHA verification or similar posting challenge on 5chan boards that support the
5chan Pass challenge. Checkout will be <strong>crypto only</strong>. There is no live purchase, activation, or renewal flow today.
<Trans
i18nKey='pass_intro_body'
components={{
strong: <strong />,
vip: <Link to='/vip' />,
}}
/>
</div>
</div>
<div className={styles.columns}>
<div className={`${styles.box} ${styles.leftBox}`}>
<div className={styles.boxBar}>
<h2>Questions</h2>
<h2>{t('pass_questions')}</h2>
</div>
<div className={styles.boxContent}>
<ul className={styles.list}>
<li>
<strong>
<HashLink to='#overview'>Overview</HashLink>
<HashLink to='#overview'>{t('pass_overview')}</HashLink>
</strong>
<ul>
<li>
<HashLink to='#whatpass'>What is 5chan Pass?</HashLink>
</li>
<li>
<HashLink to='#available'>Is it available now?</HashLink>
</li>
<li>
<HashLink to='#whatdoes'>What will it do?</HashLink>
</li>
<li>
<HashLink to='#whatnot'>What will it not do?</HashLink>
</li>
<li>
<HashLink to='#vip'>What is the /vip/ benefit?</HashLink>
</li>
<li>
<HashLink to='#challenge'>How does zero-friction posting work?</HashLink>
</li>
<li>
<HashLink to='#payment'>How will payment work?</HashLink>
</li>
<li>
<HashLink to='#pricing'>Has pricing been announced?</HashLink>
</li>
<li>
<HashLink to='#nft'>How does the NFT work?</HashLink>
</li>
<li>
<HashLink to='#proceeds'>What happens to the proceeds?</HashLink>
</li>
{overviewQuestionLinks.map(({ id, key }) => (
<li key={id}>
<HashLink to={`#${id}`}>{t(key)}</HashLink>
</li>
))}
</ul>
</li>
<li>
<strong>
<HashLink to='#faq'>FAQ</HashLink>
<HashLink to='#faq'>{t('pass_faq')}</HashLink>
</strong>
<ul>
<li>
<HashLink to='#needpass'>Will I need it to browse or post?</HashLink>
</li>
<li>
<HashLink to='#fiat'>Will fiat payments be accepted?</HashLink>
</li>
<li>
<HashLink to='#vpn'>Will VPNs, proxies, or IP changes matter?</HashLink>
</li>
<li>
<HashLink to='#bypass'>Will it bypass bans or board rules?</HashLink>
</li>
<li>
<HashLink to='#privileges'>Will it grant moderation powers or board ownership?</HashLink>
</li>
{faqQuestionLinks.map(({ id, key }) => (
<li key={id}>
<HashLink to={`#${id}`}>{t(key)}</HashLink>
</li>
))}
</ul>
</li>
</ul>
@@ -95,84 +91,91 @@ const Pass = () => {
</div>
<div className={`${styles.box} ${styles.rightBox}`}>
<div className={styles.boxBar}>
<h2 id='overview'>Overview</h2>
<h2 id='overview'>{t('pass_overview')}</h2>
</div>
<div className={styles.boxContent}>
<dl>
<dt className={styles.first} id='whatpass'>
What is 5chan Pass?
{t('pass_question_what_is')}
</dt>
<dd>
5chan Pass is an upcoming supporter purchase for people who want to fund 5chan directly. When it launches, it will be used for directory voting,
zero-friction posting on boards that support the 5chan Pass challenge, and posting on <Link to='/vip'>/vip/</Link>.
<Trans
i18nKey='pass_answer_what_is'
components={{
vip: <Link to='/vip' />,
}}
/>
</dd>
<dt id='available'>Is it available now?</dt>
<dd>No. This page is a placeholder so the future 5chan Pass route already exists, but there is nothing to buy, activate, or renew yet.</dd>
<dt id='whatdoes'>What will it do?</dt>
<dt id='available'>{t('pass_question_available')}</dt>
<dd>{t('pass_answer_available')}</dd>
<dt id='whatdoes'>{t('pass_question_what_does')}</dt>
<dd>
When it launches, 5chan Pass will let eligible users vote on directory assignments through each directory&apos;s voting page, post on{' '}
<Link to='/vip'>/vip/</Link>, and bypass typing a CAPTCHA verification or similar posting challenge on all 5chan boards that support the pass challenge.
Reporting is not available yet, so the pass does not currently apply to reports.
<Trans
i18nKey='pass_answer_what_does'
components={{
vip: <Link to='/vip' />,
}}
/>
</dd>
<dt id='whatnot'>What will it not do?</dt>
<dt id='whatnot'>{t('pass_question_what_not')}</dt>
<dd>
5chan Pass will not give moderation powers, board ownership, special posting privileges, or any way to ignore board-specific{' '}
<Link to='/rules'>rules</Link>. It also will not unlock anything today, because the feature has not launched yet.
<Trans
i18nKey='pass_answer_what_not'
components={{
rules: <Link to='/rules' />,
}}
/>
</dd>
<dt id='vip'>What is the /vip/ benefit?</dt>
<dt id='vip'>{t('pass_question_vip')}</dt>
<dd>
Only 5chan Pass users will be able to post on <Link to='/vip'>/vip/</Link>. Browsing rules and long-term direction for that board can still evolve, but
the current plan is that posting there is restricted to pass holders.
<Trans
i18nKey='pass_answer_vip'
components={{
vip: <Link to='/vip' />,
}}
/>
</dd>
<dt id='challenge'>How does zero-friction posting work?</dt>
<dt id='challenge'>{t('pass_question_challenge')}</dt>
<dd>{t('pass_answer_challenge')}</dd>
<dt id='payment'>{t('pass_question_payment')}</dt>
<dd>{t('pass_answer_payment')}</dd>
<dt id='pricing'>{t('pass_question_pricing')}</dt>
<dd>{t('pass_answer_pricing')}</dd>
<dt id='nft'>{t('pass_question_nft')}</dt>
<dd>
On boards that support the 5chan Pass challenge, pass holders will be able to bypass typing a CAPTCHA verification or similar challenge before posting.
This is similar in spirit to how 4chan Pass reduces posting friction, except 5chan reports are not available yet.
<Trans
i18nKey='pass_answer_nft'
components={{
mintPass: <a href='https://github.com/bitsocialnet/mintpass' target='_blank' rel='noopener noreferrer' />,
}}
/>
</dd>
<dt id='payment'>How will payment work?</dt>
<dt id='proceeds'>{t('pass_question_proceeds')}</dt>
<dd>
5chan Pass will be sold through a crypto-only checkout when it becomes available. No credit cards, PayPal, or other fiat payment methods will be
accepted.
<Trans
i18nKey='pass_answer_proceeds'
components={{
bitsocial: <a href='https://bitsocial.net' target='_blank' rel='noopener noreferrer' />,
}}
/>
</dd>
<dt id='pricing'>Has pricing been announced?</dt>
<dd>
Yes, the planned pricing model is the same simple structure: Passes cost $30 for 1 year or $60 for 3 years, which is about $1.67 per month or less than
a single 20oz bottle of soda.
</dd>
<dt id='nft'>How does the NFT work?</dt>
<dd>
5chan Pass will be an NFT under the hood, powered by the open-source{' '}
<a href='https://github.com/bitsocialnet/mintpass' target='_blank' rel='noopener noreferrer'>
MintPass
</a>{' '}
service. The 5chan Pass challenge will only accept NFTs minted within the last year, so an NFT older than one year will count as expired until it is
renewed or replaced.
</dd>
<dt id='proceeds'>What happens to the proceeds?</dt>
<dd>
All proceeds from 5chan Pass are intended to burn BSO, the official cryptocurrency token of{' '}
<a href='https://bitsocial.net' target='_blank' rel='noopener noreferrer'>
Bitsocial
</a>
. This is planned to happen automatically in the smart contract. The resulting on-chain transactions will be transparent, so buyers should assume their
purchase wallet activity can be publicly visible.
</dd>
<dt id='faq'>Will I need it to browse or post?</dt>
<dt id='faq'>{t('pass_question_need')}</dt>
<dd id='needpass'>
No. 5chan Pass is not required to read boards, subscribe to boards, or post normally. It is for specific benefits like directory voting, posting on{' '}
<Link to='/vip'>/vip/</Link>, and zero-friction posting on boards that support the challenge.
<Trans
i18nKey='pass_answer_need'
components={{
vip: <Link to='/vip' />,
}}
/>
</dd>
<dt id='fiat'>Will fiat payments be accepted?</dt>
<dd>No. When 5chan Pass launches, payment will be crypto only.</dd>
<dt id='vpn'>Will VPNs, proxies, or IP changes matter?</dt>
<dd>
No. Because 5chan Pass is planned as an NFT-based credential, it is not meant to depend on a fixed IP address. There is no intended restriction based on
VPN use, proxies, or changing networks.
</dd>
<dt id='bypass'>Will it bypass bans or board rules?</dt>
<dd>No. A pass is not a way around bans, posting restrictions, or board moderation. Every board still sets and enforces its own rules.</dd>
<dt id='privileges'>Will it grant moderation powers or board ownership?</dt>
<dd>No. Buying a pass will not make someone a moderator, an admin, or the owner of a board. It is a supporter purchase, not a governance override.</dd>
<dt id='fiat'>{t('pass_question_fiat')}</dt>
<dd>{t('pass_answer_fiat')}</dd>
<dt id='vpn'>{t('pass_question_vpn')}</dt>
<dd>{t('pass_answer_vpn')}</dd>
<dt id='bypass'>{t('pass_question_bypass')}</dt>
<dd>{t('pass_answer_bypass')}</dd>
<dt id='privileges'>{t('pass_question_privileges')}</dt>
<dd>{t('pass_answer_privileges')}</dd>
</dl>
</div>
</div>