diff --git a/backend/src/index.js b/backend/src/index.js index 8eea53c..f199a22 100644 --- a/backend/src/index.js +++ b/backend/src/index.js @@ -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; \ No newline at end of file diff --git a/backend/src/routes/locales.js b/backend/src/routes/locales.js new file mode 100644 index 0000000..5147309 --- /dev/null +++ b/backend/src/routes/locales.js @@ -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; diff --git a/frontend/src/components/LanguageSwitcher.tsx b/frontend/src/components/LanguageSwitcher.tsx index dac03d1..13c8562 100644 --- a/frontend/src/components/LanguageSwitcher.tsx +++ b/frontend/src/components/LanguageSwitcher.tsx @@ -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) => ( @@ -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 ( -
+