commit e56123f314fa2a880f988ab9e74347955550ac8e Author: Fabio Bonetti Date: Wed Feb 11 14:57:12 2026 +0100 Initial commit diff --git a/README.md b/README.md new file mode 100644 index 0000000..aafa92a --- /dev/null +++ b/README.md @@ -0,0 +1,174 @@ +# EU Media Guide CMS + +CMS basato su Grav per la gestione dei contenuti multilingua della media guide. + +## Struttura del Progetto + +``` +grav-cms-mediaguide/ +├── docker-compose.yml # Config Docker per Coolify +├── config/ +│ ├── system.yaml # Config sistema (24 lingue EU) +│ └── site.yaml # Config sito +├── blueprints/ +│ └── station.yaml # Blueprint per le stazioni/moduli +├── pages/ +│ └── stations/ +│ └── test-module/ # Modulo di test +├── plugins/ +│ └── json-export/ # Plugin per export JSON + API +└── README.md +``` + +## Deploy su Coolify + +### 1. Crea Repository Git + +```bash +cd grav-cms-mediaguide +git init +git add . +git commit -m "Initial CMS setup" +git remote add origin +git push -u origin main +``` + +### 2. Deploy su Coolify + +1. In Coolify, crea un nuovo servizio **Docker Compose** +2. Collega il repository Git +3. Imposta la porta pubblica su `8080` +4. Deploy! + +### 3. Setup Iniziale Grav + +Dopo il primo deploy, accedi al container e crea l'utente admin: + +```bash +# Accedi al container +docker exec -it mediaguide-cms bash + +# Crea utente admin +bin/plugin login new-user +``` + +### 4. Installa Plugin Admin + +```bash +bin/gpm install admin +``` + +## API Endpoints + +Il plugin `json-export` espone questi endpoint: + +| Endpoint | Descrizione | +|----------|-------------| +| `GET /api/stations?lang=en` | Lista tutte le stazioni | +| `GET /api/station/{id}?lang=en` | Contenuto singola stazione | +| `GET /api/translations?lang=en` | Traduzioni UI | +| `GET /api/export` | Esporta tutto in file JSON | + +### Esempio Risposta `/api/station/test-welcome?lang=en` + +```json +{ + "id": "test-welcome", + "type": "intro", + "language": "en", + "title": "Welcome to the European Parliament", + "description": "Your interactive guide to European democracy", + "components": [ + { + "id": "welcome-heading", + "type": "heading", + "level": "h1", + "content": "Welcome to the European Parliament" + }, + { + "id": "intro-text", + "type": "text", + "content": "

Welcome to the heart of European democracy...

" + }, + { + "id": "welcome-video", + "type": "video", + "src": "/media/videos/welcome-en.mp4", + "poster": "/media/images/video-poster.jpg", + "caption": "A brief introduction to the European Parliament" + } + ] +} +``` + +## Lingue Supportate (Ordine Protocollo UE) + +| Codice | Lingua | +|--------|--------| +| bg | български | +| es | español | +| cs | čeština | +| da | dansk | +| de | Deutsch | +| et | eesti | +| el | ελληνικά | +| en | English | +| fr | français | +| ga | Gaeilge | +| hr | hrvatski | +| it | italiano | +| lv | latviešu | +| lt | lietuvių | +| hu | magyar | +| mt | Malti | +| nl | Nederlands | +| pl | polski | +| pt | português | +| ro | română | +| sk | slovenčina | +| sl | slovenščina | +| fi | suomi | +| sv | svenska | + +## Gestione Contenuti + +### Creare una Nuova Stazione + +1. Accedi all'admin panel: `http://your-domain:8080/admin` +2. Vai su **Pages** → **Add** → **Station** +3. Compila i campi multilingua +4. Aggiungi i componenti (heading, text, video, audio, tabs, slideshow) +5. Salva + +### Struttura File Markdown + +Ogni stazione è un file `.md` con frontmatter YAML: + +```yaml +--- +title: Station Title +station_id: unique-id +station_type: exhibit +active: true +order: 1 + +title_translations: + en: English Title + fr: Titre français + +components: + - type: heading + id: heading-1 + level: h1 + content: + en: English Heading + fr: Titre français +--- +``` + +## Prossimi Passi + +1. [ ] Deploy del CMS su Coolify +2. [ ] Configurare l'admin panel +3. [ ] Creare il container React per l'app +4. [ ] Collegare l'app agli endpoint API del CMS diff --git a/blueprints/station.yaml b/blueprints/station.yaml new file mode 100644 index 0000000..78af6e3 --- /dev/null +++ b/blueprints/station.yaml @@ -0,0 +1,181 @@ +# Station Blueprint +# Defines a content module/station for the media guide +# /user/blueprints/station.yaml + +title: Station +'@extends': + type: default + context: blueprints://pages + +form: + fields: + tabs: + type: tabs + active: 1 + + fields: + # Station Info Tab + station_info: + type: tab + title: Station Info + fields: + header.station_id: + type: text + label: Station ID + help: Unique identifier for this station (used in QR codes) + validate: + required: true + pattern: '^[a-z0-9-]+$' + + header.station_type: + type: select + label: Station Type + options: + intro: Introduction + exhibit: Exhibit + information: Information Point + navigation: Navigation + default: exhibit + + header.exhibition_ref: + type: text + label: Exhibition Reference + help: Reference to parent exhibition + + header.active: + type: toggle + label: Active + highlight: 1 + default: 1 + options: + 1: Yes + 0: No + + header.order: + type: number + label: Display Order + default: 0 + + # Multilingual Content Tab + content_tab: + type: tab + title: Content + fields: + header.title: + type: multilang + label: Title + languages: true + + header.description: + type: multilang + label: Short Description + type: textarea + languages: true + + # Components Tab + components_tab: + type: tab + title: Components + fields: + header.components: + type: list + label: Content Components + style: vertical + btnLabel: Add Component + collapsed: true + + fields: + .type: + type: select + label: Component Type + options: + heading: Heading + text: Text Block + video: Video Player + audio: Audio Player + tabs: Tabbed Content + slideshow: Slideshow + + .id: + type: text + label: Component ID + help: Unique ID for this component + + # Heading specific + .level: + type: select + label: Heading Level + options: + h1: H1 + h2: H2 + h3: H3 + + # Text/Heading content (multilingual) + .content: + type: editor + label: Content + + # Video specific + .video_src: + type: filepicker + label: Video File + folder: 'user://media/videos' + accept: + - video/* + + .video_poster: + type: filepicker + label: Video Poster + folder: 'user://media/images' + accept: + - image/* + + # Audio specific + .audio_src: + type: filepicker + label: Audio File + folder: 'user://media/audio' + accept: + - audio/* + + .caption: + type: text + label: Caption + + # Tabs specific + .tabs: + type: list + label: Tabs + fields: + .title: + type: text + label: Tab Title + .content: + type: editor + label: Tab Content + + # Slideshow specific + .slides: + type: list + label: Slides + fields: + .image: + type: filepicker + label: Slide Image + folder: 'user://media/images' + accept: + - image/* + .caption: + type: text + label: Slide Caption + + # Media Tab + media_tab: + type: tab + title: Media Files + fields: + header.media_folder: + type: text + label: Media Folder + help: Folder containing media files for this station + placeholder: /media/station-id diff --git a/config/site.yaml b/config/site.yaml new file mode 100644 index 0000000..6b4f5e2 --- /dev/null +++ b/config/site.yaml @@ -0,0 +1,27 @@ +# Grav Site Configuration +# /user/config/site.yaml + +title: EU Media Guide CMS +default_lang: en + +author: + name: Admin + email: admin@example.com + +metadata: + description: Multilingual Media Guide Content Management System + +taxonomies: + - category + - tag + - exhibition + - station_type + +# Custom fields for the media guide +mediaguide: + exhibitions: [] + station_types: + - intro + - exhibit + - information + - navigation diff --git a/config/system.yaml b/config/system.yaml new file mode 100644 index 0000000..c69a858 --- /dev/null +++ b/config/system.yaml @@ -0,0 +1,73 @@ +# Grav System Configuration +# /user/config/system.yaml + +languages: + supported: + # EU Protocol Order (alphabetical by native name) + - bg # български (Bulgarian) + - es # español (Spanish) + - cs # čeština (Czech) + - da # dansk (Danish) + - de # Deutsch (German) + - et # eesti (Estonian) + - el # ελληνικά (Greek) + - en # English + - fr # français (French) + - ga # Gaeilge (Irish) + - hr # hrvatski (Croatian) + - it # italiano (Italian) + - lv # latviešu (Latvian) + - lt # lietuvių (Lithuanian) + - hu # magyar (Hungarian) + - mt # Malti (Maltese) + - nl # Nederlands (Dutch) + - pl # polski (Polish) + - pt # português (Portuguese) + - ro # română (Romanian) + - sk # slovenčina (Slovak) + - sl # slovenščina (Slovenian) + - fi # suomi (Finnish) + - sv # svenska (Swedish) + default_lang: en + include_default_lang: true + translations: true + translations_fallback: true + session_store_active: false + http_accept_language: true + override_locale: false + +pages: + theme: quark + markdown: + extra: true + types: + - txt + - xml + - html + - htm + - json + - rss + - atom + +cache: + enabled: false # Disable during development + check: + method: file + driver: auto + prefix: 'g' + lifetime: 604800 + +debugger: + enabled: false + shutdown: + close_connection: true + +twig: + cache: false # Disable during development + debug: true + auto_reload: true + autoescape: false + +assets: + css_pipeline: false + js_pipeline: false diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..53dcb41 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,27 @@ +version: '3.8' + +services: + grav-cms: + image: getgrav/grav:latest + container_name: mediaguide-cms + restart: unless-stopped + ports: + - "8080:80" + volumes: + # Persistent storage for all Grav user content + - grav_user:/var/www/html/user + # Custom blueprints (mounted from host) + - ./blueprints:/var/www/html/user/blueprints + # Custom plugins config + - ./config:/var/www/html/user/config + environment: + - GRAV_MULTISITE=false + healthcheck: + test: ["CMD", "curl", "-f", "http://localhost/"] + interval: 30s + timeout: 10s + retries: 3 + +volumes: + grav_user: + driver: local diff --git a/pages/stations/test-module/station.en.md b/pages/stations/test-module/station.en.md new file mode 100644 index 0000000..837e1f3 --- /dev/null +++ b/pages/stations/test-module/station.en.md @@ -0,0 +1,145 @@ +--- +title: Test Module - Welcome +station_id: test-welcome +station_type: intro +exhibition_ref: main-exhibition +active: true +order: 1 + +# Multilingual titles +title_translations: + en: Welcome to the European Parliament + fr: Bienvenue au Parlement européen + de: Willkommen im Europäischen Parlament + nl: Welkom bij het Europees Parlement + it: Benvenuti al Parlamento europeo + es: Bienvenidos al Parlamento Europeo + +# Multilingual descriptions +description_translations: + en: Your interactive guide to European democracy + fr: Votre guide interactif de la démocratie européenne + de: Ihr interaktiver Führer zur europäischen Demokratie + nl: Uw interactieve gids voor Europese democratie + it: La tua guida interattiva alla democrazia europea + es: Su guía interactiva de la democracia europea + +# Content components +components: + - type: heading + id: welcome-heading + level: h1 + content: + en: Welcome to the European Parliament + fr: Bienvenue au Parlement européen + de: Willkommen im Europäischen Parlament + nl: Welkom bij het Europees Parlement + + - type: text + id: intro-text + content: + en: | +

Welcome to the heart of European democracy. This interactive guide + will help you navigate through the European Parliament building and + learn about its institutions.

+

Use the menu to select your preferred language and scan QR codes + throughout the building to access specific information.

+ fr: | +

Bienvenue au cœur de la démocratie européenne. Ce guide interactif + vous aidera à naviguer dans le bâtiment du Parlement européen et à + découvrir ses institutions.

+ de: | +

Willkommen im Herzen der europäischen Demokratie. Dieser interaktive + Führer hilft Ihnen bei der Navigation durch das Gebäude des Europäischen + Parlaments.

+ nl: | +

Welkom in het hart van de Europese democratie. Deze interactieve gids + helpt u door het gebouw van het Europees Parlement te navigeren.

+ + - type: video + id: welcome-video + src: + en: /media/videos/welcome-en.mp4 + fr: /media/videos/welcome-fr.mp4 + de: /media/videos/welcome-de.mp4 + poster: /media/images/video-poster.jpg + caption: + en: A brief introduction to the European Parliament + fr: Une brève introduction au Parlement européen + de: Eine kurze Einführung in das Europäische Parlament + + - type: audio + id: welcome-audio + src: + en: /media/audio/welcome-en.mp3 + fr: /media/audio/welcome-fr.mp3 + de: /media/audio/welcome-de.mp3 + caption: + en: Audio welcome message + fr: Message d'accueil audio + de: Audio-Willkommensnachricht + + - type: tabs + id: info-tabs + tabs: + - title: + en: Information + fr: Informations + de: Information + nl: Informatie + content: + en: | +

The European Parliament is one of the EU's law-making bodies. + It is directly elected by EU voters every 5 years.

+ fr: | +

Le Parlement européen est l'un des organes législatifs de l'UE. + Il est élu directement par les citoyens européens tous les 5 ans.

+ de: | +

Das Europäische Parlament ist eines der gesetzgebenden Organe der EU. + Es wird alle 5 Jahre direkt von den EU-Bürgern gewählt.

+ - title: + en: Opening Hours + fr: Horaires d'ouverture + de: Öffnungszeiten + nl: Openingstijden + content: + en: | +

Monday-Friday: 9:00 - 18:00

+

Saturday: 10:00 - 18:00

+

Sunday: Closed

+ fr: | +

Lundi-Vendredi: 9h00 - 18h00

+

Samedi: 10h00 - 18h00

+

Dimanche: Fermé

+ - title: + en: Services + fr: Services + de: Dienstleistungen + nl: Diensten + content: + en: | +

Guided Tours, Information Desk, Cafeteria, Gift Shop, Accessibility Services

+ fr: | +

Visites guidées, Bureau d'information, Cafétéria, Boutique, Services d'accessibilité

+ + - type: slideshow + id: building-slideshow + slides: + - image: /media/images/exterior.jpg + caption: + en: Exterior of the European Parliament building + fr: Extérieur du bâtiment du Parlement européen + de: Außenansicht des Europäischen Parlaments + - image: /media/images/hemicycle.jpg + caption: + en: The Hemicycle where plenary sessions take place + fr: L'hémicycle où se déroulent les sessions plénières + de: Das Plenum, in dem die Plenarsitzungen stattfinden + - image: /media/images/parliamentarium.jpg + caption: + en: The Parliamentarium visitors center + fr: Le centre des visiteurs Parlamentarium + de: Das Besucherzentrum Parlamentarium +--- + +This is the main content body for the test module. diff --git a/plugins/json-export/blueprints.yaml b/plugins/json-export/blueprints.yaml new file mode 100644 index 0000000..ab44fdf --- /dev/null +++ b/plugins/json-export/blueprints.yaml @@ -0,0 +1,10 @@ +name: JSON Export +version: 1.0.0 +description: Exports Grav content to JSON format for the React media guide app +icon: file-code +author: + name: Media Guide Team +homepage: https://github.com/your-org/mediaguide +keywords: json, export, api, multilingual +bugs: https://github.com/your-org/mediaguide/issues +license: MIT diff --git a/plugins/json-export/json-export.php b/plugins/json-export/json-export.php new file mode 100644 index 0000000..af73251 --- /dev/null +++ b/plugins/json-export/json-export.php @@ -0,0 +1,340 @@ + ['onPluginsInitialized', 0] + ]; + } + + public function onPluginsInitialized() + { + // Check if we're in admin context + if ($this->isAdmin()) { + $this->enable([ + 'onAdminTaskExecute' => ['onAdminTaskExecute', 0] + ]); + } + + // Enable API endpoint + $this->enable([ + 'onPagesInitialized' => ['onPagesInitialized', 0] + ]); + } + + public function onPagesInitialized() + { + $uri = $this->grav['uri']; + + // API endpoints + if (strpos($uri->path(), '/api/') === 0) { + $this->handleApiRequest($uri); + } + } + + private function handleApiRequest($uri) + { + $path = $uri->path(); + $lang = $uri->param('lang') ?? 'en'; + + header('Content-Type: application/json'); + header('Access-Control-Allow-Origin: *'); + + // GET /api/stations - List all stations + if ($path === '/api/stations') { + echo json_encode($this->getStationsList($lang), JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE); + exit; + } + + // GET /api/station/{id} - Get single station + if (preg_match('#^/api/station/([a-z0-9-]+)$#', $path, $matches)) { + $stationId = $matches[1]; + $station = $this->getStation($stationId, $lang); + if ($station) { + echo json_encode($station, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE); + } else { + http_response_code(404); + echo json_encode(['error' => 'Station not found']); + } + exit; + } + + // GET /api/translations - Get UI translations + if ($path === '/api/translations') { + echo json_encode($this->getTranslations($lang), JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE); + exit; + } + + // GET /api/export - Export all content to JSON files + if ($path === '/api/export') { + $result = $this->exportAllToJson(); + echo json_encode($result, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE); + exit; + } + } + + private function getStationsList($lang) + { + $pages = $this->grav['pages']; + $stations = []; + + foreach ($pages->all() as $page) { + if ($page->template() === 'station' && $page->header()->active) { + $header = $page->header(); + $stations[] = [ + 'id' => $header->station_id ?? $page->slug(), + 'type' => $header->station_type ?? 'exhibit', + 'title' => $this->getTranslatedField($header->title_translations ?? [], $lang), + 'description' => $this->getTranslatedField($header->description_translations ?? [], $lang), + 'order' => $header->order ?? 0, + 'active' => $header->active ?? true + ]; + } + } + + // Sort by order + usort($stations, fn($a, $b) => $a['order'] <=> $b['order']); + + return $stations; + } + + private function getStation($stationId, $lang) + { + $pages = $this->grav['pages']; + + foreach ($pages->all() as $page) { + if ($page->template() === 'station') { + $header = $page->header(); + $id = $header->station_id ?? $page->slug(); + + if ($id === $stationId) { + return $this->formatStationForLang($page, $lang); + } + } + } + + return null; + } + + private function formatStationForLang($page, $lang) + { + $header = $page->header(); + + $station = [ + 'id' => $header->station_id ?? $page->slug(), + 'type' => $header->station_type ?? 'exhibit', + 'language' => $lang, + 'title' => $this->getTranslatedField($header->title_translations ?? [], $lang), + 'description' => $this->getTranslatedField($header->description_translations ?? [], $lang), + 'components' => [] + ]; + + // Process components + if (isset($header->components) && is_array($header->components)) { + foreach ($header->components as $component) { + $station['components'][] = $this->formatComponent($component, $lang); + } + } + + return $station; + } + + private function formatComponent($component, $lang) + { + $formatted = [ + 'id' => $component['id'] ?? uniqid(), + 'type' => $component['type'] ?? 'text' + ]; + + switch ($component['type']) { + case 'heading': + $formatted['level'] = $component['level'] ?? 'h2'; + $formatted['content'] = $this->getTranslatedField($component['content'] ?? [], $lang); + break; + + case 'text': + $formatted['content'] = $this->getTranslatedField($component['content'] ?? [], $lang); + break; + + case 'video': + $formatted['src'] = $this->getTranslatedField($component['src'] ?? [], $lang); + $formatted['poster'] = $component['poster'] ?? null; + $formatted['caption'] = $this->getTranslatedField($component['caption'] ?? [], $lang); + break; + + case 'audio': + $formatted['src'] = $this->getTranslatedField($component['src'] ?? [], $lang); + $formatted['caption'] = $this->getTranslatedField($component['caption'] ?? [], $lang); + break; + + case 'tabs': + $formatted['tabs'] = []; + if (isset($component['tabs']) && is_array($component['tabs'])) { + foreach ($component['tabs'] as $tab) { + $formatted['tabs'][] = [ + 'title' => $this->getTranslatedField($tab['title'] ?? [], $lang), + 'content' => $this->getTranslatedField($tab['content'] ?? [], $lang) + ]; + } + } + break; + + case 'slideshow': + $formatted['slides'] = []; + if (isset($component['slides']) && is_array($component['slides'])) { + foreach ($component['slides'] as $slide) { + $formatted['slides'][] = [ + 'image' => $slide['image'] ?? '', + 'caption' => $this->getTranslatedField($slide['caption'] ?? [], $lang) + ]; + } + } + break; + } + + return $formatted; + } + + private function getTranslatedField($field, $lang, $fallback = 'en') + { + if (is_string($field)) { + return $field; + } + + if (is_array($field)) { + return $field[$lang] ?? $field[$fallback] ?? reset($field) ?: ''; + } + + return ''; + } + + private function getTranslations($lang) + { + // UI translations - these should be stored in config or separate files + $translations = [ + 'en' => [ + 'appTitle' => 'EU Media Guide', + 'scanQR' => 'Scan QR', + 'selectLanguage' => 'Select Language', + 'contentLibrary' => 'Content Library', + 'noContent' => 'Scan a QR code or select content from the menu', + 'errorLoading' => 'Failed to load content. Please try again.', + 'playAudio' => 'Play Audio', + 'pauseAudio' => 'Pause Audio', + 'next' => 'Next', + 'previous' => 'Previous', + 'closeMenu' => 'Close Menu', + 'openMenu' => 'Open Menu', + 'loading' => 'Loading...' + ], + 'fr' => [ + 'appTitle' => 'Guide Média UE', + 'scanQR' => 'Scanner QR', + 'selectLanguage' => 'Choisir la langue', + 'contentLibrary' => 'Bibliothèque de contenu', + 'noContent' => 'Scannez un code QR ou sélectionnez du contenu dans le menu', + 'errorLoading' => 'Échec du chargement. Veuillez réessayer.', + 'playAudio' => 'Lire l\'audio', + 'pauseAudio' => 'Pause audio', + 'next' => 'Suivant', + 'previous' => 'Précédent', + 'closeMenu' => 'Fermer le menu', + 'openMenu' => 'Ouvrir le menu', + 'loading' => 'Chargement...' + ], + 'de' => [ + 'appTitle' => 'EU Media Guide', + 'scanQR' => 'QR scannen', + 'selectLanguage' => 'Sprache wählen', + 'contentLibrary' => 'Inhaltsbibliothek', + 'noContent' => 'Scannen Sie einen QR-Code oder wählen Sie Inhalte aus dem Menü', + 'errorLoading' => 'Laden fehlgeschlagen. Bitte versuchen Sie es erneut.', + 'playAudio' => 'Audio abspielen', + 'pauseAudio' => 'Audio pausieren', + 'next' => 'Weiter', + 'previous' => 'Zurück', + 'closeMenu' => 'Menü schließen', + 'openMenu' => 'Menü öffnen', + 'loading' => 'Laden...' + ], + 'nl' => [ + 'appTitle' => 'EU Media Gids', + 'scanQR' => 'QR scannen', + 'selectLanguage' => 'Taal selecteren', + 'contentLibrary' => 'Inhoudsbibliotheek', + 'noContent' => 'Scan een QR-code of selecteer inhoud uit het menu', + 'errorLoading' => 'Laden mislukt. Probeer het opnieuw.', + 'playAudio' => 'Audio afspelen', + 'pauseAudio' => 'Audio pauzeren', + 'next' => 'Volgende', + 'previous' => 'Vorige', + 'closeMenu' => 'Menu sluiten', + 'openMenu' => 'Menu openen', + 'loading' => 'Laden...' + ] + // Add more languages as needed + ]; + + return $translations[$lang] ?? $translations['en']; + } + + private function exportAllToJson() + { + $exportPath = $this->grav['locator']->findResource('user://') . '/export/'; + + if (!is_dir($exportPath)) { + mkdir($exportPath, 0755, true); + } + + $languages = $this->grav['config']->get('system.languages.supported', ['en']); + $exported = []; + + foreach ($languages as $lang) { + // Export stations + $stations = $this->getStationsList($lang); + file_put_contents( + $exportPath . "stations.{$lang}.json", + json_encode($stations, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE) + ); + + // Export each station content + foreach ($stations as $station) { + $content = $this->getStation($station['id'], $lang); + if ($content) { + file_put_contents( + $exportPath . "{$station['id']}.{$lang}.json", + json_encode($content, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE) + ); + } + } + + // Export translations + $translations = $this->getTranslations($lang); + file_put_contents( + $exportPath . "translations.{$lang}.json", + json_encode($translations, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE) + ); + + $exported[] = $lang; + } + + return [ + 'success' => true, + 'exported_languages' => $exported, + 'path' => $exportPath + ]; + } +} diff --git a/plugins/json-export/json-export.yaml b/plugins/json-export/json-export.yaml new file mode 100644 index 0000000..67256e1 --- /dev/null +++ b/plugins/json-export/json-export.yaml @@ -0,0 +1,24 @@ +name: JSON Export +version: 1.0.0 +description: Exports Grav content to JSON format for the React media guide app +icon: file-code +author: + name: Media Guide Team +homepage: https://github.com/your-org/mediaguide +keywords: json, export, api, multilingual +bugs: https://github.com/your-org/mediaguide/issues +license: MIT + +form: + validation: strict + fields: + enabled: + type: toggle + label: Plugin status + highlight: 1 + default: 1 + options: + 1: Enabled + 0: Disabled + validate: + type: bool