Skip to content

Commit

Permalink
feat: implement support for custom localization in markdown content
Browse files Browse the repository at this point in the history
  • Loading branch information
MikaelSiidorow committed Oct 30, 2024
1 parent 13fee16 commit c011147
Show file tree
Hide file tree
Showing 4 changed files with 137 additions and 14 deletions.
5 changes: 4 additions & 1 deletion apps/web/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@
"tailwind-merge": "catalog:",
"tailwindcss": "catalog:",
"tailwindcss-animate": "catalog:",
"unist-util-visit": "^5.0.0",
"use-scramble": "^2.2.15",
"zod": "^3.23.8"
},
Expand All @@ -46,6 +47,7 @@
"@tietokilta/config-typescript": "workspace:*",
"@tietokilta/eslint-config": "workspace:*",
"@types/lodash": "^4.17.13",
"@types/mdast": "^4.0.4",
"@types/node": "catalog:",
"@types/qs": "^6.9.16",
"@types/react": "catalog:",
Expand All @@ -54,6 +56,7 @@
"eslint": "catalog:",
"react-dvd-screensaver": "^0.1.1",
"typescript": "catalog:",
"typescript-eslint": "catalog:"
"typescript-eslint": "catalog:",
"unified": "^11.0.5"
}
}
6 changes: 5 additions & 1 deletion apps/web/src/app/[locale]/events/[slug]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ import { BackButton } from "../../../../components/back-button";
import { getCurrentLocale, getScopedI18n } from "../../../../locales/server";
import { DateTime } from "../../../../components/datetime";
import { openGraphImage } from "../../../shared-metadata";
import { remarkI18n } from "../../../../lib/plugins/remark-i18n";
import { SignUpButton } from "./signup-button";

async function SignUpText({
Expand Down Expand Up @@ -432,6 +433,7 @@ export const generateMetadata = async ({
};

export default async function Page({ params: { slug } }: PageProps) {
const locale = await getCurrentLocale();
const event = await fetchEvent(slug);
const t = await getScopedI18n("action");
if (!event.ok && event.error === "ilmomasiina-event-not-found") {
Expand All @@ -458,7 +460,9 @@ export default async function Page({ params: { slug } }: PageProps) {
<Tldr event={event.data} />
{event.data.description ? (
<div className="prose">
<Markdown remarkPlugins={[remarkGfm]}>
<Markdown
remarkPlugins={[[remarkI18n, { locale }], remarkGfm]}
>
{event.data.description}
</Markdown>
</div>
Expand Down
107 changes: 107 additions & 0 deletions apps/web/src/lib/plugins/remark-i18n.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
import type { Plugin } from "unified";
import type { Root, Definition, Parent } from "mdast";
import { visit } from "unist-util-visit";

interface Options {
locale?: string;
}

/**
* Split markdown content into multiple trees based on language definitions.
*
* Example:
* ```markdown
* Default language content
*
* [lang]: # (en)
* English content
* ```
*
* Will be transformed depending on the locale option to either:
* ```markdown
* Default language content
* ```
*
* or:
*
* ```markdown
* English content
* ```
*/
export const remarkI18n: Plugin<[options: Options] | undefined[], Root> =
function (options = {}, ..._ignored) {
const defaultLocaleTree: Root = { type: "root", children: [] };
const localeTrees = new Map<string, Root>();

return (tree: Root) => {
let currentLocale: string | null = null;
let lastDefinitionIndex = -1;

// Find all language definitions and split content
visit(
tree,
"definition",
(node: Definition, index?: number, parent?: Parent) => {
const isLangDefinition =
node.label === "lang" &&
node.identifier === "lang" &&
node.url === "#";
if (!isLangDefinition || !node.title) {
return;
}

const locale = node.title;
if (!localeTrees.has(locale)) {
localeTrees.set(locale, { type: "root", children: [] });
}

// Move nodes between last definition and this one to appropriate tree
const targetTree = currentLocale
? // eslint-disable-next-line @typescript-eslint/no-non-null-assertion -- checked above
localeTrees.get(currentLocale)!
: defaultLocaleTree;

const nodesToMove =
parent?.children.slice(lastDefinitionIndex + 1, index) ?? [];
targetTree.children.push(...nodesToMove);

currentLocale = locale;
lastDefinitionIndex = index ?? -1;
},
);

// Handle remaining nodes after the last definition
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition -- extra safety just in case
if (tree.type === "root") {
const remainingNodes = tree.children.slice(lastDefinitionIndex + 1);
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition -- false negative
const targetTree = currentLocale
? // eslint-disable-next-line @typescript-eslint/no-non-null-assertion -- we know it's there because of the last definition
localeTrees.get(currentLocale)!
: defaultLocaleTree;
targetTree.children.push(...remainingNodes);
}

const cleanTree = (_tree: Root) => {
_tree.children = _tree.children.filter(
(node) =>
!(
node.type === "definition" &&
node.label === "lang" &&
node.url === "#"
),
);
};

cleanTree(defaultLocaleTree);
localeTrees.forEach(cleanTree);

// Return the appropriate tree based on options
if (options.locale && localeTrees.has(options.locale)) {
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion -- checked above
return localeTrees.get(options.locale)!;
}

return defaultLocaleTree;
};
};
33 changes: 21 additions & 12 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

0 comments on commit c011147

Please sign in to comment.