feat:app remaining text supports 18n && enable translate (#148)

* feat:app remaining text supports 18n && enable translate

* refactor:remove unused canI18n code

* feat:save user language settings

Thanks to @AmazingRain !
This commit is contained in:
mu ziyu
2025-10-02 02:18:42 +08:00
committed by GitHub
parent cd6cc85703
commit 4d12c01393
8 changed files with 354 additions and 173 deletions

View File

@@ -1,11 +1,58 @@
{
"nav": {
"newDiagram": "New Diagram",
"saveSessionOnly": "Save (Session Only)",
"loadSessionOnly": "Load (Session Only)",
"importFile": "Import File",
"exportFile": "Export File",
"quickSaveSession": "Quick Save(Session)",
"serverStorage": "Server Storage"
}
"nav": {
"newDiagram": "New Diagram",
"saveSessionOnly": "Save (Session Only)",
"loadSessionOnly": "Load (Session Only)",
"importFile": "Import File",
"exportFile": "Export File",
"quickSaveSession": "Quick Save (Session)",
"serverStorage": "Server Storage"
},
"status": {
"current": "Current",
"untitled": "Untitled Diagram",
"modified": "Modified",
"sessionStorageNote": "Session storage only - export to save permanently"
},
"dialog": {
"save": {
"title": "Save Diagram (Current Session Only)",
"warningTitle": "Important",
"warningMessage": "This save is temporary and will be lost when you close the browser.",
"warningExport": "Use <strong>Export File</strong> to permanently save your work.",
"placeholder": "Enter diagram name",
"btnSave": "Save",
"btnCancel": "Cancel"
},
"load": {
"title": "Load Diagram (Current Session Only)",
"noteTitle": "Note",
"noteMessage": "These saves are temporary. Export your diagrams to keep them permanently.",
"noSavedDiagrams": "No saved diagrams found in this session",
"updated": "Updated",
"btnLoad": "Load",
"btnDelete": "Delete",
"btnClose": "Close"
},
"export": {
"title": "Export Diagram",
"recommendedTitle": "Recommended",
"recommendedMessage": "This is the best way to save your work permanently.",
"noteMessage": "Exported JSON files can be imported later or shared with others.",
"btnDownload": "Download JSON",
"btnCancel": "Cancel"
}
},
"alert": {
"enterDiagramName": "Please enter a diagram name",
"diagramExists": "A diagram named \"{{name}}\" already exists in this session. This will overwrite it. Are you sure you want to continue?",
"unsavedChanges": "You have unsaved changes. Continue loading?",
"createNewDiagram": "Create a new diagram?",
"unsavedChangesExport": "You have unsaved changes. Export your diagram first to save it. Continue?",
"confirmDelete": "Are you sure you want to delete this diagram?",
"storageFull": "Storage full! Opening Storage Manager...",
"autoSaveFailed": "Storage full! Please use Storage Manager to free up space.",
"beforeUnload": "You have unsaved changes. Are you sure you want to leave?",
"quotaExceeded": "Storage quota exceeded. Please export important diagrams and clear some space."
}
}

View File

@@ -1,11 +1,58 @@
{
"nav": {
"newDiagram": "新建图表",
"saveSessionOnly": "保存(仅会话)",
"loadSessionOnly": "加载(仅会话)",
"importFile": "导入文件",
"exportFile": "导出文件",
"quickSaveSession": "快速保存(会话)",
"serverStorage": "服务端存储"
}
"nav": {
"newDiagram": "新建图表",
"saveSessionOnly": "保存(仅会话)",
"loadSessionOnly": "加载(仅会话)",
"importFile": "导入文件",
"exportFile": "导出文件",
"quickSaveSession": "快速保存(会话)",
"serverStorage": "服务端存储"
},
"status": {
"current": "当前",
"untitled": "未命名图表",
"modified": "已修改",
"sessionStorageNote": "仅会话存储 - 导出以永久保存"
},
"dialog": {
"save": {
"title": "保存图表(仅当前会话)",
"warningTitle": "重要提示",
"warningMessage": "此保存是临时的,关闭浏览器后将丢失。",
"warningExport": "使用<strong>导出文件</strong>功能永久保存您的工作。",
"placeholder": "输入图表名称",
"btnSave": "保存",
"btnCancel": "取消"
},
"load": {
"title": "加载图表(仅当前会话)",
"noteTitle": "提示",
"noteMessage": "这些保存是临时的。导出您的图表以永久保存。",
"noSavedDiagrams": "当前会话中未找到已保存的图表",
"updated": "更新时间",
"btnLoad": "加载",
"btnDelete": "删除",
"btnClose": "关闭"
},
"export": {
"title": "导出图表",
"recommendedTitle": "推荐",
"recommendedMessage": "这是永久保存工作的最佳方式。",
"noteMessage": "导出的 JSON 文件可以稍后导入或与他人共享。",
"btnDownload": "下载 JSON",
"btnCancel": "取消"
}
},
"alert": {
"enterDiagramName": "请输入图表名称",
"diagramExists": "名为\"{{name}}\"的图表已存在于此会话中。这将覆盖它。您确定要继续吗?",
"unsavedChanges": "您有未保存的更改。继续加载?",
"createNewDiagram": "创建新图表?",
"unsavedChangesExport": "您有未保存的更改。请先导出图表以保存。继续?",
"confirmDelete": "您确定要删除此图表吗?",
"storageFull": "存储空间已满!正在打开存储管理器...",
"autoSaveFailed": "存储空间已满!请使用存储管理器释放空间。",
"beforeUnload": "您有未保存的更改。您确定要离开吗?",
"quotaExceeded": "存储配额已超出。请导出重要图表并清理一些空间。"
}
}

View File

@@ -138,14 +138,14 @@ function App() {
} catch (e) {
console.error('Failed to save diagrams:', e);
if (e instanceof DOMException && e.name === 'QuotaExceededError') {
alert('Storage quota exceeded. Please export important diagrams and clear some space.');
alert(t('alert.quotaExceeded'));
}
}
}, [diagrams]);
const saveDiagram = () => {
if (!diagramName.trim()) {
alert('Please enter a diagram name');
alert(t('alert.enterDiagramName'));
return;
}
@@ -156,7 +156,7 @@ function App() {
if (existingDiagram) {
const confirmOverwrite = window.confirm(
`A diagram named "${diagramName}" already exists in this session. This will overwrite it. Are you sure you want to continue?`
t('alert.diagramExists', { name: diagramName })
);
if (!confirmOverwrite) {
return;
@@ -210,14 +210,14 @@ function App() {
} catch (e) {
console.error('Failed to save diagram:', e);
if (e instanceof DOMException && e.name === 'QuotaExceededError') {
alert('Storage full! Opening Storage Manager...');
alert(t('alert.storageFull'));
setShowStorageManager(true);
}
}
};
const loadDiagram = (diagram: SavedDiagram) => {
if (hasUnsavedChanges && !window.confirm('You have unsaved changes. Continue loading?')) {
if (hasUnsavedChanges && !window.confirm(t('alert.unsavedChanges'))) {
return;
}
@@ -247,7 +247,7 @@ function App() {
};
const deleteDiagram = (id: string) => {
if (window.confirm('Are you sure you want to delete this diagram?')) {
if (window.confirm(t('alert.confirmDelete'))) {
setDiagrams(diagrams.filter(d => d.id !== id));
if (currentDiagram?.id === id) {
setCurrentDiagram(null);
@@ -258,8 +258,8 @@ function App() {
const newDiagram = () => {
const message = hasUnsavedChanges
? 'You have unsaved changes. Export your diagram first to save it. Continue?'
: 'Create a new diagram?';
? t('alert.unsavedChangesExport')
: t('alert.createNewDiagram');
if (window.confirm(message)) {
const emptyDiagram: DiagramData = {
@@ -400,14 +400,7 @@ function App() {
};
// i18n
const [canI18n, setCanI18n] = useState(false);
const { t, i18n } = useTranslation('app');
useEffect(() => {
// http://localhost:3000/?canI18n=1
const params = new URLSearchParams(window.location.search);
// show demo
setCanI18n(params.get('canI18n') === '1');
}, [window.location.search]);
// Auto-save functionality
useEffect(() => {
@@ -445,7 +438,7 @@ function App() {
} catch (e) {
console.error('Auto-save failed:', e);
if (e instanceof DOMException && e.name === 'QuotaExceededError') {
alert('Storage full! Please use Storage Manager to free up space.');
alert(t('alert.autoSaveFailed'));
setShowStorageManager(true);
}
}
@@ -459,7 +452,7 @@ function App() {
const handleBeforeUnload = (e: BeforeUnloadEvent) => {
if (hasUnsavedChanges) {
e.preventDefault();
e.returnValue = 'You have unsaved changes. Are you sure you want to leave?';
e.returnValue = t('alert.beforeUnload');
return e.returnValue;
}
};
@@ -471,7 +464,6 @@ function App() {
return (
<div className="App">
<div className="toolbar">
{canI18n && <ChangeLanguage />}
<button onClick={newDiagram}>{t('nav.newDiagram')}</button>
{serverStorageAvailable && (
<button
@@ -505,11 +497,12 @@ function App() {
>
{t('nav.quickSaveSession')}
</button>
<ChangeLanguage />
<span className="current-diagram">
{currentDiagram ? `Current: ${currentDiagram.name}` : diagramName || 'Untitled Diagram'}
{hasUnsavedChanges && <span style={{ color: '#ff9800', marginLeft: '10px' }}> Modified</span>}
{currentDiagram ? `${t('status.current')}: ${currentDiagram.name}` : diagramName || t('status.untitled')}
{hasUnsavedChanges && <span style={{ color: '#ff9800', marginLeft: '10px' }}> {t('status.modified')}</span>}
<span style={{ fontSize: '12px', color: '#666', marginLeft: '10px' }}>
(Session storage only - export to save permanently)
({t('status.sessionStorageNote')})
</span>
</span>
</div>
@@ -528,7 +521,7 @@ function App() {
{showSaveDialog && (
<div className="dialog-overlay">
<div className="dialog">
<h2>Save Diagram (Current Session Only)</h2>
<h2>{t('dialog.save.title')}</h2>
<div style={{
backgroundColor: '#fff3cd',
border: '1px solid #ffeeba',
@@ -536,21 +529,21 @@ function App() {
borderRadius: '4px',
marginBottom: '20px'
}}>
<strong> Important:</strong> This save is temporary and will be lost when you close the browser.
<strong> {t('dialog.save.warningTitle')}:</strong> {t('dialog.save.warningMessage')}
<br />
Use <strong>Export File</strong> to permanently save your work.
<span dangerouslySetInnerHTML={{ __html: t('dialog.save.warningExport') }} />
</div>
<input
type="text"
placeholder="Enter diagram name"
placeholder={t('dialog.save.placeholder')}
value={diagramName}
onChange={(e) => setDiagramName(e.target.value)}
onKeyDown={(e) => e.key === 'Enter' && saveDiagram()}
autoFocus
/>
<div className="dialog-buttons">
<button onClick={saveDiagram}>Save</button>
<button onClick={() => setShowSaveDialog(false)}>Cancel</button>
<button onClick={saveDiagram}>{t('dialog.save.btnSave')}</button>
<button onClick={() => setShowSaveDialog(false)}>{t('dialog.save.btnCancel')}</button>
</div>
</div>
</div>
@@ -560,7 +553,7 @@ function App() {
{showLoadDialog && (
<div className="dialog-overlay">
<div className="dialog">
<h2>Load Diagram (Current Session Only)</h2>
<h2>{t('dialog.load.title')}</h2>
<div style={{
backgroundColor: '#fff3cd',
border: '1px solid #ffeeba',
@@ -568,29 +561,29 @@ function App() {
borderRadius: '4px',
marginBottom: '20px'
}}>
<strong> Note:</strong> These saves are temporary. Export your diagrams to keep them permanently.
<strong> {t('dialog.load.noteTitle')}:</strong> {t('dialog.load.noteMessage')}
</div>
<div className="diagram-list">
{diagrams.length === 0 ? (
<p>No saved diagrams found in this session</p>
<p>{t('dialog.load.noSavedDiagrams')}</p>
) : (
diagrams.map(diagram => (
<div key={diagram.id} className="diagram-item">
<div>
<strong>{diagram.name}</strong>
<br />
<small>Updated: {new Date(diagram.updatedAt).toLocaleString()}</small>
<small>{t('dialog.load.updated')}: {new Date(diagram.updatedAt).toLocaleString()}</small>
</div>
<div className="diagram-actions">
<button onClick={() => loadDiagram(diagram)}>Load</button>
<button onClick={() => deleteDiagram(diagram.id)}>Delete</button>
<button onClick={() => loadDiagram(diagram)}>{t('dialog.load.btnLoad')}</button>
<button onClick={() => deleteDiagram(diagram.id)}>{t('dialog.load.btnDelete')}</button>
</div>
</div>
))
)}
</div>
<div className="dialog-buttons">
<button onClick={() => setShowLoadDialog(false)}>Close</button>
<button onClick={() => setShowLoadDialog(false)}>{t('dialog.load.btnClose')}</button>
</div>
</div>
</div>
@@ -601,7 +594,7 @@ function App() {
{showExportDialog && (
<div className="dialog-overlay">
<div className="dialog">
<h2>Export Diagram</h2>
<h2>{t('dialog.export.title')}</h2>
<div style={{
backgroundColor: '#d4edda',
border: '1px solid #c3e6cb',
@@ -610,15 +603,15 @@ function App() {
marginBottom: '20px'
}}>
<p style={{ margin: '0 0 10px 0' }}>
<strong> Recommended:</strong> This is the best way to save your work permanently.
<strong> {t('dialog.export.recommendedTitle')}:</strong> {t('dialog.export.recommendedMessage')}
</p>
<p style={{ margin: 0, fontSize: '14px', color: '#155724' }}>
Exported JSON files can be imported later or shared with others.
{t('dialog.export.noteMessage')}
</p>
</div>
<div className="dialog-buttons">
<button onClick={exportDiagram}>Download JSON</button>
<button onClick={() => setShowExportDialog(false)}>Cancel</button>
<button onClick={exportDiagram}>{t('dialog.export.btnDownload')}</button>
<button onClick={() => setShowExportDialog(false)}>{t('dialog.export.btnCancel')}</button>
</div>
</div>
</div>

View File

@@ -4,54 +4,55 @@ import './styles.css';
import { supportedLanguages } from '../../i18n';
const ChangeLanguage = () => {
const { i18n } = useTranslation();
const [isOpen, setIsOpen] = useState(false);
const [currentLang, setCurrentLang] = useState(i18n.language || 'en');
const dropdownRef = useRef<HTMLDivElement>(null);
const { i18n } = useTranslation();
const [isOpen, setIsOpen] = useState(false);
const [currentLang, setCurrentLang] = useState(i18n.language || 'en-US');
const dropdownRef = useRef<HTMLDivElement>(null);
const changeLanguage = (lang: string) => {
i18n.changeLanguage(lang);
setCurrentLang(lang);
const changeLanguage = (lang: string) => {
i18n.changeLanguage(lang);
setCurrentLang(lang);
setIsOpen(false);
localStorage.setItem('i18nextLng', lang);
};
useEffect(() => {
const handleClickOutside = (event: MouseEvent) => {
if (dropdownRef.current && !dropdownRef.current.contains(event.target as Node)) {
setIsOpen(false);
}
};
useEffect(() => {
const handleClickOutside = (event: MouseEvent) => {
if (dropdownRef.current && !dropdownRef.current.contains(event.target as Node)) {
setIsOpen(false);
}
};
document.addEventListener('mousedown', handleClickOutside);
return () => {
document.removeEventListener('mousedown', handleClickOutside);
};
}, []);
document.addEventListener('mousedown', handleClickOutside);
return () => {
document.removeEventListener('mousedown', handleClickOutside);
};
}, []);
return (
<div className="language-selector" ref={dropdownRef}>
<div
className="language-display"
onMouseEnter={() => setIsOpen(true)}
return (
<div className="language-selector" ref={dropdownRef}>
<div
className="language-display"
onMouseEnter={() => setIsOpen(true)}
>
A/
</div>
{isOpen && (
<div className="language-dropdown">
{supportedLanguages.map(item => (
<div
key={item.value}
className={`language-option ${currentLang === item.value ? 'active' : ''}`}
onClick={() => changeLanguage(item.value)}
>
A/
{item.label}
</div>
{isOpen && (
<div className="language-dropdown">
{supportedLanguages.map(item => (
<div
key={item.value}
className={`language-option ${currentLang === item.value ? 'active' : ''}`}
onClick={() => changeLanguage(item.value)}
>
{item.label}
</div>
))
}
</div>
)}
))
}
</div>
);
)}
</div>
);
};
export default ChangeLanguage;
export default ChangeLanguage;

View File

@@ -1,45 +1,45 @@
.language-selector {
position: relative;
display: inline-block;
font-size: 14px;
cursor: pointer;
position: relative;
display: inline-block;
font-size: 14px;
cursor: pointer;
}
.language-display {
padding: 8px 12px;
border-radius: 4px;
background-color: #f5f5f5;
display: flex;
align-items: center;
justify-content: center;
min-width: 60px;
text-align: center;
padding: 8px 12px;
border-radius: 4px;
background-color: #f5f5f5;
display: flex;
align-items: center;
justify-content: center;
min-width: 60px;
text-align: center;
}
.language-dropdown {
position: absolute;
top: 100%;
left: 0;
background-color: white;
border-radius: 4px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
width: 100%;
z-index: 1000;
margin-top: 4px;
overflow: hidden;
position: absolute;
top: 100%;
left: 0;
background-color: white;
border-radius: 4px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
width: 100%;
z-index: 1000;
margin-top: 4px;
overflow: hidden;
}
.language-option {
padding: 8px 12px;
transition: background-color 0.2s;
text-align: center;
padding: 8px 12px;
transition: background-color 0.2s;
text-align: center;
}
.language-option:hover {
background-color: #f0f0f0;
background-color: #f0f0f0;
}
.language-option.active {
background-color: #e6f7ff;
color: #1890ff;
}
background-color: #e6f7ff;
color: #1890ff;
}

View File

@@ -4,35 +4,34 @@ import Backend from 'i18next-http-backend';
import LanguageDetector from 'i18next-browser-languagedetector';
i18n
.use(Backend)
.use(LanguageDetector)
.use(initReactI18next)
.init({
fallbackLng: 'en-US',
debug: process.env.NODE_ENV === 'development',
interpolation: {
escapeValue: false,
},
ns: ['app'],
backend: {
loadPath: './i18n/{{ns}}/{{lng}}.json'
},
detection: {
// configure detection options
order: ['navigator', 'htmlTag', 'querystring', 'cookie', 'localStorage'],
caches: ['localStorage', 'cookie'],
}
});
.use(Backend)
.use(LanguageDetector)
.use(initReactI18next)
.init({
fallbackLng: 'en-US',
debug: process.env.NODE_ENV === 'development',
interpolation: {
escapeValue: false,
},
ns: ['app'],
backend: {
loadPath: './i18n/{{ns}}/{{lng}}.json'
},
detection: {
order: ['localStorage'],
caches: ['localStorage'],
}
});
export const supportedLanguages = [
{
label: 'English',
value: 'en-US',
},
{
label: '中文',
value: 'zh-CN',
},
{
label: 'English',
value: 'en-US',
},
{
label: '中文',
value: 'zh-CN',
},
];
export default i18n;
export default i18n;

View File

@@ -1,11 +1,58 @@
{
"nav": {
"newDiagram": "New Diagram",
"saveSessionOnly": "Save (Session Only)",
"loadSessionOnly": "Load (Session Only)",
"importFile": "Import File",
"exportFile": "Export File",
"quickSaveSession": "Quick Save(Session)",
"serverStorage": "Server Storage"
}
"nav": {
"newDiagram": "New Diagram",
"saveSessionOnly": "Save (Session Only)",
"loadSessionOnly": "Load (Session Only)",
"importFile": "Import File",
"exportFile": "Export File",
"quickSaveSession": "Quick Save (Session)",
"serverStorage": "Server Storage"
},
"status": {
"current": "Current",
"untitled": "Untitled Diagram",
"modified": "Modified",
"sessionStorageNote": "Session storage only - export to save permanently"
},
"dialog": {
"save": {
"title": "Save Diagram (Current Session Only)",
"warningTitle": "Important",
"warningMessage": "This save is temporary and will be lost when you close the browser.",
"warningExport": "Use <strong>Export File</strong> to permanently save your work.",
"placeholder": "Enter diagram name",
"btnSave": "Save",
"btnCancel": "Cancel"
},
"load": {
"title": "Load Diagram (Current Session Only)",
"noteTitle": "Note",
"noteMessage": "These saves are temporary. Export your diagrams to keep them permanently.",
"noSavedDiagrams": "No saved diagrams found in this session",
"updated": "Updated",
"btnLoad": "Load",
"btnDelete": "Delete",
"btnClose": "Close"
},
"export": {
"title": "Export Diagram",
"recommendedTitle": "Recommended",
"recommendedMessage": "This is the best way to save your work permanently.",
"noteMessage": "Exported JSON files can be imported later or shared with others.",
"btnDownload": "Download JSON",
"btnCancel": "Cancel"
}
},
"alert": {
"enterDiagramName": "Please enter a diagram name",
"diagramExists": "A diagram named \"{{name}}\" already exists in this session. This will overwrite it. Are you sure you want to continue?",
"unsavedChanges": "You have unsaved changes. Continue loading?",
"createNewDiagram": "Create a new diagram?",
"unsavedChangesExport": "You have unsaved changes. Export your diagram first to save it. Continue?",
"confirmDelete": "Are you sure you want to delete this diagram?",
"storageFull": "Storage full! Opening Storage Manager...",
"autoSaveFailed": "Storage full! Please use Storage Manager to free up space.",
"beforeUnload": "You have unsaved changes. Are you sure you want to leave?",
"quotaExceeded": "Storage quota exceeded. Please export important diagrams and clear some space."
}
}

View File

@@ -1,11 +1,58 @@
{
"nav": {
"newDiagram": "新建图表",
"saveSessionOnly": "保存(仅会话)",
"loadSessionOnly": "加载(仅会话)",
"importFile": "导入文件",
"exportFile": "导出文件",
"quickSaveSession": "快速保存(会话)",
"serverStorage": "服务端存储"
}
"nav": {
"newDiagram": "新建图表",
"saveSessionOnly": "保存(仅会话)",
"loadSessionOnly": "加载(仅会话)",
"importFile": "导入文件",
"exportFile": "导出文件",
"quickSaveSession": "快速保存(会话)",
"serverStorage": "服务端存储"
},
"status": {
"current": "当前",
"untitled": "未命名图表",
"modified": "已修改",
"sessionStorageNote": "仅会话存储 - 导出以永久保存"
},
"dialog": {
"save": {
"title": "保存图表(仅当前会话)",
"warningTitle": "重要提示",
"warningMessage": "此保存是临时的,关闭浏览器后将丢失。",
"warningExport": "使用<strong>导出文件</strong>功能永久保存您的工作。",
"placeholder": "输入图表名称",
"btnSave": "保存",
"btnCancel": "取消"
},
"load": {
"title": "加载图表(仅当前会话)",
"noteTitle": "提示",
"noteMessage": "这些保存是临时的。导出您的图表以永久保存。",
"noSavedDiagrams": "当前会话中未找到已保存的图表",
"updated": "更新时间",
"btnLoad": "加载",
"btnDelete": "删除",
"btnClose": "关闭"
},
"export": {
"title": "导出图表",
"recommendedTitle": "推荐",
"recommendedMessage": "这是永久保存工作的最佳方式。",
"noteMessage": "导出的 JSON 文件可以稍后导入或与他人共享。",
"btnDownload": "下载 JSON",
"btnCancel": "取消"
}
},
"alert": {
"enterDiagramName": "请输入图表名称",
"diagramExists": "名为\"{{name}}\"的图表已存在于此会话中。这将覆盖它。您确定要继续吗?",
"unsavedChanges": "您有未保存的更改。继续加载?",
"createNewDiagram": "创建新图表?",
"unsavedChangesExport": "您有未保存的更改。请先导出图表以保存。继续?",
"confirmDelete": "您确定要删除此图表吗?",
"storageFull": "存储空间已满!正在打开存储管理器...",
"autoSaveFailed": "存储空间已满!请使用存储管理器释放空间。",
"beforeUnload": "您有未保存的更改。您确定要离开吗?",
"quotaExceeded": "存储配额已超出。请导出重要图表并清理一些空间。"
}
}