3
This commit is contained in:
parent
e1f7182cfc
commit
0adfe0fe78
@ -1,4 +1,3 @@
|
||||
|
||||
const express = require('express');
|
||||
const cors = require('cors');
|
||||
const app = express();
|
||||
@ -16,52 +15,30 @@ const fileRoutes = require('./routes/file');
|
||||
const searchRoutes = require('./routes/search');
|
||||
const sqlRoutes = require('./routes/sql');
|
||||
const pexelsRoutes = require('./routes/pexels');
|
||||
|
||||
const openaiRoutes = require('./routes/openai');
|
||||
|
||||
|
||||
const localesRoutes = require('./routes/locales');
|
||||
|
||||
const usersRoutes = require('./routes/users');
|
||||
|
||||
const rolesRoutes = require('./routes/roles');
|
||||
|
||||
const permissionsRoutes = require('./routes/permissions');
|
||||
|
||||
const access_codesRoutes = require('./routes/access_codes');
|
||||
|
||||
const plansRoutes = require('./routes/plans');
|
||||
|
||||
const genresRoutes = require('./routes/genres');
|
||||
|
||||
const instrumentsRoutes = require('./routes/instruments');
|
||||
|
||||
const instrument_presetsRoutes = require('./routes/instrument_presets');
|
||||
|
||||
const rhythm_patternsRoutes = require('./routes/rhythm_patterns');
|
||||
|
||||
const projectsRoutes = require('./routes/projects');
|
||||
|
||||
const project_versionsRoutes = require('./routes/project_versions');
|
||||
|
||||
const song_sectionsRoutes = require('./routes/song_sections');
|
||||
|
||||
const tracksRoutes = require('./routes/tracks');
|
||||
|
||||
const midi_clipsRoutes = require('./routes/midi_clips');
|
||||
|
||||
const audio_clipsRoutes = require('./routes/audio_clips');
|
||||
|
||||
const lyricsRoutes = require('./routes/lyrics');
|
||||
|
||||
const ai_song_requestsRoutes = require('./routes/ai_song_requests');
|
||||
|
||||
const exportsRoutes = require('./routes/exports');
|
||||
|
||||
const collaborationsRoutes = require('./routes/collaborations');
|
||||
|
||||
const project_assetsRoutes = require('./routes/project_assets');
|
||||
|
||||
|
||||
const getBaseUrl = (url) => {
|
||||
if (!url) return '';
|
||||
return url.endsWith('/api') ? url.slice(0, -4) : url;
|
||||
@ -116,47 +93,29 @@ app.use(bodyParser.json());
|
||||
app.use('/api/auth', authRoutes);
|
||||
app.use('/api/file', fileRoutes);
|
||||
app.use('/api/pexels', pexelsRoutes);
|
||||
app.use('/api/locales', localesRoutes); // Public translation route
|
||||
app.enable('trust proxy');
|
||||
|
||||
|
||||
app.use('/api/users', passport.authenticate('jwt', {session: false}), usersRoutes);
|
||||
|
||||
app.use('/api/roles', passport.authenticate('jwt', {session: false}), rolesRoutes);
|
||||
|
||||
app.use('/api/permissions', passport.authenticate('jwt', {session: false}), permissionsRoutes);
|
||||
|
||||
app.use('/api/access_codes', passport.authenticate('jwt', {session: false}), access_codesRoutes);
|
||||
|
||||
app.use('/api/plans', passport.authenticate('jwt', {session: false}), plansRoutes);
|
||||
|
||||
app.use('/api/genres', passport.authenticate('jwt', {session: false}), genresRoutes);
|
||||
|
||||
app.use('/api/instruments', passport.authenticate('jwt', {session: false}), instrumentsRoutes);
|
||||
|
||||
app.use('/api/instrument_presets', passport.authenticate('jwt', {session: false}), instrument_presetsRoutes);
|
||||
|
||||
app.use('/api/rhythm_patterns', passport.authenticate('jwt', {session: false}), rhythm_patternsRoutes);
|
||||
|
||||
app.use('/api/projects', passport.authenticate('jwt', {session: false}), projectsRoutes);
|
||||
|
||||
app.use('/api/project_versions', passport.authenticate('jwt', {session: false}), project_versionsRoutes);
|
||||
|
||||
app.use('/api/song_sections', passport.authenticate('jwt', {session: false}), song_sectionsRoutes);
|
||||
|
||||
app.use('/api/tracks', passport.authenticate('jwt', {session: false}), tracksRoutes);
|
||||
|
||||
app.use('/api/midi_clips', passport.authenticate('jwt', {session: false}), midi_clipsRoutes);
|
||||
|
||||
app.use('/api/audio_clips', passport.authenticate('jwt', {session: false}), audio_clipsRoutes);
|
||||
|
||||
app.use('/api/lyrics', passport.authenticate('jwt', {session: false}), lyricsRoutes);
|
||||
|
||||
app.use('/api/ai_song_requests', passport.authenticate('jwt', {session: false}), ai_song_requestsRoutes);
|
||||
|
||||
app.use('/api/exports', passport.authenticate('jwt', {session: false}), exportsRoutes);
|
||||
|
||||
app.use('/api/collaborations', passport.authenticate('jwt', {session: false}), collaborationsRoutes);
|
||||
|
||||
app.use('/api/project_assets', passport.authenticate('jwt', {session: false}), project_assetsRoutes);
|
||||
|
||||
app.use(
|
||||
@ -203,4 +162,4 @@ db.sequelize.sync().then(function () {
|
||||
});
|
||||
});
|
||||
|
||||
module.exports = app;
|
||||
module.exports = app;
|
||||
78
backend/src/routes/locales.js
Normal file
78
backend/src/routes/locales.js
Normal file
@ -0,0 +1,78 @@
|
||||
const express = require('express');
|
||||
const router = express.Router();
|
||||
const path = require('path');
|
||||
const fs = require('fs');
|
||||
const { LocalAIApi } = require('../ai/LocalAIApi');
|
||||
const wrapAsync = require('../helpers').wrapAsync;
|
||||
|
||||
router.get('/:lng/:ns.json', wrapAsync(async (req, res) => {
|
||||
const { lng, ns } = req.params;
|
||||
|
||||
// Path to the requested locale file in the frontend public directory
|
||||
const frontendLocalesDir = path.resolve(__dirname, '../../../frontend/public/locales');
|
||||
const targetFilePath = path.join(frontendLocalesDir, lng, `${ns}.json`);
|
||||
const enFilePath = path.join(frontendLocalesDir, 'en', `${ns}.json`);
|
||||
|
||||
// If the file already exists, serve it
|
||||
if (fs.existsSync(targetFilePath)) {
|
||||
return res.sendFile(targetFilePath);
|
||||
}
|
||||
|
||||
// If the English base file doesn't exist, we can't translate it
|
||||
if (!fs.existsSync(enFilePath)) {
|
||||
return res.status(404).send({ error: 'Source translation not found' });
|
||||
}
|
||||
|
||||
// Read English base file
|
||||
const enContent = JSON.parse(fs.readFileSync(enFilePath, 'utf-8'));
|
||||
|
||||
// Use AI to translate the content
|
||||
// We send the whole JSON and ask the AI to translate values while keeping keys
|
||||
const prompt = `Translate the following JSON object values into ${lng} language. Keep the keys and the structure exactly as they are.
|
||||
Only return the translated JSON object, nothing else.
|
||||
JSON: ${JSON.stringify(enContent, null, 2)}`;
|
||||
|
||||
try {
|
||||
const aiResponse = await LocalAIApi.createResponse({
|
||||
input: [
|
||||
{ role: 'system', content: 'You are a translation assistant. You translate JSON values while preserving keys.' },
|
||||
{ role: 'user', content: prompt }
|
||||
]
|
||||
});
|
||||
|
||||
if (aiResponse.success) {
|
||||
let translatedText = LocalAIApi.extractText(aiResponse);
|
||||
|
||||
// Clean up the response in case AI wrapped it in code blocks
|
||||
if (translatedText.includes('```json')) {
|
||||
translatedText = translatedText.split('```json')[1].split('```')[0].trim();
|
||||
} else if (translatedText.includes('```')) {
|
||||
translatedText = translatedText.split('```')[1].split('```')[0].trim();
|
||||
}
|
||||
|
||||
try {
|
||||
const translatedJson = JSON.parse(translatedText);
|
||||
|
||||
// Optionally save the translated file to cache it for future use
|
||||
const lngDir = path.join(frontendLocalesDir, lng);
|
||||
if (!fs.existsSync(lngDir)) {
|
||||
fs.mkdirSync(lngDir, { recursive: true });
|
||||
}
|
||||
fs.writeFileSync(targetFilePath, JSON.stringify(translatedJson, null, 2));
|
||||
|
||||
return res.status(200).json(translatedJson);
|
||||
} catch (parseError) {
|
||||
console.error('Failed to parse AI translation JSON:', parseError, translatedText);
|
||||
return res.status(500).json({ error: 'Invalid AI response format', details: translatedText });
|
||||
}
|
||||
} else {
|
||||
console.error('AI translation failed:', aiResponse);
|
||||
return res.status(502).json({ error: 'AI translation service error' });
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error during AI translation:', error);
|
||||
return res.status(500).json({ error: 'Internal server error' });
|
||||
}
|
||||
}));
|
||||
|
||||
module.exports = router;
|
||||
@ -5,11 +5,32 @@ import { useTranslation } from 'react-i18next';
|
||||
type LanguageOption = { label: string; value: string };
|
||||
|
||||
const LANGS: LanguageOption[] = [
|
||||
{ value: 'en', label: '🇬🇧 EN' },
|
||||
{ value: 'fr', label: '🇫🇷 FR' },
|
||||
{ value: 'es', label: '🇪🇸 ES' },
|
||||
{ value: 'de', label: '🇩🇪 DE' },
|
||||
{ value: 'pt', label: '🇧🇷 PT' },
|
||||
{ value: 'en', label: '🇬🇧 English' },
|
||||
{ value: 'pt', label: '🇧🇷 Português' },
|
||||
{ value: 'es', label: '🇪🇸 Español' },
|
||||
{ value: 'fr', label: '🇫🇷 Français' },
|
||||
{ value: 'de', label: '🇩🇪 Deutsch' },
|
||||
{ value: 'it', label: '🇮🇹 Italiano' },
|
||||
{ value: 'ru', label: '🇷🇺 Русский' },
|
||||
{ value: 'zh', label: '🇨🇳 中文' },
|
||||
{ value: 'ja', label: '🇯🇵 日本語' },
|
||||
{ value: 'ko', label: '🇰🇷 한국어' },
|
||||
{ value: 'ar', label: '🇸🇦 العربية' },
|
||||
{ value: 'hi', label: '🇮🇳 हिन्दी' },
|
||||
{ value: 'tr', label: '🇹🇷 Türkçe' },
|
||||
{ value: 'nl', label: '🇳🇱 Nederlands' },
|
||||
{ value: 'pl', label: '🇵🇱 Polski' },
|
||||
{ value: 'sv', label: '🇸🇪 Svenska' },
|
||||
{ value: 'no', label: '🇳🇴 Norsk' },
|
||||
{ value: 'fi', label: '🇫🇮 Suomi' },
|
||||
{ value: 'da', label: '🇩🇰 Dansk' },
|
||||
{ value: 'el', label: '🇬🇷 Ελληνικά' },
|
||||
{ value: 'cs', label: '🇨🇿 Čeština' },
|
||||
{ value: 'hu', label: '🇭🇺 Magyar' },
|
||||
{ value: 'ro', label: '🇷🇴 Română' },
|
||||
{ value: 'th', label: '🇹🇭 ไทย' },
|
||||
{ value: 'vi', label: '🇻🇳 Tiếng Việt' },
|
||||
{ value: 'id', label: '🇮🇩 Indonesia' },
|
||||
];
|
||||
|
||||
const Option = (props: OptionProps<LanguageOption, false>) => (
|
||||
@ -34,9 +55,12 @@ const LanguageSwitcher: React.FC = () => {
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
const currentLang = LANGS.find(l => l.value === i18n.language);
|
||||
const currentLang = LANGS.find(l => l.value === i18n.language.split('-')[0]);
|
||||
if (currentLang) {
|
||||
setSelected(currentLang);
|
||||
} else if (i18n.language) {
|
||||
// Handle languages not in our hardcoded list but detected/switched
|
||||
setSelected({ value: i18n.language, label: `🌐 ${i18n.language.toUpperCase()}` });
|
||||
}
|
||||
}, [i18n.language]);
|
||||
|
||||
@ -50,13 +74,13 @@ const LanguageSwitcher: React.FC = () => {
|
||||
if (!mounted) return null;
|
||||
|
||||
return (
|
||||
<div style={{ width: 88 }}>
|
||||
<div style={{ width: 140 }}>
|
||||
<Select
|
||||
value={selected}
|
||||
options={LANGS}
|
||||
onChange={handleChange}
|
||||
isSearchable={false}
|
||||
menuPlacement='top'
|
||||
isSearchable={true}
|
||||
menuPlacement='bottom'
|
||||
components={{
|
||||
Option,
|
||||
SingleValue: SingleVal,
|
||||
@ -71,6 +95,7 @@ const LanguageSwitcher: React.FC = () => {
|
||||
paddingBottom: 0,
|
||||
borderColor: '#d1d5db',
|
||||
cursor: 'pointer',
|
||||
fontSize: '0.875rem',
|
||||
}),
|
||||
valueContainer: (base) => ({
|
||||
...base,
|
||||
@ -90,10 +115,10 @@ const LanguageSwitcher: React.FC = () => {
|
||||
...base,
|
||||
paddingTop: 4,
|
||||
paddingBottom: 4,
|
||||
height: 26,
|
||||
height: 'auto',
|
||||
fontSize: '0.875rem',
|
||||
backgroundColor: state.isFocused ? '#f3f4f6' : 'white',
|
||||
color: '#111827',
|
||||
backgroundColor: state.isSelected ? '#3b82f6' : state.isFocused ? '#f3f4f6' : 'white',
|
||||
color: state.isSelected ? 'white' : '#111827',
|
||||
}),
|
||||
menu: (base) => ({
|
||||
...base,
|
||||
@ -105,4 +130,4 @@ const LanguageSwitcher: React.FC = () => {
|
||||
);
|
||||
};
|
||||
|
||||
export default LanguageSwitcher;
|
||||
export default LanguageSwitcher;
|
||||
|
||||
@ -28,7 +28,7 @@ i18n
|
||||
.use(initReactI18next)
|
||||
.init({
|
||||
fallbackLng: 'en',
|
||||
supportedLngs: ['en', 'fr', 'es', 'de', 'pt'],
|
||||
// Removed supportedLngs to allow any language to be requested and translated on-the-fly by AI
|
||||
ns: ['common'],
|
||||
defaultNS: 'common',
|
||||
detection: {
|
||||
@ -37,7 +37,8 @@ i18n
|
||||
caches: ['localStorage', 'cookie'],
|
||||
},
|
||||
backend: {
|
||||
loadPath: '/locales/{{lng}}/{{ns}}.json',
|
||||
// Pointing to our new dynamic backend translation endpoint
|
||||
loadPath: '/api/locales/{{lng}}/{{ns}}.json',
|
||||
},
|
||||
interpolation: { escapeValue: false },
|
||||
});
|
||||
@ -50,14 +51,11 @@ if (typeof window !== 'undefined' && window.localStorage && !localStorage.getIte
|
||||
if (data && data.languages) {
|
||||
// languages is a comma-separated list like "en-US,es-US,..."
|
||||
const languages = data.languages.split(',');
|
||||
for (const lang of languages) {
|
||||
const baseLang = lang.split('-')[0];
|
||||
if (['en', 'fr', 'es', 'de', 'pt'].includes(baseLang)) {
|
||||
localStorage.setItem('detected_country_lang', baseLang);
|
||||
if (!localStorage.getItem('app_lang_')) {
|
||||
i18n.changeLanguage(baseLang);
|
||||
}
|
||||
break;
|
||||
if (languages.length > 0) {
|
||||
const baseLang = languages[0].split('-')[0];
|
||||
localStorage.setItem('detected_country_lang', baseLang);
|
||||
if (!localStorage.getItem('app_lang_')) {
|
||||
i18n.changeLanguage(baseLang);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -65,4 +63,4 @@ if (typeof window !== 'undefined' && window.localStorage && !localStorage.getIte
|
||||
.catch(err => console.error('Country language detection failed', err));
|
||||
}
|
||||
|
||||
export default i18n;
|
||||
export default i18n;
|
||||
Loading…
x
Reference in New Issue
Block a user