mirror of
https://github.com/duckietm/Nitro-V3.git
synced 2026-06-19 15:06:20 +00:00
Merge pull request #150 from duckietm/Dev
🆙 Added translation to the Catalog Text
This commit is contained in:
@@ -90,7 +90,6 @@ export const CatalogAdminProvider: FC<{ children: ReactNode }> = ({ children })
|
||||
const pendingActionRef = useRef<string | null>(null);
|
||||
const { simpleAlert = null } = useNotification();
|
||||
|
||||
// Keyboard shortcuts: Esc to close edit panels
|
||||
useEffect(() =>
|
||||
{
|
||||
if(!adminMode) return;
|
||||
@@ -178,11 +177,13 @@ export const CatalogAdminProvider: FC<{ children: ReactNode }> = ({ children })
|
||||
setLoading(true);
|
||||
setLastError(null);
|
||||
pendingActionRef.current = 'savePage';
|
||||
|
||||
SendMessageComposer(new CatalogAdminSavePageComposer(
|
||||
data.pageId || 0, data.caption, data.captionSave, data.pageLayout, data.iconImage,
|
||||
data.minRank, data.visible === '1', data.enabled === '1',
|
||||
data.orderNum, data.parentId,
|
||||
data.pageHeadline || '', data.pageTeaser || '', data.pageTextDetails || '', currentType, data.catalogMode
|
||||
data.pageHeadline || '', data.pageTeaser || '', data.pageTextDetails || '', currentType, data.catalogMode,
|
||||
data.pageText1 || ''
|
||||
));
|
||||
}, [ currentType ]);
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { FC, useEffect, useState } from 'react';
|
||||
import { FaSave, FaSpinner, FaTimes, FaTrash } from 'react-icons/fa';
|
||||
import { FaLanguage, FaSave, FaSpinner, FaTimes, FaTrash } from 'react-icons/fa';
|
||||
import { CatalogType, LocalizeText } from '../../../../api';
|
||||
import { useCatalogData, useCatalogUiState } from '../../../../hooks';
|
||||
import { useCatalogData, useCatalogUiState, useTranslationActions, useTranslationState } from '../../../../hooks';
|
||||
import { IPageEditData, useCatalogAdmin } from '../../CatalogAdminContext';
|
||||
|
||||
const LAYOUT_OPTIONS = [
|
||||
@@ -40,6 +40,13 @@ export const CatalogAdminPageEditView: FC<{}> = () =>
|
||||
const [ enabled, setEnabled ] = useState('1');
|
||||
const [ orderNum, setOrderNum ] = useState(0);
|
||||
const [ parentId, setParentId ] = useState(-1);
|
||||
const [ pageText1, setPageText1 ] = useState('');
|
||||
const [ showTranslate, setShowTranslate ] = useState(false);
|
||||
const [ translateTargetLanguage, setTranslateTargetLanguage ] = useState('en');
|
||||
const [ isTranslating, setIsTranslating ] = useState(false);
|
||||
const [ translateError, setTranslateError ] = useState<string | null>(null);
|
||||
const { supportedLanguages = [], languagesLoading = false } = useTranslationState();
|
||||
const { translateText, ensureSupportedLanguagesLoaded } = useTranslationActions();
|
||||
const targetNode = editingPageNode
|
||||
? editingPageNode
|
||||
: editingRootPage
|
||||
@@ -69,6 +76,14 @@ export const CatalogAdminPageEditView: FC<{}> = () =>
|
||||
setEnabled('1');
|
||||
setMinRank(1);
|
||||
setOrderNum(0);
|
||||
const matchesLoadedPage = currentPage && targetPageId === currentPage.pageId;
|
||||
const existingText1 = matchesLoadedPage && currentPage.localization
|
||||
? currentPage.localization.getText(0)
|
||||
: '';
|
||||
setPageText1(existingText1 || '');
|
||||
setShowTranslate(false);
|
||||
setIsTranslating(false);
|
||||
setTranslateError(null);
|
||||
const wireParentId = targetNode.parentId;
|
||||
setParentId(typeof wireParentId === 'number' && wireParentId !== -1
|
||||
? wireParentId
|
||||
@@ -95,6 +110,7 @@ export const CatalogAdminPageEditView: FC<{}> = () =>
|
||||
enabled,
|
||||
orderNum,
|
||||
parentId,
|
||||
pageText1,
|
||||
};
|
||||
|
||||
catalogAdmin.savePage(data);
|
||||
@@ -102,6 +118,47 @@ export const CatalogAdminPageEditView: FC<{}> = () =>
|
||||
closeForm();
|
||||
};
|
||||
|
||||
const openTranslate = () =>
|
||||
{
|
||||
const next = !showTranslate;
|
||||
setShowTranslate(next);
|
||||
setTranslateError(null);
|
||||
if(next) ensureSupportedLanguagesLoaded();
|
||||
};
|
||||
|
||||
const runTranslate = async () =>
|
||||
{
|
||||
if(!pageText1.trim().length)
|
||||
{
|
||||
setTranslateError('Nothing to translate yet.');
|
||||
return;
|
||||
}
|
||||
|
||||
if(!translateTargetLanguage)
|
||||
{
|
||||
setTranslateError('Pick a language first.');
|
||||
return;
|
||||
}
|
||||
|
||||
setIsTranslating(true);
|
||||
setTranslateError(null);
|
||||
|
||||
try
|
||||
{
|
||||
const result = await translateText(pageText1, translateTargetLanguage);
|
||||
setPageText1(result?.translatedText || pageText1);
|
||||
setShowTranslate(false);
|
||||
}
|
||||
catch(error)
|
||||
{
|
||||
setTranslateError((error as Error)?.message || 'Translation failed.');
|
||||
}
|
||||
finally
|
||||
{
|
||||
setIsTranslating(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleDelete = async () =>
|
||||
{
|
||||
if(!catalogAdmin?.deletePage || isRoot) return;
|
||||
@@ -168,6 +225,50 @@ export const CatalogAdminPageEditView: FC<{}> = () =>
|
||||
{ LocalizeText('catalog.admin.enabled') }
|
||||
</label>
|
||||
</div>
|
||||
<div className="flex flex-col gap-0.5 col-span-3">
|
||||
<div className="flex items-center justify-between">
|
||||
<label className="text-[9px] text-muted uppercase font-bold">Page Text 1 <span className="text-muted normal-case font-normal opacity-70">(leave blank to keep current)</span></label>
|
||||
<button
|
||||
className="flex items-center gap-1 px-1.5 py-0.5 rounded text-[9px] font-bold text-primary border border-primary/40 hover:bg-primary/10 transition-colors cursor-pointer disabled:opacity-50"
|
||||
disabled={ isTranslating || !pageText1.trim().length }
|
||||
title="Translate via Google Translate"
|
||||
type="button"
|
||||
onClick={ openTranslate }>
|
||||
{ isTranslating ? <FaSpinner className="text-[8px] animate-spin" /> : <FaLanguage className="text-[10px]" /> }
|
||||
Translate
|
||||
</button>
|
||||
</div>
|
||||
{ showTranslate &&
|
||||
<div className="flex items-center gap-1 mb-1 p-1 bg-gray-50 border border-card-grid-item-border rounded">
|
||||
<select
|
||||
className={ `${ inputClass } flex-1` }
|
||||
disabled={ isTranslating || languagesLoading }
|
||||
value={ translateTargetLanguage }
|
||||
onChange={ e => setTranslateTargetLanguage(e.target.value) }>
|
||||
{ languagesLoading && !supportedLanguages.length &&
|
||||
<option value="">Loading languages…</option> }
|
||||
{ supportedLanguages.map(lang => (
|
||||
<option key={ lang.code } value={ lang.code }>{ lang.name } ({ lang.code })</option>
|
||||
)) }
|
||||
</select>
|
||||
<button
|
||||
className="px-2 py-1 rounded text-[10px] font-bold bg-primary text-white hover:bg-secondary transition-colors cursor-pointer disabled:opacity-50"
|
||||
disabled={ isTranslating || !translateTargetLanguage || !pageText1.trim().length }
|
||||
type="button"
|
||||
onClick={ runTranslate }>
|
||||
{ isTranslating ? <FaSpinner className="text-[8px] animate-spin" /> : 'Apply' }
|
||||
</button>
|
||||
<button
|
||||
className="px-2 py-1 rounded text-[10px] font-bold text-muted border border-card-grid-item-border hover:bg-gray-100 transition-colors cursor-pointer"
|
||||
disabled={ isTranslating }
|
||||
type="button"
|
||||
onClick={ () => { setShowTranslate(false); setTranslateError(null); } }>
|
||||
Cancel
|
||||
</button>
|
||||
</div> }
|
||||
{ translateError && <span className="text-[9px] text-danger">{ translateError }</span> }
|
||||
<textarea className={ `${ inputClass } min-h-[60px] resize-y` } value={ pageText1 } onChange={ e => setPageText1(e.target.value) } />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex justify-between mt-2">
|
||||
|
||||
@@ -228,16 +228,6 @@ const resolveSupportedLanguage = (value: string, languages: ITranslationLanguage
|
||||
return languages[0].code;
|
||||
};
|
||||
|
||||
/**
|
||||
* Internal singleton state + actions hook. Public consumers should
|
||||
* call useTranslationState (read-only) or useTranslationActions
|
||||
* (imperative) instead. useTranslation is the deprecated shim that
|
||||
* composes both.
|
||||
*
|
||||
* Wrapped in useBetween at each public-hook layer so every consumer
|
||||
* in the tree sees the same instance (preserves the previous
|
||||
* useBetween(useTranslationState) behavior).
|
||||
*/
|
||||
const useTranslationStore = () =>
|
||||
{
|
||||
const defaultTargetLanguage = getBrowserLanguageCode();
|
||||
@@ -599,6 +589,7 @@ const useTranslationStore = () =>
|
||||
lastError,
|
||||
updateSettings,
|
||||
ensureSupportedLanguagesLoaded,
|
||||
translateText,
|
||||
translateIncoming,
|
||||
translateOutgoing,
|
||||
enqueueOutgoingTranslation,
|
||||
@@ -607,14 +598,6 @@ const useTranslationStore = () =>
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Read-only slice of the translation store: persisted settings, the
|
||||
* supported languages list, the loading/loaded flags, the last
|
||||
* incoming/outgoing detected language tags, and the last error message
|
||||
* surfaced to the UI.
|
||||
*
|
||||
* Components that only render translation state subscribe here.
|
||||
*/
|
||||
export const useTranslationState = () =>
|
||||
{
|
||||
const {
|
||||
@@ -644,19 +627,13 @@ export const useTranslationState = () =>
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Imperative slice of the translation store: settings mutation,
|
||||
* supported-languages refresh, the translate* helpers, and the
|
||||
* outgoing-queue write/read pair. Stays separate so components that
|
||||
* only invoke actions (e.g. ChatInputActions) don't pull the full
|
||||
* state shape.
|
||||
*/
|
||||
export const useTranslationActions = () =>
|
||||
{
|
||||
const {
|
||||
settings,
|
||||
updateSettings,
|
||||
ensureSupportedLanguagesLoaded,
|
||||
translateText,
|
||||
translateIncoming,
|
||||
translateOutgoing,
|
||||
enqueueOutgoingTranslation,
|
||||
@@ -664,11 +641,10 @@ export const useTranslationActions = () =>
|
||||
} = useBetween(useTranslationStore);
|
||||
|
||||
return {
|
||||
// settings is exposed here too because most action call sites
|
||||
// need `if(settings.enabled)` checks before dispatching.
|
||||
settings,
|
||||
updateSettings,
|
||||
ensureSupportedLanguagesLoaded,
|
||||
translateText,
|
||||
translateIncoming,
|
||||
translateOutgoing,
|
||||
enqueueOutgoingTranslation,
|
||||
@@ -676,10 +652,4 @@ export const useTranslationActions = () =>
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* @deprecated Prefer `useTranslationState` (read-only) and
|
||||
* `useTranslationActions` (imperative) directly. This shim composes
|
||||
* both into the historical `useTranslation()` shape so the six
|
||||
* existing consumers keep working unchanged.
|
||||
*/
|
||||
export const useTranslation = () => useBetween(useTranslationStore);
|
||||
|
||||
Reference in New Issue
Block a user