diff --git a/src/components/menu/account-view/account-info.tsx b/src/components/menu/account-view/account-info.tsx
index 98451db..da7ad69 100644
--- a/src/components/menu/account-view/account-info.tsx
+++ b/src/components/menu/account-view/account-info.tsx
@@ -45,7 +45,7 @@ const AccountInfo = () => {
token
);
if (rep.status === 401) {
- showErrorToast(t('Login status expired'));
+ showErrorToast(t('Login status expired.'));
dispatch(logout());
return;
}
@@ -68,7 +68,7 @@ const AccountInfo = () => {
token
);
if (rep.status === 401) {
- showErrorToast(t('Login status expired'));
+ showErrorToast(t('Login status expired.'));
dispatch(logout());
return;
}
diff --git a/src/components/menu/account-view/saves-section.tsx b/src/components/menu/account-view/saves-section.tsx
index 1f64956..31f79bf 100644
--- a/src/components/menu/account-view/saves-section.tsx
+++ b/src/components/menu/account-view/saves-section.tsx
@@ -67,7 +67,7 @@ const SavesSection = () => {
token
);
if (rep.status === 401) {
- showErrorToast(t('Login status expired'));
+ showErrorToast(t('Login status expired.'));
dispatch(logout());
return;
}
@@ -94,7 +94,7 @@ const SavesSection = () => {
// fetch cloud save metadata
const savesRep = await dispatch(fetchSaveList());
if (savesRep.meta.requestStatus !== 'fulfilled') {
- showErrorToast(t('Login status expired')); // TODO: also might be !200 response
+ showErrorToast(t('Login status expired.')); // TODO: also might be !200 response
setSyncButtonIsLoading(undefined);
return;
}
@@ -135,7 +135,7 @@ const SavesSection = () => {
token
);
if (rep.status === 401) {
- showErrorToast(t('Login status expired'));
+ showErrorToast(t('Login status expired.'));
setSyncButtonIsLoading(undefined);
dispatch(logout());
return;
@@ -151,7 +151,7 @@ const SavesSection = () => {
setSyncButtonIsLoading(saveId);
const rep = await apiFetch(API_ENDPOINT.SAVES + '/' + saveId, {}, token);
if (rep.status === 401) {
- showErrorToast(t('Login status expired'));
+ showErrorToast(t('Login status expired.'));
setSyncButtonIsLoading(undefined);
dispatch(logout());
return;
@@ -174,7 +174,7 @@ const SavesSection = () => {
setDeleteButtonIsLoading(saveId);
const rep = await apiFetch(API_ENDPOINT.SAVES + '/' + currentSaveId, { method: 'DELETE' }, token);
if (rep.status === 401) {
- showErrorToast(t('Login status expired'));
+ showErrorToast(t('Login status expired.'));
setDeleteButtonIsLoading(undefined);
dispatch(logout());
return;
@@ -195,7 +195,7 @@ const SavesSection = () => {
token
);
if (rep.status === 401) {
- showErrorToast(t('Login status expired'));
+ showErrorToast(t('Login status expired.'));
dispatch(logout());
return;
}
diff --git a/src/components/menu/account-view/subscription-section.tsx b/src/components/menu/account-view/subscription-section.tsx
index 0374be3..095444a 100644
--- a/src/components/menu/account-view/subscription-section.tsx
+++ b/src/components/menu/account-view/subscription-section.tsx
@@ -1,70 +1,20 @@
+import { Button, Card, List, Stack, Text, Title, Tooltip } from '@mantine/core';
+import { RMSection, RMSectionBody, RMSectionHeader } from '@railmapgen/mantine-components';
import React from 'react';
import { useTranslation } from 'react-i18next';
-import { useRootDispatch, useRootSelector } from '../../../redux';
-import {
- ActiveSubscriptions,
- defaultActiveSubscriptions,
- logout,
- setActiveSubscriptions,
-} from '../../../redux/account/account-slice';
-import { apiFetch } from '../../../util/api';
-import { API_ENDPOINT } from '../../../util/constants';
+import { useRootSelector } from '../../../redux';
import RedeemModal from '../../modal/redeem-modal';
-import { RMSection, RMSectionBody, RMSectionHeader } from '@railmapgen/mantine-components';
-import { Button, Card, List, Stack, Text, Title } from '@mantine/core';
-import { addNotification } from '../../../redux/notification/notification-slice';
-interface APISubscription {
- type: 'RMP' | 'RMP_CLOUD' | 'RMP_EXPORT';
- expires: string;
-}
+const DAYS_TO_REMIND_RENEW = 90;
+const MILLISECONDS_TO_REMIND_RENEW = DAYS_TO_REMIND_RENEW * 24 * 60 * 60 * 1000;
const SubscriptionSection = () => {
const { t } = useTranslation();
- const { isLoggedIn, token } = useRootSelector(state => state.account);
- const dispatch = useRootDispatch();
- const [subscriptions, setSubscriptions] = React.useState([] as APISubscription[]);
+ const { activeSubscriptions } = useRootSelector(state => state.account);
const [isRedeemModalOpen, setIsRedeemModalOpen] = React.useState(false);
- const showErrorToast = (msg: string) =>
- dispatch(
- addNotification({
- title: t('Unable to retrieve your subscriptions'),
- message: msg,
- type: 'error',
- duration: 9000,
- })
- );
-
- const getSubscriptions = async () => {
- if (!isLoggedIn) return;
- const rep = await apiFetch(API_ENDPOINT.SUBSCRIPTION, {}, token);
- if (rep.status === 401) {
- showErrorToast(t('Login status expired'));
- dispatch(logout());
- return;
- }
- if (rep.status !== 200) {
- showErrorToast(await rep.text());
- return;
- }
- const subscriptions = (await rep.json()).subscriptions as APISubscription[];
- if (!subscriptions.map(_ => _.type).includes('RMP_CLOUD')) return;
- setSubscriptions([{ type: 'RMP', expires: subscriptions[0].expires }]);
-
- const activeSubscriptions = structuredClone(defaultActiveSubscriptions);
- for (const subscription of subscriptions) {
- const type = subscription.type;
- if (type in activeSubscriptions) {
- activeSubscriptions[type as keyof ActiveSubscriptions] = true;
- }
- }
- dispatch(setActiveSubscriptions(activeSubscriptions));
- };
- React.useEffect(() => {
- getSubscriptions();
- }, []);
+ const noneSubscription = !activeSubscriptions.RMP_CLOUD && !activeSubscriptions.RMP_EXPORT;
return (
@@ -78,12 +28,10 @@ const SubscriptionSection = () => {
- {subscriptions.length === 0 && (
+ {noneSubscription && (
-
- {t('Rail Map Painter')}
-
+ {t('Rail Map Painter')}
{t('With this subscription, the following features are unlocked:')}
@@ -92,6 +40,7 @@ const SubscriptionSection = () => {
{t('Sync 9 more saves')}
{t('Unlimited master nodes')}
{t('Unlimited parallel lines')}
+ {t('Random station names')}
@@ -101,30 +50,70 @@ const SubscriptionSection = () => {
)}
- {subscriptions.map(_ => (
-
+ {!noneSubscription && (
+
-
- {t(_.type === 'RMP' ? 'Rail Map Painter' : _.type)}
-
+ {t('Rail Map Painter')}
- {t('Expires at:')} {new Date(_.expires).toLocaleString()}
+ {t('Expires at:')} {new Date(activeSubscriptions.RMP_CLOUD!).toLocaleString()}
-
+ {new Date(activeSubscriptions.RMP_CLOUD!).getTime() - new Date().getTime() <
+ MILLISECONDS_TO_REMIND_RENEW ? (
+
+
+
+ ) : (
+
+ )}
- ))}
+ )}
+
+ {/* {Object.entries(activeSubscriptions)
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
+ .filter(([_, expires]) => expires)
+ .map(([type, expires]) => (
+
+
+ {t(type)}
+
+
+
+ {t('Expires at:')} {new Date(expires).toLocaleString()}
+
+ {new Date(expires).getTime() - new Date().getTime() < MILLISECONDS_TO_REMIND_RENEW ? (
+
+
+
+ ) : (
+
+ )}
+
+
+ ))} */}
- setIsRedeemModalOpen(false)}
- getSubscriptions={getSubscriptions}
- />
+ setIsRedeemModalOpen(false)} />
);
};
diff --git a/src/components/modal/redeem-modal.tsx b/src/components/modal/redeem-modal.tsx
index dbb57a7..c62e504 100644
--- a/src/components/modal/redeem-modal.tsx
+++ b/src/components/modal/redeem-modal.tsx
@@ -1,16 +1,16 @@
+import { Anchor, Button, Group, List, Modal, Text, TextInput } from '@mantine/core';
import React from 'react';
import { useTranslation } from 'react-i18next';
import { MdOpenInNew } from 'react-icons/md';
import { useRootDispatch, useRootSelector } from '../../redux';
-import { logout } from '../../redux/account/account-slice';
+import { fetchSubscription, logout } from '../../redux/account/account-slice';
+import { addNotification } from '../../redux/notification/notification-slice';
import { apiFetch } from '../../util/api';
import { API_ENDPOINT } from '../../util/constants';
-import { Anchor, Button, Group, List, Modal, Text, TextInput } from '@mantine/core';
-import { addNotification } from '../../redux/notification/notification-slice';
-const RedeemModal = (props: { opened: boolean; onClose: () => void; getSubscriptions: () => Promise }) => {
+const RedeemModal = (props: { opened: boolean; onClose: () => void }) => {
const { t } = useTranslation();
- const { opened, onClose, getSubscriptions } = props;
+ const { opened, onClose } = props;
const { isLoggedIn, token } = useRootSelector(state => state.account);
const dispatch = useRootDispatch();
@@ -34,7 +34,7 @@ const RedeemModal = (props: { opened: boolean; onClose: () => void; getSubscript
token
);
if (rep.status === 401) {
- showErrorToast(t('Login status expired'));
+ showErrorToast(t('Login status expired.'));
dispatch(logout());
return;
}
@@ -43,7 +43,7 @@ const RedeemModal = (props: { opened: boolean; onClose: () => void; getSubscript
showErrorToast(t(msg));
return;
}
- await getSubscriptions();
+ dispatch(fetchSubscription());
// TODO: let RMP refresh its subscription status
onClose();
};
diff --git a/src/i18n/translations/en.json b/src/i18n/translations/en.json
index 9758994..8c763a9 100644
--- a/src/i18n/translations/en.json
+++ b/src/i18n/translations/en.json
@@ -21,5 +21,8 @@
"text2": " from local storage. This item is created and used by ",
"text3": ". Deleting it may cause unrecoverable data loss or unexpected behaviour to the applications.",
"text4": "Please close this modal unless you're certain to do so or are supervised by our developers."
- }
+ },
+
+ "RMP_CLOUD": "Rail Map Painter・Tianshu",
+ "RMP_EXPORT": "Rail Map Painter・Yuheng"
}
\ No newline at end of file
diff --git a/src/i18n/translations/ja.json b/src/i18n/translations/ja.json
index 584e3c0..7eb57e3 100644
--- a/src/i18n/translations/ja.json
+++ b/src/i18n/translations/ja.json
@@ -16,117 +16,145 @@
"safari": "Safariでは使用できません ☹️"
},
+ "LocalStorageModal": {
+ "text1": "ローカルストレージから ",
+ "text2": " を削除しようとしています。このプロジェクトは ",
+ "text3": " によって作成および使用されました。削除すると、データが失われて復元できなくなったり、アプリケーションが異常終了したりする可能性があります。",
+ "text4": "ご不明な場合、または当社の開発者の指示なしに操作している場合は、このダイアログを閉じてください。"
+ },
+
"About": "概要",
- "Allow cookies to help improve our website": "クッキーを許可して網頁の改善に協力する",
+ "Allow cookies to help improve our website": "Cookie を許可して網頁の改善にご協力ください",
"Appearance": "外観",
"Back to production environment": "本番環境に戻る",
"Close all tabs": "すべてのタブを閉じる",
"Close app": "アプリを閉じる",
"Close current tab": "現在のタブを閉じる",
"Close tab": "タブを閉じる",
- "Contribution Wiki": "コントリビューションガイド",
+ "Contribution Wiki": "貢献者ガイド",
"Contributors": "貢献者",
"Current session has been terminated. Please close this window.": "現在のセッションは終了しました。このウィンドウを閉じてください。",
- "Dark": "ダーク",
+ "Dark": "闇",
+ "Delete and reload": "削除して再読み込み",
"Developer team": "開発チーム",
"Devtools": "開発者ツール",
- "Export Local Storage": "ローカルストレージのエクスポート",
+ "Export Local Storage": "ローカルストレージをエクスポート",
"Donators": "寄付者",
"Don't show again": "再度表示しない",
"Download desktop app": "デスクトップアプリをダウンロード",
- "Fonts are slow to load? Learn how to speed it up!": "フォントの読み込みが遅い?スピードアップの方法を学びましょう!",
- "Official website": "公式網頁",
+ "Follow us on Bilibili": "Bilibili でフォローしてください",
+ "Fonts are slow to load? Learn how to speed it up!": "フォントの読み込みが遅いですか?高速化する方法をご覧ください!",
"GitHub Pages mirror": "GitHub Pages ミラー",
"GitLab Pages mirror": "GitLab Pages ミラー",
"Happy Chinese New Year!": "新年あけましておめでとうございます!",
"Help & support": "助けと支援",
- "Join us on Slack": "Slackに参加する",
- "Follow us on Bilibili": "ビリビリでフォローしてください",
- "Light": "ライト",
+ "Includes at least 8 characters": "8 文字以上を含める",
+ "Join us on Slack": "Slack に参加してください",
+ "Light": "光",
+ "Login success": "ログイン成功",
"Main languages": "主要言語",
- "More": "もっと見る",
- "More mirrors": "他のミラー",
+ "More": "その他",
+ "More mirrors": "その他のミラー",
"New tab": "新しいタブ",
- "Notes: Contributors are sorted by number of commits and commit time.": "注:貢献者はコミット数とコミット時間に基づいて並べられています。",
- "OK": "OK",
- "Other languages": "他の言語",
- "Pro tip": "プロのコツ",
- "Rail Map Toolkit is opened in another window": "レールマップツールキットは別のウィンドウで開かれています",
- "Raise an Issue on GitHub": "GitHubにIssueを報告",
- "Refreshing is required for changes to take effect.": "変更を適用するには再読み込みが必要です。",
+ "Notes: Contributors are sorted by number of commits and commit time.": "注:貢献者はコミット数とコミット時間によって並べ替えられます。",
+ "Official website": "公式網頁",
+ "Other languages": "その他の言語",
+ "Pro tip": "プロのヒント",
+ "Rail Map Toolkit is opened in another window": "地下鉄路線図工具一式は別のウィンドウで開かれています",
+ "Raise an Issue on GitHub": "GitHub で Issue を作成",
+ "Refreshing is required for changes to take effect.": "変更を有効にするには更新が必要です。",
+ "Remove this item": "このアイテムを削除",
"Resource administrators": "リソース管理者",
"Resource contributors": "リソース提供者",
- "Restart RMT in this window": "このウィンドウでRMTを再起動する",
+ "Restart RMT in this window": "このウィンドウで地下鉄路線図工具一式を再起動",
"Running": "実行中",
"Save": "保存",
- "Select an app to start your own rail map design!": "アプリを選択して自分のレールマップデザインを始めましょう!",
- "Show dev tools for 1 day": "1日間開発ツールを表示",
+ "Select an app to start your own rail map design!": "アプリを選択して、独自の路線図デザインを始めましょう!",
+ "Show dev tools for 1 day": "開発者ツールを 1 日間表示",
"Switch": "切り替え",
- "Switch to": "切り替え",
+ "Switch to": "切り替え先",
"System": "システムに従う",
"Tab": "タブ",
"Terms and conditions": "利用規約",
"Tutorial": "チュートリアル",
"Official Website": "公式網頁",
"Mini Metro Web": "ミニ地下鉄描画ツール",
- "Unable to load contributors": "貢献者を読み込めません",
+ "Unable to create your account": "アカウントを作成できません",
+ "Unable to reset your password": "パスワードをリセットできません",
+ "Unable to load contributors": "コントリビューターを読み込めません",
"Unable to load donators": "寄付者を読み込めません",
+ "Unable to login": "ログインできません",
+ "Unable to redeem": "引き換えできません",
+ "Unable to retrieve your subscriptions": "契約を取得できません",
+ "Unable to sync your save": "保存を同期できません",
+ "Unable to update account info": "アカウント情報を更新できません",
+ "Unknown app": "不明なアプリ",
"Useful links": "便利なリンク",
"Visit GitHub": "GitHubを訪問",
"Web fonts": "ウェブフォント",
- "Welcome to Rail Map Toolkit": "レールマップツールキットへようこそ",
- "You cannot open multiple Rail Map Toolkit at the same time. Please close this window.": "複数のレールマップツールキットを同時に開くことはできません。このウィンドウを閉じてください。",
+ "Welcome to Rail Map Toolkit": "地下鉄路線図工具一式へようこそ",
+ "We'll never share your email.": "あなたのメールアドレスを共有することはありません。",
+ "You cannot open multiple Rail Map Toolkit at the same time. Please close this window.": "同時に複数の地下鉄路線図工具一式を開くことはできません。このウィンドウを閉じてください。",
+ "Your account is created successfully.": "アカウントは正常に作成されました。",
+ "Your password has been reset successfully.": "パスワードは正常にリセットされました。",
"You're currently viewing a testing environment.": "現在、テスト環境を閲覧しています。",
- "Login status expired": "ログイン状態が期限切れです",
- "Log in / Sign up": "ログイン / サインアップ",
+ "Login status expired.": "ログイン状態の有効期限が切れました。",
+ "Log in / Sign up": "ログイン / 新規登録",
"Log out": "ログアウト",
- "Change password": "パスワードを変更",
- "Update account info": "アカウント情報を更新",
+ "Change password": "パスワードの変更",
+ "Update account info": "アカウント情報の更新",
"Change": "変更",
"Account": "アカウント",
- "Welcome": "ようこそ",
- "Forgot password": "パスワードを忘れた",
- "Check your email again!": "メールを再確認してください!",
- "Email with reset link is sent to": "リセットリンクを含むメールが送信されました",
+ "Welcome": "ようこそ ",
+ "Forgot password": "パスワードをお忘れですか",
+ "Check your email again!": "もう一度メールアドレスを確認してください!",
+ "Email with reset link is sent to": "リセットリンク付きのメールが送信されました:",
"Email": "メールアドレス",
"Send reset link": "リセットリンクを送信",
+ "Reset link sent": "リセットリンク送信済み",
"Reset password token": "パスワードリセットトークン",
"Password": "パスワード",
"Reset password": "パスワードをリセット",
"Back to log in": "ログインに戻る",
"Log in": "ログイン",
- "Sign up": "サインアップ",
+ "Create an account": "アカウントを作成",
+ "Sign up": "新規登録",
"The email is not valid!": "メールアドレスが無効です!",
- "Verification email is sent to": "確認メールが送信されました",
- "Name": "名前",
+ "Verification email is sent to": "確認メールが送信されました:",
"You may always change it later.": "後でいつでも変更できます。",
"Send verification code": "確認コードを送信",
+ "Verification code sent": "確認コード送信済み",
"Verification code": "確認コード",
- "Failed to get the RMP save!": "RMP保存を取得できませんでした!",
+ "Minimum 8 characters. Contain at least 1 letter and 1 number.": "8 文字以上。少なくとも 1 つの文字と 1 つの数字を含めてください。",
+ "Failed to get the RMP save!": "RMP 保存の取得に失敗しました!",
"Can not sync this save!": "この保存を同期できません!",
- "Synced saves": "同期された保存",
- "Subscribe to sync more": "購読でより多く同期",
+ "Synced saves": "同期済み保存",
+ "Subscribe to sync more": "購読してさらに同期",
"Create": "作成",
"Current save": "現在の保存",
- "Cloud save": "クラウド保存",
+ "Cloud save": "雲保存",
"ID": "一意の識別子",
"Status": "状態",
"Last update at": "最終更新日:",
- "Delete ": "削除",
+ "Delete": "削除",
"Sync now": "今すぐ同期",
"Sync this slot": "このスロットを同期",
- "All subscriptions": "すべての購読",
+ "All subscriptions": "すべての契約",
"Redeem": "引き換え",
+ "RMP_CLOUD": "地下鉄路線図画家・天枢",
+ "RMP_EXPORT": "地下鉄路線図画家・玉衡",
"With this subscription, the following features are unlocked:": "この購読で次の機能が解放されます:",
- "PRO features": "PRO機能",
- "Sync 9 more saves": "追加の9つの保存を同期",
+ "PRO features": "プロ機能",
+ "Sync 9 more saves": "さらに 9 つの保存を同期",
"Unlimited master nodes": "大師節点無制限",
"Unlimited parallel lines": "平行路線無制限",
- "Expires at:": "期限:",
+ "Random station names": "無作為の駅名",
+ "Expires at:": "有効期限:",
"Not applicable": "該当なし",
"Renew": "更新",
- "Redeem your subscription": "購読を引き換え",
+ "Renew now and get an extra 45 days!": "今すぐ更新してさらに 45 日間延長!",
+ "Redeem your subscription": "契約を引き換える",
"CDKey could be purchased in the following sites:": "CD鍵は以下の網頁で購入できます:",
"Enter your CDKey here": "ここにCD鍵を入力",
diff --git a/src/i18n/translations/ko.json b/src/i18n/translations/ko.json
index 87a633d..dff86c0 100644
--- a/src/i18n/translations/ko.json
+++ b/src/i18n/translations/ko.json
@@ -1,139 +1,168 @@
{
"CookiesModal": {
- "header": "본 사이트의 쿠키",
- "text1": "RMG에서는 본 사이트 및 서비스가 정상적으로 작동하도록 쿠키를 사용합니다. 이러한 쿠키는 필수적이며 기본적으로 활성화되어 있습니다.",
- "text2": "또한 우리는 다음과 같은 방법으로 쿠키를 사용합니다:",
- "item1": "사이트 개선을 위해 사용자의 사용 패턴을 분석합니다.",
- "text3": "이 쿠키들은 선택 사항입니다. 모든 선택적 쿠키를 허용하려면 '모든 쿠키 허용'을 선택하세요. 쿠키를 비활성화하려면 '거부'를 선택하세요.",
- "accept": "모든 쿠키 허용",
+ "header": "본 웹사이트의 쿠키",
+ "text1": "RMG에서는 본 웹사이트 및 서비스가 정상적으로 작동하도록 쿠키를 사용합니다. 이러한 쿠키는 필수적이므로 기본적으로 활성화되어 있습니다.",
+ "text2": "또한 다음과 같은 방식으로 쿠키를 사용합니다.",
+ "item1": "귀하의 사용 패턴을 분석하여 본 웹사이트를 개선합니다.",
+ "text3": "이러한 쿠키는 선택 사항입니다. 모든 선택적 쿠키를 허용하려면 '모든 쿠키 수락'을 선택하십시오. 쿠키를 비활성화하려면 '거부'를 선택하십시오.",
+ "accept": "모든 쿠키 수락",
"reject": "거부"
},
"FontsSection": {
- "text1": "프로젝트에서 사용하는 다음 글꼴은 사용자의 장치가 아닌 우리의 서버에서 로드됩니다.",
- "text2": "이로 인해 불필요한 네트워크 트래픽이 발생할 수 있으며, 연결이 불안정할 경우 로딩이 느려질 수 있습니다.",
- "text3": "이 오픈 소스 무료 글꼴을 다운로드하고 설치하여 사용 경험을 향상시킬 수 있습니다.",
- "safari": "Safari에서는 지원되지 않음 ☹️"
+ "text1": "귀하의 프로젝트에 사용된 다음 글꼴은 귀하의 장치가 아닌 당사 서버에서 로드됩니다.",
+ "text2": "이로 인해 불필요한 네트워크 트래픽이 발생하거나 연결이 불안정한 경우 로딩 속도가 느려질 수 있습니다.",
+ "text3": "이러한 오픈 소스 무료 글꼴을 다운로드하여 설치하면 사용 환경을 개선할 수 있습니다.",
+ "safari": "Safari에는 적용되지 않음 ☹️"
+ },
+
+ "LocalStorageModal": {
+ "text1": "로컬 저장소에서 ",
+ "text2": " 을(를) 삭제하려고 합니다. 이 프로젝트는 ",
+ "text3": " 에서 만들고 사용했습니다. 삭제하면 데이터를 복구할 수 없게 되거나 애플리케이션이 오작동할 수 있습니다.",
+ "text4": "확실하지 않거나 당사 개발자의 안내 없이 작업하는 경우 이 대화 상자를 닫으십시오."
},
"About": "정보",
"Allow cookies to help improve our website": "쿠키를 허용하여 웹사이트 개선에 도움을 주세요",
- "Appearance": "외관",
+ "Appearance": "모양",
"Back to production environment": "프로덕션 환경으로 돌아가기",
"Close all tabs": "모든 탭 닫기",
"Close app": "앱 닫기",
"Close current tab": "현재 탭 닫기",
"Close tab": "탭 닫기",
- "Contribution Wiki": "기여자 안내서",
+ "Contribution Wiki": "기여자 가이드",
"Contributors": "기여자",
- "Current session has been terminated. Please close this window.": "현재 세션이 종료되었습니다. 이 창을 닫아주세요.",
- "Dark": "어두운 모드",
- "Developer team": "개발자 팀",
+ "Current session has been terminated. Please close this window.": "현재 세션이 종료되었습니다. 이 창을 닫으십시오.",
+ "Dark": "어둡게",
+ "Delete and reload": "삭제 및 새로고침",
+ "Developer team": "개발팀",
"Devtools": "개발자 도구",
"Export Local Storage": "로컬 저장소 내보내기",
"Donators": "기부자",
- "Don't show again": "다시 보지 않기",
- "Download desktop app": "데스크탑 앱 다운로드",
+ "Don't show again": "다시 표시 안 함",
+ "Download desktop app": "데스크톱 앱 다운로드",
+ "Follow us on Bilibili": "Bilibili에서 저희를 팔로우하세요",
"Fonts are slow to load? Learn how to speed it up!": "글꼴 로딩이 느린가요? 속도를 높이는 방법을 알아보세요!",
- "Official website": "공식 웹사이트",
"GitHub Pages mirror": "GitHub Pages 미러",
"GitLab Pages mirror": "GitLab Pages 미러",
"Happy Chinese New Year!": "새해 복 많이 받으세요!",
"Help & support": "도움말 및 지원",
- "Join us on Slack": "Slack에 참여하기",
- "Follow us on Bilibili": "빌리빌리에서 팔로우해주세요",
- "Light": "밝은 모드",
+ "Includes at least 8 characters": "최소 8자 이상 포함",
+ "Join us on Slack": "Slack 그룹에 참여하세요",
+ "Light": "밝게",
+ "Login success": "로그인 성공",
"Main languages": "주요 언어",
- "More": "더보기",
+ "More": "더 보기",
"More mirrors": "더 많은 미러",
"New tab": "새 탭",
"Notes: Contributors are sorted by number of commits and commit time.": "참고: 기여자는 커밋 수와 커밋 시간을 기준으로 정렬됩니다.",
- "OK": "확인",
+ "Official website": "공식 웹사이트",
"Other languages": "기타 언어",
- "Pro tip": "팁",
- "Rail Map Toolkit is opened in another window": "철도 노선도 도구가 다른 창에서 열려 있습니다.",
- "Raise an Issue on GitHub": "GitHub에서 이슈 제기",
- "Refreshing is required for changes to take effect.": "변경 사항이 적용되려면 새로 고침이 필요합니다.",
- "Resource administrators": "자원 관리자",
- "Resource contributors": "자원 기여자",
- "Restart RMT in this window": "이 창에서 RMT 재시작",
+ "Pro tip": "전문가 팁",
+ "Rail Map Toolkit is opened in another window": "노선도 툴킷이 다른 창에서 열렸습니다.",
+ "Raise an Issue on GitHub": "GitHub에 문제 제기",
+ "Refreshing is required for changes to take effect.": "변경 사항을 적용하려면 새로고침해야 합니다.",
+ "Remove this item": "이 항목 제거",
+ "Resource administrators": "리소스 관리자",
+ "Resource contributors": "리소스 기여자",
+ "Restart RMT in this window": "이 창에서 RMT 다시 시작",
"Running": "실행 중",
"Save": "저장",
- "Select an app to start your own rail map design!": "앱을 선택하여 나만의 철도 노선도 디자인을 시작하세요!",
+ "Select an app to start your own rail map design!": "앱을 선택하여 나만의 노선도 디자인을 시작하세요!",
"Show dev tools for 1 day": "1일 동안 개발자 도구 표시",
"Switch": "전환",
- "Switch to": "로 전환",
- "System": "시스템 설정 따르기",
+ "Switch to": "다음으로 전환",
+ "System": "시스템 설정 따름",
"Tab": "탭",
"Terms and conditions": "이용 약관",
- "Tutorial": "튜토리얼",
+ "Tutorial": "사용 설명서",
"Official Website": "공식 웹사이트",
- "Mini Metro Web": "미니 지하철 그리기 도구",
- "Unable to load contributors": "기여자 목록을 불러올 수 없습니다",
+ "Mini Metro Web": "미니 메트로 웹",
+ "Unable to create your account": "계정을 만들 수 없습니다.",
+ "Unable to reset your password": "비밀번호를 재설정할 수 없습니다.",
+ "Unable to load contributors": "기여자 목록을 불러올 수 없습니다.",
+ "Unable to load donators": "기부자 목록을 불러올 수 없습니다.",
+ "Unable to login": "로그인할 수 없습니다.",
+ "Unable to redeem": "교환할 수 없습니다.",
+ "Unable to retrieve your subscriptions": "구독 정보를 가져올 수 없습니다.",
+ "Unable to sync your save": "저장 파일을 동기화할 수 없습니다.",
+ "Unable to update account info": "계정 정보를 업데이트할 수 없습니다.",
+ "Unknown app": "알 수 없는 앱",
"Useful links": "유용한 링크",
"Visit GitHub": "GitHub 방문",
"Web fonts": "웹 글꼴",
- "Welcome to Rail Map Toolkit": "철도 노선도 도구에 오신 것을 환영합니다",
- "You cannot open multiple Rail Map Toolkit at the same time. Please close this window.": "여러 철도 노선도 도구를 동시에 열 수 없습니다. 이 창을 닫아주세요.",
- "You're currently viewing a testing environment.": "현재 테스트 환경을 보고 있습니다.",
+ "Welcome to Rail Map Toolkit": "지하철 노선도 툴킷에 오신 것을 환영합니다",
+ "We'll never share your email.": "귀하의 이메일 주소는 절대 공유하지 않습니다.",
+ "You cannot open multiple Rail Map Toolkit at the same time. Please close this window.": "동시에 여러 개의 노선도 툴킷을 열 수 없습니다. 이 창을 닫으십시오.",
+ "Your account is created successfully.": "계정이 성공적으로 만들어졌습니다.",
+ "Your password has been reset successfully.": "비밀번호가 성공적으로 재설정되었습니다.",
+ "You're currently viewing a testing environment.": "현재 테스트 환경을 보고 계십니다.",
- "Login status expired": "로그인 상태가 만료되었습니다",
- "Log in / Sign up": "로그인 / 가입",
+ "Login status expired.": "로그인 상태가 만료되었습니다.",
+ "Log in / Sign up": "로그인 / 회원가입",
"Log out": "로그아웃",
"Change password": "비밀번호 변경",
- "Update account info": "계정 정보 업데이트",
+ "Update account info": "계정 정보 수정",
"Change": "변경",
"Account": "계정",
- "Welcome": "환영합니다",
+ "Welcome": "환영합니다 ",
"Forgot password": "비밀번호를 잊으셨나요",
- "Check your email again!": "이메일을 다시 확인하세요!",
- "Email with reset link is sent to": "비밀번호 재설정 링크가 이메일로 전송되었습니다",
+ "Check your email again!": "이메일 주소를 다시 확인하세요!",
+ "Email with reset link is sent to": "비밀번호 재설정 링크가 포함된 이메일이 다음 주소로 전송되었습니다:",
"Email": "이메일 주소",
- "Send reset link": "재설정 링크 전송",
+ "Send reset link": "재설정 이메일 보내기",
+ "Reset link sent": "재설정 이메일 전송됨",
"Reset password token": "비밀번호 재설정 토큰",
"Password": "비밀번호",
"Reset password": "비밀번호 재설정",
"Back to log in": "로그인으로 돌아가기",
"Log in": "로그인",
- "Sign up": "가입",
+ "Create an account": "새 계정 만들기",
+ "Sign up": "회원가입",
"The email is not valid!": "이메일 주소가 유효하지 않습니다!",
- "Verification email is sent to": "인증 이메일이 전송되었습니다",
- "Name": "이름",
+ "Verification email is sent to": "인증 이메일이 다음 주소로 전송되었습니다:",
"You may always change it later.": "나중에 언제든지 변경할 수 있습니다.",
- "Send verification code": "인증 코드 전송",
+ "Send verification code": "인증 코드 보내기",
+ "Verification code sent": "인증 코드 전송됨",
"Verification code": "인증 코드",
- "Failed to get the RMP save!": "RMP 저장을 가져오지 못했습니다!",
- "Can not sync this save!": "이 저장을 동기화할 수 없습니다!",
- "Synced saves": "동기화된 저장",
- "Subscribe to sync more": "더 많은 동기화를 위해 구독하세요",
- "Create": "생성",
- "Current save": "현재 저장",
- "Cloud save": "클라우드 저장",
+ "Minimum 8 characters. Contain at least 1 letter and 1 number.": "8자 이상, 최소 1개의 문자와 1개의 숫자를 포함해야 합니다.",
+ "Failed to get the RMP save!": "RMP 저장 파일을 가져오는 데 실패했습니다!",
+ "Can not sync this save!": "이 저장 파일을 동기화할 수 없습니다!",
+ "Synced saves": "동기화된 저장 파일",
+ "Subscribe to sync more": "더 많이 동기화하려면 구독하세요",
+ "Create": "만들기",
+ "Current save": "현재 저장 파일",
+ "Cloud save": "클라우드 저장 파일",
"ID": "고유 식별자",
"Status": "상태",
- "Last update at": "마지막 업데이트:",
+ "Last update at": "마지막 업데이트",
"Delete": "삭제",
"Sync now": "지금 동기화",
"Sync this slot": "이 슬롯 동기화",
"All subscriptions": "모든 구독",
"Redeem": "교환",
- "With this subscription, the following features are unlocked:": "이 구독으로 다음 기능이 잠금 해제됩니다:",
- "PRO features": "PRO 기능",
- "Sync 9 more saves": "추가 9개 저장 동기화",
+ "RMP_CLOUD": "지하철 노선도 툴킷・천추",
+ "RMP_EXPORT": "지하철 노선도 툴킷・옥형",
+ "With this subscription, the following features are unlocked:": "이 구독을 통해 다음 기능이 잠금 해제됩니다:",
+ "PRO features": "프로 기능",
+ "Sync 9 more saves": "추가 9개 저장 파일 동기화",
"Unlimited master nodes": "마스터 노드 무제한",
"Unlimited parallel lines": "평행선 무제한",
+ "Random station names": "무작위 역 이름",
"Expires at:": "만료일:",
"Not applicable": "해당 없음",
"Renew": "갱신",
- "Redeem your subscription": "구독을 교환하세요",
- "CDKey could be purchased in the following sites:": "CDKey는 다음 사이트에서 구매할 수 있습니다:",
- "Enter your CDKey here": "여기에 CDKey를 입력하세요",
+ "Renew now and get an extra 45 days!": "지금 갱신하고 추가 45일을 받으세요!",
+ "Redeem your subscription": "구독 교환",
+ "CDKey could be purchased in the following sites:": "CD키는 다음 사이트에서 구매할 수 있습니다:",
+ "Enter your CDKey here": "여기에 CD키를 입력하세요",
- "Oops! It seems there's a conflict": "이런! 충돌이 발생한 것 같습니다",
- "The local save is newer than the cloud one. Which one would you like to keep?": "로컬 저장이 클라우드 저장보다 최신입니다. 어느 것을 유지하시겠습니까?",
- "Local save": "로컬 저장",
- "Replace cloud with local": "클라우드를 로컬로 교체",
- "Download Local save": "로컬 저장 다운로드",
- "Replace local with cloud": "로컬을 클라우드로 교체",
- "Download Cloud save": "클라우드 저장 다운로드"
+ "Oops! It seems there's a conflict": "おっと!競合が発生したようです (おっと!きょうごうが はっせいした ようです)",
+ "The local save is newer than the cloud one. Which one would you like to keep?": "로컬 저장 파일이 클라우드 저장 파일보다 최신입니다. 어떤 버전을 유지하시겠습니까?",
+ "Local save": "로컬 저장 파일",
+ "Replace cloud with local": "클라우드를 로컬 저장 파일로 교체",
+ "Download Local save": "로컬 저장 파일 다운로드",
+ "Replace local with cloud": "로컬을 클라우드 저장 파일로 교체",
+ "Download Cloud save": "클라우드 저장 파일 다운로드"
}
diff --git a/src/i18n/translations/zh-Hans.json b/src/i18n/translations/zh-Hans.json
index 34abfc8..7026e88 100644
--- a/src/i18n/translations/zh-Hans.json
+++ b/src/i18n/translations/zh-Hans.json
@@ -99,7 +99,7 @@
"Your password has been reset successfully.": "已成功重置您的密码。",
"You're currently viewing a testing environment.": "您正在浏览测试环境。",
- "Login status expired":"登录过期",
+ "Login status expired.":"登录过期。",
"Log in / Sign up":"登录 / 注册",
"Log out":"登出",
"Change password":"更改密码",
@@ -142,14 +142,18 @@
"Sync this slot":"同步此存档",
"All subscriptions":"所有订阅",
"Redeem":"兑换",
+ "RMP_CLOUD": "地铁线路图绘制器・天枢",
+ "RMP_EXPORT": "地铁线路图绘制器・玉衡",
"With this subscription, the following features are unlocked:":"订阅后以下功能将会解锁",
"PRO features":"专业功能",
"Sync 9 more saves":"同步额外9个存档",
"Unlimited master nodes":"大师节点无数量限制",
"Unlimited parallel lines":"平行线段无数量限制",
+ "Random station names":"随机车站名称",
"Expires at:":"过期于:",
"Not applicable":"不适用",
"Renew":"续期",
+ "Renew now and get an extra 45 days!": "现在续期并获得额外45天!",
"Redeem your subscription":"兑换您的订阅",
"CDKey could be purchased in the following sites:":"兑换码可在以下网站获取:",
"Enter your CDKey here":"输入您的兑换码",
diff --git a/src/i18n/translations/zh-Hant.json b/src/i18n/translations/zh-Hant.json
index 411798b..4b83a86 100644
--- a/src/i18n/translations/zh-Hant.json
+++ b/src/i18n/translations/zh-Hant.json
@@ -99,7 +99,7 @@
"Your password has been reset successfully.": "已成功重設你的密碼。",
"You're currently viewing a testing environment.": "你正在檢視測試環境。",
- "Login status expired": "登入狀態已過期",
+ "Login status expired.": "登入狀態已過期。",
"Log in / Sign up": "登入 / 註冊",
"Log out": "登出",
"Change password": "更改密碼",
@@ -142,14 +142,18 @@
"Sync this slot": "同步此存檔",
"All subscriptions": "所有訂閱",
"Redeem": "兌換",
+ "RMP_CLOUD": "地鐵綫路圖繪製器・天樞",
+ "RMP_EXPORT": "地鐵綫路圖繪製器・玉衡",
"With this subscription, the following features are unlocked:": "訂閱後將解鎖以下功能:",
"PRO features": "專業功能",
"Sync 9 more saves": "同步額外9個存檔",
"Unlimited master nodes": "無限大師節點",
"Unlimited parallel lines": "無限平行線段",
+ "Random station names": "隨機車站名稱",
"Expires at:": "過期於:",
"Not applicable": "不適用",
"Renew": "續期",
+ "Renew now and get an extra 45 days!": "現在續期並獲得額外45天!",
"Redeem your subscription": "兌換您的訂閱",
"CDKey could be purchased in the following sites:": "CDKey可在以下網站購買:",
"Enter your CDKey here": "在此輸入您的CDKey",
diff --git a/src/index.tsx b/src/index.tsx
index 22b1713..4db1d58 100644
--- a/src/index.tsx
+++ b/src/index.tsx
@@ -7,7 +7,7 @@ import i18n from './i18n/config';
import './index.css';
import './inject-seo';
import store from './redux';
-import { syncAfterLogin } from './redux/account/account-slice';
+import { fetchSubscription, syncAfterLogin } from './redux/account/account-slice';
import { addRemoteFont, closeApp, isShowDevtools, openApp, updateTabMetadata } from './redux/app/app-slice';
import initStore from './redux/init';
import { checkTokenAndRefreshStore } from './util/api';
@@ -46,6 +46,8 @@ rmgRuntime.ready().then(async () => {
// Otherwise this is a no-op.
await checkTokenAndRefreshStore(store);
await store.dispatch(syncAfterLogin());
+ // Fetch subscription info only if user is logged in.
+ store.dispatch(fetchSubscription());
renderApp();
diff --git a/src/redux/account/account-slice.ts b/src/redux/account/account-slice.ts
index 8ca56e9..bc49ed8 100644
--- a/src/redux/account/account-slice.ts
+++ b/src/redux/account/account-slice.ts
@@ -1,19 +1,29 @@
import { logger } from '@railmapgen/rmg-runtime';
import { createAsyncThunk, createSlice, PayloadAction } from '@reduxjs/toolkit';
-import { RootState } from '../index';
-import { API_ENDPOINT, APILoginResponse, APISaveInfo, APISaveList, SAVE_KEY } from '../../util/constants';
-import { getRMPSave, notifyRMPSaveChange, setRMPSave } from '../../util/local-storage-save';
+import i18n from '../../i18n/config';
import { apiFetch } from '../../util/api';
+import {
+ API_ENDPOINT,
+ APILoginResponse,
+ APISaveInfo,
+ APISaveList,
+ APISubscription,
+ SAVE_KEY,
+} from '../../util/constants';
+import { getRMPSave, notifyRMPSaveChange, setRMPSave } from '../../util/local-storage-save';
+import { RootState } from '../index';
+import { addNotification } from '../notification/notification-slice';
import { setLastChangedAtTimeStamp, setResolveConflictModal } from '../rmp-save/rmp-save-slice';
+type DateTimeString = `${string}T${string}Z`;
export interface ActiveSubscriptions {
- RMP_CLOUD: boolean;
- RMP_EXPORT: boolean;
+ RMP_CLOUD?: DateTimeString;
+ RMP_EXPORT?: DateTimeString;
}
export const defaultActiveSubscriptions: ActiveSubscriptions = {
- RMP_CLOUD: false,
- RMP_EXPORT: false,
+ RMP_CLOUD: undefined,
+ RMP_EXPORT: undefined,
};
export interface AccountState {
@@ -91,7 +101,11 @@ export const fetchLogin = createAsyncThunk<{ error?: string; username?: string }
},
} = (await loginRes.json()) as APILoginResponse;
dispatch(login({ id: userId, name: username, email, token, expires, refreshToken, refreshExpires }));
- await dispatch(fetchSaveList()); // make sure saves are set before syncAfterLogin
+
+ dispatch(fetchSubscription());
+
+ // make sure saves are set before syncAfterLogin
+ await dispatch(fetchSaveList());
await dispatch(syncAfterLogin());
@@ -152,6 +166,47 @@ export const syncAfterLogin = createAsyncThunk(
}
);
+export const fetchSubscription = createAsyncThunk(
+ 'account/fetchSubscription',
+ async (_, { getState, dispatch, rejectWithValue }) => {
+ const { isLoggedIn, token } = (getState() as RootState).account;
+ if (!isLoggedIn || !token) return rejectWithValue('No token.');
+ const rep = await apiFetch(API_ENDPOINT.SUBSCRIPTION, {}, token);
+ if (rep.status === 401) {
+ dispatch(
+ addNotification({
+ title: i18n.t('Unable to retrieve your subscriptions'),
+ message: i18n.t('Login status expired.'),
+ duration: 60,
+ type: 'error',
+ })
+ );
+ dispatch(logout());
+ return rejectWithValue('Login status expired.');
+ }
+ if (rep.status !== 200) {
+ dispatch(
+ addNotification({
+ title: i18n.t('Unable to retrieve your subscriptions'),
+ message: await rep.text(),
+ duration: 60,
+ type: 'error',
+ })
+ );
+ return rejectWithValue(await rep.text());
+ }
+ const subscriptions = (await rep.json()).subscriptions as APISubscription[];
+ const activeSubscriptions = structuredClone(defaultActiveSubscriptions);
+ for (const subscription of subscriptions) {
+ const type = subscription.type;
+ if (type in activeSubscriptions) {
+ activeSubscriptions[type as keyof ActiveSubscriptions] = subscription.expires as DateTimeString;
+ }
+ }
+ dispatch(setActiveSubscriptions(activeSubscriptions));
+ }
+);
+
const accountSlice = createSlice({
name: 'account',
initialState,
diff --git a/src/redux/notification/notification-slice.ts b/src/redux/notification/notification-slice.ts
index 26699ed..f86b208 100644
--- a/src/redux/notification/notification-slice.ts
+++ b/src/redux/notification/notification-slice.ts
@@ -1,5 +1,5 @@
-import { createSlice, PayloadAction } from '@reduxjs/toolkit';
import rmgRuntime, { RMNotification } from '@railmapgen/rmg-runtime';
+import { createSlice, PayloadAction } from '@reduxjs/toolkit';
export interface NotificationState {
notifications: RMNotification[];
diff --git a/src/util/constants.ts b/src/util/constants.ts
index 5bf32a6..b85533f 100644
--- a/src/util/constants.ts
+++ b/src/util/constants.ts
@@ -73,6 +73,11 @@ export interface APISaveList {
currentSaveId: number;
}
+export interface APISubscription {
+ type: 'RMP' | 'RMP_CLOUD' | 'RMP_EXPORT';
+ expires: string;
+}
+
export enum SAVE_KEY {
RMP = 'rmp__param',
}