get_param( 'lang' ) ); $folder = sanitize_text_field( $request->get_param( 'folder' ) ); // Define directories. $upload_dir = wp_upload_dir(); $custom_dir = trailingslashit( $upload_dir['basedir'] ) . 'ai-agent/translations/'; $plugin_dir = RWPAI_PLUGIN_DIR . 'public/translations/'; // LIST MODE: If no lang parameter provided, list all available translation files. if ( empty( $lang ) ) { $target_dir = $plugin_dir; // default to plugin directory. if ( 'upload' === $folder ) { $target_dir = $custom_dir; // Create directory if it doesn't exist for upload folder. if ( ! file_exists( $target_dir ) ) { wp_mkdir_p( $target_dir ); } } if ( ! is_dir( $target_dir ) ) { return array(); } // Read directory and filter for .json files. $files = array(); $dh = opendir( $target_dir ); if ( false !== $dh ) { // phpcs:ignore Squiz.PHP.DisallowMultipleAssignments.FoundInControlStructure while ( false !== ( $entry = readdir( $dh ) ) ) { if ( '.' === $entry || '..' === $entry ) { continue; } // Only include .json files. if ( is_file( $target_dir . $entry ) && '.json' === substr( $entry, -5 ) ) { $files[] = $entry; } } closedir( $dh ); } // Sort alphabetically. sort( $files ); return rest_ensure_response( array( 'status' => true, 'data' => $files, ) ); } // GET MODE: Return specific translation content. // Take only the first part before '-' (e.g., 'en' from 'en-US'). $lang = explode( '-', $lang )[0]; // If folder is specified, only check that specific folder. if ( 'upload' === $folder ) { $custom_file = $custom_dir . "{$lang}.json"; if ( file_exists( $custom_file ) ) { // phpcs:ignore WordPress.WP.AlternativeFunctions.file_get_contents -- Reading local file, not remote. $content = file_get_contents( $custom_file ); return json_decode( $content, true ); } return new WP_Error( 'not_found', 'Translation not found in upload directory: ' . $lang, array( 'status' => 404 ) ); } elseif ( 'plugin' === $folder ) { $plugin_file = $plugin_dir . "{$lang}.json"; if ( file_exists( $plugin_file ) ) { // phpcs:ignore WordPress.WP.AlternativeFunctions.file_get_contents -- Reading local file, not remote. $content = file_get_contents( $plugin_file ); return json_decode( $content, true ); } return new WP_Error( 'not_found', 'Translation not found in plugin directory: ' . $lang, array( 'status' => 404 ) ); } // Default: Check both with priority (custom first, then plugin). // Check custom translation first (user override). $custom_file = $custom_dir . "{$lang}.json"; if ( file_exists( $custom_file ) ) { // phpcs:ignore WordPress.WP.AlternativeFunctions.file_get_contents -- Reading local file, not remote. $content = file_get_contents( $custom_file ); return json_decode( $content, true ); } // Check plugin default translation. $plugin_file = $plugin_dir . "{$lang}.json"; if ( file_exists( $plugin_file ) ) { // phpcs:ignore WordPress.WP.AlternativeFunctions.file_get_contents -- Reading local file, not remote. $content = file_get_contents( $plugin_file ); return json_decode( $content, true ); } // Fallback to English - check custom first, then plugin. $custom_en = $custom_dir . 'en.json'; if ( file_exists( $custom_en ) ) { // phpcs:ignore WordPress.WP.AlternativeFunctions.file_get_contents -- Reading local file, not remote. $content = file_get_contents( $custom_en ); return json_decode( $content, true ); } $plugin_en = $plugin_dir . 'en.json'; if ( file_exists( $plugin_en ) ) { // phpcs:ignore WordPress.WP.AlternativeFunctions.file_get_contents -- Reading local file, not remote. $content = file_get_contents( $plugin_en ); return json_decode( $content, true ); } // If even English is missing, return error. return new WP_Error( 'not_found', 'Translation not found: ' . $lang, array( 'status' => 404 ) ); } /** * Upload a custom translation file. * * This function allows administrators to upload custom translation JSON files * that will override the default plugin translations. The uploaded file is saved * to wp-content/uploads/ai-agent/translations/. * * @param WP_REST_Request $request The REST API request object containing: * - 'lang' (string): Language code (e.g., 'en', 'fr', 'es'). * - 'translation' (array): Translation data as a JSON object. * * @return WP_REST_Response|WP_Error Response with success status or error. * * Example usage (via REST API): * POST /wp-json/natuteto/v1/localization/upload * Body: { * "lang": "en", * "translation": { * "key1": "value1", * "key2": "value2" * } * } */ function rwpai_upload_language_pack( WP_REST_Request $request ) { // Get and validate language code. $lang = sanitize_text_field( $request->get_param( 'lang' ) ); if ( empty( $lang ) ) { return new WP_Error( 'missing_lang', 'Language code is required.', array( 'status' => 400 ) ); } // Take only the first part before '-' (e.g., 'en' from 'en-US'). $lang = explode( '-', $lang )[0]; // Validate language code format (2-11 letter code). if ( ! preg_match( '/^[a-z]{2,11}$/', $lang ) ) { return new WP_Error( 'invalid_lang', 'Invalid language code format. Use 2-11 letter codes like "en", "fr", "es".', array( 'status' => 400 ) ); } // Get translation data. $translation = $request->get_param( 'translation' ); if ( empty( $translation ) || ! is_array( $translation ) ) { return new WP_Error( 'missing_translation', 'Translation data is required and must be a JSON object.', array( 'status' => 400 ) ); } // Prepare upload directory. $upload_dir = wp_upload_dir(); $custom_dir = trailingslashit( $upload_dir['basedir'] ) . 'ai-agent/translations/'; // Create directory if it doesn't exist. if ( ! file_exists( $custom_dir ) ) { // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_mkdir if ( ! wp_mkdir_p( $custom_dir ) ) { return new WP_Error( 'mkdir_failed', 'Failed to create translations directory.', array( 'status' => 500 ) ); } } // Prepare file path. $file_path = $custom_dir . $lang . '.json'; // Convert translation array to JSON. $json_content = wp_json_encode( $translation, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE ); if ( false === $json_content ) { return new WP_Error( 'json_encode_failed', 'Failed to encode translation data to JSON.', array( 'status' => 500 ) ); } // Write file. // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_file_put_contents $result = file_put_contents( $file_path, $json_content ); if ( false === $result ) { return new WP_Error( 'write_failed', 'Failed to write translation file.', array( 'status' => 500 ) ); } return rest_ensure_response( array( 'status' => true, 'message' => 'Translation uploaded successfully.', 'lang' => $lang, 'path' => str_replace( $upload_dir['basedir'], '', $file_path ), ) ); } /** * Delete a custom translation file. * * This function allows administrators to delete custom translation JSON files * from wp-content/uploads/ai-agent/translations/ * Note: This only deletes custom translations, not plugin default translations. * * @param WP_REST_Request $request The REST API request object containing:. * - 'lang' (string): Language code (e.g., 'en', 'fr', 'es'). * * @return WP_REST_Response|WP_Error Response with success status or error. * * Example usage (via REST API): * DELETE /wp-json/natuteto/v1/localization?lang=en * Returns: {"status": true, "message": "Translation deleted successfully."} */ function rwpai_delete_language_pack( WP_REST_Request $request ) { // Get and validate language code. $lang = sanitize_text_field( $request->get_param( 'lang' ) ); if ( empty( $lang ) ) { return new WP_Error( 'missing_lang', 'Language code is required.', array( 'status' => 400 ) ); } // Take only the first part before '-' (e.g., 'en' from 'en-US'). $lang = explode( '-', $lang )[0]; // Validate language code format (2-11 letter code). if ( ! preg_match( '/^[a-z]{2,11}$/', $lang ) ) { return new WP_Error( 'invalid_lang', 'Invalid language code format. Use 2-11 letter codes like "en", "fr", "es".', array( 'status' => 400 ) ); } // Prepare upload directory path. $upload_dir = wp_upload_dir(); $custom_dir = trailingslashit( $upload_dir['basedir'] ) . 'ai-agent/translations/'; $file_path = $custom_dir . $lang . '.json'; // Check if file exists. if ( ! file_exists( $file_path ) ) { return rest_ensure_response( array( 'status' => true, 'message' => 'Translation file not found: ' . $lang . '.json', 'lang' => $lang, ) ); } // Security check: ensure we're only deleting from the custom translations directory. $real_custom_dir = realpath( $custom_dir ); $real_file_path = realpath( $file_path ); if ( false === $real_custom_dir || false === $real_file_path || strpos( $real_file_path, $real_custom_dir ) !== 0 ) { return new WP_Error( 'forbidden', 'Cannot delete files outside custom translations directory.', array( 'status' => 403 ) ); } // Delete the file. // phpcs:ignore WordPress.WP.AlternativeFunctions.unlink_unlink if ( ! unlink( $file_path ) ) { return new WP_Error( 'delete_failed', 'Failed to delete translation file.', array( 'status' => 500 ) ); } return rest_ensure_response( array( 'status' => true, 'message' => 'Translation deleted successfully.', 'lang' => $lang, ) ); } /** * Handles REST request for sample posts. * * @param WP_REST_Request $request The REST request providing optional filters. * * @return WP_REST_Response The response containing sample post data. */ function rwpai_get_sample_post( $request ) { $post_type = $request->get_param( 'type' ) ?? 'post'; $args = array( 'post_type' => $post_type, 'posts_per_page' => 2, 'post_status' => 'publish', 'orderby' => 'date', 'order' => 'DESC', ); $posts = get_posts( $args ); if ( empty( $posts ) ) { return rest_ensure_response( array() ); } $data = array_map( function ( $post ) { return array( 'title' => get_the_title( $post ), 'url' => get_permalink( $post ), ); }, $posts ); return rest_ensure_response( $data ); } /** * Get list of all post categories. * * @return array List of categories with id, name, and slug. */ function rwpai_get_post_categories() { $cats = get_categories( array( 'hide_empty' => false, ) ); return array_map( function ( $cat ) { return array( 'id' => $cat->term_id, 'name' => $cat->name, 'slug' => $cat->slug, ); }, $cats ); }