Skip to content
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

feat(guide): added language switcher dropdown and Japanese language support. #544

Merged
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
212 changes: 146 additions & 66 deletions guide/app/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,76 +6,142 @@ import {
usePageLoaderData,
MainNavigation,
} from '~/components';
import { logo } from '~/util';
import { allLanguages, getLanguage, logo } from '~/util';

const menus: Menu[] = [
{
title: 'Getting Started',
links: [
{ title: 'Overview', to: '/' },
{ title: 'Tutorial', to: '/tutorial' },
{ title: 'Upgrading to v1', to: '/upgrading-v1' },
{ title: 'GitHub', to: 'https://github.com/edmundhung/conform' },
],
},
{
title: 'Guides',
links: [
{ title: 'Validation', to: '/validation' },
{ title: 'Nested object and Array', to: '/complex-structures' },
{ title: 'Intent button', to: '/intent-button' },
{ title: 'Accessibility', to: '/accessibility' },
{ title: 'Checkbox and Radio Group', to: '/checkbox-and-radio-group' },
{ title: 'File Upload', to: '/file-upload' },
],
},
{
title: 'Integration',
links: [
{ title: 'UI Libraries', to: '/integration/ui-libraries' },
{ title: 'Remix', to: '/integration/remix' },
{ title: 'Next.js', to: '/integration/nextjs' },
],
},
{
title: 'API Reference',
links: [
{ title: 'useForm', to: '/api/react/useForm' },
{ title: 'useField', to: '/api/react/useField' },
{ title: 'useFormMetadata', to: '/api/react/useFormMetadata' },
{ title: 'useInputControl', to: '/api/react/useInputControl' },
{ title: 'FormProvider', to: '/api/react/FormProvider' },
{ title: 'FormStateInput', to: '/api/react/FormStateInput' },
],
},
{
title: 'Utilities',
links: [
{ title: 'getFormProps', to: '/api/react/getFormProps' },
{ title: 'getFieldsetProps', to: '/api/react/getFieldsetProps' },
{ title: 'getInputProps', to: '/api/react/getInputProps' },
{ title: 'getSelectProps', to: '/api/react/getSelectProps' },
{ title: 'getTextareaProps', to: '/api/react/getTextareaProps' },
{ title: 'getCollectionProps', to: '/api/react/getCollectionProps' },
],
},
{
title: 'Schema related',
links: [
{ title: 'parseWithZod', to: '/api/zod/parseWithZod' },
{ title: 'parseWithYup', to: '/api/yup/parseWithYup' },
{ title: 'getZodConstraint', to: '/api/zod/getZodConstraint' },
{ title: 'getYupConstraint', to: '/api/yup/getYupConstraint' },
],
},
];
const menus: { [code: string]: Menu[] } = {
en: [
{
title: 'Getting Started',
links: [
{ title: 'Overview', to: '/' },
{ title: 'Tutorial', to: '/tutorial' },
{ title: 'Upgrading to v1', to: '/upgrading-v1' },
{ title: 'GitHub', to: 'https://github.com/edmundhung/conform' },
],
},
{
title: 'Guides',
links: [
{ title: 'Validation', to: '/validation' },
{ title: 'Nested object and Array', to: '/complex-structures' },
{ title: 'Intent button', to: '/intent-button' },
{ title: 'Accessibility', to: '/accessibility' },
{ title: 'Checkbox and Radio Group', to: '/checkbox-and-radio-group' },
{ title: 'File Upload', to: '/file-upload' },
],
},
{
title: 'Integration',
links: [
{ title: 'UI Libraries', to: '/integration/ui-libraries' },
{ title: 'Remix', to: '/integration/remix' },
{ title: 'Next.js', to: '/integration/nextjs' },
],
},
{
title: 'API Reference',
links: [
{ title: 'useForm', to: '/api/react/useForm' },
{ title: 'useField', to: '/api/react/useField' },
{ title: 'useFormMetadata', to: '/api/react/useFormMetadata' },
{ title: 'useInputControl', to: '/api/react/useInputControl' },
{ title: 'FormProvider', to: '/api/react/FormProvider' },
{ title: 'FormStateInput', to: '/api/react/FormStateInput' },
],
},
{
title: 'Utilities',
links: [
{ title: 'getFormProps', to: '/api/react/getFormProps' },
{ title: 'getFieldsetProps', to: '/api/react/getFieldsetProps' },
{ title: 'getInputProps', to: '/api/react/getInputProps' },
{ title: 'getSelectProps', to: '/api/react/getSelectProps' },
{ title: 'getTextareaProps', to: '/api/react/getTextareaProps' },
{ title: 'getCollectionProps', to: '/api/react/getCollectionProps' },
],
},
{
title: 'Schema related',
links: [
{ title: 'parseWithZod', to: '/api/zod/parseWithZod' },
{ title: 'parseWithYup', to: '/api/yup/parseWithYup' },
{ title: 'getZodConstraint', to: '/api/zod/getZodConstraint' },
{ title: 'getYupConstraint', to: '/api/yup/getYupConstraint' },
],
},
],
ja: [
{
title: 'はじめに',
links: [
{ title: '概要', to: '/' },
{ title: 'チュートリアル', to: '/tutorial' },
{ title: 'v1 へのアップグレード', to: '/upgrading-v1' },
{ title: 'GitHub', to: 'https://github.com/edmundhung/conform' },
],
},
{
title: 'ガイド',
links: [
{ title: 'バリデーション', to: '/validation' },
{ title: 'ネストされたオブジェクトと配列', to: '/complex-structures' },
{ title: 'インテントボタン', to: '/intent-button' },
{ title: 'アクセシビリティ', to: '/accessibility' },
{
title: 'チェックボックスとラジオグループ',
to: '/checkbox-and-radio-group',
},
{ title: 'ファイルのアップロード', to: '/file-upload' },
],
},
{
title: 'インテグレーション',
links: [
{ title: 'UI ライブラリ', to: '/integration/ui-libraries' },
{ title: 'Remix', to: '/integration/remix' },
{ title: 'Next.js', to: '/integration/nextjs' },
],
},
{
title: 'API リファレンス',
links: [
{ title: 'useForm', to: '/api/react/useForm' },
{ title: 'useField', to: '/api/react/useField' },
{ title: 'useFormMetadata', to: '/api/react/useFormMetadata' },
{ title: 'useInputControl', to: '/api/react/useInputControl' },
{ title: 'FormProvider', to: '/api/react/FormProvider' },
{ title: 'FormStateInput', to: '/api/react/FormStateInput' },
],
},
{
title: 'ユーティリティ',
links: [
{ title: 'getFormProps', to: '/api/react/getFormProps' },
{ title: 'getFieldsetProps', to: '/api/react/getFieldsetProps' },
{ title: 'getInputProps', to: '/api/react/getInputProps' },
{ title: 'getSelectProps', to: '/api/react/getSelectProps' },
{ title: 'getTextareaProps', to: '/api/react/getTextareaProps' },
{ title: 'getCollectionProps', to: '/api/react/getCollectionProps' },
],
},
{
title: 'スキーマ関連',
links: [
{ title: 'parseWithZod', to: '/api/zod/parseWithZod' },
{ title: 'parseWithYup', to: '/api/yup/parseWithYup' },
{ title: 'getZodConstraint', to: '/api/zod/getZodConstraint' },
{ title: 'getYupConstraint', to: '/api/yup/getYupConstraint' },
],
},
],
};

export function Guide({
children,
}: {
children: React.ReactNode;
}): React.ReactNode {
const { owner, repo, ref } = useRootLoaderData();
const { owner, repo, ref, language } = useRootLoaderData();
const { file, toc } = usePageLoaderData() ?? {};
const location = useLocation();

Expand Down Expand Up @@ -106,16 +172,30 @@ export function Guide({
return (
<div className="xl:container mx-auto xl:grid xl:grid-cols-5 gap-10 px-8 relative">
<header className="bg-zinc-900 xl:bg-transparent sticky top-0 max-h-screen z-10 flex flex-col">
<div className="py-2 xl:py-4">
<div className="py-6 xl:py-10 flex items-end justify-between">
<Link
className="font-mono inline-block py-4 text-[.25rem] leading-[.25rem] xl:text-[.35rem] xl:leading-[.40rem] whitespace-pre"
className="font-mono inline-block text-[.20rem] leading-[.25rem] 2xl:text-[.3rem] 2xl:leading-[.3rem] whitespace-pre"
title="Conform"
to="/"
>
{logo}
</Link>
<select
className="bg-zinc-900 text-xs rounded-md py-1 pl-1 pr-8"
defaultValue={language.code}
onChange={(e) => {
const selectedLanguage = getLanguage(e.currentTarget.value);
window.location.href = `//${selectedLanguage.domain}${location.pathname}${location.search}`;
}}
>
{allLanguages.map(({ code, label }) => (
<option key={code} value={code}>
{label}
</option>
))}
</select>
</div>
<MainNavigation menus={menus} />
<MainNavigation menus={menus[language.code]} />
</header>
<main className="xl:col-span-3">{children}</main>
<footer className="xl:col-span-1 top-0 sticky py-4 xl:flex xl:flex-col xl:h-screen -mx-8 px-8 mt-8 xl:mt-0 border-t xl:border-t-0 border-dotted">
Expand Down
4 changes: 2 additions & 2 deletions guide/app/routes/$.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import {
import { useLoaderData } from '@remix-run/react';
import { collectHeadings, parse } from '~/markdoc';
import { Markdown } from '~/components';
import { formatTitle, getFileContent } from '~/util';
import { getDocPath, formatTitle, getFileContent } from '~/util';

export const headers: HeadersFunction = ({ loaderHeaders }) => {
return loaderHeaders;
Expand All @@ -22,7 +22,7 @@ export const meta: MetaFunction<typeof loader> = ({ data }) => {
};

export async function loader({ params, context }: LoaderFunctionArgs) {
const file = `docs/${params['*']}.md`;
const file = `${getDocPath(context)}/${params['*']}.md`;
const readme = await getFileContent(context, file);
const content = parse(readme);

Expand Down
4 changes: 2 additions & 2 deletions guide/app/routes/_index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import {
import { useLoaderData } from '@remix-run/react';
import { collectHeadings, parse } from '~/markdoc';
import { Markdown } from '~/components';
import { formatTitle, getFileContent } from '~/util';
import { getDocPath, formatTitle, getFileContent } from '~/util';

export const headers: HeadersFunction = ({ loaderHeaders }) => {
return loaderHeaders;
Expand All @@ -22,7 +22,7 @@ export const meta: MetaFunction<typeof loader> = ({ data }) => {
};

export async function loader({ context }: LoaderFunctionArgs) {
const file = 'docs/overview.md';
const file = `${getDocPath(context)}/overview.md`;
const readme = await getFileContent(context, file);
const content = parse(readme);

Expand Down
57 changes: 54 additions & 3 deletions guide/app/util.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,33 @@
import type { Endpoints } from '@octokit/types';
import type { AppLoadContext } from '@remix-run/cloudflare';

interface Language {
code: string;
label: string;
branch: string;
docPath: string;
domain: string;
isDecodeUtf8: boolean;
}
export const allLanguages: Language[] = [
{
code: 'en',
label: 'en',
branch: 'main',
docPath: 'docs',
domain: 'conform.guide',
isDecodeUtf8: false,
},
{
code: 'ja',
label: 'ja',
branch: 'ja',
docPath: 'docs/ja',
domain: 'ja.conform.guide',
isDecodeUtf8: true,
},
];

export function invariant(
expectedCondition: boolean,
message: string,
Expand All @@ -11,10 +38,12 @@ export function invariant(
}

export function getMetadata(context: AppLoadContext) {
const branch = getBranch(context);
return {
owner: 'edmundhung',
repo: 'conform',
ref: getBranch(context),
ref: branch,
language: getLanguage(branch),
};
}

Expand All @@ -34,11 +63,30 @@ export function getCache(context: AppLoadContext): KVNamespace {
return context.env.CACHE;
}

export const getDocPath = (context: AppLoadContext) => {
const { docPath } = getMetadata(context).language;
return docPath;
};

export function getLanguage(code: string): Language {
const language = allLanguages.find((lang) => lang.code === code);
return language ?? allLanguages[0]; // default to English
}

function base64DecodeUtf8(base64String: string) {
var binaryString = atob(base64String);
var charCodeArray = Array.from(binaryString).map((char) =>
char.charCodeAt(0),
);
var uintArray = new Uint8Array(charCodeArray);
return new TextDecoder('utf-8').decode(uintArray);
}

export async function getFileContent(
context: AppLoadContext,
path: string,
): Promise<string> {
const { ref, owner, repo } = getMetadata(context);
const { ref, owner, repo, language } = getMetadata(context);
const cache = getCache(context);
const cacheKey = `${ref}/${path}`;

Expand All @@ -54,7 +102,10 @@ export async function getFileContent(
repo,
});

content = atob(file.content);
// Japanese characters including UTF-8 are garbled, so convert to binary string before decoding
content = language.isDecodeUtf8
? base64DecodeUtf8(file.content)
: atob(file.content);
context.waitUntil(
cache.put(cacheKey, content, {
expirationTtl: 3600,
Expand Down
Loading