Skip to content

Commit

Permalink
i18n: Translate compatibility page
Browse files Browse the repository at this point in the history
  • Loading branch information
danielhjacobs committed Nov 15, 2024
1 parent a6c5493 commit 1ab4da2
Show file tree
Hide file tree
Showing 7 changed files with 166 additions and 86 deletions.
23 changes: 16 additions & 7 deletions src/app/compatibility/avm.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
"use client";

import classes from "./avm.module.css";
import {
Button,
Expand All @@ -9,6 +11,7 @@ import {
Title,
} from "@mantine/core";
import Link from "next/link";
import { useTranslation } from "@/app/translate";

interface AvmProgressProps {
done: number;
Expand All @@ -21,25 +24,26 @@ interface AvmProgressPropsFull extends AvmProgressProps {
}

function AvmProgress(props: AvmProgressPropsFull) {
const { t } = useTranslation();
return (
<Group align="center" justify="spread-between" mt={props.mt}>
<Text size="sm" className={classes.progressName}>
{props.name}: {props.done}%
{t(props.name)}: {props.done}%
</Text>
<ProgressRoot size="xl" radius={10} className={classes.progress}>
<ProgressSection
striped
value={props.done}
color="var(--mantine-color-green-9)"
title={`${props.done}% done`}
title={`${props.done}% ${t("compatibility.done")}`}
></ProgressSection>
{props.stubbed && (
<ProgressSection
striped
value={props.stubbed}
color="ruffle-orange"
className={classes.stub}
title={`${props.stubbed}% partially done`}
title={`${props.stubbed}% ${t("compatibility.partial")}`}
></ProgressSection>
)}
</ProgressRoot>
Expand All @@ -57,25 +61,30 @@ interface AvmBlockProps {
}

export function AvmBlock(props: AvmBlockProps) {
const { t } = useTranslation();
return (
<Stack className={classes.avm}>
<Group justify="space-between">
<Title order={2}>{props.name}</Title>
<Title order={2}>{t(props.name)}</Title>
<Button
component={Link}
href={props.info_link}
target={props.info_link_target}
size="compact-md"
color="var(--ruffle-blue-7)"
>
More Info
{t("compatibility.more")}
</Button>
</Group>

{props.children}

<AvmProgress name="Language" mt="auto" {...props.language} />
<AvmProgress name="API" {...props.api} />
<AvmProgress
name="compatibility.language"
mt="auto"
{...props.language}
/>
<AvmProgress name="compatibility.api" {...props.api} />
</Stack>
);
}
25 changes: 25 additions & 0 deletions src/app/compatibility/fetch-report/route.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import { NextResponse } from "next/server";
import { fetchReport } from "@/app/downloads/github";
import { AVM2Report } from "@/app/downloads/config";

let cachedReport: AVM2Report | undefined;

export async function GET() {
if (cachedReport) {
return NextResponse.json(cachedReport); // Return cached result
}

try {
const report = await fetchReport();
cachedReport = report; // Cache the result
return NextResponse.json(report);
} catch (error) {
console.error("Error fetching report:", error);
return NextResponse.json(
{ error: "Failed to fetch report" },
{ status: 500 },
);
}
}

export const dynamic = "force-static";
149 changes: 78 additions & 71 deletions src/app/compatibility/page.tsx
Original file line number Diff line number Diff line change
@@ -1,38 +1,69 @@
"use client";

import React, { useEffect, useState } from "react";
import { Container, Flex, Group, Stack, Text } from "@mantine/core";
import classes from "./compatibility.module.css";
import { AvmBlock } from "@/app/compatibility/avm";
import Image from "next/image";
import React from "react";
import { Title } from "@mantine/core";
import { List, ListItem } from "@mantine/core";
import { WeeklyContributions } from "@/app/compatibility/weekly_contributions";
import {
fetchReport,
getAVM1Progress,
getWeeklyContributions,
} from "@/app/downloads/github";
import { useTranslation, Trans } from "@/app/translate";

interface DataPoint {
week: string;
Commits: number;
}

export default function Downloads() {
const { t } = useTranslation();
const [data, setData] = useState<DataPoint[]>([]);
const [avm1ApiDone, setAvm1ApiDone] = useState<number>(0);
const [avm2ApiDone, setAvm2ApiDone] = useState<number>(0);
const [avm2ApiStubbed, setAvm2ApiStubbed] = useState<number>(0);
useEffect(() => {
const fetchData = async () => {
try {
// Fetch weekly contributions
const contributionsRes = await getWeeklyContributions();
const contributionsData = contributionsRes.data.map((item) => ({
week: new Date(item.week * 1000).toISOString().split("T")[0],
Commits: item.total,
}));
setData(contributionsData);

// Fetch AVM1 progress
const avm1ApiRes = await getAVM1Progress();
setAvm1ApiDone(avm1ApiRes);

export default async function Downloads() {
const contributions = await getWeeklyContributions();
const data = contributions.data.map((item) => {
return {
week: new Date(item.week * 1000).toISOString().split("T")[0],
Commits: item.total,
// Fetch report
const reportReq = await fetch("/compatibility/fetch-report");
const reportRes = await reportReq.json();
if (reportRes) {
const { summary } = reportRes;
const maxPoints = summary.max_points;
const implPoints = summary.impl_points;
const stubPenalty = summary.stub_penalty;

const avm2ApiDone = Math.round(
((implPoints - stubPenalty) / maxPoints) * 100,
);
setAvm2ApiDone(avm2ApiDone);

const avm2ApiStubbed = Math.round((stubPenalty / maxPoints) * 100);
setAvm2ApiStubbed(avm2ApiStubbed);
}
} catch (error) {
console.error("Error fetching data", error);
}
};
});
const avm1ApiDone = await getAVM1Progress();
const report = await fetchReport();
if (!report) {
return;
}
const summary = report.summary;
const maxPoints = summary.max_points;
const implPoints = summary.impl_points;
const stubPenalty = summary.stub_penalty;
const avm2ApiDone = Math.round(
((implPoints - stubPenalty) / maxPoints) * 100,
);
const avm2ApiStubbed = Math.round((stubPenalty / maxPoints) * 100);

fetchData();
}, []);

return (
<Container size="xl" className={classes.container}>
Expand All @@ -47,27 +78,23 @@ export default async function Downloads() {
className={classes.actionscriptImage}
/>
<Stack className={classes.actionscriptInfo}>
<Title className={classes.title}>ActionScript Compatibility</Title>
<Text>
The biggest factor in content compatibility is ActionScript; the
language that powers interactivity in games and applications made
with Flash. All Flash content falls in one of two categories,
depending on which version of the language was used to create it.
</Text>
<Text>
We track our progress in each AVM by splitting them up into two
different areas:
</Text>
<Title className={classes.title}>{t("compatibility.title")}</Title>
<Text>{t("compatibility.description")}</Text>
<Text>{t("compatibility.tracking")}</Text>
<List>
<ListItem>
The <b>Language</b> is the underlying virtual machine itself and
the language concepts that it understands, like variables and
classes and how they all interact together.
<Trans
i18nKey="compatibility.language-description"
components={[
<b key="language">{t("compatibility.language")}</b>,
]}
/>
</ListItem>
<ListItem>
The <b>API</b> is the original built-in methods and classes that
are available for this AVM, like the ability to interact with
objects on the stage or make web requests.
<Trans
i18nKey="compatibility.api-description"
components={[<b key="api">{t("compatibility.api")}</b>]}
/>
</ListItem>
</List>
</Stack>
Expand All @@ -81,53 +108,33 @@ export default async function Downloads() {
className={classes.avms}
>
<AvmBlock
name="AVM 1: ActionScript 1 & 2"
name="compatibility.avm1"
language={{ done: 95 }}
api={{ done: avm1ApiDone }}
info_link_target="_blank"
info_link="https://github.com/ruffle-rs/ruffle/issues/310"
>
<Text>
AVM 1 is the original ActionScript Virtual Machine. All movies
made before Flash Player 9 (June 2006) will be made with AVM 1,
and it remained supported &amp; available to authors until the
release of Flash Professional CC (2013), after which point content
started moving to AVM 2.
</Text>
<Text>
We believe that most AVM 1 content will work, but we are aware of
some graphical inaccuracies and smaller bugs here and there.
Please feel free to report any issues you find that are not
present in the original Flash Player!
</Text>
<Text>{t("compatibility.avm1-description")}</Text>
<Text>{t("compatibility.avm1-support")}</Text>
</AvmBlock>

<AvmBlock
name="AVM 2: ActionScript 3"
name="compatibility.avm2"
language={{ done: 90 }}
api={{ done: avm2ApiDone, stubbed: avm2ApiStubbed }}
info_link="/compatibility/avm2"
>
<Text>
AVM 2 was introduced with Flash Player 9 (June 2006), to replace
the earlier AVM 1. After the release of Flash Professional CC
(2013), authors are required to use ActionScript 3 - making any
movie made after that date very likely to fall under this
category.
</Text>
<Text>
Ruffle now has decent support for AVM 2, and it&apos;s our
experience that most games will work well enough to be played.
We&apos;re still rapidly improving in this area though, so bug
reports about any broken content are always welcome!
</Text>
<Text>{t("compatibility.avm2-description")}</Text>
<Text>{t("compatibility.avm2-support")}</Text>
</AvmBlock>
</Flex>

<Stack w="100%" align="center">
<Title order={2}>Weekly Contributions</Title>
<WeeklyContributions data={data} />
</Stack>
{data && (
<Stack w="100%" align="center">
<Title order={2}>{t("compatibility.weekly-contributions")}</Title>
<WeeklyContributions data={data} />
</Stack>
)}
</Stack>
</Container>
);
Expand Down
6 changes: 5 additions & 1 deletion src/app/compatibility/weekly_contributions.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
import { BarChart } from "@mantine/charts";
import { Paper, Text } from "@mantine/core";
import classes from "./weekly_contributions.module.css";
import { Trans } from "@/app/translate";

interface DataPoint {
week: string;
Expand All @@ -23,7 +24,10 @@ function ChartTooltip({ label, payload }: ChartTooltipProps) {
return (
<Paper px="md" py="sm" withBorder shadow="md" radius="md">
<Text fw={500} mb={5}>
{commits.value} commits on the week of {label}
<Trans
i18nKey="compatibility.commits-description"
values={{ commitNumber: commits.value, week: label }}
/>
</Text>
</Paper>
);
Expand Down
1 change: 1 addition & 0 deletions src/app/downloads/github.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,7 @@ export async function getWeeklyContributions(): Promise<
const octokit = new Octokit({ authStrategy: createGithubAuth });
return octokit.rest.repos.getCommitActivityStats(repository);
}

export async function fetchReport(): Promise<AVM2Report | undefined> {
const releases = await getLatestReleases();
const latest = releases.find(
Expand Down
28 changes: 21 additions & 7 deletions src/app/translate.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -113,24 +113,38 @@ export function useTranslation() {

interface TransProps {
i18nKey: string; // Translation key
values?: Record<string, React.ReactNode>; // Placeholder values
components?: React.ReactNode[]; // Components to inject into placeholders
}

export const Trans: React.FC<TransProps> = ({ i18nKey, components = [] }) => {
export const Trans: React.FC<TransProps> = ({
i18nKey,
values = {},
components = [],
}) => {
const { t } = useTranslation();
const translation = t(i18nKey);

const renderWithPlaceholders = (template: string) => {
const parts = template.split(/({{.*?}})/g); // Split on placeholders like {{key}}
return parts.map((part) => {
return parts.map((part, index) => {
const match = part.match(/{{(.*?)}}/); // Match placeholders
if (match) {
const placeholderKey = match[1];
const component = components.find(
(comp) => React.isValidElement(comp) && comp.key === placeholderKey,
);
if (component) {
return component;
if (placeholderKey in values) {
const value = values[placeholderKey];
return typeof value === "string" ? (
<React.Fragment key={index}>{value}</React.Fragment>
) : (
value
);
} else {
const component = components.find(
(comp) => React.isValidElement(comp) && comp.key === placeholderKey,
);
if (component) {
return component;
}
}
}
return part; // Return plain text if no placeholder
Expand Down
Loading

0 comments on commit 1ab4da2

Please sign in to comment.