3
This commit is contained in:
parent
e1f7182cfc
commit
0adfe0fe78
@ -1,4 +1,3 @@
|
|||||||
|
|
||||||
const express = require('express');
|
const express = require('express');
|
||||||
const cors = require('cors');
|
const cors = require('cors');
|
||||||
const app = express();
|
const app = express();
|
||||||
@ -16,52 +15,30 @@ const fileRoutes = require('./routes/file');
|
|||||||
const searchRoutes = require('./routes/search');
|
const searchRoutes = require('./routes/search');
|
||||||
const sqlRoutes = require('./routes/sql');
|
const sqlRoutes = require('./routes/sql');
|
||||||
const pexelsRoutes = require('./routes/pexels');
|
const pexelsRoutes = require('./routes/pexels');
|
||||||
|
|
||||||
const openaiRoutes = require('./routes/openai');
|
const openaiRoutes = require('./routes/openai');
|
||||||
|
const localesRoutes = require('./routes/locales');
|
||||||
|
|
||||||
|
|
||||||
const usersRoutes = require('./routes/users');
|
const usersRoutes = require('./routes/users');
|
||||||
|
|
||||||
const rolesRoutes = require('./routes/roles');
|
const rolesRoutes = require('./routes/roles');
|
||||||
|
|
||||||
const permissionsRoutes = require('./routes/permissions');
|
const permissionsRoutes = require('./routes/permissions');
|
||||||
|
|
||||||
const access_codesRoutes = require('./routes/access_codes');
|
const access_codesRoutes = require('./routes/access_codes');
|
||||||
|
|
||||||
const plansRoutes = require('./routes/plans');
|
const plansRoutes = require('./routes/plans');
|
||||||
|
|
||||||
const genresRoutes = require('./routes/genres');
|
const genresRoutes = require('./routes/genres');
|
||||||
|
|
||||||
const instrumentsRoutes = require('./routes/instruments');
|
const instrumentsRoutes = require('./routes/instruments');
|
||||||
|
|
||||||
const instrument_presetsRoutes = require('./routes/instrument_presets');
|
const instrument_presetsRoutes = require('./routes/instrument_presets');
|
||||||
|
|
||||||
const rhythm_patternsRoutes = require('./routes/rhythm_patterns');
|
const rhythm_patternsRoutes = require('./routes/rhythm_patterns');
|
||||||
|
|
||||||
const projectsRoutes = require('./routes/projects');
|
const projectsRoutes = require('./routes/projects');
|
||||||
|
|
||||||
const project_versionsRoutes = require('./routes/project_versions');
|
const project_versionsRoutes = require('./routes/project_versions');
|
||||||
|
|
||||||
const song_sectionsRoutes = require('./routes/song_sections');
|
const song_sectionsRoutes = require('./routes/song_sections');
|
||||||
|
|
||||||
const tracksRoutes = require('./routes/tracks');
|
const tracksRoutes = require('./routes/tracks');
|
||||||
|
|
||||||
const midi_clipsRoutes = require('./routes/midi_clips');
|
const midi_clipsRoutes = require('./routes/midi_clips');
|
||||||
|
|
||||||
const audio_clipsRoutes = require('./routes/audio_clips');
|
const audio_clipsRoutes = require('./routes/audio_clips');
|
||||||
|
|
||||||
const lyricsRoutes = require('./routes/lyrics');
|
const lyricsRoutes = require('./routes/lyrics');
|
||||||
|
|
||||||
const ai_song_requestsRoutes = require('./routes/ai_song_requests');
|
const ai_song_requestsRoutes = require('./routes/ai_song_requests');
|
||||||
|
|
||||||
const exportsRoutes = require('./routes/exports');
|
const exportsRoutes = require('./routes/exports');
|
||||||
|
|
||||||
const collaborationsRoutes = require('./routes/collaborations');
|
const collaborationsRoutes = require('./routes/collaborations');
|
||||||
|
|
||||||
const project_assetsRoutes = require('./routes/project_assets');
|
const project_assetsRoutes = require('./routes/project_assets');
|
||||||
|
|
||||||
|
|
||||||
const getBaseUrl = (url) => {
|
const getBaseUrl = (url) => {
|
||||||
if (!url) return '';
|
if (!url) return '';
|
||||||
return url.endsWith('/api') ? url.slice(0, -4) : url;
|
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/auth', authRoutes);
|
||||||
app.use('/api/file', fileRoutes);
|
app.use('/api/file', fileRoutes);
|
||||||
app.use('/api/pexels', pexelsRoutes);
|
app.use('/api/pexels', pexelsRoutes);
|
||||||
|
app.use('/api/locales', localesRoutes); // Public translation route
|
||||||
app.enable('trust proxy');
|
app.enable('trust proxy');
|
||||||
|
|
||||||
|
|
||||||
app.use('/api/users', passport.authenticate('jwt', {session: false}), usersRoutes);
|
app.use('/api/users', passport.authenticate('jwt', {session: false}), usersRoutes);
|
||||||
|
|
||||||
app.use('/api/roles', passport.authenticate('jwt', {session: false}), rolesRoutes);
|
app.use('/api/roles', passport.authenticate('jwt', {session: false}), rolesRoutes);
|
||||||
|
|
||||||
app.use('/api/permissions', passport.authenticate('jwt', {session: false}), permissionsRoutes);
|
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/access_codes', passport.authenticate('jwt', {session: false}), access_codesRoutes);
|
||||||
|
|
||||||
app.use('/api/plans', passport.authenticate('jwt', {session: false}), plansRoutes);
|
app.use('/api/plans', passport.authenticate('jwt', {session: false}), plansRoutes);
|
||||||
|
|
||||||
app.use('/api/genres', passport.authenticate('jwt', {session: false}), genresRoutes);
|
app.use('/api/genres', passport.authenticate('jwt', {session: false}), genresRoutes);
|
||||||
|
|
||||||
app.use('/api/instruments', passport.authenticate('jwt', {session: false}), instrumentsRoutes);
|
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/instrument_presets', passport.authenticate('jwt', {session: false}), instrument_presetsRoutes);
|
||||||
|
|
||||||
app.use('/api/rhythm_patterns', passport.authenticate('jwt', {session: false}), rhythm_patternsRoutes);
|
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/projects', passport.authenticate('jwt', {session: false}), projectsRoutes);
|
||||||
|
|
||||||
app.use('/api/project_versions', passport.authenticate('jwt', {session: false}), project_versionsRoutes);
|
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/song_sections', passport.authenticate('jwt', {session: false}), song_sectionsRoutes);
|
||||||
|
|
||||||
app.use('/api/tracks', passport.authenticate('jwt', {session: false}), tracksRoutes);
|
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/midi_clips', passport.authenticate('jwt', {session: false}), midi_clipsRoutes);
|
||||||
|
|
||||||
app.use('/api/audio_clips', passport.authenticate('jwt', {session: false}), audio_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/lyrics', passport.authenticate('jwt', {session: false}), lyricsRoutes);
|
||||||
|
|
||||||
app.use('/api/ai_song_requests', passport.authenticate('jwt', {session: false}), ai_song_requestsRoutes);
|
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/exports', passport.authenticate('jwt', {session: false}), exportsRoutes);
|
||||||
|
|
||||||
app.use('/api/collaborations', passport.authenticate('jwt', {session: false}), collaborationsRoutes);
|
app.use('/api/collaborations', passport.authenticate('jwt', {session: false}), collaborationsRoutes);
|
||||||
|
|
||||||
app.use('/api/project_assets', passport.authenticate('jwt', {session: false}), project_assetsRoutes);
|
app.use('/api/project_assets', passport.authenticate('jwt', {session: false}), project_assetsRoutes);
|
||||||
|
|
||||||
app.use(
|
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 };
|
type LanguageOption = { label: string; value: string };
|
||||||
|
|
||||||
const LANGS: LanguageOption[] = [
|
const LANGS: LanguageOption[] = [
|
||||||
{ value: 'en', label: '🇬🇧 EN' },
|
{ value: 'en', label: '🇬🇧 English' },
|
||||||
{ value: 'fr', label: '🇫🇷 FR' },
|
{ value: 'pt', label: '🇧🇷 Português' },
|
||||||
{ value: 'es', label: '🇪🇸 ES' },
|
{ value: 'es', label: '🇪🇸 Español' },
|
||||||
{ value: 'de', label: '🇩🇪 DE' },
|
{ value: 'fr', label: '🇫🇷 Français' },
|
||||||
{ value: 'pt', label: '🇧🇷 PT' },
|
{ 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>) => (
|
const Option = (props: OptionProps<LanguageOption, false>) => (
|
||||||
@ -34,9 +55,12 @@ const LanguageSwitcher: React.FC = () => {
|
|||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const currentLang = LANGS.find(l => l.value === i18n.language);
|
const currentLang = LANGS.find(l => l.value === i18n.language.split('-')[0]);
|
||||||
if (currentLang) {
|
if (currentLang) {
|
||||||
setSelected(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]);
|
}, [i18n.language]);
|
||||||
|
|
||||||
@ -50,13 +74,13 @@ const LanguageSwitcher: React.FC = () => {
|
|||||||
if (!mounted) return null;
|
if (!mounted) return null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div style={{ width: 88 }}>
|
<div style={{ width: 140 }}>
|
||||||
<Select
|
<Select
|
||||||
value={selected}
|
value={selected}
|
||||||
options={LANGS}
|
options={LANGS}
|
||||||
onChange={handleChange}
|
onChange={handleChange}
|
||||||
isSearchable={false}
|
isSearchable={true}
|
||||||
menuPlacement='top'
|
menuPlacement='bottom'
|
||||||
components={{
|
components={{
|
||||||
Option,
|
Option,
|
||||||
SingleValue: SingleVal,
|
SingleValue: SingleVal,
|
||||||
@ -71,6 +95,7 @@ const LanguageSwitcher: React.FC = () => {
|
|||||||
paddingBottom: 0,
|
paddingBottom: 0,
|
||||||
borderColor: '#d1d5db',
|
borderColor: '#d1d5db',
|
||||||
cursor: 'pointer',
|
cursor: 'pointer',
|
||||||
|
fontSize: '0.875rem',
|
||||||
}),
|
}),
|
||||||
valueContainer: (base) => ({
|
valueContainer: (base) => ({
|
||||||
...base,
|
...base,
|
||||||
@ -90,10 +115,10 @@ const LanguageSwitcher: React.FC = () => {
|
|||||||
...base,
|
...base,
|
||||||
paddingTop: 4,
|
paddingTop: 4,
|
||||||
paddingBottom: 4,
|
paddingBottom: 4,
|
||||||
height: 26,
|
height: 'auto',
|
||||||
fontSize: '0.875rem',
|
fontSize: '0.875rem',
|
||||||
backgroundColor: state.isFocused ? '#f3f4f6' : 'white',
|
backgroundColor: state.isSelected ? '#3b82f6' : state.isFocused ? '#f3f4f6' : 'white',
|
||||||
color: '#111827',
|
color: state.isSelected ? 'white' : '#111827',
|
||||||
}),
|
}),
|
||||||
menu: (base) => ({
|
menu: (base) => ({
|
||||||
...base,
|
...base,
|
||||||
@ -105,4 +130,4 @@ const LanguageSwitcher: React.FC = () => {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default LanguageSwitcher;
|
export default LanguageSwitcher;
|
||||||
|
|||||||
@ -28,7 +28,7 @@ i18n
|
|||||||
.use(initReactI18next)
|
.use(initReactI18next)
|
||||||
.init({
|
.init({
|
||||||
fallbackLng: 'en',
|
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'],
|
ns: ['common'],
|
||||||
defaultNS: 'common',
|
defaultNS: 'common',
|
||||||
detection: {
|
detection: {
|
||||||
@ -37,7 +37,8 @@ i18n
|
|||||||
caches: ['localStorage', 'cookie'],
|
caches: ['localStorage', 'cookie'],
|
||||||
},
|
},
|
||||||
backend: {
|
backend: {
|
||||||
loadPath: '/locales/{{lng}}/{{ns}}.json',
|
// Pointing to our new dynamic backend translation endpoint
|
||||||
|
loadPath: '/api/locales/{{lng}}/{{ns}}.json',
|
||||||
},
|
},
|
||||||
interpolation: { escapeValue: false },
|
interpolation: { escapeValue: false },
|
||||||
});
|
});
|
||||||
@ -50,14 +51,11 @@ if (typeof window !== 'undefined' && window.localStorage && !localStorage.getIte
|
|||||||
if (data && data.languages) {
|
if (data && data.languages) {
|
||||||
// languages is a comma-separated list like "en-US,es-US,..."
|
// languages is a comma-separated list like "en-US,es-US,..."
|
||||||
const languages = data.languages.split(',');
|
const languages = data.languages.split(',');
|
||||||
for (const lang of languages) {
|
if (languages.length > 0) {
|
||||||
const baseLang = lang.split('-')[0];
|
const baseLang = languages[0].split('-')[0];
|
||||||
if (['en', 'fr', 'es', 'de', 'pt'].includes(baseLang)) {
|
localStorage.setItem('detected_country_lang', baseLang);
|
||||||
localStorage.setItem('detected_country_lang', baseLang);
|
if (!localStorage.getItem('app_lang_')) {
|
||||||
if (!localStorage.getItem('app_lang_')) {
|
i18n.changeLanguage(baseLang);
|
||||||
i18n.changeLanguage(baseLang);
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -65,4 +63,4 @@ if (typeof window !== 'undefined' && window.localStorage && !localStorage.getIte
|
|||||||
.catch(err => console.error('Country language detection failed', err));
|
.catch(err => console.error('Country language detection failed', err));
|
||||||
}
|
}
|
||||||
|
|
||||||
export default i18n;
|
export default i18n;
|
||||||
Loading…
x
Reference in New Issue
Block a user