Skip to content

Commit 6848620

Browse files
committed
i18n: Translate compatibility page
1 parent ad8b031 commit 6848620

File tree

7 files changed

+166
-86
lines changed

7 files changed

+166
-86
lines changed

src/app/compatibility/avm.tsx

Lines changed: 16 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
"use client";
2+
13
import classes from "./avm.module.css";
24
import {
35
Button,
@@ -9,6 +11,7 @@ import {
911
Title,
1012
} from "@mantine/core";
1113
import Link from "next/link";
14+
import { useTranslation } from "@/app/translate";
1215

1316
interface AvmProgressProps {
1417
done: number;
@@ -21,25 +24,26 @@ interface AvmProgressPropsFull extends AvmProgressProps {
2124
}
2225

2326
function AvmProgress(props: AvmProgressPropsFull) {
27+
const { t } = useTranslation();
2428
return (
2529
<Group align="center" justify="spread-between" mt={props.mt}>
2630
<Text size="sm" className={classes.progressName}>
27-
{props.name}: {props.done}%
31+
{t(props.name)}: {props.done}%
2832
</Text>
2933
<ProgressRoot size="xl" radius={10} className={classes.progress}>
3034
<ProgressSection
3135
striped
3236
value={props.done}
3337
color="var(--mantine-color-green-9)"
34-
title={`${props.done}% done`}
38+
title={`${props.done}% ${t("compatibility.done")}`}
3539
></ProgressSection>
3640
{props.stubbed && (
3741
<ProgressSection
3842
striped
3943
value={props.stubbed}
4044
color="ruffle-orange"
4145
className={classes.stub}
42-
title={`${props.stubbed}% partially done`}
46+
title={`${props.stubbed}% ${t("compatibility.partial")}`}
4347
></ProgressSection>
4448
)}
4549
</ProgressRoot>
@@ -57,25 +61,30 @@ interface AvmBlockProps {
5761
}
5862

5963
export function AvmBlock(props: AvmBlockProps) {
64+
const { t } = useTranslation();
6065
return (
6166
<Stack className={classes.avm}>
6267
<Group justify="space-between">
63-
<Title order={2}>{props.name}</Title>
68+
<Title order={2}>{t(props.name)}</Title>
6469
<Button
6570
component={Link}
6671
href={props.info_link}
6772
target={props.info_link_target}
6873
size="compact-md"
6974
color="var(--ruffle-blue-7)"
7075
>
71-
More Info
76+
{t("compatibility.more")}
7277
</Button>
7378
</Group>
7479

7580
{props.children}
7681

77-
<AvmProgress name="Language" mt="auto" {...props.language} />
78-
<AvmProgress name="API" {...props.api} />
82+
<AvmProgress
83+
name="compatibility.language"
84+
mt="auto"
85+
{...props.language}
86+
/>
87+
<AvmProgress name="compatibility.api" {...props.api} />
7988
</Stack>
8089
);
8190
}
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
import { NextResponse } from "next/server";
2+
import { fetchReport } from "@/app/downloads/github";
3+
import { AVM2Report } from "@/app/downloads/config";
4+
5+
let cachedReport: AVM2Report | undefined;
6+
7+
export async function GET() {
8+
if (cachedReport) {
9+
return NextResponse.json(cachedReport); // Return cached result
10+
}
11+
12+
try {
13+
const report = await fetchReport();
14+
cachedReport = report; // Cache the result
15+
return NextResponse.json(report);
16+
} catch (error) {
17+
console.error("Error fetching report:", error);
18+
return NextResponse.json(
19+
{ error: "Failed to fetch report" },
20+
{ status: 500 },
21+
);
22+
}
23+
}
24+
25+
export const dynamic = "force-static";

src/app/compatibility/page.tsx

Lines changed: 78 additions & 71 deletions
Original file line numberDiff line numberDiff line change
@@ -1,38 +1,69 @@
1+
"use client";
2+
3+
import React, { useEffect, useState } from "react";
14
import { Container, Flex, Group, Stack, Text } from "@mantine/core";
25
import classes from "./compatibility.module.css";
36
import { AvmBlock } from "@/app/compatibility/avm";
47
import Image from "next/image";
5-
import React from "react";
68
import { Title } from "@mantine/core";
79
import { List, ListItem } from "@mantine/core";
810
import { WeeklyContributions } from "@/app/compatibility/weekly_contributions";
911
import {
10-
fetchReport,
1112
getAVM1Progress,
1213
getWeeklyContributions,
1314
} from "@/app/downloads/github";
15+
import { useTranslation, Trans } from "@/app/translate";
16+
17+
interface DataPoint {
18+
week: string;
19+
Commits: number;
20+
}
21+
22+
export default function Downloads() {
23+
const { t } = useTranslation();
24+
const [data, setData] = useState<DataPoint[]>([]);
25+
const [avm1ApiDone, setAvm1ApiDone] = useState<number>(0);
26+
const [avm2ApiDone, setAvm2ApiDone] = useState<number>(0);
27+
const [avm2ApiStubbed, setAvm2ApiStubbed] = useState<number>(0);
28+
useEffect(() => {
29+
const fetchData = async () => {
30+
try {
31+
// Fetch weekly contributions
32+
const contributionsRes = await getWeeklyContributions();
33+
const contributionsData = contributionsRes.data.map((item) => ({
34+
week: new Date(item.week * 1000).toISOString().split("T")[0],
35+
Commits: item.total,
36+
}));
37+
setData(contributionsData);
38+
39+
// Fetch AVM1 progress
40+
const avm1ApiRes = await getAVM1Progress();
41+
setAvm1ApiDone(avm1ApiRes);
1442

15-
export default async function Downloads() {
16-
const contributions = await getWeeklyContributions();
17-
const data = contributions.data.map((item) => {
18-
return {
19-
week: new Date(item.week * 1000).toISOString().split("T")[0],
20-
Commits: item.total,
43+
// Fetch report
44+
const reportReq = await fetch("/compatibility/fetch-report");
45+
const reportRes = await reportReq.json();
46+
if (reportRes) {
47+
const { summary } = reportRes;
48+
const maxPoints = summary.max_points;
49+
const implPoints = summary.impl_points;
50+
const stubPenalty = summary.stub_penalty;
51+
52+
const avm2ApiDone = Math.round(
53+
((implPoints - stubPenalty) / maxPoints) * 100,
54+
);
55+
setAvm2ApiDone(avm2ApiDone);
56+
57+
const avm2ApiStubbed = Math.round((stubPenalty / maxPoints) * 100);
58+
setAvm2ApiStubbed(avm2ApiStubbed);
59+
}
60+
} catch (error) {
61+
console.error("Error fetching data", error);
62+
}
2163
};
22-
});
23-
const avm1ApiDone = await getAVM1Progress();
24-
const report = await fetchReport();
25-
if (!report) {
26-
return;
27-
}
28-
const summary = report.summary;
29-
const maxPoints = summary.max_points;
30-
const implPoints = summary.impl_points;
31-
const stubPenalty = summary.stub_penalty;
32-
const avm2ApiDone = Math.round(
33-
((implPoints - stubPenalty) / maxPoints) * 100,
34-
);
35-
const avm2ApiStubbed = Math.round((stubPenalty / maxPoints) * 100);
64+
65+
fetchData();
66+
}, []);
3667

3768
return (
3869
<Container size="xl" className={classes.container}>
@@ -47,27 +78,23 @@ export default async function Downloads() {
4778
className={classes.actionscriptImage}
4879
/>
4980
<Stack className={classes.actionscriptInfo}>
50-
<Title className={classes.title}>ActionScript Compatibility</Title>
51-
<Text>
52-
The biggest factor in content compatibility is ActionScript; the
53-
language that powers interactivity in games and applications made
54-
with Flash. All Flash content falls in one of two categories,
55-
depending on which version of the language was used to create it.
56-
</Text>
57-
<Text>
58-
We track our progress in each AVM by splitting them up into two
59-
different areas:
60-
</Text>
81+
<Title className={classes.title}>{t("compatibility.title")}</Title>
82+
<Text>{t("compatibility.description")}</Text>
83+
<Text>{t("compatibility.tracking")}</Text>
6184
<List>
6285
<ListItem>
63-
The <b>Language</b> is the underlying virtual machine itself and
64-
the language concepts that it understands, like variables and
65-
classes and how they all interact together.
86+
<Trans
87+
i18nKey="compatibility.language-description"
88+
components={[
89+
<b key="language">{t("compatibility.language")}</b>,
90+
]}
91+
/>
6692
</ListItem>
6793
<ListItem>
68-
The <b>API</b> is the original built-in methods and classes that
69-
are available for this AVM, like the ability to interact with
70-
objects on the stage or make web requests.
94+
<Trans
95+
i18nKey="compatibility.api-description"
96+
components={[<b key="api">{t("compatibility.api")}</b>]}
97+
/>
7198
</ListItem>
7299
</List>
73100
</Stack>
@@ -81,53 +108,33 @@ export default async function Downloads() {
81108
className={classes.avms}
82109
>
83110
<AvmBlock
84-
name="AVM 1: ActionScript 1 & 2"
111+
name="compatibility.avm1"
85112
language={{ done: 95 }}
86113
api={{ done: avm1ApiDone }}
87114
info_link_target="_blank"
88115
info_link="https://github.com/ruffle-rs/ruffle/issues/310"
89116
>
90-
<Text>
91-
AVM 1 is the original ActionScript Virtual Machine. All movies
92-
made before Flash Player 9 (June 2006) will be made with AVM 1,
93-
and it remained supported &amp; available to authors until the
94-
release of Flash Professional CC (2013), after which point content
95-
started moving to AVM 2.
96-
</Text>
97-
<Text>
98-
We believe that most AVM 1 content will work, but we are aware of
99-
some graphical inaccuracies and smaller bugs here and there.
100-
Please feel free to report any issues you find that are not
101-
present in the original Flash Player!
102-
</Text>
117+
<Text>{t("compatibility.avm1-description")}</Text>
118+
<Text>{t("compatibility.avm1-support")}</Text>
103119
</AvmBlock>
104120

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

127-
<Stack w="100%" align="center">
128-
<Title order={2}>Weekly Contributions</Title>
129-
<WeeklyContributions data={data} />
130-
</Stack>
132+
{data && (
133+
<Stack w="100%" align="center">
134+
<Title order={2}>{t("compatibility.weekly-contributions")}</Title>
135+
<WeeklyContributions data={data} />
136+
</Stack>
137+
)}
131138
</Stack>
132139
</Container>
133140
);

src/app/compatibility/weekly_contributions.tsx

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
import { BarChart } from "@mantine/charts";
33
import { Paper, Text } from "@mantine/core";
44
import classes from "./weekly_contributions.module.css";
5+
import { Trans } from "@/app/translate";
56

67
interface DataPoint {
78
week: string;
@@ -23,7 +24,10 @@ function ChartTooltip({ label, payload }: ChartTooltipProps) {
2324
return (
2425
<Paper px="md" py="sm" withBorder shadow="md" radius="md">
2526
<Text fw={500} mb={5}>
26-
{commits.value} commits on the week of {label}
27+
<Trans
28+
i18nKey="compatibility.commits-description"
29+
values={{ commitNumber: commits.value, week: label }}
30+
/>
2731
</Text>
2832
</Paper>
2933
);

src/app/downloads/github.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,7 @@ export async function getWeeklyContributions(): Promise<
7373
const octokit = new Octokit({ authStrategy: createGithubAuth });
7474
return octokit.rest.repos.getCommitActivityStats(repository);
7575
}
76+
7677
export async function fetchReport(): Promise<AVM2Report | undefined> {
7778
const releases = await getLatestReleases();
7879
const latest = releases.find(

src/app/translate.tsx

Lines changed: 21 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -113,24 +113,38 @@ export function useTranslation() {
113113

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

119-
export const Trans: React.FC<TransProps> = ({ i18nKey, components = [] }) => {
120+
export const Trans: React.FC<TransProps> = ({
121+
i18nKey,
122+
values = {},
123+
components = [],
124+
}) => {
120125
const { t } = useTranslation();
121126
const translation = t(i18nKey);
122127

123128
const renderWithPlaceholders = (template: string) => {
124129
const parts = template.split(/({{.*?}})/g); // Split on placeholders like {{key}}
125-
return parts.map((part) => {
130+
return parts.map((part, index) => {
126131
const match = part.match(/{{(.*?)}}/); // Match placeholders
127132
if (match) {
128133
const placeholderKey = match[1];
129-
const component = components.find(
130-
(comp) => React.isValidElement(comp) && comp.key === placeholderKey,
131-
);
132-
if (component) {
133-
return component;
134+
if (placeholderKey in values) {
135+
const value = values[placeholderKey];
136+
return typeof value === "string" ? (
137+
<React.Fragment key={index}>{value}</React.Fragment>
138+
) : (
139+
value
140+
);
141+
} else {
142+
const component = components.find(
143+
(comp) => React.isValidElement(comp) && comp.key === placeholderKey,
144+
);
145+
if (component) {
146+
return component;
147+
}
134148
}
135149
}
136150
return part; // Return plain text if no placeholder

0 commit comments

Comments
 (0)