Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
dd04f60
Add localization infrastructure setup
devin-ai-integration[bot] Jun 1, 2025
ddca024
Switch to on-demand key creation approach
devin-ai-integration[bot] Jun 1, 2025
f476d1e
Implement Phase 2: Extract strings from high-impact components
devin-ai-integration[bot] Jun 1, 2025
f2197a8
Extract strings from authentication and checkout components
devin-ai-integration[bot] Jun 1, 2025
0890316
Extract strings from PaymentForm error handling and Nav component
devin-ai-integration[bot] Jun 1, 2025
2ae7c19
Extract strings from PurchaseArchiveButton component
devin-ai-integration[bot] Jun 1, 2025
2f779dc
Extract strings from FollowButton and DiscordButton components
devin-ai-integration[bot] Jun 1, 2025
0f6e57e
Extract strings from LoginPage and PostCommentsSection components
devin-ai-integration[bot] Jun 1, 2025
cb3bc29
Extract strings from PayPal settings component
devin-ai-integration[bot] Jun 1, 2025
ceee6ae
Extract strings from ActionsPopover component
devin-ai-integration[bot] Jun 1, 2025
62f210a
Extract strings from WishlistsSectionView component
devin-ai-integration[bot] Jun 1, 2025
100c1da
Extract strings from Profile SettingsPage component
devin-ai-integration[bot] Jun 1, 2025
e93caaf
Extract error messages from Product CardGrid component
devin-ai-integration[bot] Jun 1, 2025
5f6476e
Extract error message from AnalyticsPage component
devin-ai-integration[bot] Jun 1, 2025
fbaf671
Extract error messages from Nav and ImageUploader components
devin-ai-integration[bot] Jun 1, 2025
a88d8ce
Extract validation messages from Product Layout and index components
devin-ai-integration[bot] Jun 1, 2025
897e8f8
Extract strings from BundleEdit Layout and WishlistsPage components
devin-ai-integration[bot] Jun 1, 2025
5de17be
Extract strings from CollaboratorsPage component
devin-ai-integration[bot] Jun 1, 2025
9d1c56d
Extract strings from AffiliatesPage component
devin-ai-integration[bot] Jun 1, 2025
16606a1
Extract strings from ProductEditPage and ReviewForm components
devin-ai-integration[bot] Jun 1, 2025
f4d096f
Phase 2: Mass string extraction from 71 React components
devin-ai-integration[bot] Jun 1, 2025
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
1 change: 1 addition & 0 deletions Gemfile
Original file line number Diff line number Diff line change
Expand Up @@ -197,3 +197,4 @@ gem "ruby-openai", "~> 7.0"
gem "anycable-rails", "~> 1.5"
gem "react_on_rails", "~> 14.0"
gem "psych", "~> 5.2.3"
gem "i18n-tasks", "~> 1.0"
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import cx from "classnames";
import * as React from "react";
import { Link, useLoaderData } from "react-router-dom";
import { cast } from "ts-safe-cast";
import { useTranslation } from "react-i18next";

import {
submitAffiliateSignupForm,
Expand Down Expand Up @@ -41,6 +42,7 @@ const validateProduct = (product: SelfServeAffiliateProduct): InvalidProductAttr
};

export const AffiliateSignupForm = () => {
const { t } = useTranslation('common');
const data = cast<AffiliateSignupFormPageData>(useLoaderData());
const loggedInUser = useLoggedInUser();
const [isSaving, setIsSaving] = React.useState(false);
Expand All @@ -57,17 +59,17 @@ export const AffiliateSignupForm = () => {

const handleSaveChanges = asyncVoid(async () => {
if (products.some((product) => validateProduct(product).size > 0)) {
showAlert("There are some errors on the page. Please fix them and try again.", "error");
showAlert(t("errors.fix_errors_and_try_again"), "error");
return;
}

try {
setIsSaving(true);
await submitAffiliateSignupForm({ products, disable_global_affiliate: disableGlobalAffiliate });
showAlert("Changes saved!", "success");
showAlert(t("actions.changes_saved"), "success");
} catch (e) {
assertResponseError(e);
showAlert(`An error occurred while saving changes${e.message ? ` - ${e.message}` : ""}`, "error");
showAlert(t("errors.error_saving_changes", { message: e.message || "" }), "error");
} finally {
setIsSaving(false);
}
Expand Down
12 changes: 7 additions & 5 deletions app/javascript/components/Authentication/ForgotPasswordForm.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import * as React from "react";
import { useTranslation } from "react-i18next";

import { renewPassword } from "$app/data/login";
import { assertResponseError } from "$app/utils/request";
Expand All @@ -10,6 +11,7 @@ import { showAlert } from "$app/components/server-components/Alert";
type SaveState = { type: "initial" | "submitting" } | { type: "error"; message: string };

export const ForgotPasswordForm = ({ onClose }: { onClose: () => void }) => {
const { t } = useTranslation('authentication');
const uid = React.useId();
const [email, setEmail] = React.useState("");
const [saveState, setSaveState] = React.useState<SaveState>({ type: "initial" });
Expand All @@ -19,7 +21,7 @@ export const ForgotPasswordForm = ({ onClose }: { onClose: () => void }) => {
setSaveState({ type: "submitting" });
try {
await renewPassword(email);
showAlert("Password reset sent! Please make sure to check your spam folder.", "success");
showAlert(t("forgot_password.password_reset_sent"), "success");
setSaveState({ type: "initial" });
} catch (e) {
assertResponseError(e);
Expand All @@ -31,7 +33,7 @@ export const ForgotPasswordForm = ({ onClose }: { onClose: () => void }) => {
<form onSubmit={(e) => void handleSubmit(e)}>
<SocialAuth />
<div role="separator">
<span>or</span>
<span>{t("forgot_password.or")}</span>
</div>
<section>
{saveState.type === "error" ? (
Expand All @@ -41,14 +43,14 @@ export const ForgotPasswordForm = ({ onClose }: { onClose: () => void }) => {
) : null}
<fieldset>
<legend>
<label htmlFor={uid}>Email to send reset instructions to</label>
<label htmlFor={uid}>{t("forgot_password.email_label")}</label>
</legend>
<input id={uid} type="email" value={email} onChange={(e) => setEmail(e.target.value)} required />
</fieldset>
<Button color="primary" type="submit" disabled={saveState.type === "submitting"}>
{saveState.type === "submitting" ? "Sending..." : "Send"}
{saveState.type === "submitting" ? t("forgot_password.sending") : t("forgot_password.send")}
</Button>
<Button onClick={onClose}>Cancel</Button>
<Button onClick={onClose}>{t("forgot_password.cancel")}</Button>
</section>
</form>
);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import * as React from "react";
import { useTranslation } from "react-i18next";

import { updatePurchasesContent } from "$app/data/bundle";
import { assertResponseError } from "$app/utils/request";
Expand All @@ -8,6 +9,7 @@ import { Button } from "$app/components/Button";
import { showAlert } from "$app/components/server-components/Alert";

export const BundleContentUpdatedStatus = () => {
const { t } = useTranslation('common');
const { id } = useBundleEditContext();
const [isHidden, setIsHidden] = React.useState(false);
const [isLoading, setIsLoading] = React.useState(false);
Expand All @@ -16,7 +18,7 @@ export const BundleContentUpdatedStatus = () => {
setIsLoading(true);
try {
await updatePurchasesContent(id);
showAlert("Queued an update to the content of all outdated purchases.", "success");
showAlert(t("actions.queued_content_update"), "success");
setIsHidden(true);
} catch (e) {
assertResponseError(e);
Expand Down
12 changes: 7 additions & 5 deletions app/javascript/components/BundleEdit/Layout.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import * as React from "react";
import { Link, useMatches, useNavigate } from "react-router-dom";
import { useTranslation } from "react-i18next";

import { saveBundle } from "$app/data/bundle";
import { setProductPublished } from "$app/data/publish_product";
Expand Down Expand Up @@ -42,12 +43,13 @@ export const Layout = ({
const [match] = useMatches();
const tab = match?.handle ?? "product";

const { t } = useTranslation('common');
const [isSaving, setIsSaving] = React.useState(false);
const handleSave = async () => {
try {
setIsSaving(true);
await saveBundle(id, bundle);
showAlert("Changes saved!", "success");
showAlert(t("actions.changes_saved"), "success");
} catch (e) {
assertResponseError(e);
showAlert(e.message, "error");
Expand All @@ -62,7 +64,7 @@ export const Layout = ({
await saveBundle(id, bundle);
await setProductPublished(uniquePermalink, published);
updateBundle({ is_published: published });
showAlert(published ? "Published!" : "Unpublished!", "success");
showAlert(published ? t("actions.published") : t("actions.unpublished"), "success");
if (tab === "share") navigate(`/bundles/${id}/content`);
} catch (e) {
assertResponseError(e);
Expand All @@ -87,9 +89,9 @@ export const Layout = ({

const onTabClick = (e: React.MouseEvent<HTMLAnchorElement>, callback?: () => void) => {
const message = isUploadingFiles
? "Some files are still uploading, please wait..."
? t("errors.files_still_uploading")
: isUploadingFilesOrImages
? "Some images are still uploading, please wait..."
? t("errors.images_still_uploading")
: undefined;

if (message) {
Expand Down Expand Up @@ -155,7 +157,7 @@ export const Layout = ({
if (!bundle.is_published) {
evt.preventDefault();
showAlert(
"Not yet! You've got to publish your awesome product before you can share it with your audience and the world.",
t("errors.must_publish_before_share"),
"warning",
);
}
Expand Down
28 changes: 14 additions & 14 deletions app/javascript/components/Checkout/GiftForm.tsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
import cx from "classnames";
import * as React from "react";
import { useTranslation } from "react-i18next";

import { Button } from "$app/components/Button";
import { useState, getErrors } from "$app/components/Checkout/payment";
import { Icon } from "$app/components/Icons";
import { Modal } from "$app/components/Modal";

export const GiftForm = ({ isMembership }: { isMembership: boolean }) => {
const { t } = useTranslation('checkout');
const giftEmailUID = React.useId();
const giftNoteUID = React.useId();
const [cancellingPresetGift, setCancellingPresetGift] = React.useState(false);
Expand All @@ -20,7 +22,7 @@ export const GiftForm = ({ isMembership }: { isMembership: boolean }) => {
<label className="flex w-full items-center justify-between">
<div className="flex items-center">
<Icon name="gift-fill" className="mr-2" />
<h4>Give as a gift?</h4>
<h4>{t("gift.give_as_gift")}</h4>
</div>
<input
type="checkbox"
Expand All @@ -42,67 +44,65 @@ export const GiftForm = ({ isMembership }: { isMembership: boolean }) => {
{isMembership ? (
<div role="alert" className="info">
<div>
Note: Free trials will be charged immediately. The membership will not auto-renew. The recipient must
update the payment method to renew the membership.
{t("gift.membership_note")}
</div>
</div>
) : null}
{gift.type === "normal" ? (
<fieldset className={cx({ danger: hasError })}>
<legend>
<label htmlFor={giftEmailUID}>Recipient email</label>
<label htmlFor={giftEmailUID}>{t("gift.recipient_email")}</label>
</legend>
<input
id={giftEmailUID}
type="email"
value={gift.email}
onChange={(evt) => dispatch({ type: "set-value", gift: { ...gift, email: evt.target.value } })}
placeholder="Recipient email address"
placeholder={t("gift.recipient_email_address")}
aria-invalid={hasError}
className="w-full"
/>
</fieldset>
) : (
<div role="alert" className="info">
<div>
{gift.name}'s email has been hidden for privacy purposes.{" "}
{t("gift.email_hidden", { name: gift.name })}{" "}
<button className="link" onClick={() => setCancellingPresetGift(true)}>
Cancel gift option
{t("gift.cancel_gift_option")}
</button>
</div>
<Modal
open={cancellingPresetGift}
onClose={() => setCancellingPresetGift(false)}
footer={
<>
<Button onClick={() => setCancellingPresetGift(false)}>No, cancel</Button>
<Button onClick={() => setCancellingPresetGift(false)}>{t("gift.no_cancel")}</Button>
<Button
color="primary"
onClick={() => {
dispatch({ type: "set-value", gift: null });
setCancellingPresetGift(false);
}}
>
Yes, reset
{t("gift.yes_reset")}
</Button>
</>
}
title="Reset gift option?"
title={t("gift.reset_gift_option")}
>
You are about to switch off the gift option. To gift this wishlist again, you will need to return to the
wishlist page and select "Gift this product".
{t("gift.switch_off_gift")}
</Modal>
</div>
)}
<fieldset className="w-full">
<legend>
<label htmlFor={giftNoteUID}>Message</label>
<label htmlFor={giftNoteUID}>{t("gift.message")}</label>
</legend>
<textarea
id={giftNoteUID}
value={gift.note}
onChange={(evt) => dispatch({ type: "set-value", gift: { ...gift, note: evt.target.value } })}
placeholder="A personalized message (optional)"
placeholder={t("gift.personalized_message")}
className="w-full"
/>
</fieldset>
Expand Down
4 changes: 3 additions & 1 deletion app/javascript/components/Checkout/PaymentForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import * as BraintreeDataCollector from "braintree-web/data-collector";
import * as BraintreePaypal from "braintree-web/paypal";
import cx from "classnames";
import * as React from "react";
import { useTranslation } from "react-i18next";

import { useBraintreeToken } from "$app/data/braintree_client_token_data";
import { preparePaymentRequestPaymentMethodData } from "$app/data/card_payment_method_data";
Expand Down Expand Up @@ -419,9 +420,10 @@ const PaymentMethodRadio = ({
};

const useFail = () => {
const { t } = useTranslation('checkout');
const [_, dispatch] = useState();
return () => {
showAlert("Sorry, something went wrong. You were not charged.", "error");
showAlert(t("errors.something_went_wrong"), "error");
dispatch({ type: "cancel" });
};
};
Expand Down
6 changes: 4 additions & 2 deletions app/javascript/components/Developer/FollowFormEmbed.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import * as React from "react";
import { useTranslation } from "react-i18next";

import { followFromEmbed } from "$app/data/follow_embed";
import { asyncVoid } from "$app/utils/promise";
Expand All @@ -12,6 +13,7 @@ import { showAlert } from "$app/components/server-components/Alert";
export const FOLLOW_FORM_EMBED_INPUT_ID = "gumroad-follow-form-embed-input";

export const FollowFormEmbed = ({ sellerId, preview }: { sellerId: string; preview?: boolean }) => {
const { t } = useTranslation('common');
const [email, setEmail] = React.useState("");
const appDomain = useAppDomain();
const followFormRef = React.useRef<HTMLDivElement & HTMLFormElement>(null);
Expand Down Expand Up @@ -64,10 +66,10 @@ export const FollowFormEmbed = ({ sellerId, preview }: { sellerId: string; previ
evt.preventDefault();
try {
await followFromEmbed(sellerId, email);
showAlert("Check your inbox to confirm your follow request.", "success");
showAlert(t("actions.check_inbox_confirm_follow"), "success");
} catch (e) {
assertResponseError(e);
showAlert("Sorry, something went wrong. Please try again.", "error");
showAlert(t("errors.sorry_something_went_wrong"), "error");
}
})}
>
Expand Down
14 changes: 8 additions & 6 deletions app/javascript/components/DiscordButton.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import * as React from "react";
import { useTranslation } from "react-i18next";

import { joinServer, leaveServer } from "$app/data/discord_integration";
import { DISCORD_CLIENT_ID, DISCORD_OAUTH_URL } from "$app/utils/integrations";
Expand All @@ -19,6 +20,7 @@ export const DiscordButton = ({
redirectSettings?: { host: string; protocol: string };
customState?: string;
}) => {
const { t } = useTranslation('common');
const [discordConnected, setDiscordConnected] = React.useState(connected);
const [loading, setLoading] = React.useState(false);

Expand All @@ -40,15 +42,15 @@ export const DiscordButton = ({
onSuccess: async (code) => {
const response = await joinServer(code, purchaseId);
if (response.ok) {
showAlert(`You've been added to the Discord server #${response.serverName}!`, "success");
showAlert(t("actions.discord_joined", { serverName: response.serverName }), "success");
setDiscordConnected(true);
} else {
showAlert("Could not join the Discord server, please try again.", "error");
showAlert(t("errors.discord_join_failed"), "error");
}
setLoading(false);
},
onError: () => {
showAlert("Could not join the Discord server, please try again.", "error");
showAlert(t("errors.discord_join_failed"), "error");
setLoading(false);
},
onPopupClose: () => setLoading(false),
Expand All @@ -61,10 +63,10 @@ export const DiscordButton = ({

const response = await leaveServer(purchaseId);
if (response.ok) {
showAlert(`You've left the Discord server #${response.serverName}.`, "success");
showAlert(t("actions.discord_left", { serverName: response.serverName }), "success");
setDiscordConnected(false);
} else {
showAlert("Could not leave the Discord server.", "error");
showAlert(t("errors.discord_leave_failed"), "error");
}
setLoading(false);
};
Expand All @@ -75,7 +77,7 @@ export const DiscordButton = ({
</div>
) : (
<Button className="button-discord" onClick={discordConnected ? leaveDiscord : openJoinDiscordPopup}>
{discordConnected ? "Leave Discord" : "Join Discord"}
{discordConnected ? t("actions.leave_discord") : t("actions.join_discord")}
</Button>
);
};
4 changes: 3 additions & 1 deletion app/javascript/components/Discover/Search.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import cx from "classnames";
import * as React from "react";
import { useTranslation } from "react-i18next";

import { getAutocompleteSearchResults, AutocompleteSearchResults, deleteAutocompleteSearch } from "$app/data/discover";
import { escapeRegExp } from "$app/utils";
Expand All @@ -15,6 +16,7 @@ import { useOnChange } from "$app/components/useOnChange";
import thumbnailPlaceholder from "$assets/images/placeholders/product-cover.png";

export const Search = ({ query, setQuery }: { query?: string | undefined; setQuery: (query: string) => void }) => {
const { t } = useTranslation('common');
const [enteredQuery, setEnteredQuery] = React.useState(query ?? "");
useOnChange(() => setEnteredQuery(query ?? ""), [query]);

Expand All @@ -27,7 +29,7 @@ export const Search = ({ query, setQuery }: { query?: string | undefined; setQue
setResults(await getAutocompleteSearchResults({ query: enteredQuery }, abortController.signal));
} catch (e) {
assertResponseError(e);
showAlert("Sorry, something went wrong. Please try again.", "error");
showAlert(t("errors.sorry_something_went_wrong"), "error");
}
}),
300,
Expand Down
Loading