Skip to content
Open
3 changes: 3 additions & 0 deletions docs/localization.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,9 @@ Each file within these collection folders corresponds to a real page on the rend

The ["ui" content collection](src/content/ui/) is a little different than the others. It contains yaml files that cover strings that are used across different pages for things like the navigation bar.

**Note on Translating Examples:**
When translating Example pages (`.mdx`), you do not need to copy or include the `remix` metadata block in the frontmatter. The website will automatically fall back to the English version to fetch the remix data.

## Routes and Layouts

Astro uses a file-based approach to generating routes. The filepath of each file in `src/pages` becomes the url of the page it renders. Because we need to support a url scheme where English translations of pages are served at a URL with no locale prefix (for example, the English version of the tutorials page is at https://p5.js/tutorials _not_ https://p5.js/en/tutorials), there are 2 sets of routing files and folders: one in `src/pages` and another `src/pages/[locale]`. The `[locale]` set are needed to build and serve the non-English pages (whose URLs are also prefixed by the language codes).
Expand Down
16 changes: 12 additions & 4 deletions src/layouts/ExampleLayout.astro
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import EditableSketch from "@components/EditableSketch/index.astro";
import RelatedItems from "@components/RelatedItems/index.astro";
import OutdatedTranslationBanner from "@components/OutdatedTranslationBanner/index.astro";
import { checkTranslationBanner } from "../utils/translationBanner";
import { getFallbackRemixData } from "../pages/_utils";

interface Props {
example: CollectionEntry<"examples">;
Expand Down Expand Up @@ -46,8 +47,15 @@ const relatedReferences =

const { Content } = await render(example);

// Use the fallback function to retrieve English remix data if the current locale doesn't have any
let remixData = (await getFallbackRemixData(
example.id,
currentLocale,
example.data.remix
)) as typeof example.data.remix;

// Extract the collective attribution year. If multiple provided, uses last shown.
const collectivelyAttributedSince = example.data.remix?.reduce(
const collectivelyAttributedSince = remixData?.reduce(
(acc: number | null, item) => {
if (item.collectivelyAttributedSince) {
return item.collectivelyAttributedSince;
Expand All @@ -58,7 +66,7 @@ const collectivelyAttributedSince = example.data.remix?.reduce(
);

// Boolean value on whether the remix history contains links to code
const remixHistoryHasCodeLinks = example.data.remix?.some(
const remixHistoryHasCodeLinks = remixData?.some(
(item) => Array.isArray(item.code) && item.code.length > 0
);

Expand Down Expand Up @@ -98,7 +106,7 @@ const { showBanner, englishUrl } = checkTranslationBanner(

<a href={Astro.url.pathname}>{example.data.title}</a>:{" "}

{example.data.remix?.map((item, i) => {
{remixData?.map((item, i) => {
const parts = [];

// Each remix entry requires at least one attribution
Expand Down Expand Up @@ -147,7 +155,7 @@ const { showBanner, englishUrl } = checkTranslationBanner(
{remixHistoryHasCodeLinks ? (
<>
{t("attribution", "You can find the code history of these examples here")}{": "}
{example.data.remix
{remixData
.map(item => item?.code)
.flat()
.filter(codeItem => codeItem && codeItem.URL)
Expand Down
34 changes: 34 additions & 0 deletions src/pages/_utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -465,3 +465,37 @@ const getUrl = (
return "";
}
};

/**
* Retrieves fallback remix (attribution/code history) data from the English example
* if the current localized example is missing it.
*
* @param currentId The id of the current example
* @param currentLocale The current locale string
* @param currentRemixData The remix data from the current locale (if any)
* @returns An array of remix data
*/
export const getFallbackRemixData = async (
currentId: string,
currentLocale: string,
currentRemixData: any[] | undefined,
) => {
// Return early if data already exists or if we are already on the English page
if (currentRemixData && currentRemixData.length > 0) {
return currentRemixData;
}
if (currentLocale === "en") {
return currentRemixData;
}
// Main logic
// replace the core path with the English path to find the corresponding English example
// e.g., "zh-Hans/02_Animation_And_Variables/00_Drawing_Lines/description.mdx"
// -> "en/02_Animation_And_Variables/00_Drawing_Lines/description.mdx"
const englishId = currentId.replace(`${currentLocale}/`, "en/");
const allExamples = await getCollection("examples");
const englishExample = allExamples.find((e) => e.id === englishId);
if (englishExample?.data.remix && englishExample.data.remix.length > 0) {
return englishExample.data.remix;
}
return currentRemixData;
}
53 changes: 52 additions & 1 deletion test/pages/_utils.test.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
import { expect, test, suite } from "vitest";
import { expect, test, suite, vi} from "vitest";
import {
exampleContentSlugToLegacyWebsiteSlug,
removeContentFileExt,
removeLeadingSlash,
removeLocaleAndExtension,
getFallbackRemixData,
} from "@pages/_utils";

suite("exampleContentSlugToLegacyWebsiteSlug", () => {
Expand Down Expand Up @@ -50,3 +51,53 @@ suite("removeLocaleAndExtensionFromId", () => {
);
});
});

vi.mock("astro:content", async (importOriginal) => {
// 1. Fetch all original exports from astro:content (e.g., reference, z) to prevent schema validation failure
const actual = await importOriginal() as any;

return {
...actual, // 2. Preserve all original module functionalities

// 3. Intercept and mock the getCollection method
getCollection: vi.fn(async (collectionName) => {
if (collectionName === "examples") {
return [
// English version data: contains remix data, which should be returned when fallback is triggered
{
id: "en/02_Animation_And_Variables/00_Drawing_Lines/description.mdx",
data: {
remix: [
{
description: "Revised by",
attribution: [],
code: []
}
]
}
},
// zh-Hans version data: remix data is empty, which should trigger fallback to English remix data
{
id: "zh-Hans/02_Animation_And_Variables/00_Drawing_Lines/description.mdx",
data: {
remix: []
}
}
];
}
return [];
}),
};
});

suite("getFallbackRemixData", () => {
test("returns remix data for English example when current locale example has no remix data", async () => {
const remixData = await getFallbackRemixData(
"zh-Hans/02_Animation_And_Variables/00_Drawing_Lines/description.mdx",
"zh-Hans",
undefined
);
expect(remixData).toBeDefined();
expect(remixData?.length).toBeGreaterThan(0);
});
});