-
Notifications
You must be signed in to change notification settings - Fork 10
Translate navigation and shared documentation UI with GT #350
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
Changes from all commits
Commits
Show all changes
7 commits
Select commit
Hold shift + click to select a range
02ef069
Make sidebar navigation translatable by GT
Blargian 7d32dd6
Normalize non-standard Shiki language labels
Blargian d3b45f0
Keep OpenAPI operations out of locale path transforms
Blargian f45a3b3
Translate shared documentation interface with GT
Blargian 6edd4d0
Add locale routes for generated API pages
Blargian 739a791
Merge remote-tracking branch 'origin/main' into codex/translate-sideb…
Blargian 6757853
Read translations project ID from Actions secrets
Blargian File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,214 @@ | ||
| import fs from "node:fs"; | ||
| import path from "node:path"; | ||
|
|
||
| const repositoryRoot = process.cwd(); | ||
| const configPath = path.join(repositoryRoot, "gt.config.json"); | ||
| const docsPath = path.join(repositoryRoot, "docs.json"); | ||
| const uiStringsPath = path.join(repositoryRoot, "i18n", "en", "ui.json"); | ||
| const translatableKeys = [ | ||
| "group", | ||
| "tab", | ||
| "item", | ||
| "anchor", | ||
| "dropdown", | ||
| ] as const; | ||
| const expectedSelectors = translatableKeys.map((key) => `$..${key}`); | ||
| const pagePathMatch = "^/?([^\\s]+)$"; | ||
|
|
||
| function fail(message: string): never { | ||
| throw new Error(`check-gt-navigation: ${message}`); | ||
| } | ||
|
|
||
| function readJson(filePath: string): unknown { | ||
| return JSON.parse(fs.readFileSync(filePath, "utf8")); | ||
| } | ||
|
|
||
| function asRecord( | ||
| value: unknown, | ||
| description: string, | ||
| ): Record<string, unknown> { | ||
| if (!value || typeof value !== "object" || Array.isArray(value)) { | ||
| fail(`${description} must be an object`); | ||
| } | ||
|
|
||
| return value as Record<string, unknown>; | ||
| } | ||
|
|
||
| function assertTranslationSchema(): void { | ||
| const config = asRecord(readJson(configPath), "gt.config.json"); | ||
| const options = asRecord(config.options, "gt.config.json options"); | ||
| const jsonSchema = asRecord( | ||
| options.jsonSchema, | ||
| "gt.config.json options.jsonSchema", | ||
| ); | ||
| const docsSchema = asRecord( | ||
| jsonSchema["./docs.json"], | ||
| "the ./docs.json translation schema", | ||
| ); | ||
| const files = asRecord(config.files, "gt.config.json files"); | ||
| const jsonFiles = asRecord(files.json, "gt.config.json JSON files"); | ||
|
|
||
| if ( | ||
| !Array.isArray(jsonFiles.include) || | ||
| !jsonFiles.include.includes("./i18n/[locale]/ui.json") | ||
| ) { | ||
| fail("the JSON translation inputs must include ./i18n/[locale]/ui.json"); | ||
| } | ||
|
|
||
| if (docsSchema.resolveRefs !== true) { | ||
| fail( | ||
| "the ./docs.json translation schema must resolve navigation $ref files", | ||
| ); | ||
| } | ||
|
|
||
| const composite = asRecord( | ||
| docsSchema.composite, | ||
| "the ./docs.json composite schema", | ||
| ); | ||
| const languages = asRecord( | ||
| composite["$.navigation.languages"], | ||
| "the $.navigation.languages composite schema", | ||
| ); | ||
|
|
||
| if ( | ||
| languages.type !== "array" || | ||
| languages.key !== "$.language" || | ||
| languages.splitEntries !== true | ||
| ) { | ||
| fail( | ||
| "the language navigation schema must remain a split array keyed by $.language", | ||
| ); | ||
| } | ||
|
|
||
| if (!Array.isArray(languages.include)) { | ||
| fail("the language navigation schema must declare its translatable fields"); | ||
| } | ||
|
|
||
| const included = new Set(languages.include); | ||
| for (const selector of expectedSelectors) { | ||
| if (!included.has(selector)) { | ||
| fail(`the language navigation schema must include ${selector}`); | ||
| } | ||
| } | ||
|
|
||
| const transform = asRecord( | ||
| languages.transform, | ||
| "the language navigation path transforms", | ||
| ); | ||
| const pagesRule = asRecord( | ||
| transform["$..pages[*]"], | ||
| "the $..pages[*] transform", | ||
| ); | ||
| if ( | ||
| pagesRule.match !== pagePathMatch || | ||
| pagesRule.replace !== "{locale}/$1" | ||
| ) { | ||
| fail( | ||
| "$..pages[*] must localize page paths without matching OpenAPI operation pointers", | ||
| ); | ||
| } | ||
|
|
||
| const rootRule = asRecord(transform["$..root"], "the $..root transform"); | ||
| if (rootRule.match !== "^/?(.*)$" || rootRule.replace !== "{locale}/$1") { | ||
| fail("$..root must map navigation roots to {locale}/$1"); | ||
| } | ||
| } | ||
|
|
||
| function assertUiStrings(): void { | ||
| const ui = asRecord(readJson(uiStringsPath), "the English UI dictionary"); | ||
| for (const section of [ | ||
| "account", | ||
| "actions", | ||
| "navigation", | ||
| "search", | ||
| "status", | ||
| "theme", | ||
| "toc", | ||
| ]) { | ||
| const messages = asRecord( | ||
| ui[section], | ||
| `the English UI dictionary's ${section} section`, | ||
| ); | ||
| if (Object.keys(messages).length === 0) { | ||
| fail(`the English UI dictionary's ${section} section must not be empty`); | ||
| } | ||
| for (const [key, value] of Object.entries(messages)) { | ||
| if (typeof value !== "string" || value.trim() === "") { | ||
| fail( | ||
| `the English UI dictionary entry ${section}.${key} must be a non-empty string`, | ||
| ); | ||
| } | ||
| } | ||
| } | ||
| } | ||
|
|
||
| function assertNavigationCoverage(): void { | ||
| const docs = asRecord(readJson(docsPath), "docs.json"); | ||
| const navigation = asRecord(docs.navigation, "docs.json navigation"); | ||
| if (!Array.isArray(navigation.languages)) { | ||
| fail("docs.json navigation.languages must be an array"); | ||
| } | ||
|
|
||
| const english = navigation.languages.find((entry) => { | ||
| return Boolean( | ||
| entry && | ||
| typeof entry === "object" && | ||
| !Array.isArray(entry) && | ||
| entry.language === "en", | ||
| ); | ||
| }); | ||
| if (!english) { | ||
| fail("docs.json must contain an English navigation entry"); | ||
| } | ||
|
|
||
| const visitedFiles = new Set<string>(); | ||
| let labelCount = 0; | ||
|
|
||
| function visit(value: unknown, sourceDirectory: string): void { | ||
| if (Array.isArray(value)) { | ||
| for (const entry of value) visit(entry, sourceDirectory); | ||
| return; | ||
| } | ||
|
|
||
| if (!value || typeof value !== "object") return; | ||
| const record = value as Record<string, unknown>; | ||
|
|
||
| if (typeof record.$ref === "string") { | ||
| const referencedPath = path.resolve(sourceDirectory, record.$ref); | ||
| const relativePath = path.relative(repositoryRoot, referencedPath); | ||
| if (relativePath.startsWith("..") || path.isAbsolute(relativePath)) { | ||
| fail(`navigation reference escapes the repository: ${record.$ref}`); | ||
| } | ||
| if (!fs.existsSync(referencedPath)) { | ||
| fail(`navigation reference does not exist: ${relativePath}`); | ||
| } | ||
| if (visitedFiles.has(referencedPath)) return; | ||
| visitedFiles.add(referencedPath); | ||
| visit(readJson(referencedPath), path.dirname(referencedPath)); | ||
| return; | ||
| } | ||
|
|
||
| for (const [key, entry] of Object.entries(record)) { | ||
| if (translatableKeys.includes(key as (typeof translatableKeys)[number])) { | ||
| if (typeof entry !== "string" || entry.trim() === "") { | ||
| fail(`${key} in the English navigation must be a non-empty string`); | ||
| } | ||
| labelCount += 1; | ||
| } | ||
| visit(entry, sourceDirectory); | ||
| } | ||
| } | ||
|
|
||
| visit(english, repositoryRoot); | ||
| if (labelCount === 0) { | ||
| fail("the English navigation contains no translatable labels"); | ||
| } | ||
|
|
||
| console.log( | ||
| `check-gt-navigation: ${labelCount} labels across ${visitedFiles.size} referenced navigation files`, | ||
| ); | ||
| } | ||
|
|
||
| assertTranslationSchema(); | ||
| assertUiStrings(); | ||
| assertNavigationCoverage(); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,63 @@ | ||
| { | ||
| "account": { | ||
| "clickHouseOnGitHub": "ClickHouse on GitHub", | ||
| "getStarted": "Get Started", | ||
| "signIn": "Sign in" | ||
| }, | ||
| "actions": { | ||
| "choosePath": "Choose your path", | ||
| "copied": "Copied", | ||
| "copyFailed": "Couldn't copy", | ||
| "copyPage": "Copy page", | ||
| "editPage": "Edit this page", | ||
| "reportIssue": "Report an issue", | ||
| "setupDocsMcp": "Set up docs MCP", | ||
| "updated": "Updated", | ||
| "view": "View", | ||
| "viewMarkdown": "View as Markdown" | ||
| }, | ||
| "navigation": { | ||
| "breadcrumb": "Breadcrumb", | ||
| "chooseLanguage": "Choose documentation language", | ||
| "close": "Close navigation", | ||
| "documentationTools": "Documentation tools", | ||
| "home": "Home", | ||
| "homeAria": "ClickHouse Documentation home page", | ||
| "navigation": "Navigation", | ||
| "next": "Next", | ||
| "open": "Open navigation", | ||
| "pages": "Pages", | ||
| "pagination": "Pagination", | ||
| "previous": "Previous", | ||
| "primary": "Primary navigation", | ||
| "site": "Site navigation", | ||
| "toggleSection": "Toggle {label} section" | ||
| }, | ||
| "search": { | ||
| "ask": "Ask", | ||
| "askAi": "Ask AI", | ||
| "askAiAboutClickHouse": "Ask AI about ClickHouse", | ||
| "filter": "Filter…", | ||
| "filterNavigation": "Filter navigation", | ||
| "search": "Search", | ||
| "searchDocumentation": "Search documentation", | ||
| "searchShort": "Search…" | ||
| }, | ||
| "status": { | ||
| "draft": "Draft", | ||
| "forHumans": "For humans", | ||
| "loading": "Loading…" | ||
| }, | ||
| "theme": { | ||
| "dark": "Switch to dark theme", | ||
| "light": "Switch to light theme", | ||
| "preference": "Theme preference", | ||
| "system": "Switch to system theme" | ||
| }, | ||
| "toc": { | ||
| "jumpToSection": "Jump to section", | ||
| "onThisPage": "On this page", | ||
| "overview": "Overview", | ||
| "tableOfContents": "Table of contents" | ||
| } | ||
| } |
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.