feat: i18n control, Gitea auto-update CI, license-gated updater, fixes

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Ivan Fontosh
2026-05-11 22:20:14 +08:00
parent 36776f4c5d
commit f462e65581
23 changed files with 2049 additions and 440 deletions
@@ -8,6 +8,7 @@ import { EULA_RU_MARKDOWN } from '../../legal/eulaRu';
import { getDndApi } from '../../shared/dndApi';
import { Button } from '../../shared/ui/controls';
import styles from '../EditorApp.module.css';
import { useEditorI18n } from '../i18n/EditorI18nContext';
type LicenseTokenModalProps = {
open: boolean;
@@ -16,6 +17,7 @@ type LicenseTokenModalProps = {
};
export function LicenseTokenModal({ open, onClose, onSaved }: LicenseTokenModalProps) {
const { t } = useEditorI18n();
const [token, setToken] = useState('');
const [saving, setSaving] = useState(false);
const [error, setError] = useState<string | null>(null);
@@ -40,28 +42,38 @@ export function LicenseTokenModal({ open, onClose, onSaved }: LicenseTokenModalP
return createPortal(
<>
<button type="button" aria-label="Закрыть" onClick={onClose} className={styles.modalBackdrop} />
<button
type="button"
aria-label={t('common.close')}
onClick={onClose}
className={styles.modalBackdrop}
/>
<div role="dialog" aria-modal="true" className={styles.modalDialog}>
<div className={styles.modalHeader}>
<div className={styles.modalTitle}>Указать ключ</div>
<button type="button" aria-label="Закрыть" onClick={onClose} className={styles.modalClose}>
<div className={styles.modalTitle}>{t('license.tokenTitle')}</div>
<button
type="button"
aria-label={t('common.close')}
onClick={onClose}
className={styles.modalClose}
>
×
</button>
</div>
<div className={styles.fieldGrid}>
<div className={styles.fieldLabel}>КЛЮЧ</div>
<div className={styles.fieldLabel}>{t('license.tokenKey')}</div>
<textarea
className={styles.licenseTextarea}
value={token}
onChange={(e) => setToken(e.target.value)}
placeholder="Продуктовый ключ DND-..."
placeholder={t('license.tokenPlaceholder')}
spellCheck={false}
/>
</div>
{error ? <div className={styles.fieldError}>{error}</div> : null}
<div className={styles.modalFooter}>
<Button onClick={onClose} disabled={saving}>
Отмена
{t('common.cancel')}
</Button>
<Button
variant="primary"
@@ -82,7 +94,7 @@ export function LicenseTokenModal({ open, onClose, onSaved }: LicenseTokenModalP
})();
}}
>
Сохранить
{t('common.save')}
</Button>
</div>
</div>
@@ -98,6 +110,7 @@ type EulaModalProps = {
};
export function EulaModal({ open, onClose, onAccepted }: EulaModalProps) {
const { t, locale } = useEditorI18n();
const [saving, setSaving] = useState(false);
useEffect(() => {
@@ -113,18 +126,29 @@ export function EulaModal({ open, onClose, onAccepted }: EulaModalProps) {
return createPortal(
<>
<button type="button" aria-label="Закрыть" onClick={onClose} className={styles.modalBackdrop} />
<button
type="button"
aria-label={t('common.close')}
onClick={onClose}
className={styles.modalBackdrop}
/>
<div role="dialog" aria-modal="true" className={styles.modalDialog}>
<div className={styles.modalHeader}>
<div className={styles.modalTitle}>Лицензионное соглашение</div>
<button type="button" aria-label="Закрыть" onClick={onClose} className={styles.modalClose}>
<div className={styles.modalTitle}>{t('license.eulaTitle')}</div>
<button
type="button"
aria-label={t('common.close')}
onClick={onClose}
className={styles.modalClose}
>
×
</button>
</div>
{locale === 'en' ? <div className={styles.muted}>{t('license.eulaNoteEn')}</div> : null}
<div className={styles.eulaScroll}>{EULA_RU_MARKDOWN}</div>
<div className={styles.modalFooter}>
<Button onClick={onClose} disabled={saving}>
Не принимаю
{t('license.eulaReject')}
</Button>
<Button
variant="primary"
@@ -144,7 +168,7 @@ export function EulaModal({ open, onClose, onAccepted }: EulaModalProps) {
})();
}}
>
Принимаю условия
{t('license.eulaAccept')}
</Button>
</div>
</div>
@@ -159,32 +183,16 @@ type LicenseAboutModalProps = {
snapshot: LicenseSnapshot | null;
};
function reasonLabel(reason: LicenseSnapshot['reason']): string {
switch (reason) {
case 'ok':
return 'Активна';
case 'none':
return 'Ключ не указан';
case 'expired':
return 'Срок действия истёк';
case 'bad_signature':
return 'Недействительная подпись';
case 'bad_payload':
return 'Неверный формат токена';
case 'malformed':
return 'Повреждённый токен';
case 'not_yet_valid':
return 'Ещё не действует';
case 'wrong_device':
return 'Другой привязанный компьютер';
case 'revoked_remote':
return 'Отозвана на сервере';
default:
return reason;
}
function licenseReasonLabel(t: (key: string) => string, reason: LicenseSnapshot['reason']): string {
const key = `license.reason.${reason}`;
const label = t(key);
return label === key ? reason : label;
}
export function LicenseAboutModal({ open, onClose, snapshot }: LicenseAboutModalProps) {
const { t, locale } = useEditorI18n();
const dateLocale = locale === 'en' ? 'en-US' : 'ru-RU';
useEffect(() => {
if (!open) return;
const onKey = (e: KeyboardEvent) => {
@@ -198,7 +206,7 @@ export function LicenseAboutModal({ open, onClose, snapshot }: LicenseAboutModal
const expText =
snapshot?.summary?.exp != null
? new Date(snapshot.summary.exp * 1000).toLocaleString('ru-RU', {
? new Date(snapshot.summary.exp * 1000).toLocaleString(dateLocale, {
dateStyle: 'long',
timeStyle: 'short',
})
@@ -206,48 +214,54 @@ export function LicenseAboutModal({ open, onClose, snapshot }: LicenseAboutModal
return createPortal(
<>
<button type="button" aria-label="Закрыть" onClick={onClose} className={styles.modalBackdrop} />
<button
type="button"
aria-label={t('common.close')}
onClick={onClose}
className={styles.modalBackdrop}
/>
<div role="dialog" aria-modal="true" className={styles.modalDialog}>
<div className={styles.modalHeader}>
<div className={styles.modalTitle}>О лицензии</div>
<button type="button" aria-label="Закрыть" onClick={onClose} className={styles.modalClose}>
<div className={styles.modalTitle}>{t('license.aboutTitle')}</div>
<button
type="button"
aria-label={t('common.close')}
onClick={onClose}
className={styles.modalClose}
>
×
</button>
</div>
{snapshot?.devSkip ? (
<div className={styles.fieldError}>
Режим разработки: проверка лицензии отключена (DND_SKIP_LICENSE).
</div>
) : null}
{snapshot?.devSkip ? <div className={styles.fieldError}>{t('license.aboutDevSkip')}</div> : null}
<div className={styles.fieldGrid}>
<div className={styles.fieldLabel}>СТАТУС</div>
<div>{snapshot ? reasonLabel(snapshot.reason) : '—'}</div>
<div className={styles.fieldLabel}>{t('license.aboutStatus')}</div>
<div>{snapshot ? licenseReasonLabel(t, snapshot.reason) : '—'}</div>
</div>
{snapshot?.summary ? (
<>
<div className={styles.fieldGrid}>
<div className={styles.fieldLabel}>ПРОДУКТ</div>
<div className={styles.fieldLabel}>{t('license.aboutProduct')}</div>
<div>{snapshot.summary.pid}</div>
</div>
<div className={styles.fieldGrid}>
<div className={styles.fieldLabel}>ID ЛИЦЕНЗИИ</div>
<div className={styles.fieldLabel}>{t('license.aboutLicenseId')}</div>
<div style={{ wordBreak: 'break-all' }}>{snapshot.summary.sub}</div>
</div>
<div className={styles.fieldGrid}>
<div className={styles.fieldLabel}>ОКОНЧАНИЕ</div>
<div className={styles.fieldLabel}>{t('license.aboutExpiry')}</div>
<div>{expText}</div>
</div>
<div className={styles.fieldGrid}>
<div className={styles.fieldLabel}>УСТРОЙСТВО</div>
<div className={styles.fieldLabel}>{t('license.aboutDevice')}</div>
<div style={{ wordBreak: 'break-all' }}>{snapshot.deviceId}</div>
</div>
</>
) : (
<div className={styles.muted}>Нет данных лицензии.</div>
<div className={styles.muted}>{t('license.aboutNoData')}</div>
)}
<div className={styles.modalFooter}>
<Button variant="primary" onClick={onClose}>
Закрыть
{t('common.close')}
</Button>
</div>
</div>