diff --git a/.github/workflows/web-crowdin-update.yml b/.github/workflows/web-crowdin-update.yml new file mode 100644 index 000000000..63a643cfc --- /dev/null +++ b/.github/workflows/web-crowdin-update.yml @@ -0,0 +1,39 @@ +name: "Update Crowdin translations (web)" + +# This is a variant of web-crowdin.yml that also uploads the translated strings +# (in addition to the source strings). This allows us to change the strings in +# our source code for an automated refactoring (e.g. renaming a key), and then +# run this workflow to update the data in Crowdin taking our source code as the +# source of truth. + +on: + # Only allow running manually. + workflow_dispatch: + +jobs: + synchronize-with-crowdin: + runs-on: ubuntu-latest + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Crowdin's action + uses: crowdin/github-action@v1 + with: + base_path: "web/" + config: "web/crowdin.yml" + upload_sources: true + # This is what differs from web-crowdin.yml + upload_translations: true + download_translations: true + localization_branch_name: translations/web + create_pull_request: true + skip_untranslated_strings: true + pull_request_title: "[web] Updated translations" + pull_request_body: "Updated translations from [Crowdin](https://crowdin.com/project/ente-photos-web)" + pull_request_base_branch_name: "main" + project_id: 569613 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + CROWDIN_PERSONAL_TOKEN: ${{ secrets.CROWDIN_PERSONAL_TOKEN }} diff --git a/web/apps/cast/src/pages/index.tsx b/web/apps/cast/src/pages/index.tsx index 00a9fdc72..bc0f6253d 100644 --- a/web/apps/cast/src/pages/index.tsx +++ b/web/apps/cast/src/pages/index.tsx @@ -83,7 +83,8 @@ export default function Index() { fontWeight: "normal", }} > - Enter this code on Ente Photos to pair this screen + Enter this code on Ente Photos to pair this + screen
{listItem.fileCount} {t("FILES")},{" "} - {convertBytesToHumanReadable(listItem.fileSize || 0)}{" "} - {t("EACH")} + {formattedByteSize(listItem.fileSize || 0)} {t("EACH")} ); case ITEM_TYPE.FILE: { diff --git a/web/apps/photos/src/components/PhotoList/index.tsx b/web/apps/photos/src/components/PhotoList/index.tsx index 4803995d4..5ac6b263e 100644 --- a/web/apps/photos/src/components/PhotoList/index.tsx +++ b/web/apps/photos/src/components/PhotoList/index.tsx @@ -22,9 +22,9 @@ import { areEqual, } from "react-window"; import { EnteFile } from "types/file"; -import { convertBytesToHumanReadable } from "utils/file"; import { handleSelectCreator } from "utils/photoFrame"; import { PublicCollectionGalleryContext } from "utils/publicCollectionGallery"; +import { formattedByteSize } from "utils/units"; const A_DAY = 24 * 60 * 60 * 1000; const FOOTER_HEIGHT = 90; @@ -829,8 +829,7 @@ export function PhotoList({ return ( {listItem.fileCount} {t("FILES")},{" "} - {convertBytesToHumanReadable(listItem.fileSize || 0)}{" "} - {t("EACH")} + {formattedByteSize(listItem.fileSize || 0)} {t("EACH")} ); case ITEM_TYPE.FILE: { diff --git a/web/apps/photos/src/components/PhotoViewer/FileInfo/RenderFileName.tsx b/web/apps/photos/src/components/PhotoViewer/FileInfo/RenderFileName.tsx index 399051185..e9e27d55e 100644 --- a/web/apps/photos/src/components/PhotoViewer/FileInfo/RenderFileName.tsx +++ b/web/apps/photos/src/components/PhotoViewer/FileInfo/RenderFileName.tsx @@ -7,8 +7,8 @@ import VideocamOutlined from "@mui/icons-material/VideocamOutlined"; import Box from "@mui/material/Box"; import { useEffect, useState } from "react"; import { EnteFile } from "types/file"; -import { makeHumanReadableStorage } from "utils/billing"; import { changeFileName, updateExistingFilePubMetadata } from "utils/file"; +import { formattedByteSize } from "utils/units"; import { FileNameEditDialog } from "./FileNameEditDialog"; import InfoItem from "./InfoItem"; @@ -33,7 +33,7 @@ const getCaption = (file: EnteFile, parsedExifData) => { captionParts.push(resolution); } if (fileSize) { - captionParts.push(makeHumanReadableStorage(fileSize)); + captionParts.push(formattedByteSize(fileSize)); } return ( diff --git a/web/apps/photos/src/components/Sidebar/SubscriptionCard/contentOverlay/individual/usageSection.tsx b/web/apps/photos/src/components/Sidebar/SubscriptionCard/contentOverlay/individual/usageSection.tsx index 4b0ce31b0..8975941ad 100644 --- a/web/apps/photos/src/components/Sidebar/SubscriptionCard/contentOverlay/individual/usageSection.tsx +++ b/web/apps/photos/src/components/Sidebar/SubscriptionCard/contentOverlay/individual/usageSection.tsx @@ -1,7 +1,7 @@ import { SpaceBetweenFlex } from "@ente/shared/components/Container"; import { Box, Typography } from "@mui/material"; import { t } from "i18next"; -import { makeHumanReadableStorage } from "utils/billing"; +import { formattedStorageByteSize } from "utils/units"; import { Progressbar } from "../../styledComponents"; @@ -19,7 +19,7 @@ export function IndividualUsageSection({ usage, storage, fileCount }: Iprops) { marginTop: 1.5, }} > - {`${makeHumanReadableStorage( + {`${formattedStorageByteSize( storage - usage, )} ${t("FREE")}`} diff --git a/web/apps/photos/src/components/Sidebar/SubscriptionCard/contentOverlay/storageSection.tsx b/web/apps/photos/src/components/Sidebar/SubscriptionCard/contentOverlay/storageSection.tsx index 6143044f0..78a367797 100644 --- a/web/apps/photos/src/components/Sidebar/SubscriptionCard/contentOverlay/storageSection.tsx +++ b/web/apps/photos/src/components/Sidebar/SubscriptionCard/contentOverlay/storageSection.tsx @@ -1,6 +1,6 @@ import { Box, styled, Typography } from "@mui/material"; import { t } from "i18next"; -import { convertBytesToGBs, makeHumanReadableStorage } from "utils/billing"; +import { bytesInGB, formattedStorageByteSize } from "utils/units"; const MobileSmallBox = styled(Box)` display: none; @@ -30,9 +30,9 @@ export default function StorageSection({ usage, storage }: Iprops) { fontWeight={"bold"} sx={{ fontSize: "24px", lineHeight: "30px" }} > - {`${makeHumanReadableStorage(usage, { roundUp: true })} ${t( + {`${formattedStorageByteSize(usage, { round: true })} ${t( "OF", - )} ${makeHumanReadableStorage(storage)} ${t("USED")}`} + )} ${formattedStorageByteSize(storage)} ${t("USED")}`} @@ -40,9 +40,7 @@ export default function StorageSection({ usage, storage }: Iprops) { fontWeight={"bold"} sx={{ fontSize: "24px", lineHeight: "30px" }} > - {`${convertBytesToGBs(usage)} / ${convertBytesToGBs( - storage, - )} ${t("GB")} ${t("USED")}`} + {`${bytesInGB(usage)} / ${bytesInGB(storage)} ${t("GB")} ${t("USED")}`} diff --git a/web/apps/photos/src/components/pages/gallery/PlanSelector/card/paid.tsx b/web/apps/photos/src/components/pages/gallery/PlanSelector/card/paid.tsx index 4ef76a491..0ef4b1594 100644 --- a/web/apps/photos/src/components/pages/gallery/PlanSelector/card/paid.tsx +++ b/web/apps/photos/src/components/pages/gallery/PlanSelector/card/paid.tsx @@ -5,11 +5,8 @@ import Box from "@mui/material/Box"; import Typography from "@mui/material/Typography"; import { t } from "i18next"; import { Trans } from "react-i18next"; -import { - convertBytesToGBs, - hasAddOnBonus, - isSubscriptionCancelled, -} from "utils/billing"; +import { hasAddOnBonus, isSubscriptionCancelled } from "utils/billing"; +import { bytesInGB } from "utils/units"; import { ManageSubscription } from "../manageSubscription"; import { PeriodToggler } from "../periodToggler"; import Plans from "../plans"; @@ -35,8 +32,7 @@ export default function PaidSubscriptionPlanSelectorCard({ {t("SUBSCRIPTION")} - {convertBytesToGBs(subscription.storage, 2)}{" "} - {t("GB")} + {bytesInGB(subscription.storage, 2)} {t("GB")} @@ -50,7 +46,7 @@ export default function PaidSubscriptionPlanSelectorCard({ diff --git a/web/apps/photos/src/components/pages/gallery/PlanSelector/plans/BfAddOnRow.tsx b/web/apps/photos/src/components/pages/gallery/PlanSelector/plans/BfAddOnRow.tsx index 8b0ce7bd5..5f7e13deb 100644 --- a/web/apps/photos/src/components/pages/gallery/PlanSelector/plans/BfAddOnRow.tsx +++ b/web/apps/photos/src/components/pages/gallery/PlanSelector/plans/BfAddOnRow.tsx @@ -2,7 +2,7 @@ import { SpaceBetweenFlex } from "@ente/shared/components/Container"; import { Box, styled, Typography } from "@mui/material"; import { Trans } from "react-i18next"; -import { makeHumanReadableStorage } from "utils/billing"; +import { formattedStorageByteSize } from "utils/units"; const RowContainer = styled(SpaceBetweenFlex)(({ theme }) => ({ // gap: theme.spacing(1.5), @@ -24,7 +24,7 @@ export function BFAddOnRow({ bonusData, closeModal }) { - {convertBytesToGBs(plan.storage)} + {bytesInGB(plan.storage)} diff --git a/web/apps/photos/src/utils/billing/index.ts b/web/apps/photos/src/utils/billing/index.ts index 3dfde5384..d2e593e9e 100644 --- a/web/apps/photos/src/utils/billing/index.ts +++ b/web/apps/photos/src/utils/billing/index.ts @@ -31,44 +31,6 @@ enum RESPONSE_STATUS { fail = "fail", } -const StorageUnits = ["B", "KB", "MB", "GB", "TB"]; - -const ONE_GB = 1024 * 1024 * 1024; - -export function convertBytesToGBs(bytes: number, precision = 0): string { - return (bytes / (1024 * 1024 * 1024)).toFixed(precision); -} - -export function makeHumanReadableStorage( - bytes: number, - { roundUp } = { roundUp: false }, -): string { - if (bytes <= 0) { - return `0 ${t("STORAGE_UNITS.MB")}`; - } - const i = Math.floor(Math.log(bytes) / Math.log(1024)); - - let quantity = bytes / Math.pow(1024, i); - let unit = StorageUnits[i]; - - if (quantity > 100 && unit !== "GB") { - quantity /= 1024; - unit = StorageUnits[i + 1]; - } - - quantity = Number(quantity.toFixed(1)); - - if (bytes >= 10 * ONE_GB) { - if (roundUp) { - quantity = Math.ceil(quantity); - } else { - quantity = Math.round(quantity); - } - } - - return `${quantity} ${t(`STORAGE_UNITS.${unit}`)}`; -} - export function hasPaidSubscription(subscription: Subscription) { return ( subscription && @@ -160,9 +122,8 @@ export function isSubscriptionPastDue(subscription: Subscription) { ); } -export function isPopularPlan(plan: Plan) { - return plan.storage === 100 * ONE_GB; -} +export const isPopularPlan = (plan: Plan) => + plan.storage === 100 * 1024 * 1024 * 1024; /* 100 GB */ export async function updateSubscription( plan: Plan, diff --git a/web/apps/photos/src/utils/file/index.ts b/web/apps/photos/src/utils/file/index.ts index bb212ff21..98a8dd948 100644 --- a/web/apps/photos/src/utils/file/index.ts +++ b/web/apps/photos/src/utils/file/index.ts @@ -103,19 +103,6 @@ export async function getUpdatedEXIFFileForDownload( } } -export function convertBytesToHumanReadable( - bytes: number, - precision = 2, -): string { - if (bytes === 0 || isNaN(bytes)) { - return "0 MB"; - } - - const i = Math.floor(Math.log(bytes) / Math.log(1024)); - const sizes = ["B", "KB", "MB", "GB", "TB", "PB", "EB", "ZB", "YB"]; - return (bytes / Math.pow(1024, i)).toFixed(precision) + " " + sizes[i]; -} - export async function downloadFile(file: EnteFile) { try { const fileReader = new FileReader(); diff --git a/web/apps/photos/src/utils/units.ts b/web/apps/photos/src/utils/units.ts new file mode 100644 index 000000000..4cb875b4d --- /dev/null +++ b/web/apps/photos/src/utils/units.ts @@ -0,0 +1,85 @@ +import { t } from "i18next"; + +const StorageUnits = ["B", "KB", "MB", "GB", "TB"]; + +/** + * Convert the given number of {@link bytes} to their equivalent GB string with + * {@link precision}. + * + * The returned string does not have the GB suffix. + */ +export const bytesInGB = (bytes: number, precision = 0): string => + (bytes / (1024 * 1024 * 1024)).toFixed(precision); + +/** + * Convert the given number of {@link bytes} to a user visible string in an + * appropriately sized unit. + * + * The returned string includes the (localized) unit suffix, e.g. "TB". + * + * @param precision Modify the number of digits after the decimal point. + * Defaults to 2. + */ +export function formattedByteSize(bytes: number, precision = 2): string { + if (bytes === 0 || isNaN(bytes)) { + return "0 MB"; + } + + const i = Math.floor(Math.log(bytes) / Math.log(1024)); + const sizes = ["B", "KB", "MB", "GB", "TB", "PB", "EB", "ZB", "YB"]; + return (bytes / Math.pow(1024, i)).toFixed(precision) + " " + sizes[i]; +} + +interface FormattedStorageByteSizeOptions { + /** + * If `true` then round up the fractional quantity we obtain when dividing + * the number of bytes by the number of bytes in the unit that got chosen. + * + * The default behaviour is to take the ceiling. + */ + round?: boolean; +} + +/** + * Convert the given number of storage {@link bytes} to a user visible string in + * an appropriately sized unit. + * + * This differs from {@link formattedByteSize} in that while + * {@link formattedByteSize} is meant for arbitrary byte sizes, this function + * has a few additional beautification heuristics that we want to apply when + * displaying the "storage size" (in different contexts) as opposed to, say, a + * generic "file size". + * + * @param options + * + * @return A user visible string, including the localized unit suffix. + */ +export const formattedStorageByteSize = ( + bytes: number, + options?: FormattedStorageByteSizeOptions, +): string => { + if (bytes <= 0) { + return `0 ${t("STORAGE_UNITS.MB")}`; + } + const i = Math.floor(Math.log(bytes) / Math.log(1024)); + + let quantity = bytes / Math.pow(1024, i); + let unit = StorageUnits[i]; + + if (quantity > 100 && unit !== "GB") { + quantity /= 1024; + unit = StorageUnits[i + 1]; + } + + quantity = Number(quantity.toFixed(1)); + + if (bytes >= 10 * 1024 * 1024 * 1024 /* 10 GB */) { + if (options?.round) { + quantity = Math.ceil(quantity); + } else { + quantity = Math.round(quantity); + } + } + + return `${quantity} ${t(`STORAGE_UNITS.${unit}`)}`; +};