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"}`;