Skip to content

Commit

Permalink
docs(guide): added language switcher dropdown and Japanese language s…
Browse files Browse the repository at this point in the history
…upport. (#544)
  • Loading branch information
coji authored Mar 31, 2024
1 parent 28f453b commit c540df8
Show file tree
Hide file tree
Showing 4 changed files with 204 additions and 73 deletions.
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

0 comments on commit c540df8

Please sign in to comment.