mirror of
https://github.com/duckietm/Nitro-V3.git
synced 2026-06-20 07:26:19 +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 pendingActionRef = useRef<string | null>(null);
|
||||||
const { simpleAlert = null } = useNotification();
|
const { simpleAlert = null } = useNotification();
|
||||||
|
|
||||||
// Keyboard shortcuts: Esc to close edit panels
|
|
||||||
useEffect(() =>
|
useEffect(() =>
|
||||||
{
|
{
|
||||||
if(!adminMode) return;
|
if(!adminMode) return;
|
||||||
@@ -178,11 +177,13 @@ export const CatalogAdminProvider: FC<{ children: ReactNode }> = ({ children })
|
|||||||
setLoading(true);
|
setLoading(true);
|
||||||
setLastError(null);
|
setLastError(null);
|
||||||
pendingActionRef.current = 'savePage';
|
pendingActionRef.current = 'savePage';
|
||||||
|
|
||||||
SendMessageComposer(new CatalogAdminSavePageComposer(
|
SendMessageComposer(new CatalogAdminSavePageComposer(
|
||||||
data.pageId || 0, data.caption, data.captionSave, data.pageLayout, data.iconImage,
|
data.pageId || 0, data.caption, data.captionSave, data.pageLayout, data.iconImage,
|
||||||
data.minRank, data.visible === '1', data.enabled === '1',
|
data.minRank, data.visible === '1', data.enabled === '1',
|
||||||
data.orderNum, data.parentId,
|
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 ]);
|
}, [ currentType ]);
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { FC, useEffect, useState } from 'react';
|
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 { CatalogType, LocalizeText } from '../../../../api';
|
||||||
import { useCatalogData, useCatalogUiState } from '../../../../hooks';
|
import { useCatalogData, useCatalogUiState, useTranslationActions, useTranslationState } from '../../../../hooks';
|
||||||
import { IPageEditData, useCatalogAdmin } from '../../CatalogAdminContext';
|
import { IPageEditData, useCatalogAdmin } from '../../CatalogAdminContext';
|
||||||
|
|
||||||
const LAYOUT_OPTIONS = [
|
const LAYOUT_OPTIONS = [
|
||||||
@@ -40,6 +40,13 @@ export const CatalogAdminPageEditView: FC<{}> = () =>
|
|||||||
const [ enabled, setEnabled ] = useState('1');
|
const [ enabled, setEnabled ] = useState('1');
|
||||||
const [ orderNum, setOrderNum ] = useState(0);
|
const [ orderNum, setOrderNum ] = useState(0);
|
||||||
const [ parentId, setParentId ] = useState(-1);
|
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
|
const targetNode = editingPageNode
|
||||||
? editingPageNode
|
? editingPageNode
|
||||||
: editingRootPage
|
: editingRootPage
|
||||||
@@ -69,6 +76,14 @@ export const CatalogAdminPageEditView: FC<{}> = () =>
|
|||||||
setEnabled('1');
|
setEnabled('1');
|
||||||
setMinRank(1);
|
setMinRank(1);
|
||||||
setOrderNum(0);
|
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;
|
const wireParentId = targetNode.parentId;
|
||||||
setParentId(typeof wireParentId === 'number' && wireParentId !== -1
|
setParentId(typeof wireParentId === 'number' && wireParentId !== -1
|
||||||
? wireParentId
|
? wireParentId
|
||||||
@@ -95,6 +110,7 @@ export const CatalogAdminPageEditView: FC<{}> = () =>
|
|||||||
enabled,
|
enabled,
|
||||||
orderNum,
|
orderNum,
|
||||||
parentId,
|
parentId,
|
||||||
|
pageText1,
|
||||||
};
|
};
|
||||||
|
|
||||||
catalogAdmin.savePage(data);
|
catalogAdmin.savePage(data);
|
||||||
@@ -102,6 +118,47 @@ export const CatalogAdminPageEditView: FC<{}> = () =>
|
|||||||
closeForm();
|
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 () =>
|
const handleDelete = async () =>
|
||||||
{
|
{
|
||||||
if(!catalogAdmin?.deletePage || isRoot) return;
|
if(!catalogAdmin?.deletePage || isRoot) return;
|
||||||
@@ -168,6 +225,50 @@ export const CatalogAdminPageEditView: FC<{}> = () =>
|
|||||||
{ LocalizeText('catalog.admin.enabled') }
|
{ LocalizeText('catalog.admin.enabled') }
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</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>
|
||||||
|
|
||||||
<div className="flex justify-between mt-2">
|
<div className="flex justify-between mt-2">
|
||||||
|
|||||||
@@ -228,16 +228,6 @@ const resolveSupportedLanguage = (value: string, languages: ITranslationLanguage
|
|||||||
return languages[0].code;
|
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 useTranslationStore = () =>
|
||||||
{
|
{
|
||||||
const defaultTargetLanguage = getBrowserLanguageCode();
|
const defaultTargetLanguage = getBrowserLanguageCode();
|
||||||
@@ -599,6 +589,7 @@ const useTranslationStore = () =>
|
|||||||
lastError,
|
lastError,
|
||||||
updateSettings,
|
updateSettings,
|
||||||
ensureSupportedLanguagesLoaded,
|
ensureSupportedLanguagesLoaded,
|
||||||
|
translateText,
|
||||||
translateIncoming,
|
translateIncoming,
|
||||||
translateOutgoing,
|
translateOutgoing,
|
||||||
enqueueOutgoingTranslation,
|
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 = () =>
|
export const useTranslationState = () =>
|
||||||
{
|
{
|
||||||
const {
|
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 = () =>
|
export const useTranslationActions = () =>
|
||||||
{
|
{
|
||||||
const {
|
const {
|
||||||
settings,
|
settings,
|
||||||
updateSettings,
|
updateSettings,
|
||||||
ensureSupportedLanguagesLoaded,
|
ensureSupportedLanguagesLoaded,
|
||||||
|
translateText,
|
||||||
translateIncoming,
|
translateIncoming,
|
||||||
translateOutgoing,
|
translateOutgoing,
|
||||||
enqueueOutgoingTranslation,
|
enqueueOutgoingTranslation,
|
||||||
@@ -664,11 +641,10 @@ export const useTranslationActions = () =>
|
|||||||
} = useBetween(useTranslationStore);
|
} = useBetween(useTranslationStore);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
// settings is exposed here too because most action call sites
|
|
||||||
// need `if(settings.enabled)` checks before dispatching.
|
|
||||||
settings,
|
settings,
|
||||||
updateSettings,
|
updateSettings,
|
||||||
ensureSupportedLanguagesLoaded,
|
ensureSupportedLanguagesLoaded,
|
||||||
|
translateText,
|
||||||
translateIncoming,
|
translateIncoming,
|
||||||
translateOutgoing,
|
translateOutgoing,
|
||||||
enqueueOutgoingTranslation,
|
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);
|
export const useTranslation = () => useBetween(useTranslationStore);
|
||||||
|
|||||||
Reference in New Issue
Block a user