feat: add i18n

This commit is contained in:
kawamataryo
2024-12-07 22:40:33 +09:00
parent 3c03d99a16
commit fc91c4d3a2
21 changed files with 1882 additions and 93 deletions

View File

@@ -26,12 +26,14 @@ const ReSearchModal: React.FC<ReSearchModalProps> = ({
}) => {
return (
<Modal open={open} width={600} onClose={onClose}>
<h2 className="text-lg font-bold text-center py-2">Search Results</h2>
<h2 className="text-lg font-bold text-center py-2">
{chrome.i18n.getMessage("re_search_modal_title")}
</h2>
{reSearchResults.users.length === 0 && (
<div className="text-center flex justify-center items-center flex-col gap-4 mt-5">
<span className="loading loading-spinner loading-lg" />
<div className="text-center flex justify-center items-center text-sm">
Loading...
{chrome.i18n.getMessage("loading")}
</div>
</div>
)}

View File

@@ -108,12 +108,18 @@ const App = () => {
<div className="flex flex-col gap-2 items-center">
{loading && (
<p className="text-lg font-bold">
Scanning {serviceName} users to find bsky users...
{chrome.i18n.getMessage("scanning_users", [serviceName])}
</p>
)}
<p className="text-2xl font-bold">
Detected <span className="text-4xl">{users.length}</span> users
</p>
<p
className="text-2xl font-bold"
// biome-ignore lint/security/noDangerouslySetInnerHtml: <explanation>
dangerouslySetInnerHTML={{
__html: chrome.i18n.getMessage("detected_users", [
users.length.toString(),
]),
}}
/>
{errorMessage && <AlertError>{errorMessage}</AlertError>}
{loading && (
<>
@@ -122,7 +128,7 @@ const App = () => {
className="btn btn-primary mt-5 btn-ghost"
onClick={stopAndShowDetectedUsers}
>
Stop Scanning and View Results
{chrome.i18n.getMessage("stop_scanning_and_view_results")}
</button>
<LoadingCards />
</>
@@ -133,7 +139,7 @@ const App = () => {
className="btn btn-primary mt-5"
onClick={restart}
>
Resume Scanning
{chrome.i18n.getMessage("resume_scanning")}
</button>
)}
{!loading && isBottomReached && (
@@ -143,14 +149,14 @@ const App = () => {
className="btn btn-primary mt-5"
onClick={openOptionPage}
>
View Detected Users
{chrome.i18n.getMessage("view_detected_users")}
</button>
<button
type="button"
className="btn btn-primary mt-5 btn-ghost"
onClick={restart}
>
Resume Scanning
{chrome.i18n.getMessage("resume_scanning")}
</button>
</div>
)}

View File

@@ -22,7 +22,7 @@ const AsyncButton = ({ onClick, label, className }: Props) => {
onClick={handleClick}
disabled={loading}
>
{loading ? "Processing..." : label}
{loading ? chrome.i18n.getMessage("loading") : label}
</button>
);
};

View File

@@ -29,16 +29,16 @@ const DetectedUserListItem = ({
match(actionMode)
.with(ACTION_MODE.FOLLOW, ACTION_MODE.IMPORT_LIST, () => {
const follow = {
label: "Follow on Bluesky",
label: chrome.i18n.getMessage("button_follow_on_bluesky"),
class: "btn-primary",
};
const following = {
label: "Following on Bluesky",
label: chrome.i18n.getMessage("button_following_on_bluesky"),
class:
"btn-outline hover:bg-transparent hover:border hover:bg-transparent hover:text-base-content",
};
const unfollow = {
label: "Unfollow on Bluesky",
label: chrome.i18n.getMessage("button_unfollow_on_bluesky"),
class:
"text-red-500 hover:bg-transparent hover:border hover:border-red-500",
};
@@ -52,16 +52,16 @@ const DetectedUserListItem = ({
})
.with(ACTION_MODE.BLOCK, () => {
const block = {
label: "Block on Bluesky",
label: chrome.i18n.getMessage("button_block_on_bluesky"),
class: "btn-primary",
};
const blocking = {
label: "Blocking on Bluesky",
label: chrome.i18n.getMessage("button_blocking_on_bluesky"),
class:
"btn-outline hover:bg-transparent hover:border hover:bg-transparent hover:text-base-content",
};
const unblock = {
label: "Unblock on Bluesky",
label: chrome.i18n.getMessage("button_unblock_on_bluesky"),
class:
"text-red-500 hover:bg-transparent hover:border hover:border-red-500",
};
@@ -106,12 +106,12 @@ const DetectedUserListItem = ({
<div>
<div className={`w-full border-l-8 border-${matchTypeColor}`}>
<div
className={`w-full border-t border-gray-500 text-${matchTypeColor} grid grid-cols-[22%_1fr]`}
className={`w-full border-t border-gray-500 text-${matchTypeColor} grid grid-cols-[22%_1fr] text-xs`}
>
<div className="px-3 bg-slate-100 dark:bg-slate-800">
<div className="px-3 bg-slate-100 dark:bg-slate-800" />
<div className="px-3">
{MATCH_TYPE_LABEL_AND_COLOR[user.matchType].label}
</div>
<div className="px-3" />
</div>
<div className="bg-base-100 w-full relative grid grid-cols-[22%_1fr] gap-5">
<DetectedUserSource user={user} />

View File

@@ -1,5 +1,6 @@
import React from "react";
import { match } from "ts-pattern";
import { getMessageWithLink } from "~lib/utils";
import type { MatchType, MatchTypeFilterValue } from "../../types";
import {
ACTION_MODE,
@@ -78,18 +79,19 @@ const Sidebar = ({
</svg>
</div>
<div className="stat-title text-lg text-base-content font-bold">
Detected users
{chrome.i18n.getMessage("sidebar_detected_users")}
</div>
<div className="stat-value text-base-content">{detectedCount}</div>
<div className="stat-desc">
Same handle name: {matchTypeStats[BSKY_USER_MATCH_TYPE.HANDLE]}
{chrome.i18n.getMessage("same_handle_name")}:{" "}
{matchTypeStats[BSKY_USER_MATCH_TYPE.HANDLE]}
</div>
<div className="stat-desc">
Same display name:{" "}
{chrome.i18n.getMessage("same_display_name")}:{" "}
{matchTypeStats[BSKY_USER_MATCH_TYPE.DISPLAY_NAME]}
</div>
<div className="stat-desc">
Included handle in description:{" "}
{chrome.i18n.getMessage("included_handle_in_description")}:{" "}
{matchTypeStats[BSKY_USER_MATCH_TYPE.DESCRIPTION]}
</div>
</div>
@@ -118,7 +120,7 @@ const Sidebar = ({
<span className="text-sm">
{key === BSKY_USER_MATCH_TYPE.FOLLOWING &&
actionMode === ACTION_MODE.BLOCK
? "Blocked users"
? chrome.i18n.getMessage("blocked_user")
: MATCH_TYPE_LABEL_AND_COLOR[key].label}
</span>
<input
@@ -151,30 +153,39 @@ const Sidebar = ({
</div>
{match(actionMode)
.with(ACTION_MODE.FOLLOW, () => (
<AsyncButton onClick={followAll} label="Follow All" />
<AsyncButton
onClick={followAll}
label={chrome.i18n.getMessage("follow_all")}
/>
))
.with(ACTION_MODE.BLOCK, () => (
<AsyncButton onClick={blockAll} label="Block All" />
<AsyncButton
onClick={blockAll}
label={chrome.i18n.getMessage("block_all")}
/>
))
.with(ACTION_MODE.IMPORT_LIST, () => (
<AsyncButton onClick={importList} label="Import List" />
<AsyncButton
onClick={importList}
label={chrome.i18n.getMessage("import_list")}
/>
))
.otherwise(() => null)}
<p className="text-xs">
User detection is not perfect and may include false positives.
{chrome.i18n.getMessage("warning_user_detection")}
</p>
</div>
<div className="mt-auto">
<div className="divider" />
<p className="mb-2">
If you find this tool helpful, I'd appreciate{" "}
<a href="https://ko-fi.com/X8X315UWFN" className="link">
your support
</a>{" "}
to help me maintain and improve it
</p>
<p
className="mb-2 text-xs"
// biome-ignore lint/security/noDangerouslySetInnerHtml: <explanation>
dangerouslySetInnerHTML={{
__html: getMessageWithLink("donate_message"),
}}
/>
<a
href="https://ko-fi.com/X8X315UWFN"
href="https://ko-fi.com/kawamataryo"
target="_blank"
rel="noreferrer"
style={{ display: "inline-block" }}

View File

@@ -75,19 +75,19 @@ export const MAX_RELOAD_COUNT = 1;
export const MATCH_TYPE_LABEL_AND_COLOR = {
[BSKY_USER_MATCH_TYPE.HANDLE]: {
label: "Same handle name",
label: chrome.i18n.getMessage("same_handle_name"),
color: "info",
},
[BSKY_USER_MATCH_TYPE.DISPLAY_NAME]: {
label: "Same display name",
label: chrome.i18n.getMessage("same_display_name"),
color: "warning",
},
[BSKY_USER_MATCH_TYPE.DESCRIPTION]: {
label: "Included handle in description",
label: chrome.i18n.getMessage("included_handle_in_description"),
color: "secondary",
},
[BSKY_USER_MATCH_TYPE.FOLLOWING]: {
label: "Followed users",
label: chrome.i18n.getMessage("followed_users"),
color: "success",
},
};
@@ -95,6 +95,9 @@ export const MATCH_TYPE_LABEL_AND_COLOR = {
export const AUTH_FACTOR_TOKEN_REQUIRED_ERROR_MESSAGE =
"AuthFactorTokenRequiredError";
export const INVALID_IDENTIFIER_OR_PASSWORD_ERROR_MESSAGE =
"Invalid identifier or password";
export const RATE_LIMIT_ERROR_MESSAGE = "Rate limit";
export const DOCUMENT_LINK = {

View File

@@ -28,10 +28,7 @@ export class ThreadsService extends AbstractService {
'[role="dialog"] [role="tab"]>[role="button"]',
);
if (!isTargetPage) {
return [
false,
"Invalid page. please open the following or followers view.",
];
return [false, chrome.i18n.getMessage("error_invalid_page_in_threads")];
}
return [true, ""];
}

View File

@@ -36,3 +36,21 @@ export const findFirstScrollableElements = (
return scrollableElements[0] ?? null;
};
export const getMessageWithLink = (
key: string,
placeholders: string[] = [],
) => {
const linkPattern = /\[(.*?)\]\((.*?)\)/g;
let message = chrome.i18n.getMessage(key, placeholders);
const links = message.matchAll(linkPattern);
for (const link of links) {
const [fullMatch, text, url] = link;
message = message.replace(
fullMatch,
`<a href="${url}" target="_blank" class="link" rel="noreferrer">${text}</a>`,
);
}
return message;
};

View File

@@ -31,22 +31,20 @@ const Option = () => {
confirm: followAllConfirm,
ConfirmationDialog: FollowAllConfirmationDialog,
} = useConfirm({
title: "Proceed with Execution?",
message:
"User detection is not perfect and may include false positives. Do you still want to proceed?",
cancelText: "Cancel",
okText: "OK",
title: chrome.i18n.getMessage("follow_all_confirmation_title"),
message: chrome.i18n.getMessage("follow_all_confirmation_message"),
cancelText: chrome.i18n.getMessage("confirmation_cancel"),
okText: chrome.i18n.getMessage("confirmation_ok"),
});
const {
confirm: importListConfirm,
ConfirmationDialog: ImportListConfirmationDialog,
} = useConfirm({
title: "Proceed with Execution?",
message:
"Importing a list will create a new list and add all detected users to it. This feature is experimental and may not work as expected. Do you still want to proceed?",
cancelText: "Cancel",
okText: "OK",
title: chrome.i18n.getMessage("import_list_confirmation_title"),
message: chrome.i18n.getMessage("import_list_confirmation_message"),
cancelText: chrome.i18n.getMessage("confirmation_cancel"),
okText: chrome.i18n.getMessage("confirmation_ok"),
});
const handleFollowAll = async () => {
@@ -54,10 +52,16 @@ const Option = () => {
return;
}
toast.promise(followAll, {
pending: "Processing...",
pending: chrome.i18n.getMessage("toast_pending"),
success: {
render({ data }) {
return <span className="font-bold">Followed {data} users🎉</span>;
return (
<span className="font-bold">
{chrome.i18n.getMessage("toast_follow_all_success", [
data.toString(),
])}
</span>
);
},
},
});
@@ -68,10 +72,16 @@ const Option = () => {
return;
}
toast.promise(blockAll, {
pending: "Processing...",
pending: chrome.i18n.getMessage("toast_pending"),
success: {
render({ data }) {
return <span className="font-bold">Blocked {data} users🎉</span>;
return (
<span className="font-bold">
{chrome.i18n.getMessage("toast_block_all_success", [
data.toString(),
])}
</span>
);
},
},
});
@@ -82,15 +92,17 @@ const Option = () => {
return;
}
toast.promise(importList, {
pending: "Processing...",
pending: chrome.i18n.getMessage("toast_pending"),
success: {
render({ data }) {
return (
<>
<span className="font-bold">List imported successfully🎉</span>
<span className="font-bold">
{chrome.i18n.getMessage("toast_import_list_success")}
</span>
<br />
<a href={data} target="_blank" rel="noreferrer" className="link">
View Imported List
{chrome.i18n.getMessage("toast_import_list_success_view_list")}
</a>
</>
);
@@ -98,8 +110,9 @@ const Option = () => {
},
error: {
render({ data }) {
console.log(data);
return `Failed to import list: ${data}`;
return chrome.i18n.getMessage("toast_import_list_error", [
data as string,
]);
},
},
});
@@ -153,8 +166,12 @@ const Option = () => {
</div>
<div className="flex-1 ml-80 p-6 pt-0 overflow-y-auto">
<div className="grid grid-cols-[22%_1fr] sticky top-0 z-10 bg-base-100 border-b-[1px] border-gray-500">
<h2 className="text-lg font-bold text-center py-2">Source</h2>
<h2 className="text-lg font-bold text-center py-2">Detected</h2>
<h2 className="text-lg font-bold text-center py-2">
{chrome.i18n.getMessage("source")}
</h2>
<h2 className="text-lg font-bold text-center py-2">
{chrome.i18n.getMessage("detected")}
</h2>
</div>
<div className="flex flex-col border-b-[1px] border-gray-500">
{filteredUsers.map((user) => (

View File

@@ -10,6 +10,7 @@ import {
AUTH_FACTOR_TOKEN_REQUIRED_ERROR_MESSAGE,
BSKY_DOMAIN,
DOCUMENT_LINK,
INVALID_IDENTIFIER_OR_PASSWORD_ERROR_MESSAGE,
MAX_RELOAD_COUNT,
MESSAGE_NAMES,
MESSAGE_TYPE,
@@ -17,6 +18,7 @@ import {
STORAGE_KEYS,
TARGET_URLS_REGEX,
} from "~lib/constants";
import { getMessageWithLink } from "~lib/utils";
function IndexPopup() {
const [isLoading, setIsLoading] = useState(false);
@@ -81,19 +83,21 @@ function IndexPopup() {
const validateForm = () => {
if (!password && !identifier) {
setErrorMessage("Error: Please enter your password and identifier.");
setErrorMessage(
chrome.i18n.getMessage("error_enter_identifier_and_password"),
);
return false;
}
if (!password) {
setErrorMessage("Error: Please enter your password.");
setErrorMessage(chrome.i18n.getMessage("error_enter_password"));
return false;
}
if (!identifier) {
setErrorMessage("Error: Please enter your identifier.");
setErrorMessage(chrome.i18n.getMessage("error_enter_identifier"));
return false;
}
if (isShowAuthFactorTokenInput && !authFactorToken) {
setErrorMessage("Error: Please enter your auth factor token.");
setErrorMessage(chrome.i18n.getMessage("error_enter_auth_factor_token"));
return false;
}
return true;
@@ -115,7 +119,7 @@ function IndexPopup() {
if (!Object.values(TARGET_URLS_REGEX).some((r) => r.test(currentUrl))) {
setErrorMessage(
"Error: Invalid page. please open the target page.",
chrome.i18n.getMessage("error_invalid_page"),
DOCUMENT_LINK.PAGE_ERROR,
);
return;
@@ -166,6 +170,13 @@ function IndexPopup() {
await saveShowAuthFactorTokenInputToStorage(true);
} else if (error.message.includes(RATE_LIMIT_ERROR_MESSAGE)) {
setErrorMessage(error.message, DOCUMENT_LINK.RATE_LIMIT_ERROR);
} else if (
error.message.includes(INVALID_IDENTIFIER_OR_PASSWORD_ERROR_MESSAGE)
) {
setErrorMessage(
chrome.i18n.getMessage("error_invalid_identifier_or_password"),
DOCUMENT_LINK.LOGIN_ERROR,
);
} else {
setErrorMessage(error.message, DOCUMENT_LINK.LOGIN_ERROR);
}
@@ -202,7 +213,7 @@ function IndexPopup() {
await searchBskyUser();
} else {
setErrorMessage(
"Error: Something went wrong. Please reload the web page and try again.",
chrome.i18n.getMessage("error_something_went_wrong"),
DOCUMENT_LINK.OTHER_ERROR,
);
}
@@ -258,7 +269,7 @@ function IndexPopup() {
d="M17.982 18.725A7.488 7.488 0 0012 15.75a7.488 7.488 0 00-5.982 2.975m11.963 0a9 9 0 10-11.963 0m11.963 0A8.966 8.966 0 0112 21a8.966 8.966 0 01-5.982-2.275M15 9.75a3 3 0 11-6 0 3 3 0 016 0z"
/>
</svg>
Handle or Email
{chrome.i18n.getMessage("handle_or_email")}
</div>
<input
type="text"
@@ -286,20 +297,17 @@ function IndexPopup() {
/>
</svg>
<p>
Password
{chrome.i18n.getMessage("password")}
<br />
</p>
</div>
<span className="text-xs">
We recommend using the{" "}
<a
href="https://bsky.app/settings/app-passwords"
target="_blank"
rel="noreferrer"
className="link"
>
App Password.
</a>
<span
// biome-ignore lint/security/noDangerouslySetInnerHtml: <explanation>
dangerouslySetInnerHTML={{
__html: getMessageWithLink("recommended_to_use_app_password"),
}}
/>
</span>
<input
type="password"
@@ -322,8 +330,8 @@ function IndexPopup() {
className="w-4 h-4"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
strokeLinecap="round"
strokeLinejoin="round"
d="M16.5 6v.75m0 3v.75m0 3v.75m0 3V18m-9-5.25h5.25M7.5 15h3M3.375 5.25c-.621 0-1.125.504-1.125 1.125v3.026a2.999 2.999 0 0 1 0 5.198v3.026c0 .621.504 1.125 1.125 1.125h17.25c.621 0 1.125-.504 1.125-1.125v-3.026a2.999 2.999 0 0 1 0-5.198V6.375c0-.621-.504-1.125-1.125-1.125H3.375Z"
/>
</svg>
@@ -350,10 +358,12 @@ function IndexPopup() {
disabled={isLoading}
>
{isLoading && <span className="w-4 loading loading-spinner" />}
{isLoading ? "Finding Bluesky Users" : "Find Bluesky Users"}
{isLoading
? chrome.i18n.getMessage("finding_bluesky_users")
: chrome.i18n.getMessage("find_bluesky_users")}
</button>
{isShowErrorMessage && (
<div className="flex gap-2 items-center text-red-600 border border-red-600 p-2 rounded-md mt-2">
<div className="flex gap-2 items-center text-red-600 border border-red-600 p-2 rounded-md mt-2 text-xs">
<svg
xmlns="http://www.w3.org/2000/svg"
className="stroke-current flex-shrink-0 h-6 w-6"
@@ -376,10 +386,9 @@ function IndexPopup() {
rel="noreferrer"
className="link ml-2"
>
Learn more
{chrome.i18n.getMessage("learn_more")}
</a>
)}
.
</span>
</div>
)}