Skip to content
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
5 changes: 5 additions & 0 deletions .changeset/tidy-crabs-shave.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"lynx-console": patch
---

README 중국어(간체) 번역을 추가하고, 세 README 의 언어 전환 줄을 맞췄어요
9 changes: 7 additions & 2 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,11 +31,14 @@ yarn format # biome 자동 수정

## 문서 사이트 규칙

- 콘텐츠는 `docs/src/en` 과 `docs/src/ko` 에 **양쪽 다** 둬요. 한쪽만 고치지 않아요.
- 콘텐츠는 `docs/src/en` · `docs/src/ko` · `docs/src/zh` 에 **세 벌 다** 둬요. 한쪽만 고치지 않아요.
중국어는 간체(简体中文)로 써요.
- 한국어 문서는 해요체로 써요. 영어를 그대로 옮긴 듯한 문장이나, 코드 스팬 뒤 조사 앞
띄어쓰기(`` `foo` 는 ``)를 만들지 않아요.
- 기본 언어(en)는 라우트에서 `/en` 접두사가 빠져요. 영문 문서의 내부 링크는 `/guide/...`,
한국어는 `/ko/guide/...` 로 써요.
한국어는 `/ko/guide/...`, 중국어는 `/zh/guide/...` 로 써요.
- 로케일 목록은 `docs/siteMeta.mjs` 의 `LOCALES` 한 곳에 있어요. 언어를 추가하면 여기와
`docs/rspress.config.ts`, 홈 컴포넌트의 `TEXT` 사전, `head-meta.mjs` 의 `DEFAULTS` 를 같이 채워요.
- `.md` 에서는 raw HTML/JSX 가 렌더되지 않고 사라져요. `<a>`, `<img>`, style 객체가 필요하면
파일 확장자를 `.mdx` 로 바꿔요.
- `docs/src/public/` 은 전부 생성물이에요. `docs/scripts/sync-demo.mjs` 가 예제 빌드
Expand Down Expand Up @@ -67,6 +70,8 @@ yarn format # biome 자동 수정
쓰지 않아요. 클론 디렉터리 이름에 우연히 매칭돼 로컬만 통과하고 CI 에서 깨져요.
`path.resolve(__dirname, ...)` 나 `[\\/]node_modules[\\/]<pkg>[\\/]` 형태로 써요.
- `package/**` 를 고치면 changeset 이 필요해요 (CI 가 검사해요). `yarn changeset` 으로 만들어요.
- README 는 `package/README.md`(en) · `README_ko.md` · `README_zh.md` 세 벌이에요. 루트의 같은 이름
파일은 심링크예요. 하나를 고치면 나머지도 맞춰요.
- 커밋 메시지는 한국어로 써요.

## 참고 문서
Expand Down
1 change: 1 addition & 0 deletions README_zh.md
3 changes: 2 additions & 1 deletion docs/components/HomeHero.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -25,12 +25,13 @@ const TEXT = {
openInTab: "Open the demo in a new tab",
},
ko: { explorer: "Lynx Explorer로 열기", openInTab: "새 탭에서 데모 열기" },
zh: { explorer: "用 Lynx Explorer 打开", openInTab: "在新标签页打开演示" },
} as const;

export function HomeHero() {
const { page } = usePageData();
const lang = useLang();
const t = TEXT[lang === "ko" ? "ko" : "en"];
const t = TEXT[lang in TEXT ? (lang as keyof typeof TEXT) : "en"];
const hero = (page.frontmatter?.hero ?? {}) as Hero;

return (
Expand Down
8 changes: 7 additions & 1 deletion docs/components/QuickStart.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -33,11 +33,17 @@ const TEXT = {
moreLink: "/ko/guide/getting-started",
steps: ["설치", "진입점에서 초기화", "컴포넌트 렌더"],
},
zh: {
title: "三步就能跑起来",
more: "完整指南 →",
moreLink: "/zh/guide/getting-started",
steps: ["安装", "在入口初始化", "渲染组件"],
},
} as const;

export function QuickStart() {
const lang = useLang();
const t = TEXT[lang === "ko" ? "ko" : "en"];
const t = TEXT[lang in TEXT ? (lang as keyof typeof TEXT) : "en"];
const snippets = [INSTALL, INIT, RENDER];

return (
Expand Down
41 changes: 41 additions & 0 deletions docs/rspress.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,12 @@ export default defineConfig({
title: "lynx-console",
description: "Lynx 앱에 내장할 수 있는 인앱 개발자 콘솔",
},
{
lang: "zh",
label: "简体中文",
title: "lynx-console",
description: "可以嵌入 Lynx 应用的开发者控制台",
},
],
globalStyles: path.resolve("styles/global.css"),
route: {
Expand Down Expand Up @@ -107,6 +113,41 @@ export default defineConfig({
],
},
},
{
lang: "zh",
label: "简体中文",
outlineTitle: "目录",
prevPageText: "上一页",
nextPageText: "下一页",
editLink: {
docRepoBaseUrl: `${GITHUB_URL}/tree/main/docs/src`,
text: "在 GitHub 上编辑此页",
},
nav: [
{ text: "指南", link: "/zh/guide/getting-started" },
{ text: "API", link: "/zh/api/" },
{ text: "演示", link: "/zh/guide/demo" },
],
sidebar: {
"/zh/guide/": [
{
text: "指南",
items: [
{ text: "介绍", link: "/zh/guide/introduction" },
{ text: "快速开始", link: "/zh/guide/getting-started" },
{ text: "自定义标签页与 ref", link: "/zh/guide/customizing" },
{ text: "体验演示", link: "/zh/guide/demo" },
],
},
],
"/zh/api/": [
{
text: "API",
items: [{ text: "API 参考", link: "/zh/api/" }],
},
],
},
},
],
},
});
47 changes: 27 additions & 20 deletions docs/scripts/head-meta.mjs
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
import fs from "node:fs";
import { SITE_URL } from "../siteMeta.mjs";
import {
alternatePaths,
DEFAULT_LOCALE,
LOCALES,
SITE_URL,
stripLocale,
} from "../siteMeta.mjs";

const SITE_NAME = "lynx-console";
const OG_IMAGE = `${SITE_URL}/og.png`;
Expand All @@ -9,11 +15,19 @@ const DEFAULTS = {
title: "lynx-console",
description:
"An in-app developer console for Lynx apps. Read console logs, fetch requests, and performance metrics on a real device, with no debugger attached.",
ogLocale: "en_US",
},
ko: {
title: "lynx-console",
description:
"Lynx 앱에 넣는 인앱 개발자 콘솔이에요. 디버거 없이도 실기기에서 콘솔 로그, fetch 요청, 성능 지표를 봐요.",
ogLocale: "ko_KR",
},
zh: {
title: "lynx-console",
description:
"可以嵌入 Lynx 应用的开发者控制台。不用连调试器,就能在真机上查看控制台日志、fetch 请求和性能指标。",
ogLocale: "zh_CN",
},
};

Expand All @@ -37,20 +51,11 @@ function readFrontmatter(absolutePath) {
}
}

// `/ko/guide/demo` <-> `/guide/demo`. 기본 언어(en)는 접두사가 없어요.
function toPath(routePath) {
const clean = routePath.replace(/index$/, "").replace(/\/+$/, "/");
return clean.startsWith("/") ? clean : `/${clean}`;
}

function alternates(routePath) {
const p = toPath(routePath);
const isKo = p === "/ko" || p.startsWith("/ko/");
const en = isKo ? p.replace(/^\/ko/, "") || "/" : p;
const ko = isKo ? p : `/ko${p === "/" ? "/" : p}`;
return { en, ko };
}

function escapeAttr(value) {
return String(value)
.replace(/&/g, "&amp;")
Expand All @@ -59,27 +64,29 @@ function escapeAttr(value) {
}

export function buildHead(route) {
const lang = route.lang === "ko" ? "ko" : "en";
const lang = LOCALES.includes(route.lang) ? route.lang : DEFAULT_LOCALE;
const fm = readFrontmatter(route.absolutePath);
const fallback = DEFAULTS[lang];
const isHome =
toPath(route.routePath) === "/" || toPath(route.routePath) === "/ko/";
const routePath = toPath(route.routePath);
const isHome = stripLocale(routePath) === "/";
const title = isHome
? [SITE_NAME, fm.titleSuffix].filter(Boolean).join(" - ")
: `${fm.title ?? fallback.title} - ${SITE_NAME}`;
const description = fm.description ?? fallback.description;

const canonical = `${SITE_URL}${toPath(route.routePath)}`;
const { en, ko } = alternates(route.routePath);
const canonical = `${SITE_URL}${routePath}`;
const alternates = alternatePaths(routePath);

const tags = [
`<link rel="canonical" href="${escapeAttr(canonical)}">`,
`<link rel="alternate" hreflang="en" href="${escapeAttr(`${SITE_URL}${en}`)}">`,
`<link rel="alternate" hreflang="ko" href="${escapeAttr(`${SITE_URL}${ko}`)}">`,
`<link rel="alternate" hreflang="x-default" href="${escapeAttr(`${SITE_URL}${en}`)}">`,
...LOCALES.map(
(l) =>
`<link rel="alternate" hreflang="${l}" href="${escapeAttr(`${SITE_URL}${alternates[l]}`)}">`,
),
`<link rel="alternate" hreflang="x-default" href="${escapeAttr(`${SITE_URL}${alternates[DEFAULT_LOCALE]}`)}">`,
`<meta property="og:type" content="website">`,
`<meta property="og:site_name" content="${SITE_NAME}">`,
`<meta property="og:locale" content="${lang === "ko" ? "ko_KR" : "en_US"}">`,
`<meta property="og:locale" content="${fallback.ogLocale}">`,
`<meta property="og:url" content="${escapeAttr(canonical)}">`,
`<meta property="og:title" content="${escapeAttr(title)}">`,
`<meta property="og:description" content="${escapeAttr(description)}">`,
Expand All @@ -94,7 +101,7 @@ export function buildHead(route) {
"@type": "SoftwareSourceCode",
name: SITE_NAME,
description,
url: `${SITE_URL}${toPath(route.routePath)}`,
url: `${SITE_URL}${routePath}`,
codeRepository: "https://github.com/daangn/lynx-console",
programmingLanguage: "TypeScript",
runtimePlatform: "Lynx",
Expand Down
24 changes: 12 additions & 12 deletions docs/scripts/postbuild.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,12 @@
import fs from "node:fs";
import path from "node:path";
import { fileURLToPath } from "node:url";
import { SITE_URL } from "../siteMeta.mjs";
import {
alternatePaths,
DEFAULT_LOCALE,
LOCALES,
SITE_URL,
} from "../siteMeta.mjs";

const __dirname = path.dirname(fileURLToPath(import.meta.url));
const outDir = path.resolve(__dirname, "..", "doc_build");
Expand Down Expand Up @@ -30,26 +35,21 @@ function collectHtml(dir, base = "") {
return routes;
}

function alternates(route) {
const isKo = route === "/ko/" || route.startsWith("/ko/");
const en = isKo ? route.replace(/^\/ko/, "") || "/" : route;
const ko = isKo ? route : `/ko${route}`;
return { en, ko };
}

const routes = collectHtml(outDir).sort();
const today = new Date().toISOString().slice(0, 10);

const urls = routes
.map((route) => {
const { en, ko } = alternates(route);
const alternates = alternatePaths(route);
return [
" <url>",
` <loc>${SITE_URL}${route}</loc>`,
` <lastmod>${today}</lastmod>`,
` <xhtml:link rel="alternate" hreflang="en" href="${SITE_URL}${en}"/>`,
` <xhtml:link rel="alternate" hreflang="ko" href="${SITE_URL}${ko}"/>`,
` <xhtml:link rel="alternate" hreflang="x-default" href="${SITE_URL}${en}"/>`,
...LOCALES.map(
(lang) =>
` <xhtml:link rel="alternate" hreflang="${lang}" href="${SITE_URL}${alternates[lang]}"/>`,
),
` <xhtml:link rel="alternate" hreflang="x-default" href="${SITE_URL}${alternates[DEFAULT_LOCALE]}"/>`,
" </url>",
].join("\n");
})
Expand Down
25 changes: 25 additions & 0 deletions docs/siteMeta.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -8,3 +8,28 @@ export function demoBundleUrl(siteUrl = SITE_URL) {
export function lynxExplorerUrl(siteUrl = SITE_URL) {
return `lynx://open?url=${encodeURIComponent(demoBundleUrl(siteUrl))}`;
}

export const DEFAULT_LOCALE = "en";
export const LOCALES = ["en", "ko", "zh"];

// 기본 언어(en)는 라우트에서 접두사가 빠져요. `/zh/guide/demo` <-> `/guide/demo`.
export function stripLocale(routePath) {
for (const lang of LOCALES) {
if (lang === DEFAULT_LOCALE) continue;
if (routePath === `/${lang}` || routePath === `/${lang}/`) return "/";
if (routePath.startsWith(`/${lang}/`))
return routePath.slice(`/${lang}`.length);
}
return routePath;
}

// 라우트 하나를 모든 언어의 경로로 펼쳐요. hreflang 과 sitemap 이 같이 써요.
export function alternatePaths(routePath) {
const base = stripLocale(routePath);
return Object.fromEntries(
LOCALES.map((lang) => [
lang,
lang === DEFAULT_LOCALE ? base : `/${lang}${base}`,
]),
);
}
46 changes: 46 additions & 0 deletions docs/src/zh/api/index.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
---
title: API 参考
description: props、console handle 和监视器初始化函数。
---

# API 参考

## `LynxConsole` props

| Prop | 类型 | 默认值 | 说明 |
| --- | --- | --- | --- |
| `theme` | `"light" \| "dark"` | `"light"` | 控制台 UI 主题。 |
| `safeAreaInsetBottom` | `string` | `"50px"` | 面板底部的安全区域内边距。 |
| `customTabs` | `CustomTab[]` | `undefined` | 要在控制台里额外显示的标签页。 |
| `initialPosition` | `{ top?: number; left?: number; right?: number; bottom?: number }` | `{ right: 16, bottom: 84 }` | 悬浮按钮的初始位置(px)。四个方向互相独立,所以可以吸附到任意一个角(例如 `{ top: 50, left: 16 }`)。同时给了 `top` 和 `bottom`(或 `left` 和 `right`)时,`top` / `left` 生效。用户拖动过按钮之后,保存下来的位置优先。 |

## `CustomTab`

| 属性 | 类型 | 说明 |
| --- | --- | --- |
| `key` | `string` | 标签页的唯一标识。 |
| `label` | `string` | 标签页的显示文字。 |
| `renderContent` | `() => ReactNode` | 渲染标签页的内容。 |

## `LynxConsoleHandle`

通过 `LynxConsole` 上的 `ref` 拿到。

| 方法 | 说明 |
| --- | --- |
| `open()` | 打开控制台。 |
| `close()` | 关闭控制台。 |
| `isOpen()` | 返回控制台是否处于打开状态。 |

## 监视器初始化

从 `lynx-console/setup` 引入,在应用入口调用。

| 函数 | 说明 |
| --- | --- |
| `initLogMonitor()` | 捕获 `console.log`、`console.error` 等输出。 |
| `initMainThreadConsole()` | 捕获主线程的控制台输出。需要先调用 `initLogMonitor()`。 |
| `initNetworkMonitor()` | 拦截并记录 `fetch` 请求。 |
| `initPerformanceMonitor()` | 收集性能指标。 |

只有初始化过的监视器,才会显示对应的标签页。
Loading
Loading