Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Creates a script to check for uneccessary and missing translations, and implements those changes #596

Merged
merged 1 commit into from
Oct 24, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ COPY pages pages
COPY public public
COPY scripts scripts
COPY styles styles
COPY .eslintrc.json .eslintrc.json
COPY .eslintrc.js .eslintrc.js
COPY next.config.js next.config.js
COPY package-lock.json package-lock.json
COPY package.json package.json
Expand Down
19 changes: 16 additions & 3 deletions TRANSLATING.MD
Original file line number Diff line number Diff line change
Expand Up @@ -18,16 +18,29 @@ If you need to pass in a variable into the text use the following syntax. Where
t("Text to translate {variable} {variable1}", { variable, variable1 });
```

To find all the translations that may currently be missing you can run the following command. Note that this command can have a lot of false positives.

```bash
npm run missing-translations
```

Most of the time you should be able to just pass an api response into the t function.

```ts
t(await response.text());
```

# Adding a Language

1. Add the language to the locales folder as a json.
2. Add the language to the locales array in the next config file.
3. Start to translate the language(You do not have to translate the langauge fully to add it anything helps).
2. Copy over the de.json and remove all the german text.
3. Add the language to the locales array in the next config file.
4. Start to translate the language(You do not have to translate the langauge fully to add it anything helps).

# Helping Translate

@lukasdotcom does all the german translations but if you want to help translate to another language you can do so by following the steps below.

1. Go to the file in the locales folder with the language you want to translate to.
2. Find any missing text that is in the german file and add it to the file you are translating to(Note that the order is alphabetized based on the english part).
2. Find any text that currently has an empty translation in the foreign language(Note that the order is alphabetized based on the english part).
3. Translate the text to the language you are translating to.
8 changes: 4 additions & 4 deletions cypress/e2e/invite.cy.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ describe("Invite User into league and change some league Settings and run throug
cy.contains("Click here for creating an account").click();
cy.get("#username").type("Invite 1");
cy.get("#password").type("password");
cy.contains("Sign Up").click();
cy.get(".center > .MuiButtonBase-root").click();
cy.reload().then(() =>
cy
.getCookie("next-auth.session-token")
Expand Down Expand Up @@ -68,7 +68,7 @@ describe("Invite User into league and change some league Settings and run throug
); // This line is because cypress is weird and removes the callbackUrl from the url
cy.get("#username").type("Invite 2");
cy.get("#password").type("password");
cy.contains("Sign Up").click();
cy.get(".center > .MuiButtonBase-root").click();
cy.contains("404");
cy.get(".center").contains("404");
cy.getCookie("next-auth.session-token").then(
Expand Down Expand Up @@ -466,7 +466,7 @@ describe("Invite User into league and change some league Settings and run throug
cy.contains("Click here for creating an account").click();
cy.get("#username").type("Invite 3");
cy.get("#password").type("password");
cy.contains("Sign Up").click();
cy.get(".center > .MuiButtonBase-root").click();
cy.contains("Leagues").click();
cy.visit("http://localhost:3000/api/invite/invite1");
// Makes sure this user actually has points for matchday 2
Expand Down Expand Up @@ -525,7 +525,7 @@ describe("Invite User into league and change some league Settings and run throug
); // This line is because cypress is weird and removes the callbackUrl from the url
cy.get("#username").type("Invite 3");
cy.get("#password").type("password");
cy.contains("Sign Up").click();
cy.get(".center > .MuiButtonBase-root").click();
cy.contains(
"Your favorited league will be available in the menu when you are not in a league. Note that the menu only updates on a page navigation or reload.",
);
Expand Down
2 changes: 1 addition & 1 deletion cypress/e2e/user.cy.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ describe("User", () => {
cy.contains("Click here for creating an account").click();
cy.get("#username").type("Sample User");
cy.get("#password").type("Sample Password");
cy.contains("Sign Up").click();
cy.get(".center > .MuiButtonBase-root").click();
// Edits the user
cy.contains("SU").click();
cy.get("#username").should("value", "Sample User");
Expand Down
64 changes: 51 additions & 13 deletions locales/de.json

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion locales/getLocales.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
const validLocales = ["de"];
export const validLocales = ["de"];
/**
* Used to grab the correct locales data.
* @param {string | undefined} locale - The locale
Expand Down
163 changes: 163 additions & 0 deletions locales/missing_translations.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,163 @@
import { readFileSync, readdirSync, statSync } from "fs";
import { validLocales } from "./getLocales";
let translations: { locale: string; translations: Set<string> }[] = [];
let neededTranslations: { locale: string; translations: Set<string> }[] = [];
// Finds all files in pages directory
async function main() {
translations = await Promise.all(
validLocales.map((e) => {
return {
locale: e,
translations: new Set(
Object.keys(JSON.parse(String(readFileSync(`./locales/${e}.json`)))),
),
};
}),
);
neededTranslations = await Promise.all(
translations.map((e) => {
return {
locale: e.locale,
translations: new Set(e.translations),
};
}),
);
const shouldNotBeMissing = [
"Playeruid", // First are the download translations
"Name",
"Ascii Name",
"Club",
"Picture Url",
"Value",
"Sale Price",
"Position",
"Forecast",
"Total Points",
"Average Points",
"Last Match Points",
"Exists",
"League",
"Failure", // Second are the translations for the notification on the index page.
"Failing to check for updates.",
"Out of date",
"New update for more info Click Here",
"Not for Production",
"This is the development or testing version of this software. Do not use in production.",
"Player is not your player", // Third are the translations for the squad api.
"No more room in formation",
"Player is locked",
"Player is not in the field",
"Player has already played",
"att", // Fourth are the positions
"mid",
"def",
"gk",
"dark", // Fith are the themes
"light",
"locale", // Sixth is the key that shows the locale of that one.
]; // These are translations that aren't passed directly to t() or res.status().end() but are translated.
// Removes all the translations that should not be missing
neededTranslations.forEach((e) => {
shouldNotBeMissing.forEach((f) => {
e.translations.delete(f);
});
});
console.log(
"Note that this is meant to be used to check, there are a lot of false positives.",
);
console.log("Looking for missing translations...");
let missing = false;
// Fist of all checks the shouldNotBeMissing
for (let i = 0; i < translations.length; i++) {
for (const test of shouldNotBeMissing) {
if (!translations[i].translations.has(test)) {
console.log(
"Missing translation: `" + test + "` in " + translations[i].locale,
);
missing = true;
}
}
}
// Finds all the t() calls in pages
const a = locate("./pages", /\Wt\([\w|\s]*"([^"]*)"/gm, [1]);
// Finds all the t() calls in components
const b = locate("./components", /\Wt\([\w|\s]*"([^"]*)"/gm, [1]);
// Finds all the res.status() calls
const c = locate(
"./pages/api",
/res\s*.status\([0-9]*\)\s*.end\(\s*(("([^"]*)")|(`([^(`|$)]*)`))/gm,
[3, 5],
);
missing = missing || a || b || c;
if (!missing) {
console.log("No missing translations found");
}
console.log("Looking for unecessary translations...");
let unecessary = false;
for (let i = 0; i < translations.length; i++) {
unecessary = unecessary || translations[i].translations.size > 0;
neededTranslations[i].translations.forEach((e) => {
console.log("`" + e + "` is not needed in " + translations[i].locale);
});
}
if (!unecessary) {
console.log("No unecessary translations found");
}
}
function locate(dir: string, regex: RegExp, groups: number[]): boolean {
let missing = false;
const files = readdirSync(dir);
for (const file of files) {
const filePath = dir + "/" + file;
const stats = statSync(filePath);
if (stats.isDirectory()) {
const result = locate(filePath, regex, groups);
missing = missing || result;
} else {
// Makes sure this is a js/ts file
if (
file.endsWith(".tsx") ||
file.endsWith(".js") ||
file.endsWith(".ts")
) {
const data = readFileSync(filePath);
const result = validate(String(data), regex, groups);
missing = missing || result;
}
}
}
return missing;
}
function validate(data: string, regex: RegExp, groups: number[]): boolean {
let match;
let missing = false;
while ((match = regex.exec(data)) !== null) {
for (let i = 0; i < validLocales.length; i++) {
for (const valid_match of groups) {
if (match[valid_match] === undefined) {
continue;
}
// Finds all the empty t() calls
if (
match[valid_match].replaceAll(/{[^{]*}/gm, "").replaceAll(" ", "") !==
""
) {
if (!translations[i].translations.has(match[valid_match])) {
console.log(
"Missing translation: `" +
match[valid_match] +
"` in " +
translations[i].locale,
);
missing = true;
}
if (neededTranslations[i].translations.has(match[valid_match])) {
neededTranslations[i].translations.delete(match[valid_match]);
}
}
}
}
}
return missing;
}
main();
7 changes: 7 additions & 0 deletions package-lock.json

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

2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
"start2:part1": "ts-node --project=tsconfig2.json scripts/startup.ts",
"start2:part2": "ts-node --project=tsconfig2.json scripts/entrypoint.ts & next build && next start",
"lint": "eslint . --max-warnings 0",
"missing-translations": "ts-node --project=tsconfig2.json ./locales/missing_translations.ts",
"pretty": "prettier --check .",
"format": "prettier --write .",
"cypress:open": "cypress open",
Expand Down Expand Up @@ -53,6 +54,7 @@
"cypress": "13.3.2",
"eslint": "8.52.0",
"eslint-config-next": "13.5.6",
"eslint-plugin-local-rules": "^2.0.0",
"prettier": "3.0.3",
"typescript": "5.2.2",
"wait-on": "7.0.1"
Expand Down
13 changes: 11 additions & 2 deletions pages/[league]/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -356,7 +356,16 @@ function AdminPanel({
enabled: top ? t("enabled") : t("disabled"),
})}
</p>
<p>{t("This league is " + (archived ? "archived" : "not archived"))}</p>
{archived && (
<p>
{t(
"This league is {archived}",
archived
? { archived: t("archived") }
: { archived: t("not archived") },
)}
</p>
)}
</>
);
}
Expand Down Expand Up @@ -612,7 +621,7 @@ export default function Home({
};
// Used to add an anouncement
const addAnouncement = () => {
notify(t("Adding anouncement"));
notify(t("Adding announcement"));
fetch(`/api/league/${league}/announcement`, {
method: "POST",
headers: {
Expand Down
2 changes: 1 addition & 1 deletion pages/_app.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -134,7 +134,7 @@ function MyApp({ Component, pageProps }: AppProps) {
replacers?: Record<string, string | Date | number>,
): string => {
let data = text;
if (translationData[text]) {
if (translationData[text] && translationData[text] !== "") {
data = translationData[text];
}
if (replacers) {
Expand Down
2 changes: 1 addition & 1 deletion pages/api/league/[league]/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ export default async function handler(
// Used to edit a league
case "POST":
if (await isArchived) {
res.status(400).end("This League is archived");
res.status(400).end("This league is archived");
break;
}
// Checks if the user is qualified to do this
Expand Down
4 changes: 2 additions & 2 deletions pages/api/revalidate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ export default async function handler(
),
);
console.log("Revalidated all pages");
res.status(200).end("OK");
res.status(200).end("success");
} catch (err) {
console.error("Failed to revalidate all pages");
res.status(500).end("Failed to revalidate all pages");
Expand All @@ -57,7 +57,7 @@ export default async function handler(
console.log("Revalidating the page " + req.body.path);
await res.revalidate(req.body.path);
console.log("Revalidated the page " + req.body.path);
res.status(200).end("OK");
res.status(200).end("success");
} catch (err) {
console.error("Failed to revalidate page " + req.body.path);
res.status(500).end("Failed to revalidate");
Expand Down
2 changes: 1 addition & 1 deletion pages/api/squad/[league].ts
Original file line number Diff line number Diff line change
Expand Up @@ -154,7 +154,7 @@ const handler: NextApiHandler = async (req, res) => {
])
.then((e) => e.length === 0)
) {
res.status(400).end("League is archived");
res.status(400).end("This league is archived");
return;
}
switch (req.method) {
Expand Down
Loading