feat: atalhos de navegação Q/E e correções no i18n
This commit is contained in:
118
add_shortcuts.cjs
Normal file
118
add_shortcuts.cjs
Normal file
@@ -0,0 +1,118 @@
|
|||||||
|
const fs = require('fs');
|
||||||
|
|
||||||
|
let content = fs.readFileSync('src/App.jsx', 'utf8');
|
||||||
|
|
||||||
|
// 1. State
|
||||||
|
content = content.replace(
|
||||||
|
/const \[isPrivate, setIsPrivate\] = useState\(false\);/,
|
||||||
|
`const [isPrivate, setIsPrivate] = useState(false);\n const [shortcutsEnabled, setShortcutsEnabled] = useState(false);`
|
||||||
|
);
|
||||||
|
|
||||||
|
// 2. Toggle and Nav handle
|
||||||
|
const toggleInsert = `
|
||||||
|
const handlePrivacyToggle = (newVal) => {
|
||||||
|
setIsPrivate(newVal);
|
||||||
|
saveUserSetting('isPrivate', newVal);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleShortcutsToggle = (newVal) => {
|
||||||
|
setShortcutsEnabled(newVal);
|
||||||
|
saveUserSetting('shortcutsEnabled', newVal);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleNavShortcut = (direction) => {
|
||||||
|
const navItems = ['dashboard', 'closet', 'wishlist', 'laundry', 'outfits', 'planner', 'community', 'settings'];
|
||||||
|
const currentIndex = navItems.indexOf(view);
|
||||||
|
if (currentIndex === -1) return;
|
||||||
|
|
||||||
|
let nextIndex;
|
||||||
|
if (direction === 'up') {
|
||||||
|
nextIndex = currentIndex === 0 ? navItems.length - 1 : currentIndex - 1;
|
||||||
|
} else {
|
||||||
|
nextIndex = currentIndex === navItems.length - 1 ? 0 : currentIndex + 1;
|
||||||
|
}
|
||||||
|
setView(navItems[nextIndex]);
|
||||||
|
if (window.innerWidth < 768) setSidebarOpen(false);
|
||||||
|
};
|
||||||
|
`;
|
||||||
|
|
||||||
|
content = content.replace(
|
||||||
|
/const handlePrivacyToggle = \(newVal\) => \{[\s\S]*?saveUserSetting\('isPrivate', newVal\);\n \};/,
|
||||||
|
toggleInsert.trim()
|
||||||
|
);
|
||||||
|
|
||||||
|
// 3. Load from Firebase
|
||||||
|
content = content.replace(
|
||||||
|
/if \(data\.settings\.isPrivate !== undefined\) setIsPrivate\(data\.settings\.isPrivate\);/,
|
||||||
|
`if (data.settings.isPrivate !== undefined) setIsPrivate(data.settings.isPrivate);\n if (data.settings.shortcutsEnabled !== undefined) setShortcutsEnabled(data.settings.shortcutsEnabled);`
|
||||||
|
);
|
||||||
|
|
||||||
|
// 4. UseEffect
|
||||||
|
const useEffectInsert = `
|
||||||
|
useEffect(() => {
|
||||||
|
if (!shortcutsEnabled) return;
|
||||||
|
|
||||||
|
const handleKeyDown = (e) => {
|
||||||
|
if (e.target.tagName === 'INPUT' || e.target.tagName === 'TEXTAREA') return;
|
||||||
|
|
||||||
|
if (e.key.toLowerCase() === 'q') {
|
||||||
|
handleNavShortcut('up');
|
||||||
|
} else if (e.key.toLowerCase() === 'e') {
|
||||||
|
handleNavShortcut('down');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
window.addEventListener('keydown', handleKeyDown);
|
||||||
|
return () => window.removeEventListener('keydown', handleKeyDown);
|
||||||
|
}, [shortcutsEnabled, view]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (editingItem && editingItem.color) {`;
|
||||||
|
|
||||||
|
content = content.replace(
|
||||||
|
/useEffect\(\(\) => \{\n if \(editingItem && editingItem\.color\) \{/,
|
||||||
|
useEffectInsert.trim()
|
||||||
|
);
|
||||||
|
|
||||||
|
// 5. Settings UI
|
||||||
|
const settingsUI = `
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<div>
|
||||||
|
<p className="font-bold text-inherit flex items-center gap-2">Atalhos de Teclado</p>
|
||||||
|
<p className="text-[10px] uppercase tracking-widest opacity-50 text-inherit">Ativar navegação com Q e E</p>
|
||||||
|
</div>
|
||||||
|
<button onClick={() => handleShortcutsToggle(!shortcutsEnabled)} className={\`w-14 h-8 rounded-full transition-colors relative \${shortcutsEnabled ? 'bg-primary-600' : 'bg-gray-200'}\`}>
|
||||||
|
<div className={\`w-6 h-6 rounded-full bg-white absolute top-1 transition-all \${shortcutsEnabled ? 'left-7' : 'left-1'}\`}></div>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
content = content.replace(
|
||||||
|
/(<div className="flex items-center justify-between">[\s\S]*?t\('lookReminders'\)}<\/p>[\s\S]*?<\/div>[\s\S]*?<\/button>[\s\S]*?<\/div>)/,
|
||||||
|
`$1\n${settingsUI}`
|
||||||
|
);
|
||||||
|
|
||||||
|
// 6. Sidebar UI Top
|
||||||
|
const sidebarTop = `
|
||||||
|
{shortcutsEnabled && (
|
||||||
|
<button onClick={() => handleNavShortcut('up')} className="w-full flex items-center justify-center gap-2 p-2 bg-gray-100 dark:bg-gray-800 rounded-xl mb-3 text-xs font-bold hover:bg-gray-200 dark:hover:bg-gray-700 transition-colors">
|
||||||
|
<span className="px-2 py-1 bg-white dark:bg-gray-900 rounded-lg shadow-sm border border-gray-200 dark:border-gray-700">Q</span> Cima
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
<nav className="flex-1 space-y-3">
|
||||||
|
`;
|
||||||
|
content = content.replace(/<nav className="flex-1 space-y-3">/, sidebarTop.trim());
|
||||||
|
|
||||||
|
// 7. Sidebar UI Bottom
|
||||||
|
const sidebarBottom = `
|
||||||
|
))}
|
||||||
|
</nav>
|
||||||
|
{shortcutsEnabled && (
|
||||||
|
<button onClick={() => handleNavShortcut('down')} className="w-full flex items-center justify-center gap-2 p-2 bg-gray-100 dark:bg-gray-800 rounded-xl mt-3 text-xs font-bold hover:bg-gray-200 dark:hover:bg-gray-700 transition-colors">
|
||||||
|
<span className="px-2 py-1 bg-white dark:bg-gray-900 rounded-lg shadow-sm border border-gray-200 dark:border-gray-700">E</span> Baixo
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
`;
|
||||||
|
content = content.replace(/ \}\)\}\n <\/nav>/, sidebarBottom.trim());
|
||||||
|
|
||||||
|
fs.writeFileSync('src/App.jsx', content);
|
||||||
|
console.log('Shortcuts feature added successfully.');
|
||||||
1
dist/assets/index-DFJLltIj.css
vendored
Normal file
1
dist/assets/index-DFJLltIj.css
vendored
Normal file
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
1
dist/assets/index-olys378F.css
vendored
1
dist/assets/index-olys378F.css
vendored
File diff suppressed because one or more lines are too long
4
dist/index.html
vendored
4
dist/index.html
vendored
@@ -5,8 +5,8 @@
|
|||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<title>MyCloset</title>
|
<title>MyCloset</title>
|
||||||
<link rel="manifest" href="/manifest.json" />
|
<link rel="manifest" href="/manifest.json" />
|
||||||
<script type="module" crossorigin src="/assets/index-xEo-fC2P.js"></script>
|
<script type="module" crossorigin src="/assets/index-SfxBfoF_.js"></script>
|
||||||
<link rel="stylesheet" crossorigin href="/assets/index-olys378F.css">
|
<link rel="stylesheet" crossorigin href="/assets/index-DFJLltIj.css">
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div id="root"></div>
|
<div id="root"></div>
|
||||||
|
|||||||
41
fix_i18n.cjs
Normal file
41
fix_i18n.cjs
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
const fs = require('fs');
|
||||||
|
let content = fs.readFileSync('src/lib/i18n.js', 'utf8');
|
||||||
|
|
||||||
|
// The broken string is like:
|
||||||
|
// weatherCurrentAvg: "{current,
|
||||||
|
// registeredPieces: "Peças Registadas",
|
||||||
|
// ...
|
||||||
|
// }°C Atual • Média {avg}°C",
|
||||||
|
|
||||||
|
// PT
|
||||||
|
content = content.replace(
|
||||||
|
/weatherCurrentAvg: "\{current,([\s\S]*?)down: "Baixo",\n\}\°C Atual • Média \{avg\}\°C",/g,
|
||||||
|
`weatherCurrentAvg: "{current}°C Atual • Média {avg}°C",\n$1down: "Baixo",`
|
||||||
|
);
|
||||||
|
|
||||||
|
// EN
|
||||||
|
content = content.replace(
|
||||||
|
/weatherCurrentAvg: "\{current,([\s\S]*?)down: "Down",\n\}\°C Current • Average \{avg\}\°C",/g,
|
||||||
|
`weatherCurrentAvg: "{current}°C Current • Average {avg}°C",\n$1down: "Down",`
|
||||||
|
);
|
||||||
|
|
||||||
|
// ES
|
||||||
|
content = content.replace(
|
||||||
|
/weatherCurrentAvg: "\{current,([\s\S]*?)down: "Abajo",\n\}\°C Actual • Media \{avg\}\°C",/g,
|
||||||
|
`weatherCurrentAvg: "{current}°C Actual • Media {avg}°C",\n$1down: "Abajo",`
|
||||||
|
);
|
||||||
|
|
||||||
|
// FR
|
||||||
|
content = content.replace(
|
||||||
|
/weatherCurrentAvg: "\{current,([\s\S]*?)down: "Bas",\n\}\°C Actuel • Moyenne \{avg\}\°C",/g,
|
||||||
|
`weatherCurrentAvg: "{current}°C Actuel • Moyenne {avg}°C",\n$1down: "Bas",`
|
||||||
|
);
|
||||||
|
|
||||||
|
// DE
|
||||||
|
content = content.replace(
|
||||||
|
/weatherCurrentAvg: "\{current,([\s\S]*?)down: "Runter",\n\}\°C Aktuell • Durchschnitt \{avg\}\°C",/g,
|
||||||
|
`weatherCurrentAvg: "{current}°C Aktuell • Durchschnitt {avg}°C",\n$1down: "Runter",`
|
||||||
|
);
|
||||||
|
|
||||||
|
fs.writeFileSync('src/lib/i18n.js', content);
|
||||||
|
console.log('Fixed i18n.js syntax errors.');
|
||||||
10
fix_i18n_all.cjs
Normal file
10
fix_i18n_all.cjs
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
const fs = require('fs');
|
||||||
|
let content = fs.readFileSync('src/lib/i18n.js', 'utf8');
|
||||||
|
|
||||||
|
content = content.replace(
|
||||||
|
/weatherCurrentAvg: "\{current,([\s\S]*?)down: "([^"]+)",\n\}\°C ([^•]+) • ([^\{]+) \{avg\}\°C",/g,
|
||||||
|
'weatherCurrentAvg: "{current}°C $3 • $4 {avg}°C",\n$1down: "$2",'
|
||||||
|
);
|
||||||
|
|
||||||
|
fs.writeFileSync('src/lib/i18n.js', content);
|
||||||
|
console.log('Fixed i18n.js syntax errors dynamically.');
|
||||||
76
src/App.jsx
76
src/App.jsx
@@ -69,6 +69,7 @@ export default function App() {
|
|||||||
const [defaultPage, setDefaultPage] = useState('dashboard');
|
const [defaultPage, setDefaultPage] = useState('dashboard');
|
||||||
const [weatherData, setWeatherData] = useState(null);
|
const [weatherData, setWeatherData] = useState(null);
|
||||||
const [isPrivate, setIsPrivate] = useState(false);
|
const [isPrivate, setIsPrivate] = useState(false);
|
||||||
|
const [shortcutsEnabled, setShortcutsEnabled] = useState(false);
|
||||||
const [userStatus, setUserStatus] = useState('online');
|
const [userStatus, setUserStatus] = useState('online');
|
||||||
|
|
||||||
// Estado da Comunidade
|
// Estado da Comunidade
|
||||||
@@ -228,6 +229,26 @@ export default function App() {
|
|||||||
saveUserSetting('isPrivate', newVal);
|
saveUserSetting('isPrivate', newVal);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleShortcutsToggle = (newVal) => {
|
||||||
|
setShortcutsEnabled(newVal);
|
||||||
|
saveUserSetting('shortcutsEnabled', newVal);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleNavShortcut = (direction) => {
|
||||||
|
const navItems = ['dashboard', 'closet', 'wishlist', 'laundry', 'outfits', 'planner', 'community', 'settings'];
|
||||||
|
const currentIndex = navItems.indexOf(view);
|
||||||
|
if (currentIndex === -1) return;
|
||||||
|
|
||||||
|
let nextIndex;
|
||||||
|
if (direction === 'up') {
|
||||||
|
nextIndex = currentIndex === 0 ? navItems.length - 1 : currentIndex - 1;
|
||||||
|
} else {
|
||||||
|
nextIndex = currentIndex === navItems.length - 1 ? 0 : currentIndex + 1;
|
||||||
|
}
|
||||||
|
setView(navItems[nextIndex]);
|
||||||
|
if (window.innerWidth < 768) setSidebarOpen(false);
|
||||||
|
};
|
||||||
|
|
||||||
const toggleStatus = (e) => {
|
const toggleStatus = (e) => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
@@ -272,6 +293,23 @@ export default function App() {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!shortcutsEnabled) return;
|
||||||
|
|
||||||
|
const handleKeyDown = (e) => {
|
||||||
|
if (e.target.tagName === 'INPUT' || e.target.tagName === 'TEXTAREA') return;
|
||||||
|
|
||||||
|
if (e.key.toLowerCase() === 'q') {
|
||||||
|
handleNavShortcut('up');
|
||||||
|
} else if (e.key.toLowerCase() === 'e') {
|
||||||
|
handleNavShortcut('down');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
window.addEventListener('keydown', handleKeyDown);
|
||||||
|
return () => window.removeEventListener('keydown', handleKeyDown);
|
||||||
|
}, [shortcutsEnabled, view]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (editingItem && editingItem.color) {
|
if (editingItem && editingItem.color) {
|
||||||
setItemColors(editingItem.color.split(',').map(c => c.trim()).filter(Boolean));
|
setItemColors(editingItem.color.split(',').map(c => c.trim()).filter(Boolean));
|
||||||
@@ -415,6 +453,7 @@ export default function App() {
|
|||||||
setDefaultPage(data.settings.defaultPage === 'planning' ? 'planner' : data.settings.defaultPage);
|
setDefaultPage(data.settings.defaultPage === 'planning' ? 'planner' : data.settings.defaultPage);
|
||||||
}
|
}
|
||||||
if (data.settings.isPrivate !== undefined) setIsPrivate(data.settings.isPrivate);
|
if (data.settings.isPrivate !== undefined) setIsPrivate(data.settings.isPrivate);
|
||||||
|
if (data.settings.shortcutsEnabled !== undefined) setShortcutsEnabled(data.settings.shortcutsEnabled);
|
||||||
if (data.settings.status !== undefined) setUserStatus(data.settings.status);
|
if (data.settings.status !== undefined) setUserStatus(data.settings.status);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1435,6 +1474,11 @@ export default function App() {
|
|||||||
<span className="text-3xl font-black tracking-tighter italic">MyCloset</span>
|
<span className="text-3xl font-black tracking-tighter italic">MyCloset</span>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
|
{shortcutsEnabled && (
|
||||||
|
<button onClick={() => handleNavShortcut('up')} className="w-full flex items-center justify-center gap-2 p-2 bg-gray-100 dark:bg-gray-800 rounded-xl mb-3 text-xs font-bold hover:bg-gray-200 dark:hover:bg-gray-700 transition-colors">
|
||||||
|
<span className="px-2 py-1 bg-white dark:bg-gray-900 rounded-lg shadow-sm border border-gray-200 dark:border-gray-700">Q</span> {t('up')}
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
<nav className="flex-1 space-y-3">
|
<nav className="flex-1 space-y-3">
|
||||||
{[
|
{[
|
||||||
{ id: 'dashboard', label: t('dashboard'), icon: LayoutDashboard },
|
{ id: 'dashboard', label: t('dashboard'), icon: LayoutDashboard },
|
||||||
@@ -1459,6 +1503,11 @@ export default function App() {
|
|||||||
</button>
|
</button>
|
||||||
))}
|
))}
|
||||||
</nav>
|
</nav>
|
||||||
|
{shortcutsEnabled && (
|
||||||
|
<button onClick={() => handleNavShortcut('down')} className="w-full flex items-center justify-center gap-2 p-2 bg-gray-100 dark:bg-gray-800 rounded-xl mt-3 text-xs font-bold hover:bg-gray-200 dark:hover:bg-gray-700 transition-colors">
|
||||||
|
<span className="px-2 py-1 bg-white dark:bg-gray-900 rounded-lg shadow-sm border border-gray-200 dark:border-gray-700">E</span> {t('down')}
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
|
||||||
<div className="mt-auto pt-10 border-t border-inherit">
|
<div className="mt-auto pt-10 border-t border-inherit">
|
||||||
<div onClick={() => setView('profile')} className="w-full flex items-center gap-4 mb-5 md:mb-8 px-2 text-left hover:bg-gray-100 dark:hover:bg-gray-800 py-3 rounded-2xl transition-all cursor-pointer">
|
<div onClick={() => setView('profile')} className="w-full flex items-center gap-4 mb-5 md:mb-8 px-2 text-left hover:bg-gray-100 dark:hover:bg-gray-800 py-3 rounded-2xl transition-all cursor-pointer">
|
||||||
@@ -2539,29 +2588,29 @@ export default function App() {
|
|||||||
|
|
||||||
<div className="space-y-6 text-inherit bg-gray-50 dark:bg-gray-800/50 p-6 rounded-3xl">
|
<div className="space-y-6 text-inherit bg-gray-50 dark:bg-gray-800/50 p-6 rounded-3xl">
|
||||||
<div>
|
<div>
|
||||||
<p className="text-xs font-black uppercase tracking-widest opacity-50 mb-1">Data de Nascimento</p>
|
<p className="text-xs font-black uppercase tracking-widest opacity-50 mb-1">{t('dob')}</p>
|
||||||
<p className="font-bold">{selectedUserProfile?.dob ? new Date(selectedUserProfile.dob).toLocaleDateString() : 'Não especificada'}</p>
|
<p className="font-bold">{selectedUserProfile?.dob ? new Date(selectedUserProfile.dob).toLocaleDateString() : 'Não especificada'}</p>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<p className="text-xs font-black uppercase tracking-widest opacity-50 mb-1">Localidade</p>
|
<p className="text-xs font-black uppercase tracking-widest opacity-50 mb-1">{t('location')}</p>
|
||||||
<p className="font-bold">{selectedUserProfile?.location || selectedCommunityUser.location || 'Não especificada'}</p>
|
<p className="font-bold">{selectedUserProfile?.location || selectedCommunityUser.location || 'Não especificada'}</p>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<p className="text-xs font-black uppercase tracking-widest opacity-50 mb-1">Bio</p>
|
<p className="text-xs font-black uppercase tracking-widest opacity-50 mb-1">{t('bio')}</p>
|
||||||
<p className="font-bold opacity-80">{selectedUserProfile?.bio || 'Sem biografia'}</p>
|
<p className="font-bold opacity-80">{selectedUserProfile?.bio || 'Sem biografia'}</p>
|
||||||
</div>
|
</div>
|
||||||
<div className="grid grid-cols-2 gap-4 pt-4 border-t border-gray-200 dark:border-gray-700">
|
<div className="grid grid-cols-2 gap-4 pt-4 border-t border-gray-200 dark:border-gray-700">
|
||||||
<div>
|
<div>
|
||||||
<p className="text-xs font-black uppercase tracking-widest opacity-50 mb-1">Peças Registadas</p>
|
<p className="text-xs font-black uppercase tracking-widest opacity-50 mb-1">{t('registeredPieces')}</p>
|
||||||
<p className="text-2xl font-black">{selectedUserClothes.length}</p>
|
<p className="text-2xl font-black">{selectedUserClothes.length}</p>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<p className="text-xs font-black uppercase tracking-widest opacity-50 mb-1">Outfits Criados</p>
|
<p className="text-xs font-black uppercase tracking-widest opacity-50 mb-1">{t('createdOutfits')}</p>
|
||||||
<p className="text-2xl font-black">{selectedUserLooks.length}</p>
|
<p className="text-2xl font-black">{selectedUserLooks.length}</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="pt-4 border-t border-gray-200 dark:border-gray-700">
|
<div className="pt-4 border-t border-gray-200 dark:border-gray-700">
|
||||||
<p className="text-xs font-black uppercase tracking-widest opacity-50 mb-1">Data de Registo da Conta</p>
|
<p className="text-xs font-black uppercase tracking-widest opacity-50 mb-1">{t('accountRegistrationDate')}</p>
|
||||||
<p className="font-bold">{selectedUserProfile?.createdAt ? new Date(selectedUserProfile.createdAt).toLocaleDateString() : 'Desconhecida'}</p>
|
<p className="font-bold">{selectedUserProfile?.createdAt ? new Date(selectedUserProfile.createdAt).toLocaleDateString() : 'Desconhecida'}</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -2676,10 +2725,21 @@ export default function App() {
|
|||||||
<div className={`w-6 h-6 rounded-full bg-white absolute top-1 transition-all ${notificationsEnabled ? 'left-7' : 'left-1'}`}></div>
|
<div className={`w-6 h-6 rounded-full bg-white absolute top-1 transition-all ${notificationsEnabled ? 'left-7' : 'left-1'}`}></div>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<div>
|
||||||
|
<p className="font-bold text-inherit flex items-center gap-2">{t('keyboardShortcuts')}</p>
|
||||||
|
<p className="text-[10px] uppercase tracking-widest opacity-50 text-inherit">{t('enableNavigationQE')}</p>
|
||||||
|
</div>
|
||||||
|
<button onClick={() => handleShortcutsToggle(!shortcutsEnabled)} className={`w-14 h-8 rounded-full transition-colors relative ${shortcutsEnabled ? 'bg-primary-600' : 'bg-gray-200'}`}>
|
||||||
|
<div className={`w-6 h-6 rounded-full bg-white absolute top-1 transition-all ${shortcutsEnabled ? 'left-7' : 'left-1'}`}></div>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div className="flex flex-col sm:flex-row items-start sm:items-center justify-between gap-4">
|
<div className="flex flex-col sm:flex-row items-start sm:items-center justify-between gap-4">
|
||||||
<div>
|
<div>
|
||||||
<p className="font-bold text-inherit flex items-center gap-2">Notificação do Outfit Diário</p>
|
<p className="font-bold text-inherit flex items-center gap-2">{t('dailyOutfitNotification')}</p>
|
||||||
<p className="text-[10px] uppercase tracking-widest opacity-50 text-inherit">Receber notificação com o outfit planeado à hora marcada</p>
|
<p className="text-[10px] uppercase tracking-widest opacity-50 text-inherit">{t('receiveNotificationAtScheduledTime')}</p>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-4 w-full sm:w-auto justify-between sm:justify-end">
|
<div className="flex items-center gap-4 w-full sm:w-auto justify-between sm:justify-end">
|
||||||
{dailyOutfitNotifEnabled && (
|
{dailyOutfitNotifEnabled && (
|
||||||
|
|||||||
@@ -35,6 +35,16 @@ export const translations = {
|
|||||||
todayIn: "Hoje em Portugal",
|
todayIn: "Hoje em Portugal",
|
||||||
weatherUpdate: "22°C - Ensolarado",
|
weatherUpdate: "22°C - Ensolarado",
|
||||||
weatherCurrentAvg: "{current}°C Atual • Média {avg}°C",
|
weatherCurrentAvg: "{current}°C Atual • Média {avg}°C",
|
||||||
|
|
||||||
|
registeredPieces: "Peças Registadas",
|
||||||
|
createdOutfits: "Outfits Criados",
|
||||||
|
accountRegistrationDate: "Data de Registo da Conta",
|
||||||
|
keyboardShortcuts: "Atalhos de Teclado",
|
||||||
|
enableNavigationQE: "Ativar navegação com Q e E",
|
||||||
|
dailyOutfitNotification: "Notificação do Outfit Diário",
|
||||||
|
receiveNotificationAtScheduledTime: "Receber notificação com o outfit planeado à hora marcada",
|
||||||
|
up: "Cima",
|
||||||
|
down: "Baixo",
|
||||||
weatherForecastDesc: "O dia de hoje tem máximas de {max}°C e mínimas de {min}°C.",
|
weatherForecastDesc: "O dia de hoje tem máximas de {max}°C e mínimas de {min}°C.",
|
||||||
weatherMsg: "Está um dia fantástico! Recomendamos as tuas peças leves. Que tal um visual casual com as tuas sapatilhas favoritas?",
|
weatherMsg: "Está um dia fantástico! Recomendamos as tuas peças leves. Que tal um visual casual com as tuas sapatilhas favoritas?",
|
||||||
exploreSuggestions: "Explorar Sugestões",
|
exploreSuggestions: "Explorar Sugestões",
|
||||||
@@ -252,6 +262,16 @@ export const translations = {
|
|||||||
todayIn: "Today in Portugal",
|
todayIn: "Today in Portugal",
|
||||||
weatherUpdate: "22°C - Sunny",
|
weatherUpdate: "22°C - Sunny",
|
||||||
weatherCurrentAvg: "{current}°C Current • Average {avg}°C",
|
weatherCurrentAvg: "{current}°C Current • Average {avg}°C",
|
||||||
|
|
||||||
|
registeredPieces: "Registered Pieces",
|
||||||
|
createdOutfits: "Created Outfits",
|
||||||
|
accountRegistrationDate: "Account Registration Date",
|
||||||
|
keyboardShortcuts: "Keyboard Shortcuts",
|
||||||
|
enableNavigationQE: "Enable navigation with Q and E",
|
||||||
|
dailyOutfitNotification: "Daily Outfit Notification",
|
||||||
|
receiveNotificationAtScheduledTime: "Receive notification with the planned outfit at the scheduled time",
|
||||||
|
up: "Up",
|
||||||
|
down: "Down",
|
||||||
weatherForecastDesc: "Today has highs of {max}°C and lows of {min}°C.",
|
weatherForecastDesc: "Today has highs of {max}°C and lows of {min}°C.",
|
||||||
weatherMsg: "It's a fantastic day! We recommend your light pieces. How about a casual look with your favorite sneakers?",
|
weatherMsg: "It's a fantastic day! We recommend your light pieces. How about a casual look with your favorite sneakers?",
|
||||||
exploreSuggestions: "Explore Suggestions",
|
exploreSuggestions: "Explore Suggestions",
|
||||||
@@ -469,6 +489,16 @@ export const translations = {
|
|||||||
todayIn: "Hoy en Portugal",
|
todayIn: "Hoy en Portugal",
|
||||||
weatherUpdate: "22°C - Soleado",
|
weatherUpdate: "22°C - Soleado",
|
||||||
weatherCurrentAvg: "{current}°C Actual • Media {avg}°C",
|
weatherCurrentAvg: "{current}°C Actual • Media {avg}°C",
|
||||||
|
|
||||||
|
registeredPieces: "Piezas Registradas",
|
||||||
|
createdOutfits: "Outfits Creados",
|
||||||
|
accountRegistrationDate: "Fecha de Registro de la Cuenta",
|
||||||
|
keyboardShortcuts: "Atajos de Teclado",
|
||||||
|
enableNavigationQE: "Habilitar navegación con Q y E",
|
||||||
|
dailyOutfitNotification: "Notificación del Outfit Diario",
|
||||||
|
receiveNotificationAtScheduledTime: "Recibir notificación con el outfit planeado a la hora programada",
|
||||||
|
up: "Arriba",
|
||||||
|
down: "Abajo",
|
||||||
weatherForecastDesc: "El día de hoy tiene máximas de {max}°C y mínimas de {min}°C.",
|
weatherForecastDesc: "El día de hoy tiene máximas de {max}°C y mínimas de {min}°C.",
|
||||||
weatherMsg: "¡Es un día fantástico! Recomendamos tus piezas ligeras. ¿Qué tal un look casual con tus zapatillas favoritas?",
|
weatherMsg: "¡Es un día fantástico! Recomendamos tus piezas ligeras. ¿Qué tal un look casual con tus zapatillas favoritas?",
|
||||||
exploreSuggestions: "Explorar Sugerencias",
|
exploreSuggestions: "Explorar Sugerencias",
|
||||||
@@ -686,6 +716,16 @@ export const translations = {
|
|||||||
todayIn: "Aujourd'hui au Portugal",
|
todayIn: "Aujourd'hui au Portugal",
|
||||||
weatherUpdate: "22°C - Ensoleillé",
|
weatherUpdate: "22°C - Ensoleillé",
|
||||||
weatherCurrentAvg: "{current}°C Actuel • Moyenne {avg}°C",
|
weatherCurrentAvg: "{current}°C Actuel • Moyenne {avg}°C",
|
||||||
|
|
||||||
|
registeredPieces: "Pièces Enregistrées",
|
||||||
|
createdOutfits: "Outfits Créés",
|
||||||
|
accountRegistrationDate: "Date d'Inscription du Compte",
|
||||||
|
keyboardShortcuts: "Raccourcis Clavier",
|
||||||
|
enableNavigationQE: "Activer la navigation avec Q et E",
|
||||||
|
dailyOutfitNotification: "Notification de Tenue Quotidienne",
|
||||||
|
receiveNotificationAtScheduledTime: "Recevoir une notification avec la tenue prévue à l'heure programmée",
|
||||||
|
up: "Haut",
|
||||||
|
down: "Bas",
|
||||||
weatherForecastDesc: "Aujourd'hui a des maximales de {max}°C et des minimales de {min}°C.",
|
weatherForecastDesc: "Aujourd'hui a des maximales de {max}°C et des minimales de {min}°C.",
|
||||||
weatherMsg: "C'est une journée fantastique ! Nous recommandons vos pièces légères. Que diriez-vous d'un look décontracté avec vos baskets préférées ?",
|
weatherMsg: "C'est une journée fantastique ! Nous recommandons vos pièces légères. Que diriez-vous d'un look décontracté avec vos baskets préférées ?",
|
||||||
exploreSuggestions: "Explorer les Suggestions",
|
exploreSuggestions: "Explorer les Suggestions",
|
||||||
@@ -903,6 +943,16 @@ export const translations = {
|
|||||||
todayIn: "Heute in Portugal",
|
todayIn: "Heute in Portugal",
|
||||||
weatherUpdate: "22°C - Sonnig",
|
weatherUpdate: "22°C - Sonnig",
|
||||||
weatherCurrentAvg: "{current}°C Aktuell • Durchschnitt {avg}°C",
|
weatherCurrentAvg: "{current}°C Aktuell • Durchschnitt {avg}°C",
|
||||||
|
|
||||||
|
registeredPieces: "Registrierte Stücke",
|
||||||
|
createdOutfits: "Erstellte Outfits",
|
||||||
|
accountRegistrationDate: "Konto-Registrierungsdatum",
|
||||||
|
keyboardShortcuts: "Tastenkombinationen",
|
||||||
|
enableNavigationQE: "Navigation mit Q und E aktivieren",
|
||||||
|
dailyOutfitNotification: "Tägliche Outfit-Benachrichtigung",
|
||||||
|
receiveNotificationAtScheduledTime: "Benachrichtigung mit dem geplanten Outfit zur geplanten Zeit erhalten",
|
||||||
|
up: "Oben",
|
||||||
|
down: "Unten",
|
||||||
weatherForecastDesc: "Der heutige Tag hat Höchstwerte von {max}°C und Tiefstwerte von {min}°C.",
|
weatherForecastDesc: "Der heutige Tag hat Höchstwerte von {max}°C und Tiefstwerte von {min}°C.",
|
||||||
weatherMsg: "Es ist ein fantastischer Tag! Wir empfehlen leichte Stücke. Wie wäre es mit einem lässigen Look mit deinen Lieblings-Sneakern?",
|
weatherMsg: "Es ist ein fantastischer Tag! Wir empfehlen leichte Stücke. Wie wäre es mit einem lässigen Look mit deinen Lieblings-Sneakern?",
|
||||||
exploreSuggestions: "Vorschläge entdecken",
|
exploreSuggestions: "Vorschläge entdecken",
|
||||||
|
|||||||
107
translate_app.cjs
Normal file
107
translate_app.cjs
Normal file
@@ -0,0 +1,107 @@
|
|||||||
|
const fs = require('fs');
|
||||||
|
|
||||||
|
// 1. Update i18n.js
|
||||||
|
let i18nCode = fs.readFileSync('src/lib/i18n.js', 'utf8');
|
||||||
|
|
||||||
|
const newTranslations = {
|
||||||
|
PT: {
|
||||||
|
registeredPieces: "Peças Registadas",
|
||||||
|
createdOutfits: "Outfits Criados",
|
||||||
|
accountRegistrationDate: "Data de Registo da Conta",
|
||||||
|
keyboardShortcuts: "Atalhos de Teclado",
|
||||||
|
enableNavigationQE: "Ativar navegação com Q e E",
|
||||||
|
dailyOutfitNotification: "Notificação do Outfit Diário",
|
||||||
|
receiveNotificationAtScheduledTime: "Receber notificação com o outfit planeado à hora marcada",
|
||||||
|
up: "Cima",
|
||||||
|
down: "Baixo"
|
||||||
|
},
|
||||||
|
EN: {
|
||||||
|
registeredPieces: "Registered Pieces",
|
||||||
|
createdOutfits: "Created Outfits",
|
||||||
|
accountRegistrationDate: "Account Registration Date",
|
||||||
|
keyboardShortcuts: "Keyboard Shortcuts",
|
||||||
|
enableNavigationQE: "Enable navigation with Q and E",
|
||||||
|
dailyOutfitNotification: "Daily Outfit Notification",
|
||||||
|
receiveNotificationAtScheduledTime: "Receive notification with the planned outfit at the scheduled time",
|
||||||
|
up: "Up",
|
||||||
|
down: "Down"
|
||||||
|
},
|
||||||
|
ES: {
|
||||||
|
registeredPieces: "Piezas Registradas",
|
||||||
|
createdOutfits: "Outfits Creados",
|
||||||
|
accountRegistrationDate: "Fecha de Registro de la Cuenta",
|
||||||
|
keyboardShortcuts: "Atajos de Teclado",
|
||||||
|
enableNavigationQE: "Habilitar navegación con Q y E",
|
||||||
|
dailyOutfitNotification: "Notificación del Outfit Diario",
|
||||||
|
receiveNotificationAtScheduledTime: "Recibir notificación con el outfit planeado a la hora programada",
|
||||||
|
up: "Arriba",
|
||||||
|
down: "Abajo"
|
||||||
|
},
|
||||||
|
FR: {
|
||||||
|
registeredPieces: "Pièces Enregistrées",
|
||||||
|
createdOutfits: "Outfits Créés",
|
||||||
|
accountRegistrationDate: "Date d'Inscription du Compte",
|
||||||
|
keyboardShortcuts: "Raccourcis Clavier",
|
||||||
|
enableNavigationQE: "Activer la navigation avec Q et E",
|
||||||
|
dailyOutfitNotification: "Notification de Tenue Quotidienne",
|
||||||
|
receiveNotificationAtScheduledTime: "Recevoir une notification avec la tenue prévue à l'heure programmée",
|
||||||
|
up: "Haut",
|
||||||
|
down: "Bas"
|
||||||
|
},
|
||||||
|
DE: {
|
||||||
|
registeredPieces: "Registrierte Stücke",
|
||||||
|
createdOutfits: "Erstellte Outfits",
|
||||||
|
accountRegistrationDate: "Konto-Registrierungsdatum",
|
||||||
|
keyboardShortcuts: "Tastenkombinationen",
|
||||||
|
enableNavigationQE: "Navigation mit Q und E aktivieren",
|
||||||
|
dailyOutfitNotification: "Tägliche Outfit-Benachrichtigung",
|
||||||
|
receiveNotificationAtScheduledTime: "Benachrichtigung mit dem geplanten Outfit zur geplanten Zeit erhalten",
|
||||||
|
up: "Oben",
|
||||||
|
down: "Unten"
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
for (const lang in newTranslations) {
|
||||||
|
const keys = newTranslations[lang];
|
||||||
|
const langRegex = new RegExp(`${lang}: \\{([\\s\\S]*?)\\}`, 'g');
|
||||||
|
let match = langRegex.exec(i18nCode);
|
||||||
|
if (match) {
|
||||||
|
let currentKeys = match[1];
|
||||||
|
let newKeysString = "";
|
||||||
|
for (const [k, v] of Object.entries(keys)) {
|
||||||
|
if (!currentKeys.includes(`${k}:`)) {
|
||||||
|
newKeysString += ` ${k}: "${v}",\n`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (newKeysString.length > 0) {
|
||||||
|
i18nCode = i18nCode.replace(currentKeys, currentKeys.trimEnd() + ",\n" + newKeysString);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fs.writeFileSync('src/lib/i18n.js', i18nCode);
|
||||||
|
|
||||||
|
// 2. Update App.jsx
|
||||||
|
let appCode = fs.readFileSync('src/App.jsx', 'utf8');
|
||||||
|
|
||||||
|
const replacements = [
|
||||||
|
{ from: />Data de Nascimento</g, to: '>{t(\'dob\')}<' },
|
||||||
|
{ from: />Localidade</g, to: '>{t(\'location\')}<' },
|
||||||
|
{ from: />Bio</g, to: '>{t(\'bio\')}<' },
|
||||||
|
{ from: />Peças Registadas</g, to: '>{t(\'registeredPieces\')}<' },
|
||||||
|
{ from: />Outfits Criados</g, to: '>{t(\'createdOutfits\')}<' },
|
||||||
|
{ from: />Data de Registo da Conta</g, to: '>{t(\'accountRegistrationDate\')}<' },
|
||||||
|
{ from: />Atalhos de Teclado</g, to: '>{t(\'keyboardShortcuts\')}<' },
|
||||||
|
{ from: />Ativar navegação com Q e E</g, to: '>{t(\'enableNavigationQE\')}<' },
|
||||||
|
{ from: />Notificação do Outfit Diário</g, to: '>{t(\'dailyOutfitNotification\')}<' },
|
||||||
|
{ from: />Receber notificação com o outfit planeado à hora marcada</g, to: '>{t(\'receiveNotificationAtScheduledTime\')}<' },
|
||||||
|
{ from: />Cima</g, to: '>{t(\'up\')}<' },
|
||||||
|
{ from: />Baixo</g, to: '>{t(\'down\')}<' }
|
||||||
|
];
|
||||||
|
|
||||||
|
replacements.forEach(r => {
|
||||||
|
appCode = appCode.replace(r.from, r.to);
|
||||||
|
});
|
||||||
|
|
||||||
|
fs.writeFileSync('src/App.jsx', appCode);
|
||||||
|
|
||||||
|
console.log('App successfully translated and i18n updated.');
|
||||||
Reference in New Issue
Block a user