From 67aff2a6a0c7e11de3170e378fb78362f019b464 Mon Sep 17 00:00:00 2001
From: catloversg <152669316+catloversg@users.noreply.github.com>
Date: Sun, 2 Feb 2025 12:42:56 +0700
Subject: [PATCH] BUGFIX: Wrong plural form in modal of coding contract (#1939)
---
src/Achievements/AchievementList.tsx | 4 +++-
src/Bladeburner/Bladeburner.ts | 5 ++---
src/Gang/ui/RecruitButton.tsx | 5 ++---
src/Terminal/commands/grep.ts | 10 ++++------
src/Terminal/commands/runScript.ts | 5 ++++-
src/ui/React/CodingContractModal.tsx | 6 ++++--
src/utils/APIBreaks/APIBreak.ts | 5 ++++-
src/utils/I18nUtils.ts | 10 ++++++++++
src/utils/StringHelperFunctions.ts | 7 ++++---
9 files changed, 37 insertions(+), 20 deletions(-)
create mode 100644 src/utils/I18nUtils.ts
diff --git a/src/Achievements/AchievementList.tsx b/src/Achievements/AchievementList.tsx
index 74d574bd7..291fa3aaa 100644
--- a/src/Achievements/AchievementList.tsx
+++ b/src/Achievements/AchievementList.tsx
@@ -7,6 +7,7 @@ import { Achievement, PlayerAchievement } from "./Achievements";
import { Settings } from "../Settings/Settings";
import { getFiltersFromHex } from "../ThirdParty/colorUtils";
import { CorruptableText } from "../ui/React/CorruptableText";
+import { pluralize } from "../utils/I18nUtils";
interface IProps {
achievements: Achievement[];
@@ -101,7 +102,8 @@ export function AchievementList({ achievements, playerAchievements }: IProps): J
- {unavailable.length} additional achievements hidden behind content you don't have access to.
+ {pluralize(unavailable.length, "additional achievement")} hidden behind content you don't have access
+ to.
diff --git a/src/Bladeburner/Bladeburner.ts b/src/Bladeburner/Bladeburner.ts
index f213aa32e..ab79a0f12 100644
--- a/src/Bladeburner/Bladeburner.ts
+++ b/src/Bladeburner/Bladeburner.ts
@@ -54,6 +54,7 @@ import { shuffleArray } from "../Infiltration/ui/BribeGame";
import { assertObject } from "../utils/TypeAssertion";
import { throwIfReachable } from "../utils/helpers/throwIfReachable";
import { loadActionIdentifier } from "./utils/loadActionIdentifier";
+import { pluralize } from "../utils/I18nUtils";
export const BladeburnerPromise: PromisePair = { promise: null, resolve: null };
@@ -167,9 +168,7 @@ export class Bladeburner implements OperationTeam {
this.setSkillLevel(skillName, currentSkillLevel + availability.actualCount);
return {
success: true,
- message: `Upgraded skill ${skillName} by ${availability.actualCount} level${
- availability.actualCount > 1 ? "s" : ""
- }`,
+ message: `Upgraded skill ${skillName} by ${pluralize(availability.actualCount, "level")}`,
};
}
diff --git a/src/Gang/ui/RecruitButton.tsx b/src/Gang/ui/RecruitButton.tsx
index 46e1b3675..bed52b20c 100644
--- a/src/Gang/ui/RecruitButton.tsx
+++ b/src/Gang/ui/RecruitButton.tsx
@@ -6,6 +6,7 @@ import Typography from "@mui/material/Typography";
import Button from "@mui/material/Button";
import Box from "@mui/material/Box";
import { RecruitmentResult } from "../Gang";
+import { pluralize } from "../../utils/I18nUtils";
interface IProps {
onRecruit: () => void;
@@ -35,9 +36,7 @@ export function RecruitButton(props: IProps): React.ReactElement {
<>
-
- Can recruit {recruitsAvailable} more gang member{recruitsAvailable === 1 ? "" : "s"}
-
+ Can recruit {pluralize(recruitsAvailable, "more gang member")}
setOpen(false)} onRecruit={props.onRecruit} />
>
diff --git a/src/Terminal/commands/grep.ts b/src/Terminal/commands/grep.ts
index 6a1afc5fd..522bdf994 100644
--- a/src/Terminal/commands/grep.ts
+++ b/src/Terminal/commands/grep.ts
@@ -5,6 +5,7 @@ import { ContentFile, ContentFilePath, allContentFiles } from "../../Paths/Conte
import { Settings } from "../../Settings/Settings";
import { help } from "../commands/help";
import { Output } from "../OutputTypes";
+import { pluralize } from "../../utils/I18nUtils";
type LineParser = (options: Options, filename: string, line: string, i: number) => ParsedLine;
@@ -290,12 +291,9 @@ class Results {
getVerboseInfo(files: ContentFile[], pattern: string | RegExp, options: Options): string {
if (!options.isVerbose) return "";
- const suffix = (pre: string, num: number) => pre + (num === 1 ? "" : "s");
const totalLines = this.results.length;
const matchCount = Math.abs((options.isInvertMatch ? totalLines : 0) - this.numMatches);
- const inputStr = options.isPipeIn
- ? "piped from terminal "
- : `in ${files.length} ${suffix("file", files.length)}:\n`;
+ const inputStr = options.isPipeIn ? "piped from terminal " : `in ${pluralize(files.length, "file")}:\n`;
const filesStr = files
.map((file, i) => `${i % 2 ? WHITE : ""}${file.filename}(${file.content.split("\n").length}loc)${DEFAULT}`)
.join(", ");
@@ -304,9 +302,9 @@ class Results {
`\n${
(this.params.maxMatches ? this.params.maxMatches : matchCount) + (options.isInvertMatch ? " INVERTED" : "")
} `,
- suffix("line", matchCount) + " matched ",
+ pluralize(matchCount, "line", undefined, true) + " matched ",
`against PATTERN "${pattern.toString()}" `,
- `in ${totalLines} ${suffix("line", totalLines)}, `,
+ `in ${pluralize(totalLines, "line")}, `,
inputStr,
`${filesStr}`,
].join("");
diff --git a/src/Terminal/commands/runScript.ts b/src/Terminal/commands/runScript.ts
index 1f85f92f5..dfb723487 100644
--- a/src/Terminal/commands/runScript.ts
+++ b/src/Terminal/commands/runScript.ts
@@ -11,6 +11,7 @@ import { ScriptFilePath, isLegacyScript } from "../../Paths/ScriptFilePath";
import { sendDeprecationNotice } from "./common/deprecation";
import { roundToTwo } from "../../utils/helpers/roundToTwo";
import { RamCostConstants } from "../../Netscript/RamCostGenerator";
+import { pluralize } from "../../utils/I18nUtils";
export function runScript(path: ScriptFilePath, commandArgs: (string | number | boolean)[], server: BaseServer): void {
// This takes in the absolute filepath, see "run.ts"
@@ -77,7 +78,9 @@ export function runScript(path: ScriptFilePath, commandArgs: (string | number |
sendDeprecationNotice();
}
Terminal.print(
- `Running script with ${numThreads} thread(s), pid ${runningScript.pid} and args: ${JSON.stringify(args)}.`,
+ `Running script with ${pluralize(numThreads, "thread")}, pid ${runningScript.pid} and args: ${JSON.stringify(
+ args,
+ )}.`,
);
if (tailFlag) {
LogBoxEvents.emit(runningScript);
diff --git a/src/ui/React/CodingContractModal.tsx b/src/ui/React/CodingContractModal.tsx
index 88f798b4b..abe3dc913 100644
--- a/src/ui/React/CodingContractModal.tsx
+++ b/src/ui/React/CodingContractModal.tsx
@@ -9,6 +9,7 @@ import { EventEmitter } from "../../utils/EventEmitter";
import Typography from "@mui/material/Typography";
import TextField from "@mui/material/TextField";
import Button from "@mui/material/Button";
+import { pluralize } from "../../utils/I18nUtils";
interface CodingContractProps {
c: CodingContract;
@@ -63,8 +64,9 @@ export function CodingContractModal(): React.ReactElement {
- You are attempting to solve a Coding Contract. You have {contract.c.getMaxNumTries() - contract.c.tries} tries
- remaining, after which the contract will self-destruct.
+ You are attempting to solve a Coding Contract. You have{" "}
+ {pluralize(contract.c.getMaxNumTries() - contract.c.tries, "try", "tries")} remaining, after which the contract
+ will self-destruct.
{description}
diff --git a/src/utils/APIBreaks/APIBreak.ts b/src/utils/APIBreaks/APIBreak.ts
index a627188c7..8eef9d0ed 100644
--- a/src/utils/APIBreaks/APIBreak.ts
+++ b/src/utils/APIBreaks/APIBreak.ts
@@ -7,6 +7,7 @@ import { GetAllServers } from "../../Server/AllServers";
import { resolveTextFilePath } from "../../Paths/TextFilePath";
import { dialogBoxCreate as dialogBoxCreateOriginal } from "../../ui/React/DialogBox";
import { Terminal } from "../../Terminal";
+import { pluralize } from "../I18nUtils";
// Temporary until fixing alerts manager to store alerts outside of react scope
const dialogBoxCreate = (text: string) => setTimeout(() => dialogBoxCreateOriginal(text), 2000);
@@ -64,7 +65,9 @@ export function showAPIBreaks(version: string, ...breakInfos: APIBreakInfo[]) {
[...scriptImpactMap]
.map(
([filename, lineNumbers]) =>
- `${filename}: (Line number${lineNumbers.length > 1 ? "s" : ""}: ${lineNumbers.join(", ")})`,
+ `${filename}: (${pluralize(lineNumbers.length, "Line number", undefined, true)}: ${lineNumbers.join(
+ ", ",
+ )})`,
)
.join("\n"),
)
diff --git a/src/utils/I18nUtils.ts b/src/utils/I18nUtils.ts
new file mode 100644
index 000000000..318d6eb42
--- /dev/null
+++ b/src/utils/I18nUtils.ts
@@ -0,0 +1,10 @@
+const pluralRules = new Intl.PluralRules("en-US");
+
+export function pluralize(count: number, singular: string, plural?: string, skipCountInReturnedValue = false): string {
+ const countText = !skipCountInReturnedValue ? `${count} ` : "";
+ const pluralRule = pluralRules.select(count);
+ if (pluralRule === "one") {
+ return countText + singular;
+ }
+ return countText + (plural !== undefined ? plural : `${singular}s`);
+}
diff --git a/src/utils/StringHelperFunctions.ts b/src/utils/StringHelperFunctions.ts
index b77c41426..0790e1ccf 100644
--- a/src/utils/StringHelperFunctions.ts
+++ b/src/utils/StringHelperFunctions.ts
@@ -1,5 +1,6 @@
import { Settings } from "../Settings/Settings";
import { CONSTANTS } from "../Constants";
+import { pluralize } from "./I18nUtils";
/*
Converts a date representing time in milliseconds to a string with the format H hours M minutes and S seconds
@@ -38,13 +39,13 @@ export function convertTimeMsToTimeElapsedString(time: number, showMilli = false
let res = "";
if (days > 0) {
- res += `${days} day${days === 1 ? "" : "s"} `;
+ res += `${pluralize(days, "day")} `;
}
if (hours > 0 || (Settings.ShowMiddleNullTimeUnit && res != "")) {
- res += `${hours} hour${hours === 1 ? "" : "s"} `;
+ res += `${pluralize(hours, "hour")} `;
}
if (minutes > 0 || (Settings.ShowMiddleNullTimeUnit && res != "")) {
- res += `${minutes} minute${minutes === 1 ? "" : "s"} `;
+ res += `${pluralize(minutes, "minute")} `;
}
res += `${seconds} second${!showMilli && secTruncMinutes === 1 ? "" : "s"}`;