diff --git a/docs/localization.md b/docs/localization.md index 4b1abbda99..2434522f2f 100644 --- a/docs/localization.md +++ b/docs/localization.md @@ -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). diff --git a/src/layouts/ExampleLayout.astro b/src/layouts/ExampleLayout.astro index 6d343ee72e..dd21d8a586 100644 --- a/src/layouts/ExampleLayout.astro +++ b/src/layouts/ExampleLayout.astro @@ -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">; @@ -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; @@ -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 ); @@ -98,7 +106,7 @@ const { showBanner, englishUrl } = checkTranslationBanner( {example.data.title}:{" "} - {example.data.remix?.map((item, i) => { + {remixData?.map((item, i) => { const parts = []; // Each remix entry requires at least one attribution @@ -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) diff --git a/src/pages/_utils.ts b/src/pages/_utils.ts index b29022a252..e4656b91f0 100644 --- a/src/pages/_utils.ts +++ b/src/pages/_utils.ts @@ -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; + } \ No newline at end of file diff --git a/test/pages/_utils.test.ts b/test/pages/_utils.test.ts index 16415e8470..66f5f3367a 100644 --- a/test/pages/_utils.test.ts +++ b/test/pages/_utils.test.ts @@ -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", () => { @@ -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); + }); +}); \ No newline at end of file