Skip to content
Draft
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
4 changes: 3 additions & 1 deletion .github/actions/prepare-runner/action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -26,4 +26,6 @@ runs:

- name: Install dependencies
shell: bash
run: pnpm install --frozen-lockfile
run: |
./scripts/create_npmrc.sh
pnpm install --frozen-lockfile
2 changes: 2 additions & 0 deletions .github/workflows/check.yml
Original file line number Diff line number Diff line change
Expand Up @@ -15,4 +15,6 @@ jobs:
steps:
- uses: actions/checkout@v4
- uses: ./.github/actions/prepare-runner
env:
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
- run: pnpm check
2 changes: 2 additions & 0 deletions .github/workflows/lint.yml
Original file line number Diff line number Diff line change
Expand Up @@ -15,5 +15,7 @@ jobs:
steps:
- uses: actions/checkout@v4
- uses: ./.github/actions/prepare-runner
env:
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
- run: pnpm sync
- run: pnpm lint
12 changes: 12 additions & 0 deletions astro.config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,15 @@ import tailwindcss from "@tailwindcss/vite";
import starlightOpenAPI from "starlight-openapi";
import starlightDocSearch from "@astrojs/starlight-docsearch";

import { tsImport } from "tsx/esm/api";
import vercel from "@astrojs/vercel";
import remarkMath from "remark-math";
import rehypeKatex from "rehype-katex";
import rehypeRaw from "rehype-raw";
import sitemap from "@astrojs/sitemap";
import partytown from "@astrojs/partytown";
import node from "@astrojs/node";
import vercelMiddlewareIntegration from "@aptos-foundation/astro-vercel-middleware";
import react from "@astrojs/react";
import starlightLlmsTxt from "starlight-llms-txt";
import favicons from "astro-favicons";
Expand All @@ -28,6 +30,11 @@ import onDemandDirective from "./src/integrations/client-on-demand/register.js";
// import { isMoveReferenceEnabled } from "./src/utils/isMoveReferenceEnabled";
// import { rehypeAddDebug } from "./src/plugins";

const i18nMatcherGenerator = await tsImport(
"./integrations/i18n-matcher-generator/index.ts",
import.meta.url,
).then((m) => m.default);

const ALGOLIA_APP_ID = ENV.ALGOLIA_APP_ID;
const ALGOLIA_SEARCH_API_KEY = ENV.ALGOLIA_SEARCH_API_KEY;
const ALGOLIA_INDEX_NAME = ENV.ALGOLIA_INDEX_NAME;
Expand Down Expand Up @@ -201,6 +208,11 @@ export default defineConfig({
],
},
}),
i18nMatcherGenerator({
supportedLanguages: SUPPORTED_LANGUAGES,
manualMatchers: ["/en", "/en/:path*"],
}),
vercelMiddlewareIntegration(),
],
adapter: process.env.VERCEL
? vercel({
Expand Down
130 changes: 130 additions & 0 deletions integrations/i18n-matcher-generator/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
import { fileURLToPath } from "node:url";
import path from "node:path";
import fs from "node:fs/promises";
import type { AstroConfig, AstroIntegration } from "astro";
import { getDirectoriesList, getGeneratedFileTemplate, isDirectory, isPathExist } from "./utils";

interface SupportedLanguage {
code: string;
label: string;
default?: boolean;
}

/**
* Manual routes for middleware configuration
* Add any custom routes here that aren't automatically detected
*
* Examples: '/custom-page/:path*', '/api/:path*',
*/
type ManualRouteMatcher = string;

interface i18nMatcherGeneratorOptions {
supportedLanguages: SupportedLanguage[];
manualMatchers?: ManualRouteMatcher[];
}

export default function i18nMatcherGenerator({
supportedLanguages,
manualMatchers,
}: i18nMatcherGeneratorOptions) {
let astroConfig: AstroConfig;

const integration: AstroIntegration = {
name: "i18n-matcher-generator",
hooks: {
"astro:config:setup": ({ config }) => {
astroConfig = config;
},
"astro:routes:resolved": async ({ logger }) => {
const rootDir = fileURLToPath(astroConfig.root);

// Get non-English locale codes
const NON_ENGLISH_LOCALES = supportedLanguages
.filter((lang) => lang.code !== "en")
.map((lang) => lang.code);

// Discover content paths from src/content/docs
const contentDocsPath = path.join(rootDir, "src/content/docs");
const contentDirs = await getDirectoriesList(contentDocsPath, NON_ENGLISH_LOCALES).catch(
(error: unknown) => {
console.warn(`Could not read directories from ${contentDocsPath}: ${String(error)}`);

return [];
},
);
const contentPaths = contentDirs.map((dir) => `/${dir}/:path*`);
// Discover paths from src/pages/[...lang]
const langPagesPath = path.join(rootDir, "src/pages/[...lang]");
let pagePaths: string[] = [];

if (await isDirectory(langPagesPath)) {
try {
const pageFiles = await fs.readdir(langPagesPath, { withFileTypes: true });

// Get .astro files
const astroFiles = pageFiles
.filter((file) => file.isFile() && file.name.endsWith(".astro"))
.map((file) => `/${file.name.replace(".astro", "")}`);

// Get directories
const pageDirectories = pageFiles
.filter((dirent) => dirent.isDirectory())
.map((dirent) => `/${dirent.name}/:path*`);

pagePaths = [...astroFiles, ...pageDirectories];
} catch (error) {
logger.warn(`Could not read files from ${langPagesPath}: ${String(error)}`);
}
}

// Check if API reference is enabled from environment variables
const apiReferencePaths = [];
try {
const envPath = path.join(rootDir, ".env");
if (await isPathExist(envPath)) {
const envContent = await fs.readFile(envPath, "utf8");
if (envContent.includes("ENABLE_API_REFERENCE=true")) {
apiReferencePaths.push("/api-reference/:path*");
}
}
} catch (error) {
logger.warn(`Could not check for API reference configuration: ${String(error)}`);
}

// Combine all content paths
const ALL_CONTENT_PATHS = [
"/",
...contentPaths,
...pagePaths,
...apiReferencePaths,
...(manualMatchers ?? []),
];

// Generate language-specific paths
const LANGUAGE_PATHS: string[] = [];
NON_ENGLISH_LOCALES.forEach((code) => {
// Add the base language path with exact matching to avoid matching _astro paths
LANGUAGE_PATHS.push(`/${code}$`);

// Add localized versions of all content paths (except the root path)
ALL_CONTENT_PATHS.forEach((contentPath) => {
// Skip the root path as we already have /{code}
if (contentPath !== "/") {
LANGUAGE_PATHS.push(`/${code}${contentPath}`);
}
});
});

// Combine all paths
const ALL_PATHS = [...ALL_CONTENT_PATHS, ...LANGUAGE_PATHS];

// Write the file
const outputPath = path.join(rootDir, "src/middlewares/matcher-routes-dynamic.js");
await fs.writeFile(outputPath, getGeneratedFileTemplate(ALL_PATHS));
logger.info("Middleware matcher file generated successfully!");
},
},
};

return integration;
}
37 changes: 37 additions & 0 deletions integrations/i18n-matcher-generator/utils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import fs from "node:fs/promises";

/**
* Function to get directories from a path, excluding language directories
*/
export async function getDirectoriesList(path: string, excludeDirs: string[] = []) {
return fs
.readdir(path, { withFileTypes: true })
.then((dirents) =>
dirents
.filter((dirent) => dirent.isDirectory() && !excludeDirs.includes(dirent.name))
.map((dirent) => dirent.name),
);
}

export async function isPathExist(path: string) {
try {
await fs.access(path);
return true;
} catch {
return false;
}
}

export async function isDirectory(path: string) {
const stat = await fs.stat(path);

return stat.isDirectory();
}

export function getGeneratedFileTemplate(allPaths: string[]): string {
return `// THIS FILE IS AUTO-GENERATED - DO NOT EDIT DIRECTLY
// Generated on ${new Date().toISOString()}

export const i18MatcherRegexp = new RegExp(\`^(${allPaths.map((p) => p.replace(/:\w+\*/g, "[^/]+")).join("|")})$\`);
`;
}
Loading