diff --git a/native/wordpress/maple-fonts-wp/.claude/settings.local.json b/native/wordpress/maple-fonts-wp/.claude/settings.local.json new file mode 100644 index 0000000..c545817 --- /dev/null +++ b/native/wordpress/maple-fonts-wp/.claude/settings.local.json @@ -0,0 +1,19 @@ +{ + "permissions": { + "allow": [ + "Bash(mkdir:*)", + "Bash(for po in *.po)", + "Bash(do mo=\"$po%.po.mo\")", + "Bash(msgfmt:*)", + "Bash(echo:*)", + "Bash(done)", + "Bash(ls:*)", + "Bash(xargs:*)", + "Bash(chmod:*)", + "Bash(composer install:*)", + "Bash(php:*)", + "Bash(docker --version:*)", + "Bash(brew list:*)" + ] + } +} diff --git a/native/wordpress/maple-fonts-wp/.github/index.php b/native/wordpress/maple-fonts-wp/.github/index.php new file mode 100644 index 0000000..6220032 --- /dev/null +++ b/native/wordpress/maple-fonts-wp/.github/index.php @@ -0,0 +1,2 @@ +> $GITHUB_OUTPUT + + - name: Cache Composer dependencies + uses: actions/cache@v4 + with: + path: ${{ steps.composer-cache.outputs.dir }} + key: ${{ runner.os }}-composer-${{ hashFiles('**/composer.lock') }} + restore-keys: ${{ runner.os }}-composer- + + - name: Install dependencies + run: composer install --prefer-dist --no-progress + + - name: Run PHP_CodeSniffer + run: ./vendor/bin/phpcs --report=checkstyle | cs2pr + continue-on-error: true + + phpcompat: + name: PHP Compatibility + runs-on: ubuntu-latest + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Setup PHP + uses: shivammathur/setup-php@v2 + with: + php-version: '8.2' + tools: composer + coverage: none + + - name: Get Composer cache directory + id: composer-cache + run: echo "dir=$(composer config cache-files-dir)" >> $GITHUB_OUTPUT + + - name: Cache Composer dependencies + uses: actions/cache@v4 + with: + path: ${{ steps.composer-cache.outputs.dir }} + key: ${{ runner.os }}-composer-${{ hashFiles('**/composer.lock') }} + restore-keys: ${{ runner.os }}-composer- + + - name: Install dependencies + run: composer install --prefer-dist --no-progress + + - name: Run PHPCompatibility + run: ./vendor/bin/phpcs -p --standard=PHPCompatibilityWP --runtime-set testVersion 7.4- --extensions=php --ignore=vendor,tests . + + test: + name: PHP ${{ matrix.php }} / WP ${{ matrix.wordpress }} + runs-on: ubuntu-latest + needs: [phpcs, phpcompat] + + strategy: + fail-fast: false + matrix: + php: ['7.4', '8.0', '8.1', '8.2', '8.3'] + wordpress: ['6.5', '6.6', 'latest'] + exclude: + # PHP 8.3 not fully compatible with older WP versions + - php: '8.3' + wordpress: '6.5' + + services: + mysql: + image: mysql:8.0 + env: + MYSQL_ROOT_PASSWORD: root + MYSQL_DATABASE: wordpress_test + ports: + - 3306:3306 + options: >- + --health-cmd="mysqladmin ping" + --health-interval=10s + --health-timeout=5s + --health-retries=5 + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Setup PHP + uses: shivammathur/setup-php@v2 + with: + php-version: ${{ matrix.php }} + extensions: dom, curl, libxml, mbstring, zip, pcntl, pdo, pdo_mysql, mysqli, gd, exif, intl + tools: composer + coverage: none + + - name: Get Composer cache directory + id: composer-cache + run: echo "dir=$(composer config cache-files-dir)" >> $GITHUB_OUTPUT + + - name: Cache Composer dependencies + uses: actions/cache@v4 + with: + path: ${{ steps.composer-cache.outputs.dir }} + key: ${{ runner.os }}-php${{ matrix.php }}-composer-${{ hashFiles('**/composer.lock') }} + restore-keys: ${{ runner.os }}-php${{ matrix.php }}-composer- + + - name: Install dependencies + run: composer install --prefer-dist --no-progress + + - name: Install WordPress test suite + run: bash bin/install-wp-tests.sh wordpress_test root root 127.0.0.1 ${{ matrix.wordpress }} true + + - name: Run PHPUnit + run: ./vendor/bin/phpunit --configuration phpunit.xml.dist + + build: + name: Build Plugin + runs-on: ubuntu-latest + needs: test + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Setup PHP + uses: shivammathur/setup-php@v2 + with: + php-version: '8.2' + tools: composer + coverage: none + + - name: Install dependencies (production) + run: composer install --prefer-dist --no-dev --optimize-autoloader --no-progress + + - name: Create build directory + run: mkdir -p build + + - name: Build plugin zip + run: | + zip -r build/maple-local-fonts.zip . \ + -x ".git/*" \ + -x ".github/*" \ + -x "tests/*" \ + -x "bin/*" \ + -x "build/*" \ + -x "vendor/bin/*" \ + -x "*.xml" \ + -x "*.xml.dist" \ + -x "composer.*" \ + -x "phpcs.xml*" \ + -x ".editorconfig" \ + -x ".gitignore" \ + -x "CLAUDE.md" \ + -x "SECURITY.md" \ + -x "GOOGLE_FONTS_API.md" \ + -x "WORDPRESS_COMPATIBILITY.md" + + - name: Upload artifact + uses: actions/upload-artifact@v4 + with: + name: maple-local-fonts + path: native/wordpress/maple-fonts-wp/build/maple-local-fonts.zip + retention-days: 30 diff --git a/native/wordpress/maple-fonts-wp/.github/workflows/index.php b/native/wordpress/maple-fonts-wp/.github/workflows/index.php new file mode 100644 index 0000000..6220032 --- /dev/null +++ b/native/wordpress/maple-fonts-wp/.github/workflows/index.php @@ -0,0 +1,2 @@ + 15, + 'sslverify' => true, + 'user-agent' => 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36', + ]); + + if (is_wp_error($response)) { + return new WP_Error( + 'request_failed', + 'Could not connect to Google Fonts: ' . $response->get_error_message() + ); + } + + $status = wp_remote_retrieve_response_code($response); + if ($status === 400) { + return new WP_Error('font_not_found', 'Font not found on Google Fonts'); + } + if ($status !== 200) { + return new WP_Error('http_error', 'Google Fonts returned HTTP ' . $status); + } + + $css = wp_remote_retrieve_body($response); + if (empty($css)) { + return new WP_Error('empty_response', 'Empty response from Google Fonts'); + } + + // Verify we got WOFF2 (sanity check) + if (strpos($css, '.woff2)') === false) { + return new WP_Error('wrong_format', 'Did not receive WOFF2 format - check user-agent'); + } + + return $css; +} +``` + +--- + +## Parsing the CSS Response + +### Sample Response + +Google returns CSS like this: + +```css +/* latin-ext */ +@font-face { + font-family: 'Open Sans'; + font-style: normal; + font-weight: 400; + font-stretch: 100%; + font-display: swap; + src: url(https://fonts.gstatic.com/s/opensans/v40/memSYaGs126MiZpBA-UvWbX2vVnXBbObj2OVZyOOSr4dVJWUgsg-1x4gaVI.woff2) format('woff2'); + unicode-range: U+0100-02AF, U+0304, U+0308, ...; +} +/* latin */ +@font-face { + font-family: 'Open Sans'; + font-style: normal; + font-weight: 400; + font-stretch: 100%; + font-display: swap; + src: url(https://fonts.gstatic.com/s/opensans/v40/memSYaGs126MiZpBA-UvWbX2vVnXBbObj2OVZyOOSr4dVJWUgsjZ0B4gaVI.woff2) format('woff2'); + unicode-range: U+0000-00FF, U+0131, ...; +} +``` + +### Key Observations + +1. **Multiple @font-face blocks per weight** — Google splits fonts by unicode subset (latin, latin-ext, cyrillic, etc.) +2. **We want the "latin" subset** — It's the base and covers most use cases +3. **Each block has:** font-family, font-style, font-weight, src URL, unicode-range + +### Unicode Subset Strategy + +**Option A: Download only latin (simpler, smaller)** +- Parse CSS, identify latin blocks, download only those +- Good for most sites + +**Option B: Download all subsets (more complete)** +- Download all WOFF2 files +- Larger but supports more languages + +**Recommended: Option A** — Start with latin subset. Can add option for more subsets later. + +### Parsing Function + +```php +/** + * Parse Google Fonts CSS and extract font face data. + * + * @param string $css CSS content from Google Fonts + * @param string $font_name Expected font family name + * @return array|WP_Error Array of font face data or error + */ +function mlf_parse_google_css($css, $font_name) { + $font_faces = []; + + // Match all @font-face blocks + $pattern = '/@font-face\s*\{([^}]+)\}/s'; + if (!preg_match_all($pattern, $css, $matches)) { + return new WP_Error('parse_failed', 'Could not parse CSS - no @font-face rules found'); + } + + foreach ($matches[1] as $block) { + $face_data = mlf_parse_font_face_block($block); + + if (is_wp_error($face_data)) { + continue; // Skip malformed blocks + } + + // Verify font family matches (security) + if (strcasecmp($face_data['family'], $font_name) !== 0) { + continue; + } + + // Create unique key for weight+style combo + $key = $face_data['weight'] . '-' . $face_data['style']; + + // Prefer latin subset (usually comes after latin-ext) + // Check if this is a latin block by unicode-range + $is_latin = mlf_is_latin_subset($face_data['unicode_range']); + + // Only store if: + // 1. We don't have this weight/style yet, OR + // 2. This is latin and replaces non-latin + if (!isset($font_faces[$key]) || $is_latin) { + $font_faces[$key] = $face_data; + } + } + + if (empty($font_faces)) { + return new WP_Error('no_fonts', 'No valid font faces found in CSS'); + } + + return array_values($font_faces); +} + +/** + * Parse a single @font-face block. + * + * @param string $block Content inside @font-face { } + * @return array|WP_Error Parsed data or error + */ +function mlf_parse_font_face_block($block) { + $data = []; + + // Extract font-family + if (preg_match('/font-family:\s*[\'"]?([^;\'"]+)[\'"]?;/i', $block, $m)) { + $data['family'] = trim($m[1]); + } else { + return new WP_Error('missing_family', 'Missing font-family'); + } + + // Extract font-weight + if (preg_match('/font-weight:\s*(\d+);/i', $block, $m)) { + $data['weight'] = $m[1]; + } else { + return new WP_Error('missing_weight', 'Missing font-weight'); + } + + // Extract font-style + if (preg_match('/font-style:\s*(\w+);/i', $block, $m)) { + $data['style'] = $m[1]; + } else { + $data['style'] = 'normal'; // Default + } + + // Extract src URL - MUST be fonts.gstatic.com + if (preg_match('/src:\s*url\((https:\/\/fonts\.gstatic\.com\/[^)]+\.woff2)\)/i', $block, $m)) { + $data['url'] = $m[1]; + } else { + return new WP_Error('missing_src', 'Missing or invalid src URL'); + } + + // Extract unicode-range (optional, for subset detection) + if (preg_match('/unicode-range:\s*([^;]+);/i', $block, $m)) { + $data['unicode_range'] = trim($m[1]); + } else { + $data['unicode_range'] = ''; + } + + return $data; +} + +/** + * Check if unicode-range indicates latin subset. + * Latin typically starts with U+0000-00FF. + * + * @param string $range Unicode range string + * @return bool True if appears to be latin subset + */ +function mlf_is_latin_subset($range) { + // Latin subset typically includes basic ASCII range + // and does NOT include extended Latin (U+0100+) as primary + + if (empty($range)) { + return true; // Assume latin if no range specified + } + + // Latin subset usually starts with U+0000 and includes U+00FF + // Latin-ext starts with U+0100 + if (preg_match('/U\+0000/', $range) && !preg_match('/^U\+0100/', $range)) { + return true; + } + + return false; +} +``` + +--- + +## Downloading WOFF2 Files + +### Download Function + +```php +/** + * Download a single WOFF2 file from Google Fonts. + * + * @param string $url Google Fonts static URL + * @param string $font_slug Font slug (e.g., "open-sans") + * @param string $weight Font weight (e.g., "400") + * @param string $style Font style (e.g., "normal") + * @return string|WP_Error Local file path or error + */ +function mlf_download_font_file($url, $font_slug, $weight, $style) { + // Validate URL is from Google + if (!mlf_is_valid_google_fonts_url($url)) { + return new WP_Error('invalid_url', 'URL is not from Google Fonts'); + } + + // Build local filename + $filename = sprintf('%s_%s_%s.woff2', $font_slug, $style, $weight); + $filename = sanitize_file_name($filename); + + // Get destination path + $font_dir = wp_get_font_dir(); + $destination = trailingslashit($font_dir['path']) . $filename; + + // Validate destination path + if (!mlf_validate_font_path($destination)) { + return new WP_Error('invalid_path', 'Invalid destination path'); + } + + // Ensure directory exists + if (!wp_mkdir_p($font_dir['path'])) { + return new WP_Error('mkdir_failed', 'Could not create fonts directory'); + } + + // Download file + $response = wp_remote_get($url, [ + 'timeout' => 30, + 'sslverify' => true, + 'user-agent' => 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36', + ]); + + if (is_wp_error($response)) { + return new WP_Error( + 'download_failed', + 'Failed to download font file: ' . $response->get_error_message() + ); + } + + $status = wp_remote_retrieve_response_code($response); + if ($status !== 200) { + return new WP_Error('http_error', 'Font download returned HTTP ' . $status); + } + + $content = wp_remote_retrieve_body($response); + if (empty($content)) { + return new WP_Error('empty_file', 'Downloaded font file is empty'); + } + + // Verify it looks like a WOFF2 file (magic bytes: wOF2) + if (substr($content, 0, 4) !== 'wOF2') { + return new WP_Error('invalid_format', 'Downloaded file is not valid WOFF2'); + } + + // Write file using WP Filesystem + global $wp_filesystem; + if (empty($wp_filesystem)) { + require_once ABSPATH . 'wp-admin/includes/file.php'; + WP_Filesystem(); + } + + if (!$wp_filesystem->put_contents($destination, $content, FS_CHMOD_FILE)) { + return new WP_Error('write_failed', 'Could not write font file'); + } + + return $destination; +} +``` + +### Batch Download Function + +```php +/** + * Download all font files for a font family. + * + * @param string $font_name Font family name + * @param array $weights Weights to download + * @param array $styles Styles to download + * @return array|WP_Error Array of downloaded files or error + */ +function mlf_download_font_family($font_name, $weights, $styles) { + // Fetch CSS from Google + $css = mlf_fetch_google_css($font_name, $weights, $styles); + if (is_wp_error($css)) { + return $css; + } + + // Parse CSS to get font face data + $font_faces = mlf_parse_google_css($css, $font_name); + if (is_wp_error($font_faces)) { + return $font_faces; + } + + // Generate slug from font name + $font_slug = sanitize_title($font_name); + + // Download each font file + $downloaded = []; + $errors = []; + + foreach ($font_faces as $face) { + $result = mlf_download_font_file( + $face['url'], + $font_slug, + $face['weight'], + $face['style'] + ); + + if (is_wp_error($result)) { + $errors[] = $result->get_error_message(); + continue; + } + + $downloaded[] = [ + 'path' => $result, + 'weight' => $face['weight'], + 'style' => $face['style'], + ]; + } + + // If no files downloaded, return error + if (empty($downloaded)) { + return new WP_Error( + 'download_failed', + 'Could not download any font files: ' . implode(', ', $errors) + ); + } + + return [ + 'font_name' => $font_name, + 'font_slug' => $font_slug, + 'files' => $downloaded, + ]; +} +``` + +--- + +## Error Handling + +### Common Errors + +| Error | Cause | User Message | +|-------|-------|--------------| +| Font not found | Typo in font name | "Font not found on Google Fonts. Check the spelling." | +| Network timeout | Slow connection | "Could not connect to Google Fonts. Please try again." | +| Invalid format | Wrong user-agent | Internal error - should not happen | +| Write failed | Permissions | "Could not save font files. Check directory permissions." | + +### Error Messages (User-Friendly) + +```php +/** + * Convert internal error codes to user-friendly messages. + * + * @param WP_Error $error The error object + * @return string User-friendly message + */ +function mlf_get_user_error_message($error) { + $code = $error->get_error_code(); + + $messages = [ + 'font_not_found' => 'Font not found on Google Fonts. Please check the spelling and try again.', + 'request_failed' => 'Could not connect to Google Fonts. Please check your internet connection and try again.', + 'http_error' => 'Google Fonts returned an error. Please try again later.', + 'parse_failed' => 'Could not process the font data. The font may not be available.', + 'download_failed' => 'Could not download the font files. Please try again.', + 'write_failed' => 'Could not save font files. Please check that wp-content/fonts is writable.', + 'mkdir_failed' => 'Could not create fonts directory. Please check file permissions.', + 'invalid_path' => 'Invalid file path. Please contact support.', + 'invalid_url' => 'Invalid font URL. Please contact support.', + ]; + + return $messages[$code] ?? 'An unexpected error occurred. Please try again.'; +} +``` + +--- + +## Complete Downloader Class + +```php +fetch_css($font_name, $weights, $styles); + if (is_wp_error($css)) { + return $css; + } + + // Parse CSS + $font_faces = $this->parse_css($css, $font_name); + if (is_wp_error($font_faces)) { + return $font_faces; + } + + // Download files + $font_slug = sanitize_title($font_name); + $downloaded = $this->download_files($font_faces, $font_slug); + if (is_wp_error($downloaded)) { + return $downloaded; + } + + return [ + 'font_name' => $font_name, + 'font_slug' => $font_slug, + 'files' => $downloaded, + ]; + } + + private function build_url($font_name, $weights, $styles) { + $family = str_replace(' ', '+', $font_name); + sort($weights); + + $has_italic = in_array('italic', $styles, true); + $has_normal = in_array('normal', $styles, true); + + if ($has_normal && !$has_italic) { + $wght = implode(';', $weights); + return "https://fonts.googleapis.com/css2?family={$family}:wght@{$wght}&display=swap"; + } + + $variations = []; + foreach ($weights as $weight) { + if ($has_normal) { + $variations[] = "0,{$weight}"; + } + if ($has_italic) { + $variations[] = "1,{$weight}"; + } + } + + $variation_string = implode(';', $variations); + return "https://fonts.googleapis.com/css2?family={$family}:ital,wght@{$variation_string}&display=swap"; + } + + private function fetch_css($font_name, $weights, $styles) { + $url = $this->build_url($font_name, $weights, $styles); + + $response = wp_remote_get($url, [ + 'timeout' => 15, + 'sslverify' => true, + 'user-agent' => $this->user_agent, + ]); + + if (is_wp_error($response)) { + return new WP_Error('request_failed', $response->get_error_message()); + } + + $status = wp_remote_retrieve_response_code($response); + if ($status === 400) { + return new WP_Error('font_not_found', 'Font not found'); + } + if ($status !== 200) { + return new WP_Error('http_error', 'HTTP ' . $status); + } + + $css = wp_remote_retrieve_body($response); + if (empty($css) || strpos($css, '.woff2)') === false) { + return new WP_Error('invalid_response', 'Invalid CSS response'); + } + + return $css; + } + + private function parse_css($css, $font_name) { + // Implementation as shown above + // Returns array of font face data + } + + private function download_files($font_faces, $font_slug) { + // Implementation as shown above + // Returns array of downloaded file info + } +} +``` + +--- + +## Testing Checklist + +- [ ] Valid font name (Open Sans) returns CSS with WOFF2 URLs +- [ ] Invalid font name returns appropriate error +- [ ] Multiple weights are all downloaded +- [ ] Italic styles are handled correctly +- [ ] Files are saved to correct location +- [ ] Files have correct WOFF2 magic bytes +- [ ] Timeout handling works (test with slow connection) +- [ ] User-agent produces WOFF2 (not TTF/WOFF) diff --git a/native/wordpress/maple-fonts-wp/SECURITY.md b/native/wordpress/maple-fonts-wp/SECURITY.md new file mode 100644 index 0000000..3016412 --- /dev/null +++ b/native/wordpress/maple-fonts-wp/SECURITY.md @@ -0,0 +1,621 @@ +# SECURITY.md — Maple Local Fonts Security Requirements + +## Overview + +This document covers all security requirements for the Maple Local Fonts plugin. Reference this when writing ANY PHP code. + +--- + +## ABSPATH Check (Every PHP File) + +Every PHP file MUST start with this check. No exceptions. + +```php +query("SELECT * FROM table WHERE id = " . $_POST['id']); + +// ALWAYS do this +$wpdb->get_results($wpdb->prepare( + "SELECT * FROM %i WHERE id = %d", + $table_name, + absint($_POST['id']) +)); +``` + +**Note:** This plugin should rarely need direct SQL. Use WordPress APIs (`get_posts`, `wp_insert_post`, etc.) which handle escaping internally. + +### A2 - Authentication + +All admin actions require capability check: + +```php +if (!current_user_can('edit_theme_options')) { + wp_die('Unauthorized', 'Error', ['response' => 403]); +} +``` + +### A3 - Sensitive Data + +- No API keys (Google Fonts CSS2 API is public) +- No user credentials stored +- No PII collected + +### A5 - Broken Access Control + +**Order of checks for ALL AJAX handlers:** + +```php +public function handle_ajax_action() { + // 1. Nonce verification FIRST + if (!check_ajax_referer('mlf_action_name', 'nonce', false)) { + wp_send_json_error(['message' => 'Security check failed'], 403); + } + + // 2. Capability check SECOND + if (!current_user_can('edit_theme_options')) { + wp_send_json_error(['message' => 'Unauthorized'], 403); + } + + // 3. Input validation THIRD + // ... validate all inputs ... + + // 4. Process request + // ... actual logic ... +} +``` + +### A7 - Cross-Site Scripting (XSS) + +**Escape ALL output:** + +```php +// HTML content +echo esc_html($font_name); + +// HTML attributes +echo ''; + +// URLs +echo ''; + +// JavaScript data +wp_localize_script('mlf-admin', 'mlfData', [ + 'fontName' => esc_js($font_name), // Or let wp_localize_script handle it +]); + +// Translatable strings with variables +printf( + esc_html__('Installed: %s', 'maple-local-fonts'), + esc_html($font_name) +); +``` + +**Never trust input for output:** +```php +// WRONG - XSS vulnerability +echo '
' . $_POST['font_name'] . '
'; + +// RIGHT - sanitize input, escape output +$font_name = sanitize_text_field($_POST['font_name']); +echo '
' . esc_html($font_name) . '
'; +``` + +### A8 - Insecure Deserialization + +```php +// NEVER use unserialize() on external data +$data = unserialize($_POST['data']); // DANGEROUS + +// Use JSON instead +$data = json_decode(sanitize_text_field($_POST['data']), true); +if (json_last_error() !== JSON_ERROR_NONE) { + wp_send_json_error(['message' => 'Invalid data format']); +} +``` + +### A9 - Vulnerable Components + +- No external PHP libraries +- Use only WordPress core functions +- Keep dependencies to zero + +--- + +## Nonce Implementation + +### Creating Nonces + +**In admin page form:** +```php +wp_nonce_field('mlf_download_font', 'mlf_nonce'); +``` + +**For AJAX (via wp_localize_script):** +```php +wp_localize_script('mlf-admin', 'mlfData', [ + 'ajaxUrl' => admin_url('admin-ajax.php'), + 'nonce' => wp_create_nonce('mlf_download_font'), +]); +``` + +### Verifying Nonces + +**AJAX handler:** +```php +// Returns false on failure, doesn't die (we handle response ourselves) +if (!check_ajax_referer('mlf_download_font', 'nonce', false)) { + wp_send_json_error(['message' => 'Security check failed'], 403); +} +``` + +**Form submission:** +```php +if (!wp_verify_nonce($_POST['mlf_nonce'], 'mlf_download_font')) { + wp_die('Security check failed'); +} +``` + +### Nonce Names + +Use consistent, descriptive nonce action names: + +| Action | Nonce Name | +|--------|------------| +| Download font | `mlf_download_font` | +| Delete font | `mlf_delete_font` | +| Update settings | `mlf_update_settings` | + +--- + +## Input Validation + +### Font Name Validation + +```php +$font_name = isset($_POST['font_name']) ? sanitize_text_field($_POST['font_name']) : ''; + +// Strict allowlist pattern - alphanumeric, spaces, hyphens only +if (!preg_match('/^[a-zA-Z0-9\s\-]+$/', $font_name)) { + wp_send_json_error(['message' => 'Invalid font name: only letters, numbers, spaces, and hyphens allowed']); +} + +// Length limit +if (strlen($font_name) > 100) { + wp_send_json_error(['message' => 'Font name too long']); +} + +// Not empty +if (empty($font_name)) { + wp_send_json_error(['message' => 'Font name required']); +} +``` + +### Weight Validation + +```php +$weights = isset($_POST['weights']) ? (array) $_POST['weights'] : []; + +// Convert to integers +$weights = array_map('absint', $weights); + +// Strict allowlist +$allowed_weights = [100, 200, 300, 400, 500, 600, 700, 800, 900]; +$weights = array_intersect($weights, $allowed_weights); + +// Must have at least one +if (empty($weights)) { + wp_send_json_error(['message' => 'At least one weight required']); +} +``` + +### Style Validation + +```php +$styles = isset($_POST['styles']) ? (array) $_POST['styles'] : []; + +// Strict allowlist - only these two values ever +$allowed_styles = ['normal', 'italic']; +$styles = array_filter($styles, function($style) use ($allowed_styles) { + return in_array($style, $allowed_styles, true); +}); + +// Must have at least one +if (empty($styles)) { + wp_send_json_error(['message' => 'At least one style required']); +} +``` + +### Font Family ID Validation (for delete) + +```php +$font_id = isset($_POST['font_id']) ? absint($_POST['font_id']) : 0; + +if ($font_id < 1) { + wp_send_json_error(['message' => 'Invalid font ID']); +} + +// Verify it exists and is a font family +$font = get_post($font_id); +if (!$font || $font->post_type !== 'wp_font_family') { + wp_send_json_error(['message' => 'Font not found']); +} + +// Verify it's one we imported (not a theme font) +if (get_post_meta($font_id, '_mlf_imported', true) !== '1') { + wp_send_json_error(['message' => 'Cannot delete theme fonts']); +} +``` + +--- + +## File Operation Security + +### Path Traversal Prevention + +```php +/** + * Validate that a path is within the WordPress fonts directory. + * Prevents path traversal attacks. + * + * @param string $path Full path to validate + * @return bool True if path is safe, false otherwise + */ +function mlf_validate_font_path($path) { + $font_dir = wp_get_font_dir(); + $fonts_path = wp_normalize_path(trailingslashit($font_dir['path'])); + + // Resolve to real path (handles ../ etc) + $real_path = realpath($path); + + // If realpath fails, file doesn't exist yet - validate the directory + if ($real_path === false) { + $dir = dirname($path); + $real_dir = realpath($dir); + if ($real_dir === false) { + return false; + } + $real_path = wp_normalize_path($real_dir . '/' . basename($path)); + } else { + $real_path = wp_normalize_path($real_path); + } + + // Must be within fonts directory + return strpos($real_path, $fonts_path) === 0; +} +``` + +### Filename Sanitization + +```php +/** + * Sanitize and validate a font filename. + * + * @param string $filename The filename to validate + * @return string|false Sanitized filename or false if invalid + */ +function mlf_sanitize_font_filename($filename) { + // WordPress sanitization first + $filename = sanitize_file_name($filename); + + // Must have .woff2 extension + if (pathinfo($filename, PATHINFO_EXTENSION) !== 'woff2') { + return false; + } + + // No path components + if ($filename !== basename($filename)) { + return false; + } + + // Reasonable length + if (strlen($filename) > 200) { + return false; + } + + return $filename; +} +``` + +### Safe File Writing + +```php +/** + * Safely write a font file to the fonts directory. + * + * @param string $filename Sanitized filename + * @param string $content File content + * @return string|WP_Error File path on success, WP_Error on failure + */ +function mlf_write_font_file($filename, $content) { + // Validate filename + $safe_filename = mlf_sanitize_font_filename($filename); + if ($safe_filename === false) { + return new WP_Error('invalid_filename', 'Invalid filename'); + } + + // Get fonts directory + $font_dir = wp_get_font_dir(); + $destination = trailingslashit($font_dir['path']) . $safe_filename; + + // Validate path + if (!mlf_validate_font_path($destination)) { + return new WP_Error('invalid_path', 'Invalid file path'); + } + + // Ensure directory exists + if (!wp_mkdir_p($font_dir['path'])) { + return new WP_Error('mkdir_failed', 'Could not create fonts directory'); + } + + // Write file + global $wp_filesystem; + if (empty($wp_filesystem)) { + require_once ABSPATH . 'wp-admin/includes/file.php'; + WP_Filesystem(); + } + + if (!$wp_filesystem->put_contents($destination, $content, FS_CHMOD_FILE)) { + return new WP_Error('write_failed', 'Could not write font file'); + } + + return $destination; +} +``` + +### Safe File Deletion + +```php +/** + * Safely delete a font file. + * + * @param string $path Full path to the file + * @return bool True on success, false on failure + */ +function mlf_delete_font_file($path) { + // Validate path is within fonts directory + if (!mlf_validate_font_path($path)) { + return false; + } + + // Must be a .woff2 file + if (pathinfo($path, PATHINFO_EXTENSION) !== 'woff2') { + return false; + } + + // File must exist + if (!file_exists($path)) { + return true; // Already gone, that's fine + } + + return wp_delete_file($path); +} +``` + +--- + +## HTTP Request Security + +### Outbound Requests (Google Fonts) + +```php +$response = wp_remote_get($url, [ + 'timeout' => 15, + 'sslverify' => true, // Always verify SSL + 'user-agent' => 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36', +]); + +// Check for errors +if (is_wp_error($response)) { + // Log error, return gracefully + error_log('MLF: Google Fonts request failed - ' . $response->get_error_message()); + return new WP_Error('request_failed', 'Could not connect to Google Fonts'); +} + +// Check HTTP status +$status = wp_remote_retrieve_response_code($response); +if ($status !== 200) { + return new WP_Error('http_error', 'Google Fonts returned status ' . $status); +} + +// Get body +$body = wp_remote_retrieve_body($response); +if (empty($body)) { + return new WP_Error('empty_response', 'Empty response from Google Fonts'); +} +``` + +### URL Validation (Google Fonts only) + +```php +/** + * Validate that a URL is a legitimate Google Fonts URL. + * + * @param string $url URL to validate + * @return bool True if valid Google Fonts URL + */ +function mlf_is_valid_google_fonts_url($url) { + $parsed = wp_parse_url($url); + + if (!$parsed || !isset($parsed['host'])) { + return false; + } + + // Only allow Google Fonts domains + $allowed_hosts = [ + 'fonts.googleapis.com', + 'fonts.gstatic.com', + ]; + + return in_array($parsed['host'], $allowed_hosts, true); +} +``` + +--- + +## AJAX Handler Complete Template + +```php + 'Security check failed'], 403); + } + + // 2. CAPABILITY CHECK + if (!current_user_can('edit_theme_options')) { + wp_send_json_error(['message' => 'Unauthorized'], 403); + } + + // 3. INPUT VALIDATION + $font_name = isset($_POST['font_name']) ? sanitize_text_field($_POST['font_name']) : ''; + if (empty($font_name) || !preg_match('/^[a-zA-Z0-9\s\-]+$/', $font_name) || strlen($font_name) > 100) { + wp_send_json_error(['message' => 'Invalid font name']); + } + + $weights = isset($_POST['weights']) ? array_map('absint', (array) $_POST['weights']) : []; + $weights = array_intersect($weights, [100, 200, 300, 400, 500, 600, 700, 800, 900]); + if (empty($weights)) { + wp_send_json_error(['message' => 'At least one weight required']); + } + + $styles = isset($_POST['styles']) ? (array) $_POST['styles'] : []; + $styles = array_intersect($styles, ['normal', 'italic']); + if (empty($styles)) { + wp_send_json_error(['message' => 'At least one style required']); + } + + // 4. PROCESS REQUEST + try { + $downloader = new MLF_Font_Downloader(); + $result = $downloader->download($font_name, $weights, $styles); + + if (is_wp_error($result)) { + wp_send_json_error(['message' => $result->get_error_message()]); + } + + wp_send_json_success([ + 'message' => sprintf('Successfully installed %s', esc_html($font_name)), + 'font_id' => $result, + ]); + } catch (Exception $e) { + error_log('MLF Download Error: ' . $e->getMessage()); + wp_send_json_error(['message' => 'An unexpected error occurred']); + } + } + + /** + * Handle font deletion AJAX request. + */ + public function handle_delete() { + // 1. NONCE CHECK + if (!check_ajax_referer('mlf_delete_font', 'nonce', false)) { + wp_send_json_error(['message' => 'Security check failed'], 403); + } + + // 2. CAPABILITY CHECK + if (!current_user_can('edit_theme_options')) { + wp_send_json_error(['message' => 'Unauthorized'], 403); + } + + // 3. INPUT VALIDATION + $font_id = isset($_POST['font_id']) ? absint($_POST['font_id']) : 0; + if ($font_id < 1) { + wp_send_json_error(['message' => 'Invalid font ID']); + } + + // Verify font exists and is ours + $font = get_post($font_id); + if (!$font || $font->post_type !== 'wp_font_family') { + wp_send_json_error(['message' => 'Font not found']); + } + if (get_post_meta($font_id, '_mlf_imported', true) !== '1') { + wp_send_json_error(['message' => 'Cannot delete theme fonts']); + } + + // 4. PROCESS REQUEST + try { + $registry = new MLF_Font_Registry(); + $result = $registry->delete_font($font_id); + + if (is_wp_error($result)) { + wp_send_json_error(['message' => $result->get_error_message()]); + } + + wp_send_json_success(['message' => 'Font deleted successfully']); + } catch (Exception $e) { + error_log('MLF Delete Error: ' . $e->getMessage()); + wp_send_json_error(['message' => 'An unexpected error occurred']); + } + } +} +``` + +--- + +## Security Checklist + +Before committing any code: + +- [ ] ABSPATH check at top of every PHP file +- [ ] index.php exists in every directory +- [ ] All AJAX handlers verify nonce first +- [ ] All AJAX handlers check capability second +- [ ] All user input sanitized with appropriate function +- [ ] All user input validated against allowlists where applicable +- [ ] All output escaped with appropriate function +- [ ] File paths validated to prevent traversal +- [ ] No direct SQL queries (use WordPress APIs) +- [ ] No `unserialize()` on user input +- [ ] No `eval()` or similar dynamic execution +- [ ] External URLs validated before use +- [ ] Error messages don't expose sensitive info diff --git a/native/wordpress/maple-fonts-wp/WORDPRESS_COMPATIBILITY.md b/native/wordpress/maple-fonts-wp/WORDPRESS_COMPATIBILITY.md new file mode 100644 index 0000000..59d61cc --- /dev/null +++ b/native/wordpress/maple-fonts-wp/WORDPRESS_COMPATIBILITY.md @@ -0,0 +1,560 @@ +# WORDPRESS_COMPATIBILITY.md — WordPress & Plugin Compatibility + +## Overview + +This document covers compatibility requirements for WordPress core systems and popular plugins. Reference this when building the font registry class and integration points. + +--- + +## WordPress Version Requirements + +**Minimum: WordPress 6.5** + +WordPress 6.5 introduced the Font Library API which this plugin depends on. Earlier versions will not work. + +```php +// Check on activation +register_activation_hook(__FILE__, function() { + if (version_compare(get_bloginfo('version'), '6.5', '<')) { + deactivate_plugins(plugin_basename(__FILE__)); + wp_die( + 'Maple Local Fonts requires WordPress 6.5 or higher for Font Library support.', + 'Plugin Activation Error', + ['back_link' => true] + ); + } +}); + +// Also check on admin init (in case WP was downgraded) +add_action('admin_init', function() { + if (version_compare(get_bloginfo('version'), '6.5', '<')) { + deactivate_plugins(plugin_basename(__FILE__)); + add_action('admin_notices', function() { + echo '

Maple Local Fonts has been deactivated. It requires WordPress 6.5 or higher.

'; + }); + } +}); +``` + +--- + +## WordPress Font Library API + +### How It Works + +WordPress 6.5+ stores fonts using custom post types: + +| Post Type | Purpose | +|-----------|---------| +| `wp_font_family` | Font family (e.g., "Open Sans") | +| `wp_font_face` | Individual weight/style variant (child of family) | + +Fonts are stored in `wp-content/fonts/` by default. + +### Getting the Fonts Directory + +```php +// ALWAYS use this function, never hardcode paths +$font_dir = wp_get_font_dir(); + +// Returns: +[ + 'path' => '/var/www/html/wp-content/fonts', + 'url' => 'https://example.com/wp-content/fonts', + 'subdir' => '', + 'basedir' => '/var/www/html/wp-content/fonts', + 'baseurl' => 'https://example.com/wp-content/fonts', +] +``` + +### Registering a Font Family + +```php +/** + * Register a font family with WordPress Font Library. + * + * @param string $font_name Display name (e.g., "Open Sans") + * @param string $font_slug Slug (e.g., "open-sans") + * @param array $files Array of downloaded file data + * @return int|WP_Error Font family post ID or error + */ +function mlf_register_font_family($font_name, $font_slug, $files) { + // Check if font already exists + $existing = get_posts([ + 'post_type' => 'wp_font_family', + 'name' => $font_slug, + 'posts_per_page' => 1, + 'post_status' => 'publish', + ]); + + if (!empty($existing)) { + return new WP_Error('font_exists', 'Font family already installed'); + } + + // Build font family settings + $font_family_settings = [ + 'name' => $font_name, + 'slug' => $font_slug, + 'fontFamily' => sprintf('"%s", sans-serif', $font_name), + 'fontFace' => [], + ]; + + // Add each font face + foreach ($files as $file) { + $font_dir = wp_get_font_dir(); + $relative_path = str_replace($font_dir['path'], '', $file['path']); + + $font_family_settings['fontFace'][] = [ + 'fontFamily' => $font_name, + 'fontWeight' => $file['weight'], + 'fontStyle' => $file['style'], + 'src' => 'file:.' . $font_dir['basedir'] . $relative_path, + ]; + } + + // Create font family post + $family_id = wp_insert_post([ + 'post_type' => 'wp_font_family', + 'post_title' => $font_name, + 'post_name' => $font_slug, + 'post_status' => 'publish', + 'post_content' => wp_json_encode($font_family_settings), + ]); + + if (is_wp_error($family_id)) { + return $family_id; + } + + // Mark as imported by our plugin (for identification) + update_post_meta($family_id, '_mlf_imported', '1'); + update_post_meta($family_id, '_mlf_import_date', current_time('mysql')); + + // Create font face posts (children) + foreach ($files as $file) { + $font_dir = wp_get_font_dir(); + + $face_settings = [ + 'fontFamily' => $font_name, + 'fontWeight' => $file['weight'], + 'fontStyle' => $file['style'], + 'src' => 'file:.' . $font_dir['baseurl'] . '/' . basename($file['path']), + ]; + + wp_insert_post([ + 'post_type' => 'wp_font_face', + 'post_parent' => $family_id, + 'post_status' => 'publish', + 'post_content' => wp_json_encode($face_settings), + ]); + } + + // Clear font caches + delete_transient('wp_font_library_fonts'); + + return $family_id; +} +``` + +### Deleting a Font Family + +```php +/** + * Delete a font family and its files. + * + * @param int $family_id Font family post ID + * @return bool|WP_Error True on success, error on failure + */ +function mlf_delete_font_family($family_id) { + $family = get_post($family_id); + + if (!$family || $family->post_type !== 'wp_font_family') { + return new WP_Error('not_found', 'Font family not found'); + } + + // Verify it's one we imported + if (get_post_meta($family_id, '_mlf_imported', true) !== '1') { + return new WP_Error('not_ours', 'Cannot delete fonts not imported by this plugin'); + } + + // Get font faces + $faces = get_children([ + 'post_parent' => $family_id, + 'post_type' => 'wp_font_face', + ]); + + $font_dir = wp_get_font_dir(); + + // Delete font face files and posts + foreach ($faces as $face) { + $settings = json_decode($face->post_content, true); + + if (isset($settings['src'])) { + // Convert file:. URL to path + $src = $settings['src']; + $src = str_replace('file:.', '', $src); + + // Handle both URL and path formats + if (strpos($src, $font_dir['baseurl']) !== false) { + $file_path = str_replace($font_dir['baseurl'], $font_dir['path'], $src); + } else { + $file_path = $font_dir['path'] . '/' . basename($src); + } + + // Validate path before deletion + if (mlf_validate_font_path($file_path) && file_exists($file_path)) { + wp_delete_file($file_path); + } + } + + wp_delete_post($face->ID, true); + } + + // Delete family post + wp_delete_post($family_id, true); + + // Clear caches + delete_transient('wp_font_library_fonts'); + + return true; +} +``` + +### Listing Installed Fonts + +```php +/** + * Get all fonts imported by this plugin. + * + * @return array Array of font data + */ +function mlf_get_imported_fonts() { + $fonts = get_posts([ + 'post_type' => 'wp_font_family', + 'posts_per_page' => 100, + 'post_status' => 'publish', + 'meta_key' => '_mlf_imported', + 'meta_value' => '1', + ]); + + $result = []; + + foreach ($fonts as $font) { + $settings = json_decode($font->post_content, true); + + // Get variants + $faces = get_children([ + 'post_parent' => $font->ID, + 'post_type' => 'wp_font_face', + ]); + + $variants = []; + foreach ($faces as $face) { + $face_settings = json_decode($face->post_content, true); + $variants[] = [ + 'weight' => $face_settings['fontWeight'] ?? '400', + 'style' => $face_settings['fontStyle'] ?? 'normal', + ]; + } + + $result[] = [ + 'id' => $font->ID, + 'name' => $settings['name'] ?? $font->post_title, + 'slug' => $settings['slug'] ?? $font->post_name, + 'variants' => $variants, + 'import_date' => get_post_meta($font->ID, '_mlf_import_date', true), + ]; + } + + return $result; +} +``` + +--- + +## Gutenberg FSE Integration + +### How Fonts Appear in the Editor + +Once registered via the Font Library API, fonts automatically appear in: + +1. **Global Styles** → Typography → Font dropdown +2. **Block settings** → Typography → Font dropdown (when per-block typography is enabled) + +No additional integration code is needed — WordPress handles this automatically. + +### Theme.json Compatibility + +**DO NOT:** +- Directly modify theme.json +- Filter `wp_theme_json_data_theme` to inject fonts (let Font Library handle it) +- Override global styles CSS directly + +**DO:** +- Use the Font Library API (post types) +- Let WordPress generate CSS custom properties +- Trust the system + +### CSS Custom Properties + +When a font is applied in Global Styles, WordPress generates: + +```css +body { + --wp--preset--font-family--open-sans: "Open Sans", sans-serif; +} +``` + +And applies it: + +```css +body { + font-family: var(--wp--preset--font-family--open-sans); +} +``` + +Our plugin doesn't need to touch this — it's automatic. + +--- + +## WooCommerce Compatibility + +### HPOS (High-Performance Order Storage) + +WooCommerce's HPOS moves order data from post meta to custom tables. We must declare compatibility. + +```php +// Declare HPOS compatibility +add_action('before_woocommerce_init', function() { + if (class_exists(\Automattic\WooCommerce\Utilities\FeaturesUtil::class)) { + \Automattic\WooCommerce\Utilities\FeaturesUtil::declare_compatibility( + 'custom_order_tables', + __FILE__, + true + ); + } +}); +``` + +### Why We're Compatible + +Our plugin: +- Does NOT interact with orders at all +- Does NOT query wp_posts for order data +- Does NOT use wp_postmeta for order data +- Only uses wp_font_family and wp_font_face post types + +We're inherently compatible because we don't touch WooCommerce data. + +### Frontend Considerations + +**DO NOT:** +- Override `.woocommerce` class styles +- Override `.wc-block-*` styles +- Target cart/checkout elements specifically + +**DO:** +- Let WooCommerce elements inherit from body/heading fonts +- Let global styles cascade naturally + +WooCommerce product titles, descriptions, and other text will naturally inherit the fonts set via Global Styles. No special handling needed. + +--- + +## Wordfence Compatibility + +### Potential Concerns + +1. **Outbound requests** to Google Fonts during import +2. **AJAX endpoints** for admin actions +3. **File operations** in wp-content + +### Why We're Compatible + +**Outbound Requests:** +- Only occur during admin import (user-initiated action) +- Target well-known domains (fonts.googleapis.com, fonts.gstatic.com) +- Use standard `wp_remote_get()` which Wordfence allows +- No runtime external requests on frontend + +**AJAX Endpoints:** +- Use standard `admin-ajax.php` (not custom endpoints) +- Include proper nonces +- Follow WordPress patterns that Wordfence expects + +**File Operations:** +- Write only to `wp-content/fonts/` (WordPress default directory) +- Use WordPress Filesystem API +- Don't create executable files + +### Testing with Wordfence + +Test these scenarios with Wordfence active: + +- [ ] Learning Mode: Import should succeed +- [ ] Enabled Mode: Import should succeed +- [ ] Rate Limiting: Admin AJAX not blocked +- [ ] Firewall: No false positives on font download + +--- + +## LearnDash Compatibility + +### Overview + +LearnDash is a WordPress LMS that uses: +- Custom post types (courses, lessons, topics, quizzes) +- Custom templates +- Focus Mode (distraction-free learning) + +### Why We're Compatible + +Our plugin: +- Doesn't touch LearnDash post types +- Doesn't modify LearnDash templates +- Doesn't inject CSS on frontend +- Lets Global Styles cascade to LearnDash content + +LearnDash course content, lesson text, and quiz questions will inherit the fonts set in Global Styles automatically. + +### Focus Mode Consideration + +LearnDash Focus Mode uses its own template. Fonts set via Global Styles will apply because: +- Focus Mode still loads theme.json styles +- CSS custom properties cascade to all content +- No special handling needed + +**DO NOT:** +- Target `.learndash-*` classes specifically +- Override Focus Mode styles +- Inject custom CSS for LearnDash + +--- + +## WPForms Compatibility + +### Overview + +WPForms renders forms via shortcodes and blocks. Form styling is handled by WPForms. + +### Why We're Compatible + +- Form labels and text inherit from body font +- We don't override `.wpforms-*` classes +- No JavaScript conflicts (we have no frontend JS) + +### Consideration + +If a user wants form text in a different font, they should use WPForms' built-in styling options or custom CSS — not expect our plugin to handle it. + +--- + +## General Best Practices + +### What We Hook Into + +```php +// Admin menu +add_action('admin_menu', [$this, 'register_menu']); + +// Admin assets (only on our page) +add_action('admin_enqueue_scripts', [$this, 'enqueue_admin_assets']); + +// AJAX handlers +add_action('wp_ajax_mlf_download_font', [$this, 'handle_download']); +add_action('wp_ajax_mlf_delete_font', [$this, 'handle_delete']); + +// WooCommerce HPOS compatibility +add_action('before_woocommerce_init', [$this, 'declare_hpos_compatibility']); +``` + +### What We DON'T Hook Into + +```php +// NO frontend hooks +// add_action('wp_enqueue_scripts', ...); // DON'T DO THIS +// add_action('wp_head', ...); // DON'T DO THIS +// add_action('wp_footer', ...); // DON'T DO THIS + +// NO theme modification hooks +// add_filter('wp_theme_json_data_theme', ...); // Let Font Library handle it + +// NO WooCommerce hooks +// add_action('woocommerce_*', ...); // DON'T DO THIS + +// NO content filters +// add_filter('the_content', ...); // DON'T DO THIS +``` + +--- + +## Conflict Debugging + +If a user reports a conflict, check: + +### 1. Plugin Load Order + +Our plugin should load with default priority. Check if another plugin is: +- Modifying the Font Library +- Overriding font CSS +- Filtering theme.json + +### 2. CSS Specificity + +If fonts aren't applying: +- Check browser DevTools for CSS cascade +- Look for more specific selectors overriding global styles +- Check for `!important` declarations + +### 3. Cache Issues + +Font changes not appearing: +- Clear browser cache +- Clear any caching plugins (WP Rocket, W3TC, etc.) +- Clear CDN cache if applicable +- WordPress transients: `delete_transient('wp_font_library_fonts')` + +### 4. JavaScript Errors + +If admin page isn't working: +- Check browser console for JS errors +- Look for conflicts with other admin scripts +- Verify jQuery isn't being dequeued + +--- + +## Compatibility Checklist + +Before releasing: + +### WordPress Core +- [ ] Works on WordPress 6.5 +- [ ] Works on WordPress 6.6+ +- [ ] Font Library API integration works +- [ ] Fonts appear in Global Styles +- [ ] Fonts apply correctly on frontend + +### WooCommerce +- [ ] HPOS compatibility declared +- [ ] No errors in WooCommerce status page +- [ ] Product pages render correctly with custom fonts +- [ ] Cart/Checkout not affected + +### Wordfence +- [ ] Import works with firewall enabled +- [ ] No blocked requests +- [ ] No false positive security alerts + +### LearnDash +- [ ] Course content inherits fonts +- [ ] Focus Mode renders correctly +- [ ] No JavaScript conflicts + +### WPForms +- [ ] Forms render correctly +- [ ] No styling conflicts + +### Other +- [ ] No PHP errors in debug.log +- [ ] No JavaScript errors in console +- [ ] Admin page loads correctly +- [ ] No memory issues during import diff --git a/native/wordpress/maple-fonts-wp/assets/admin.css b/native/wordpress/maple-fonts-wp/assets/admin.css new file mode 100644 index 0000000..420b8b4 --- /dev/null +++ b/native/wordpress/maple-fonts-wp/assets/admin.css @@ -0,0 +1,354 @@ +/** + * Maple Local Fonts - Admin Styles + * + * @package Maple_Local_Fonts + */ + +/* Container */ +.mlf-wrap { + max-width: 900px; +} + +.mlf-container { + margin-top: 20px; +} + +/* Sections */ +.mlf-section { + background: #fff; + border: 1px solid #c3c4c7; + border-radius: 4px; + padding: 20px; + margin-bottom: 20px; +} + +.mlf-section h2 { + margin-top: 0; + padding-bottom: 10px; + border-bottom: 1px solid #c3c4c7; +} + +/* Form */ +.mlf-form-row { + margin-bottom: 20px; +} + +.mlf-form-row label { + display: block; + font-weight: 600; + margin-bottom: 8px; +} + +.mlf-form-row input[type="text"] { + width: 100%; + max-width: 400px; + padding: 8px 12px; +} + +.mlf-form-row .description { + margin-top: 8px; + color: #646970; + font-style: italic; +} + +/* Checkbox Grid */ +.mlf-checkbox-grid { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(180px, 1fr)); + gap: 10px; +} + +.mlf-checkbox-grid-small { + grid-template-columns: repeat(auto-fill, minmax(120px, 1fr)); + max-width: 300px; +} + +.mlf-checkbox-label { + display: flex; + align-items: center; + gap: 8px; + padding: 8px 12px; + background: #f6f7f7; + border: 1px solid #dcdcde; + border-radius: 4px; + cursor: pointer; + transition: background-color 0.15s ease; +} + +.mlf-checkbox-label:hover { + background: #f0f0f1; +} + +.mlf-checkbox-label input[type="checkbox"] { + margin: 0; +} + +.mlf-checkbox-label input[type="checkbox"]:checked + span { + font-weight: 600; +} + +/* File Count */ +.mlf-form-row-info { + padding: 12px 16px; + background: #f0f6fc; + border: 1px solid #c5d9ed; + border-radius: 4px; +} + +.mlf-file-count { + color: #2271b1; +} + +.mlf-file-count strong { + font-size: 1.1em; +} + +/* Font Preview */ +.mlf-preview-section { + margin-top: 20px; + padding-top: 20px; + border-top: 1px solid #dcdcde; +} + +.mlf-preview-box { + background: #fff; + border: 1px solid #dcdcde; + border-radius: 4px; + padding: 24px; + min-height: 100px; + position: relative; +} + +.mlf-preview-text { + display: flex; + flex-direction: column; + gap: 16px; +} + +.mlf-preview-sample { + display: block; + line-height: 1.4; +} + +.mlf-preview-heading { + font-size: 28px; +} + +.mlf-preview-paragraph { + font-size: 16px; + color: #50575e; +} + +.mlf-preview-loading { + display: flex; + align-items: center; + gap: 10px; + color: #646970; + padding: 20px 0; +} + +.mlf-preview-loading .spinner { + float: none; + margin: 0; +} + +.mlf-preview-error { + color: #b32d2e; + padding: 20px 0; + text-align: center; +} + +/* Submit Row */ +.mlf-form-row-submit { + display: flex; + align-items: center; + gap: 12px; +} + +.mlf-form-row-submit .spinner { + float: none; + margin: 0; +} + +/* Messages */ +.mlf-message { + padding: 12px 16px; + border-radius: 4px; + margin-top: 16px; +} + +.mlf-message-success { + background: #d4edda; + border: 1px solid #c3e6cb; + color: #155724; +} + +.mlf-message-error { + background: #f8d7da; + border: 1px solid #f5c6cb; + color: #721c24; +} + +/* Font List */ +.mlf-font-list { + display: flex; + flex-direction: column; + gap: 12px; +} + +.mlf-font-item { + display: flex; + justify-content: space-between; + align-items: center; + padding: 16px; + background: #f6f7f7; + border: 1px solid #dcdcde; + border-radius: 4px; +} + +.mlf-font-info { + flex: 1; +} + +.mlf-font-name { + margin: 0 0 4px 0; + font-size: 1.1em; +} + +.mlf-font-variants { + margin: 0; + color: #646970; + font-size: 0.9em; +} + +.mlf-font-actions { + margin-left: 20px; +} + +.mlf-delete-btn { + color: #b32d2e; + border-color: #b32d2e; +} + +.mlf-delete-btn:hover { + background: #b32d2e; + color: #fff; + border-color: #b32d2e; +} + +.mlf-delete-btn:disabled { + opacity: 0.6; + cursor: not-allowed; +} + +/* No Fonts Message */ +.mlf-no-fonts { + color: #646970; + font-style: italic; + padding: 20px; + text-align: center; + background: #f6f7f7; + border-radius: 4px; +} + +/* Info Box */ +.mlf-info-box { + display: flex; + align-items: flex-start; + gap: 12px; + padding: 16px; + background: #f0f6fc; + border: 1px solid #c5d9ed; + border-radius: 4px; +} + +.mlf-info-box .dashicons { + color: #2271b1; + margin-top: 2px; +} + +.mlf-info-box p { + margin: 0; + color: #1d2327; +} + +.mlf-info-box a { + color: #2271b1; + text-decoration: none; +} + +.mlf-info-box a:hover { + text-decoration: underline; +} + +/* Classic Theme Info Box */ +.mlf-info-box-classic { + flex-direction: row; + align-items: flex-start; + background: #fcf9e8; + border-color: #d4b106; +} + +.mlf-info-box-classic .dashicons { + color: #9d7e05; +} + +.mlf-classic-theme-info { + flex: 1; +} + +.mlf-classic-theme-info p { + margin: 0 0 12px 0; +} + +.mlf-classic-theme-info p:last-child { + margin-bottom: 0; +} + +.mlf-classic-theme-info .description { + color: #646970; + font-style: italic; + margin-top: 12px; +} + +.mlf-code-example { + background: #1d2327; + color: #f0f0f1; + padding: 12px 16px; + border-radius: 4px; + font-family: Consolas, Monaco, monospace; + font-size: 12px; + line-height: 1.6; + overflow-x: auto; + white-space: pre; + margin: 12px 0; +} + +/* Loading State */ +.mlf-loading { + opacity: 0.6; + pointer-events: none; +} + +/* Responsive */ +@media screen and (max-width: 782px) { + .mlf-checkbox-grid { + grid-template-columns: repeat(2, 1fr); + } + + .mlf-font-item { + flex-direction: column; + align-items: flex-start; + gap: 12px; + } + + .mlf-font-actions { + margin-left: 0; + } +} + +@media screen and (max-width: 480px) { + .mlf-checkbox-grid { + grid-template-columns: 1fr; + } +} diff --git a/native/wordpress/maple-fonts-wp/assets/admin.js b/native/wordpress/maple-fonts-wp/assets/admin.js new file mode 100644 index 0000000..7d0b014 --- /dev/null +++ b/native/wordpress/maple-fonts-wp/assets/admin.js @@ -0,0 +1,354 @@ +/** + * Maple Local Fonts - Admin JavaScript + * + * @package Maple_Local_Fonts + */ + +(function($) { + 'use strict'; + + var MLF = { + /** + * Preview debounce timer. + */ + previewTimer: null, + + /** + * Currently loaded preview font. + */ + currentPreviewFont: null, + + /** + * Initialize the admin functionality. + */ + init: function() { + this.bindEvents(); + this.updateFileCount(); + }, + + /** + * Bind event handlers. + */ + bindEvents: function() { + // Form submission + $('#mlf-import-form').on('submit', this.handleDownload.bind(this)); + + // Delete button clicks + $(document).on('click', '.mlf-delete-btn', this.handleDelete.bind(this)); + + // Update file count on checkbox change + $('#mlf-import-form').on('change', 'input[type="checkbox"]', this.updateFileCount.bind(this)); + + // Font name input for preview (debounced) + $('#mlf-font-name').on('input', this.handleFontNameInput.bind(this)); + }, + + /** + * Handle font name input for preview (debounced). + * + * @param {Event} e Input event. + */ + handleFontNameInput: function(e) { + var fontName = $(e.target).val().trim(); + + // Clear previous timer + if (this.previewTimer) { + clearTimeout(this.previewTimer); + } + + // Hide preview if empty + if (!fontName) { + this.hidePreview(); + return; + } + + // Validate font name (only allowed characters) + if (!/^[a-zA-Z0-9\s\-]+$/.test(fontName)) { + this.hidePreview(); + return; + } + + // Debounce: wait 500ms before loading preview + this.previewTimer = setTimeout(function() { + MLF.loadFontPreview(fontName); + }, 500); + }, + + /** + * Load font preview from Google Fonts. + * + * @param {string} fontName Font family name. + */ + loadFontPreview: function(fontName) { + var $section = $('#mlf-preview-section'); + var $text = $('#mlf-preview-text'); + var $loading = $('#mlf-preview-loading'); + var $error = $('#mlf-preview-error'); + + // Skip if same font already loaded + if (this.currentPreviewFont === fontName) { + return; + } + + // Show section with loading state + $section.show(); + $text.hide(); + $loading.show(); + $error.hide(); + + // Remove previous preview font link + $('#mlf-preview-font-link').remove(); + + // Build Google Fonts URL for preview (just 400 weight for preview) + var fontFamily = fontName.replace(/\s+/g, '+'); + var previewUrl = 'https://fonts.googleapis.com/css2?family=' + encodeURIComponent(fontFamily) + ':wght@400&display=swap'; + + // Create link element + var $link = $('', { + id: 'mlf-preview-font-link', + rel: 'stylesheet', + href: previewUrl + }); + + // Handle load success + $link.on('load', function() { + MLF.currentPreviewFont = fontName; + $text.css('font-family', '"' + fontName + '", sans-serif'); + $loading.hide(); + $text.show(); + }); + + // Handle load error + $link.on('error', function() { + MLF.currentPreviewFont = null; + $loading.hide(); + $error.show(); + }); + + // Append to head + $('head').append($link); + + // Fallback timeout (5 seconds) + setTimeout(function() { + if ($loading.is(':visible')) { + // Check if font actually loaded by measuring text width + var testSpan = $('').text('test').css({ + 'font-family': '"' + fontName + '", monospace', + 'position': 'absolute', + 'visibility': 'hidden' + }).appendTo('body'); + + var testWidth = testSpan.width(); + + var fallbackSpan = $('').text('test').css({ + 'font-family': 'monospace', + 'position': 'absolute', + 'visibility': 'hidden' + }).appendTo('body'); + + var fallbackWidth = fallbackSpan.width(); + + testSpan.remove(); + fallbackSpan.remove(); + + if (testWidth !== fallbackWidth) { + // Font loaded successfully + MLF.currentPreviewFont = fontName; + $text.css('font-family', '"' + fontName + '", sans-serif'); + $loading.hide(); + $text.show(); + } else { + // Font failed to load + MLF.currentPreviewFont = null; + $loading.hide(); + $error.show(); + } + } + }, 5000); + }, + + /** + * Hide the font preview section. + */ + hidePreview: function() { + this.currentPreviewFont = null; + $('#mlf-preview-section').hide(); + $('#mlf-preview-font-link').remove(); + }, + + /** + * Update the file count display. + */ + updateFileCount: function() { + var weights = $('input[name="weights[]"]:checked').length; + var styles = $('input[name="styles[]"]:checked').length; + var count = weights * styles; + + $('#mlf-file-count').text(count); + }, + + /** + * Handle font download form submission. + * + * @param {Event} e Form submit event. + */ + handleDownload: function(e) { + e.preventDefault(); + + var $form = $('#mlf-import-form'); + var $button = $('#mlf-download-btn'); + var $spinner = $('#mlf-spinner'); + var $message = $('#mlf-message'); + + // Get form values + var fontName = $('#mlf-font-name').val().trim(); + var weights = []; + var styles = []; + + $('input[name="weights[]"]:checked').each(function() { + weights.push($(this).val()); + }); + + $('input[name="styles[]"]:checked').each(function() { + styles.push($(this).val()); + }); + + // Validate + if (!fontName) { + this.showMessage($message, mapleLocalFontsData.strings.enterFontName, 'error'); + return; + } + + if (weights.length === 0) { + this.showMessage($message, mapleLocalFontsData.strings.selectWeight, 'error'); + return; + } + + if (styles.length === 0) { + this.showMessage($message, mapleLocalFontsData.strings.selectStyle, 'error'); + return; + } + + // Disable form + $form.addClass('mlf-loading'); + $button.prop('disabled', true).text(mapleLocalFontsData.strings.downloading); + $spinner.addClass('is-active'); + $message.hide(); + + // Send AJAX request + $.ajax({ + url: mapleLocalFontsData.ajaxUrl, + type: 'POST', + data: { + action: 'mlf_download_font', + nonce: mapleLocalFontsData.downloadNonce, + font_name: fontName, + weights: weights, + styles: styles + }, + success: function(response) { + if (response.success) { + MLF.showMessage($message, response.data.message, 'success'); + // Reload page to show new font + setTimeout(function() { + window.location.reload(); + }, 1500); + } else { + MLF.showMessage($message, response.data.message || mapleLocalFontsData.strings.error, 'error'); + } + }, + error: function() { + MLF.showMessage($message, mapleLocalFontsData.strings.error, 'error'); + }, + complete: function() { + $form.removeClass('mlf-loading'); + $button.prop('disabled', false).text($button.data('original-text') || 'Download & Install'); + $spinner.removeClass('is-active'); + } + }); + + // Store original button text + if (!$button.data('original-text')) { + $button.data('original-text', $button.text()); + } + }, + + /** + * Handle font deletion. + * + * @param {Event} e Click event. + */ + handleDelete: function(e) { + e.preventDefault(); + + var $button = $(e.currentTarget); + var fontId = $button.data('font-id'); + var $fontItem = $button.closest('.mlf-font-item'); + + // Confirm deletion + if (!confirm(mapleLocalFontsData.strings.confirmDelete)) { + return; + } + + // Disable button + $button.prop('disabled', true).text(mapleLocalFontsData.strings.deleting); + $fontItem.addClass('mlf-loading'); + + // Send AJAX request + $.ajax({ + url: mapleLocalFontsData.ajaxUrl, + type: 'POST', + data: { + action: 'mlf_delete_font', + nonce: mapleLocalFontsData.deleteNonce, + font_id: fontId + }, + success: function(response) { + if (response.success) { + // Remove font item with animation + $fontItem.slideUp(300, function() { + $(this).remove(); + + // Check if any fonts remain + if ($('.mlf-font-item').length === 0) { + $('#mlf-font-list').html( + '

No fonts installed yet.

' + ); + } + }); + } else { + alert(response.data.message || mapleLocalFontsData.strings.error); + $button.prop('disabled', false).text('Delete'); + $fontItem.removeClass('mlf-loading'); + } + }, + error: function() { + alert(mapleLocalFontsData.strings.error); + $button.prop('disabled', false).text('Delete'); + $fontItem.removeClass('mlf-loading'); + } + }); + }, + + /** + * Show a message to the user. + * + * @param {jQuery} $element Message element. + * @param {string} message Message text. + * @param {string} type Message type (success or error). + */ + showMessage: function($element, message, type) { + $element + .removeClass('mlf-message-success mlf-message-error') + .addClass('mlf-message-' + type) + .text(message) + .show(); + } + }; + + // Initialize on document ready + $(document).ready(function() { + MLF.init(); + }); + +})(jQuery); diff --git a/native/wordpress/maple-fonts-wp/assets/index.php b/native/wordpress/maple-fonts-wp/assets/index.php new file mode 100644 index 0000000..90593cd --- /dev/null +++ b/native/wordpress/maple-fonts-wp/assets/index.php @@ -0,0 +1,5 @@ +=7.4" + }, + "require-dev": { + "phpunit/phpunit": "^9.6", + "wp-coding-standards/wpcs": "^3.0", + "phpcompatibility/phpcompatibility-wp": "^2.1", + "dealerdirect/phpcodesniffer-composer-installer": "^1.0", + "yoast/phpunit-polyfills": "^2.0" + }, + "scripts": { + "test": "phpunit", + "phpcs": "phpcs", + "phpcbf": "phpcbf", + "compat": "phpcs -p --standard=PHPCompatibilityWP --runtime-set testVersion 7.4- --extensions=php --ignore=vendor,tests ." + }, + "config": { + "allow-plugins": { + "dealerdirect/phpcodesniffer-composer-installer": true + }, + "sort-packages": true + }, + "autoload-dev": { + "psr-4": { + "MapleLocalFonts\\Tests\\": "tests/" + } + } +} diff --git a/native/wordpress/maple-fonts-wp/includes/class-mlf-admin-page.php b/native/wordpress/maple-fonts-wp/includes/class-mlf-admin-page.php new file mode 100644 index 0000000..a1eb373 --- /dev/null +++ b/native/wordpress/maple-fonts-wp/includes/class-mlf-admin-page.php @@ -0,0 +1,210 @@ + 'Thin', + 200 => 'Extra Light', + 300 => 'Light', + 400 => 'Regular', + 500 => 'Medium', + 600 => 'Semi Bold', + 700 => 'Bold', + 800 => 'Extra Bold', + 900 => 'Black', + ]; + + /** + * Constructor. + */ + public function __construct() { + // Empty constructor - class is instantiated for rendering + } + + /** + * Render the admin page. + */ + public function render() { + $capability = function_exists('mlf_get_capability') ? mlf_get_capability() : 'edit_theme_options'; + if (!current_user_can($capability)) { + wp_die(esc_html__('You do not have sufficient permissions to access this page.', 'maple-local-fonts')); + } + + $registry = new MLF_Font_Registry(); + $installed_fonts = $registry->get_imported_fonts(); + ?> +
+

+ +
+ +
+

+ +
+
+ + +

+
+ +
+ +
+ weights as $weight => $label) : ?> + + +
+
+ +
+ +
+ + +
+
+ +
+ + + 2 + +
+ + + + +
+ + +
+ + +
+
+ + +
+

+ + +

+ +
+ +
+
+

+

+ +

+
+
+ +
+
+ +
+ +
+ + +
+ + + +
+ +
+

+

+
body {
+    font-family: "Open Sans", sans-serif;
+}
+h1, h2, h3, h4, h5, h6 {
+    font-family: "Open Sans", sans-serif;
+}
+

+ ' . esc_html__('Appearance → Customize → Additional CSS', 'maple-local-fonts') . '' + ); + ?> +

+
+
+ +
+
+
+ rate_limiter = new MLF_Rate_Limiter(10, 60); + } + + /** + * Handle font download AJAX request. + */ + public function handle_download() { + // 1. NONCE CHECK + if (!check_ajax_referer('mlf_download_font', 'nonce', false)) { + wp_send_json_error(['message' => __('Security check failed.', 'maple-local-fonts')], 403); + } + + // 2. CAPABILITY CHECK + $capability = function_exists('mlf_get_capability') ? mlf_get_capability() : 'edit_theme_options'; + if (!current_user_can($capability)) { + wp_send_json_error(['message' => __('Unauthorized.', 'maple-local-fonts')], 403); + } + + // 3. RATE LIMIT CHECK + if (!$this->rate_limiter->check_and_record('download')) { + wp_send_json_error([ + 'message' => __('Too many requests. Please wait a moment and try again.', 'maple-local-fonts'), + ], 429); + } + + // 4. INPUT VALIDATION + // Validate font name + $font_name = isset($_POST['font_name']) ? sanitize_text_field(wp_unslash($_POST['font_name'])) : ''; + + if (empty($font_name)) { + wp_send_json_error(['message' => __('Font name is required.', 'maple-local-fonts')]); + } + + // Strict allowlist pattern - alphanumeric, spaces, hyphens only + if (!preg_match('/^[a-zA-Z0-9\s\-]+$/', $font_name)) { + wp_send_json_error(['message' => __('Invalid font name: only letters, numbers, spaces, and hyphens allowed.', 'maple-local-fonts')]); + } + + if (strlen($font_name) > 100) { + wp_send_json_error(['message' => __('Font name is too long.', 'maple-local-fonts')]); + } + + // Validate weights + $weights = isset($_POST['weights']) ? array_map('absint', (array) $_POST['weights']) : []; + $allowed_weights = [100, 200, 300, 400, 500, 600, 700, 800, 900]; + $weights = array_intersect($weights, $allowed_weights); + + if (empty($weights)) { + wp_send_json_error(['message' => __('At least one weight is required.', 'maple-local-fonts')]); + } + + if (count($weights) > MLF_MAX_WEIGHTS_PER_FONT) { + wp_send_json_error(['message' => __('Too many weights selected.', 'maple-local-fonts')]); + } + + // Validate styles + $styles = isset($_POST['styles']) ? (array) $_POST['styles'] : []; + $allowed_styles = ['normal', 'italic']; + // Sanitize each style value before filtering + $styles = array_map('sanitize_text_field', $styles); + $styles = array_filter($styles, function($style) use ($allowed_styles) { + return in_array($style, $allowed_styles, true); + }); + + if (empty($styles)) { + wp_send_json_error(['message' => __('At least one style is required.', 'maple-local-fonts')]); + } + + // 5. PROCESS REQUEST + try { + $downloader = new MLF_Font_Downloader(); + $download_result = $downloader->download($font_name, $weights, $styles); + + if (is_wp_error($download_result)) { + wp_send_json_error(['message' => $this->get_user_error_message($download_result)]); + } + + // Register font with WordPress + $registry = new MLF_Font_Registry(); + $result = $registry->register_font( + $download_result['font_name'], + $download_result['font_slug'], + $download_result['files'] + ); + + if (is_wp_error($result)) { + wp_send_json_error(['message' => $this->get_user_error_message($result)]); + } + + wp_send_json_success([ + 'message' => sprintf( + /* translators: %s: font name */ + __('Successfully installed %s.', 'maple-local-fonts'), + esc_html($font_name) + ), + 'font_id' => $result, + ]); + } catch (Exception $e) { + // Sanitize exception message before logging (defense in depth) + error_log('MLF Download Error: ' . sanitize_text_field($e->getMessage())); + wp_send_json_error(['message' => __('An unexpected error occurred.', 'maple-local-fonts')]); + } + } + + /** + * Handle font deletion AJAX request. + */ + public function handle_delete() { + // 1. NONCE CHECK + if (!check_ajax_referer('mlf_delete_font', 'nonce', false)) { + wp_send_json_error(['message' => __('Security check failed.', 'maple-local-fonts')], 403); + } + + // 2. CAPABILITY CHECK + $capability = function_exists('mlf_get_capability') ? mlf_get_capability() : 'edit_theme_options'; + if (!current_user_can($capability)) { + wp_send_json_error(['message' => __('Unauthorized.', 'maple-local-fonts')], 403); + } + + // 3. RATE LIMIT CHECK + if (!$this->rate_limiter->check_and_record('delete')) { + wp_send_json_error([ + 'message' => __('Too many requests. Please wait a moment and try again.', 'maple-local-fonts'), + ], 429); + } + + // 4. INPUT VALIDATION + $font_id = isset($_POST['font_id']) ? absint($_POST['font_id']) : 0; + + if ($font_id < 1) { + wp_send_json_error(['message' => __('Invalid font ID.', 'maple-local-fonts')]); + } + + // Verify font exists and is a font family + $font = get_post($font_id); + if (!$font || $font->post_type !== 'wp_font_family') { + wp_send_json_error(['message' => __('Font not found.', 'maple-local-fonts')]); + } + + // Verify it's one we imported (not a theme font) + if (get_post_meta($font_id, '_mlf_imported', true) !== '1') { + wp_send_json_error(['message' => __('Cannot delete fonts not imported by this plugin.', 'maple-local-fonts')]); + } + + // 5. PROCESS REQUEST + try { + $registry = new MLF_Font_Registry(); + $result = $registry->delete_font($font_id); + + if (is_wp_error($result)) { + wp_send_json_error(['message' => $this->get_user_error_message($result)]); + } + + wp_send_json_success(['message' => __('Font deleted successfully.', 'maple-local-fonts')]); + } catch (Exception $e) { + // Sanitize exception message before logging (defense in depth) + error_log('MLF Delete Error: ' . sanitize_text_field($e->getMessage())); + wp_send_json_error(['message' => __('An unexpected error occurred.', 'maple-local-fonts')]); + } + } + + /** + * Convert internal error codes to user-friendly messages. + * + * @param WP_Error $error The error object. + * @return string User-friendly message. + */ + private function get_user_error_message($error) { + $code = $error->get_error_code(); + + $messages = [ + 'font_not_found' => __('Font not found on Google Fonts. Please check the spelling and try again.', 'maple-local-fonts'), + 'font_exists' => __('This font is already installed.', 'maple-local-fonts'), + 'request_failed' => __('Could not connect to Google Fonts. Please check your internet connection and try again.', 'maple-local-fonts'), + 'http_error' => __('Google Fonts returned an error. Please try again later.', 'maple-local-fonts'), + 'parse_failed' => __('Could not process the font data. The font may not be available.', 'maple-local-fonts'), + 'download_failed' => __('Could not download the font files. Please try again.', 'maple-local-fonts'), + 'write_failed' => __('Could not save font files. Please check that wp-content/fonts is writable.', 'maple-local-fonts'), + 'mkdir_failed' => __('Could not create fonts directory. Please check file permissions.', 'maple-local-fonts'), + 'invalid_path' => __('Invalid file path.', 'maple-local-fonts'), + 'invalid_url' => __('Invalid font URL.', 'maple-local-fonts'), + 'invalid_name' => __('Invalid font name.', 'maple-local-fonts'), + 'invalid_weights' => __('No valid weights specified.', 'maple-local-fonts'), + 'invalid_styles' => __('No valid styles specified.', 'maple-local-fonts'), + 'not_found' => __('Font not found.', 'maple-local-fonts'), + 'not_ours' => __('Cannot delete fonts not imported by this plugin.', 'maple-local-fonts'), + 'response_too_large' => __('The font data is too large to process. Please try selecting fewer weights.', 'maple-local-fonts'), + 'file_too_large' => __('The font file is too large to download.', 'maple-local-fonts'), + ]; + + return $messages[$code] ?? __('An unexpected error occurred. Please try again.', 'maple-local-fonts'); + } +} diff --git a/native/wordpress/maple-fonts-wp/includes/class-mlf-font-downloader.php b/native/wordpress/maple-fonts-wp/includes/class-mlf-font-downloader.php new file mode 100644 index 0000000..63f900c --- /dev/null +++ b/native/wordpress/maple-fonts-wp/includes/class-mlf-font-downloader.php @@ -0,0 +1,504 @@ +fetch_css($font_name, $weights, $styles); + if (is_wp_error($css)) { + return $css; + } + + // Parse CSS to get font face data + $font_faces = $this->parse_css($css, $font_name); + if (is_wp_error($font_faces)) { + return $font_faces; + } + + // Download each font file + $font_slug = sanitize_title($font_name); + $downloaded = $this->download_files($font_faces, $font_slug); + if (is_wp_error($downloaded)) { + return $downloaded; + } + + return [ + 'font_name' => $font_name, + 'font_slug' => $font_slug, + 'files' => $downloaded, + ]; + } + + /** + * Build Google Fonts CSS2 API URL. + * + * @param string $font_name Font family name. + * @param array $weights Array of weights. + * @param array $styles Array of styles. + * @return string Google Fonts CSS2 URL. + */ + private function build_url($font_name, $weights, $styles) { + // URL-encode font name (spaces become +) + $family = str_replace(' ', '+', $font_name); + + // Sort for consistent URLs + sort($weights); + + $has_italic = in_array('italic', $styles, true); + $has_normal = in_array('normal', $styles, true); + + // If only normal styles, simpler format + if ($has_normal && !$has_italic) { + $wght = implode(';', $weights); + return "https://fonts.googleapis.com/css2?family={$family}:wght@{$wght}&display=swap"; + } + + // Full format with ital axis + $variations = []; + foreach ($weights as $weight) { + if ($has_normal) { + $variations[] = "0,{$weight}"; + } + if ($has_italic) { + $variations[] = "1,{$weight}"; + } + } + + $variation_string = implode(';', $variations); + return "https://fonts.googleapis.com/css2?family={$family}:ital,wght@{$variation_string}&display=swap"; + } + + /** + * Fetch CSS from Google Fonts API. + * + * @param string $font_name Font family name. + * @param array $weights Weights to fetch. + * @param array $styles Styles to fetch. + * @return string|WP_Error CSS content or error. + */ + private function fetch_css($font_name, $weights, $styles) { + $url = $this->build_url($font_name, $weights, $styles); + + // Validate URL + if (!$this->is_valid_google_fonts_url($url)) { + return new WP_Error('invalid_url', 'Invalid Google Fonts URL'); + } + + // CRITICAL: Must use modern browser user-agent to get WOFF2 + $response = wp_remote_get($url, [ + 'timeout' => MLF_REQUEST_TIMEOUT, + 'sslverify' => true, + 'user-agent' => $this->user_agent, + ]); + + if (is_wp_error($response)) { + return new WP_Error('request_failed', $response->get_error_message()); + } + + $status = wp_remote_retrieve_response_code($response); + if ($status === 400) { + return new WP_Error('font_not_found', 'Font not found'); + } + if ($status !== 200) { + return new WP_Error('http_error', 'HTTP ' . $status); + } + + $css = wp_remote_retrieve_body($response); + if (empty($css)) { + return new WP_Error('empty_response', 'Empty response from Google Fonts'); + } + + // Check CSS response size to prevent memory issues + $max_size = defined('MLF_MAX_CSS_SIZE') ? MLF_MAX_CSS_SIZE : 512 * 1024; + if (strlen($css) > $max_size) { + return new WP_Error('response_too_large', 'CSS response exceeds maximum size limit'); + } + + // Verify we got WOFF2 (sanity check) + if (strpos($css, '.woff2)') === false) { + return new WP_Error('wrong_format', 'Did not receive WOFF2 format'); + } + + return $css; + } + + /** + * Parse Google Fonts CSS and extract font face data. + * + * @param string $css CSS content from Google Fonts. + * @param string $font_name Expected font family name. + * @return array|WP_Error Array of font face data or error. + */ + private function parse_css($css, $font_name) { + $font_faces = []; + + // Match all @font-face blocks + $pattern = '/@font-face\s*\{([^}]+)\}/s'; + if (!preg_match_all($pattern, $css, $matches)) { + return new WP_Error('parse_failed', 'Could not parse CSS - no @font-face rules found'); + } + + foreach ($matches[1] as $block) { + $face_data = $this->parse_font_face_block($block); + + if (is_wp_error($face_data)) { + continue; // Skip malformed blocks + } + + // Verify font family matches (security) + if (strcasecmp($face_data['family'], $font_name) !== 0) { + continue; + } + + // Create unique key for weight+style combo + $key = $face_data['weight'] . '-' . $face_data['style']; + + // Prefer latin subset (usually comes after latin-ext) + $is_latin = $this->is_latin_subset($face_data['unicode_range']); + + // Only store if: + // 1. We don't have this weight/style yet, OR + // 2. This is latin and replaces non-latin + if (!isset($font_faces[$key]) || $is_latin) { + $font_faces[$key] = $face_data; + } + } + + if (empty($font_faces)) { + return new WP_Error('no_fonts', 'No valid font faces found in CSS'); + } + + // Limit number of font faces to prevent excessive downloads + $max_faces = defined('MLF_MAX_FONT_FACES') ? MLF_MAX_FONT_FACES : 20; + $font_faces_array = array_values($font_faces); + if (count($font_faces_array) > $max_faces) { + $font_faces_array = array_slice($font_faces_array, 0, $max_faces); + } + + return $font_faces_array; + } + + /** + * Parse a single @font-face block. + * + * @param string $block Content inside @font-face { }. + * @return array|WP_Error Parsed data or error. + */ + private function parse_font_face_block($block) { + $data = []; + + // Extract font-family + if (preg_match('/font-family:\s*[\'"]?([^;\'"]+)[\'"]?;/i', $block, $m)) { + $data['family'] = trim($m[1]); + } else { + return new WP_Error('missing_family', 'Missing font-family'); + } + + // Extract font-weight + if (preg_match('/font-weight:\s*(\d+);/i', $block, $m)) { + $data['weight'] = $m[1]; + } else { + return new WP_Error('missing_weight', 'Missing font-weight'); + } + + // Extract font-style + if (preg_match('/font-style:\s*(\w+);/i', $block, $m)) { + $data['style'] = $m[1]; + } else { + $data['style'] = 'normal'; // Default + } + + // Extract src URL - MUST be fonts.gstatic.com + if (preg_match('/src:\s*url\((https:\/\/fonts\.gstatic\.com\/[^)]+\.woff2)\)/i', $block, $m)) { + $data['url'] = $m[1]; + } else { + return new WP_Error('missing_src', 'Missing or invalid src URL'); + } + + // Extract unicode-range (optional, for subset detection) + if (preg_match('/unicode-range:\s*([^;]+);/i', $block, $m)) { + $data['unicode_range'] = trim($m[1]); + } else { + $data['unicode_range'] = ''; + } + + return $data; + } + + /** + * Check if unicode-range indicates latin subset. + * + * @param string $range Unicode range string. + * @return bool True if appears to be latin subset. + */ + private function is_latin_subset($range) { + if (empty($range)) { + return true; // Assume latin if no range specified + } + + // Latin subset typically includes basic ASCII range + // and does NOT include extended Latin (U+0100+) as primary + if (preg_match('/U\+0000/', $range) && !preg_match('/^U\+0100/', $range)) { + return true; + } + + return false; + } + + /** + * Download all font files. + * + * @param array $font_faces Array of font face data. + * @param string $font_slug Font slug for filename. + * @return array|WP_Error Array of downloaded file info or error. + */ + private function download_files($font_faces, $font_slug) { + $downloaded = []; + $errors = []; + + foreach ($font_faces as $face) { + $result = $this->download_single_file( + $face['url'], + $font_slug, + $face['weight'], + $face['style'] + ); + + if (is_wp_error($result)) { + $errors[] = $result->get_error_message(); + continue; + } + + $downloaded[] = [ + 'path' => $result, + 'weight' => $face['weight'], + 'style' => $face['style'], + ]; + } + + // If no files downloaded, return error + if (empty($downloaded)) { + return new WP_Error( + 'download_failed', + 'Could not download any font files: ' . implode(', ', $errors) + ); + } + + return $downloaded; + } + + /** + * Download a single WOFF2 file from Google Fonts. + * + * @param string $url Google Fonts static URL. + * @param string $font_slug Font slug for filename. + * @param string $weight Font weight. + * @param string $style Font style. + * @return string|WP_Error Local file path or error. + */ + private function download_single_file($url, $font_slug, $weight, $style) { + // Validate URL is from Google + if (!$this->is_valid_google_fonts_url($url)) { + return new WP_Error('invalid_url', 'URL is not from Google Fonts'); + } + + // Build local filename + $filename = sprintf('%s_%s_%s.woff2', $font_slug, $style, $weight); + $filename = sanitize_file_name($filename); + + // Validate filename + $filename = $this->sanitize_font_filename($filename); + if ($filename === false) { + return new WP_Error('invalid_filename', 'Invalid filename'); + } + + // Get destination path + $font_dir = wp_get_font_dir(); + $destination = trailingslashit($font_dir['path']) . $filename; + + // Validate destination path before any file operations + if (!$this->validate_font_path($destination)) { + return new WP_Error('invalid_path', 'Invalid destination path'); + } + + // Ensure directory exists + if (!wp_mkdir_p($font_dir['path'])) { + return new WP_Error('mkdir_failed', 'Could not create fonts directory'); + } + + // Download file + $response = wp_remote_get($url, [ + 'timeout' => MLF_REQUEST_TIMEOUT, + 'sslverify' => true, + 'user-agent' => $this->user_agent, + ]); + + if (is_wp_error($response)) { + return new WP_Error('download_failed', 'Failed to download font file: ' . $response->get_error_message()); + } + + $status = wp_remote_retrieve_response_code($response); + if ($status !== 200) { + return new WP_Error('http_error', 'Font download returned HTTP ' . $status); + } + + $content = wp_remote_retrieve_body($response); + if (empty($content)) { + return new WP_Error('empty_file', 'Downloaded font file is empty'); + } + + // Check font file size to prevent memory issues + $max_size = defined('MLF_MAX_FONT_FILE_SIZE') ? MLF_MAX_FONT_FILE_SIZE : 5 * 1024 * 1024; + if (strlen($content) > $max_size) { + return new WP_Error('file_too_large', 'Font file exceeds maximum size limit'); + } + + // Verify it looks like a WOFF2 file (magic bytes: wOF2) + if (substr($content, 0, 4) !== 'wOF2') { + return new WP_Error('invalid_format', 'Downloaded file is not valid WOFF2'); + } + + // Write file using WP Filesystem + global $wp_filesystem; + if (empty($wp_filesystem)) { + require_once ABSPATH . 'wp-admin/includes/file.php'; + WP_Filesystem(); + } + + if (!$wp_filesystem->put_contents($destination, $content, FS_CHMOD_FILE)) { + return new WP_Error('write_failed', 'Could not write font file'); + } + + return $destination; + } + + /** + * Validate that a URL is a legitimate Google Fonts URL. + * + * @param string $url URL to validate. + * @return bool True if valid Google Fonts URL. + */ + private function is_valid_google_fonts_url($url) { + $parsed = wp_parse_url($url); + + if (!$parsed || !isset($parsed['host'])) { + return false; + } + + // Only allow Google Fonts domains + $allowed_hosts = [ + 'fonts.googleapis.com', + 'fonts.gstatic.com', + ]; + + return in_array($parsed['host'], $allowed_hosts, true); + } + + /** + * Sanitize and validate a font filename. + * + * @param string $filename The filename to validate. + * @return string|false Sanitized filename or false if invalid. + */ + private function sanitize_font_filename($filename) { + // WordPress sanitization first + $filename = sanitize_file_name($filename); + + // Must have .woff2 extension + if (pathinfo($filename, PATHINFO_EXTENSION) !== 'woff2') { + return false; + } + + // No path components + if ($filename !== basename($filename)) { + return false; + } + + // Reasonable length + if (strlen($filename) > 200) { + return false; + } + + return $filename; + } + + /** + * Validate that a path is within the WordPress fonts directory. + * + * @param string $path Full path to validate. + * @return bool True if path is safe, false otherwise. + */ + private function validate_font_path($path) { + $font_dir = wp_get_font_dir(); + $fonts_path = wp_normalize_path(trailingslashit($font_dir['path'])); + + // Resolve to real path (handles ../ etc) + $real_path = realpath($path); + + // If realpath fails, file doesn't exist yet - validate the directory + if ($real_path === false) { + $dir = dirname($path); + $real_dir = realpath($dir); + if ($real_dir === false) { + // Directory doesn't exist yet, check parent + $parent_dir = dirname($dir); + $real_parent = realpath($parent_dir); + if ($real_parent === false) { + return false; + } + $real_path = wp_normalize_path($real_parent . '/' . basename($dir) . '/' . basename($path)); + } else { + $real_path = wp_normalize_path($real_dir . '/' . basename($path)); + } + } else { + $real_path = wp_normalize_path($real_path); + } + + // Must be within fonts directory + return strpos($real_path, $fonts_path) === 0; + } +} diff --git a/native/wordpress/maple-fonts-wp/includes/class-mlf-font-registry.php b/native/wordpress/maple-fonts-wp/includes/class-mlf-font-registry.php new file mode 100644 index 0000000..db9ec47 --- /dev/null +++ b/native/wordpress/maple-fonts-wp/includes/class-mlf-font-registry.php @@ -0,0 +1,287 @@ + 'wp_font_family', + 'name' => $font_slug, + 'posts_per_page' => 1, + 'post_status' => 'publish', + ]); + + if (!empty($existing)) { + return new WP_Error('font_exists', 'Font family already installed'); + } + + // Get font directory + $font_dir = wp_get_font_dir(); + + // Build font face array for WordPress + $font_faces = []; + foreach ($files as $file) { + $filename = basename($file['path']); + $font_faces[] = [ + 'fontFamily' => $font_name, + 'fontWeight' => $file['weight'], + 'fontStyle' => $file['style'], + 'src' => 'file:./' . $filename, + ]; + } + + // Build font family settings + $font_family_settings = [ + 'name' => $font_name, + 'slug' => $font_slug, + 'fontFamily' => sprintf('"%s", sans-serif', $font_name), + 'fontFace' => $font_faces, + ]; + + // Create font family post + $family_id = wp_insert_post([ + 'post_type' => 'wp_font_family', + 'post_title' => $font_name, + 'post_name' => $font_slug, + 'post_status' => 'publish', + 'post_content' => wp_json_encode($font_family_settings), + ]); + + if (is_wp_error($family_id)) { + return $family_id; + } + + // Mark as imported by our plugin (for identification) + update_post_meta($family_id, '_mlf_imported', '1'); + update_post_meta($family_id, '_mlf_import_date', current_time('mysql')); + + // Create font face posts (children) + foreach ($files as $file) { + $filename = basename($file['path']); + + $face_settings = [ + 'fontFamily' => $font_name, + 'fontWeight' => $file['weight'], + 'fontStyle' => $file['style'], + 'src' => 'file:./' . $filename, + ]; + + wp_insert_post([ + 'post_type' => 'wp_font_face', + 'post_parent' => $family_id, + 'post_title' => sprintf('%s %s %s', $font_name, $file['weight'], $file['style']), + 'post_status' => 'publish', + 'post_content' => wp_json_encode($face_settings), + ]); + } + + // Clear font caches + delete_transient('wp_font_library_fonts'); + delete_transient('mlf_imported_fonts_list'); + + return $family_id; + } + + /** + * Delete a font family and its files. + * + * @param int $family_id Font family post ID. + * @return bool|WP_Error True on success, error on failure. + */ + public function delete_font($family_id) { + $family = get_post($family_id); + + if (!$family || $family->post_type !== 'wp_font_family') { + return new WP_Error('not_found', 'Font family not found'); + } + + // Verify it's one we imported + if (get_post_meta($family_id, '_mlf_imported', true) !== '1') { + return new WP_Error('not_ours', 'Cannot delete fonts not imported by this plugin'); + } + + // Get font faces (children) + $faces = get_children([ + 'post_parent' => $family_id, + 'post_type' => 'wp_font_face', + ]); + + $font_dir = wp_get_font_dir(); + + // Delete font face files and posts + foreach ($faces as $face) { + $settings = json_decode($face->post_content, true); + + if (isset($settings['src'])) { + // Convert file:. URL to path + $src = $settings['src']; + $src = str_replace('file:./', '', $src); + $file_path = trailingslashit($font_dir['path']) . basename($src); + + // Validate path and extension before deletion + if ($this->validate_font_path($file_path) + && pathinfo($file_path, PATHINFO_EXTENSION) === 'woff2' + && file_exists($file_path)) { + wp_delete_file($file_path); + } + } + + wp_delete_post($face->ID, true); + } + + // Delete family post + wp_delete_post($family_id, true); + + // Clear caches + delete_transient('wp_font_library_fonts'); + delete_transient('mlf_imported_fonts_list'); + + return true; + } + + /** + * Get all fonts imported by this plugin. + * + * Uses optimized queries to avoid N+1 pattern. + * + * @return array Array of font data. + */ + public function get_imported_fonts() { + // Check transient cache first + $cached = get_transient('mlf_imported_fonts_list'); + if ($cached !== false) { + return $cached; + } + + $fonts = get_posts([ + 'post_type' => 'wp_font_family', + 'posts_per_page' => 100, + 'post_status' => 'publish', + 'meta_key' => '_mlf_imported', + 'meta_value' => '1', + ]); + + if (empty($fonts)) { + set_transient('mlf_imported_fonts_list', [], 5 * MINUTE_IN_SECONDS); + return []; + } + + // Collect all font IDs for batch query + $font_ids = wp_list_pluck($fonts, 'ID'); + + // Single query to get ALL font faces for ALL fonts (fixes N+1) + $all_faces = get_posts([ + 'post_type' => 'wp_font_face', + 'posts_per_page' => 1000, // Max 100 fonts × 9 weights × 2 styles = 1800, but limit reasonably + 'post_status' => 'publish', + 'post_parent__in' => $font_ids, + ]); + + // Group faces by parent font ID + $faces_by_font = []; + foreach ($all_faces as $face) { + $parent_id = $face->post_parent; + if (!isset($faces_by_font[$parent_id])) { + $faces_by_font[$parent_id] = []; + } + $faces_by_font[$parent_id][] = $face; + } + + // Batch get all import dates in single query + $import_dates = []; + foreach ($font_ids as $font_id) { + $import_dates[$font_id] = get_post_meta($font_id, '_mlf_import_date', true); + } + + $result = []; + + foreach ($fonts as $font) { + $settings = json_decode($font->post_content, true); + + // Get variants from pre-fetched data + $faces = $faces_by_font[$font->ID] ?? []; + + $variants = []; + foreach ($faces as $face) { + $face_settings = json_decode($face->post_content, true); + $variants[] = [ + 'weight' => $face_settings['fontWeight'] ?? '400', + 'style' => $face_settings['fontStyle'] ?? 'normal', + ]; + } + + // Sort variants by weight then style + usort($variants, function($a, $b) { + $weight_cmp = intval($a['weight']) - intval($b['weight']); + if ($weight_cmp !== 0) { + return $weight_cmp; + } + return strcmp($a['style'], $b['style']); + }); + + $result[] = [ + 'id' => $font->ID, + 'name' => $settings['name'] ?? $font->post_title, + 'slug' => $settings['slug'] ?? $font->post_name, + 'variants' => $variants, + 'import_date' => $import_dates[$font->ID] ?? '', + ]; + } + + // Cache for 5 minutes + set_transient('mlf_imported_fonts_list', $result, 5 * MINUTE_IN_SECONDS); + + return $result; + } + + /** + * Validate that a path is within the WordPress fonts directory. + * + * @param string $path Full path to validate. + * @return bool True if path is safe, false otherwise. + */ + private function validate_font_path($path) { + $font_dir = wp_get_font_dir(); + $fonts_path = wp_normalize_path(trailingslashit($font_dir['path'])); + + // Resolve to real path (handles ../ etc) + $real_path = realpath($path); + + // If realpath fails, file doesn't exist yet - validate the directory + if ($real_path === false) { + $dir = dirname($path); + $real_dir = realpath($dir); + if ($real_dir === false) { + return false; + } + $real_path = wp_normalize_path($real_dir . '/' . basename($path)); + } else { + $real_path = wp_normalize_path($real_path); + } + + // Must be within fonts directory + return strpos($real_path, $fonts_path) === 0; + } +} diff --git a/native/wordpress/maple-fonts-wp/includes/class-mlf-rate-limiter.php b/native/wordpress/maple-fonts-wp/includes/class-mlf-rate-limiter.php new file mode 100644 index 0000000..79270ad --- /dev/null +++ b/native/wordpress/maple-fonts-wp/includes/class-mlf-rate-limiter.php @@ -0,0 +1,188 @@ +limit = absint($limit); + $this->window = absint($window); + } + + /** + * Check if the current user/IP is rate limited. + * + * @param string $action The action being rate limited. + * @return bool True if rate limited (should block), false if allowed. + */ + public function is_limited($action) { + $key = $this->get_rate_limit_key($action); + $data = get_transient($key); + + if ($data === false) { + // First request, not limited + return false; + } + + return $data['count'] >= $this->limit; + } + + /** + * Record a request for rate limiting purposes. + * + * @param string $action The action being recorded. + * @return void + */ + public function record_request($action) { + $key = $this->get_rate_limit_key($action); + $data = get_transient($key); + + if ($data === false) { + // First request in this window + $data = [ + 'count' => 1, + 'start' => time(), + ]; + } else { + $data['count']++; + } + + // Set/update transient with remaining window time + $elapsed = time() - $data['start']; + $remaining = max(1, $this->window - $elapsed); + + set_transient($key, $data, $remaining); + } + + /** + * Check rate limit and record request in one call. + * + * @param string $action The action being checked. + * @return bool True if request is allowed, false if rate limited. + */ + public function check_and_record($action) { + if ($this->is_limited($action)) { + return false; + } + + $this->record_request($action); + return true; + } + + /** + * Get the number of remaining requests in the current window. + * + * @param string $action The action to check. + * @return int Number of remaining requests. + */ + public function get_remaining($action) { + $key = $this->get_rate_limit_key($action); + $data = get_transient($key); + + if ($data === false) { + return $this->limit; + } + + return max(0, $this->limit - $data['count']); + } + + /** + * Get the rate limit key for the current user/IP. + * + * @param string $action The action being rate limited. + * @return string Transient key. + */ + private function get_rate_limit_key($action) { + // Use user ID if logged in, otherwise IP + $user_id = get_current_user_id(); + + if ($user_id > 0) { + $identifier = 'user_' . $user_id; + } else { + // Sanitize and hash IP for privacy + $ip = $this->get_client_ip(); + $identifier = 'ip_' . md5($ip); + } + + return 'mlf_rate_' . sanitize_key($action) . '_' . $identifier; + } + + /** + * Get the client IP address. + * + * @return string Client IP address. + */ + private function get_client_ip() { + $ip = ''; + + // Check for various headers (in order of reliability) + $headers = [ + 'HTTP_CLIENT_IP', + 'HTTP_X_FORWARDED_FOR', + 'HTTP_X_FORWARDED', + 'HTTP_X_CLUSTER_CLIENT_IP', + 'HTTP_FORWARDED_FOR', + 'HTTP_FORWARDED', + 'REMOTE_ADDR', + ]; + + foreach ($headers as $header) { + if (!empty($_SERVER[$header])) { + // X-Forwarded-For can contain multiple IPs, get the first one + $ips = explode(',', sanitize_text_field(wp_unslash($_SERVER[$header]))); + $ip = trim($ips[0]); + + // Validate IP + if (filter_var($ip, FILTER_VALIDATE_IP)) { + break; + } + } + } + + // Fallback to localhost if no valid IP found + return $ip ?: '127.0.0.1'; + } + + /** + * Clear rate limit for a specific action and user/IP. + * + * @param string $action The action to clear. + * @return void + */ + public function clear($action) { + $key = $this->get_rate_limit_key($action); + delete_transient($key); + } +} diff --git a/native/wordpress/maple-fonts-wp/includes/class-mlf-rest-controller.php b/native/wordpress/maple-fonts-wp/includes/class-mlf-rest-controller.php new file mode 100644 index 0000000..a17a716 --- /dev/null +++ b/native/wordpress/maple-fonts-wp/includes/class-mlf-rest-controller.php @@ -0,0 +1,525 @@ +rate_limiter = new MLF_Rate_Limiter(10, 60); + } + + /** + * Register the routes for the controller. + */ + public function register_routes() { + // GET /wp-json/mlf/v1/fonts - List all fonts + register_rest_route( + $this->namespace, + '/' . $this->rest_base, + [ + [ + 'methods' => WP_REST_Server::READABLE, + 'callback' => [$this, 'get_items'], + 'permission_callback' => [$this, 'get_items_permissions_check'], + ], + [ + 'methods' => WP_REST_Server::CREATABLE, + 'callback' => [$this, 'create_item'], + 'permission_callback' => [$this, 'create_item_permissions_check'], + 'args' => $this->get_create_item_args(), + ], + 'schema' => [$this, 'get_public_item_schema'], + ] + ); + + // DELETE /wp-json/mlf/v1/fonts/{id} - Delete a font + register_rest_route( + $this->namespace, + '/' . $this->rest_base . '/(?P[\d]+)', + [ + [ + 'methods' => WP_REST_Server::DELETABLE, + 'callback' => [$this, 'delete_item'], + 'permission_callback' => [$this, 'delete_item_permissions_check'], + 'args' => [ + 'id' => [ + 'description' => __('Unique identifier for the font.', 'maple-local-fonts'), + 'type' => 'integer', + 'required' => true, + ], + ], + ], + ] + ); + + // GET /wp-json/mlf/v1/fonts/{id} - Get a single font + register_rest_route( + $this->namespace, + '/' . $this->rest_base . '/(?P[\d]+)', + [ + [ + 'methods' => WP_REST_Server::READABLE, + 'callback' => [$this, 'get_item'], + 'permission_callback' => [$this, 'get_items_permissions_check'], + 'args' => [ + 'id' => [ + 'description' => __('Unique identifier for the font.', 'maple-local-fonts'), + 'type' => 'integer', + 'required' => true, + ], + ], + ], + ] + ); + } + + /** + * Check if a given request has access to get items. + * + * @param WP_REST_Request $request Full data about the request. + * @return bool|WP_Error True if the request has read access, WP_Error object otherwise. + */ + public function get_items_permissions_check($request) { + $capability = function_exists('mlf_get_capability') ? mlf_get_capability() : 'edit_theme_options'; + if (!current_user_can($capability)) { + return new WP_Error( + 'rest_forbidden', + __('Sorry, you are not allowed to view fonts.', 'maple-local-fonts'), + ['status' => rest_authorization_required_code()] + ); + } + return true; + } + + /** + * Check if a given request has access to create items. + * + * @param WP_REST_Request $request Full data about the request. + * @return bool|WP_Error True if the request has create access, WP_Error object otherwise. + */ + public function create_item_permissions_check($request) { + $capability = function_exists('mlf_get_capability') ? mlf_get_capability() : 'edit_theme_options'; + if (!current_user_can($capability)) { + return new WP_Error( + 'rest_forbidden', + __('Sorry, you are not allowed to create fonts.', 'maple-local-fonts'), + ['status' => rest_authorization_required_code()] + ); + } + + // Rate limit check + if (!$this->rate_limiter->check_and_record('rest_create')) { + return new WP_Error( + 'rest_rate_limited', + __('Too many requests. Please wait a moment and try again.', 'maple-local-fonts'), + ['status' => 429] + ); + } + + return true; + } + + /** + * Check if a given request has access to delete a specific item. + * + * @param WP_REST_Request $request Full data about the request. + * @return bool|WP_Error True if the request has delete access, WP_Error object otherwise. + */ + public function delete_item_permissions_check($request) { + $capability = function_exists('mlf_get_capability') ? mlf_get_capability() : 'edit_theme_options'; + if (!current_user_can($capability)) { + return new WP_Error( + 'rest_forbidden', + __('Sorry, you are not allowed to delete fonts.', 'maple-local-fonts'), + ['status' => rest_authorization_required_code()] + ); + } + + // Rate limit check + if (!$this->rate_limiter->check_and_record('rest_delete')) { + return new WP_Error( + 'rest_rate_limited', + __('Too many requests. Please wait a moment and try again.', 'maple-local-fonts'), + ['status' => 429] + ); + } + + return true; + } + + /** + * Get a collection of fonts. + * + * @param WP_REST_Request $request Full data about the request. + * @return WP_REST_Response|WP_Error Response object on success, or WP_Error object on failure. + */ + public function get_items($request) { + $registry = new MLF_Font_Registry(); + $fonts = $registry->get_imported_fonts(); + + $data = []; + foreach ($fonts as $font) { + $data[] = $this->prepare_item_for_response($font, $request); + } + + return rest_ensure_response($data); + } + + /** + * Get a single font. + * + * @param WP_REST_Request $request Full data about the request. + * @return WP_REST_Response|WP_Error Response object on success, or WP_Error object on failure. + */ + public function get_item($request) { + $font_id = absint($request->get_param('id')); + $font = get_post($font_id); + + if (!$font || $font->post_type !== 'wp_font_family') { + return new WP_Error( + 'rest_font_not_found', + __('Font not found.', 'maple-local-fonts'), + ['status' => 404] + ); + } + + // Verify it's one we imported + if (get_post_meta($font_id, '_mlf_imported', true) !== '1') { + return new WP_Error( + 'rest_font_not_found', + __('Font not found.', 'maple-local-fonts'), + ['status' => 404] + ); + } + + $registry = new MLF_Font_Registry(); + $fonts = $registry->get_imported_fonts(); + $font_data = null; + + foreach ($fonts as $f) { + if ($f['id'] === $font_id) { + $font_data = $f; + break; + } + } + + if (!$font_data) { + return new WP_Error( + 'rest_font_not_found', + __('Font not found.', 'maple-local-fonts'), + ['status' => 404] + ); + } + + return rest_ensure_response($this->prepare_item_for_response($font_data, $request)); + } + + /** + * Create a font. + * + * @param WP_REST_Request $request Full data about the request. + * @return WP_REST_Response|WP_Error Response object on success, or WP_Error object on failure. + */ + public function create_item($request) { + $font_name = sanitize_text_field($request->get_param('font_name')); + $weights = array_map('absint', (array) $request->get_param('weights')); + $styles = array_map('sanitize_text_field', (array) $request->get_param('styles')); + + // Validate font name + if (!preg_match('/^[a-zA-Z0-9\s\-]+$/', $font_name)) { + return new WP_Error( + 'rest_invalid_param', + __('Invalid font name: only letters, numbers, spaces, and hyphens allowed.', 'maple-local-fonts'), + ['status' => 400] + ); + } + + if (strlen($font_name) > 100) { + return new WP_Error( + 'rest_invalid_param', + __('Font name is too long.', 'maple-local-fonts'), + ['status' => 400] + ); + } + + // Validate weights + $allowed_weights = [100, 200, 300, 400, 500, 600, 700, 800, 900]; + $weights = array_intersect($weights, $allowed_weights); + + if (empty($weights)) { + return new WP_Error( + 'rest_invalid_param', + __('At least one valid weight is required.', 'maple-local-fonts'), + ['status' => 400] + ); + } + + if (count($weights) > MLF_MAX_WEIGHTS_PER_FONT) { + return new WP_Error( + 'rest_invalid_param', + __('Too many weights selected.', 'maple-local-fonts'), + ['status' => 400] + ); + } + + // Validate styles + $allowed_styles = ['normal', 'italic']; + $styles = array_filter($styles, function($style) use ($allowed_styles) { + return in_array($style, $allowed_styles, true); + }); + + if (empty($styles)) { + return new WP_Error( + 'rest_invalid_param', + __('At least one valid style is required.', 'maple-local-fonts'), + ['status' => 400] + ); + } + + try { + $downloader = new MLF_Font_Downloader(); + $download_result = $downloader->download($font_name, $weights, $styles); + + if (is_wp_error($download_result)) { + return new WP_Error( + 'rest_download_failed', + $download_result->get_error_message(), + ['status' => 400] + ); + } + + // Register font with WordPress + $registry = new MLF_Font_Registry(); + $result = $registry->register_font( + $download_result['font_name'], + $download_result['font_slug'], + $download_result['files'] + ); + + if (is_wp_error($result)) { + return new WP_Error( + 'rest_register_failed', + $result->get_error_message(), + ['status' => 400] + ); + } + + // Return the created font + $fonts = $registry->get_imported_fonts(); + $created_font = null; + foreach ($fonts as $font) { + if ($font['id'] === $result) { + $created_font = $font; + break; + } + } + + $response = rest_ensure_response($this->prepare_item_for_response($created_font, $request)); + $response->set_status(201); + $response->header('Location', rest_url(sprintf('%s/%s/%d', $this->namespace, $this->rest_base, $result))); + + return $response; + } catch (Exception $e) { + error_log('MLF REST Create Error: ' . sanitize_text_field($e->getMessage())); + return new WP_Error( + 'rest_internal_error', + __('An unexpected error occurred.', 'maple-local-fonts'), + ['status' => 500] + ); + } + } + + /** + * Delete a font. + * + * @param WP_REST_Request $request Full data about the request. + * @return WP_REST_Response|WP_Error Response object on success, or WP_Error object on failure. + */ + public function delete_item($request) { + $font_id = absint($request->get_param('id')); + $font = get_post($font_id); + + if (!$font || $font->post_type !== 'wp_font_family') { + return new WP_Error( + 'rest_font_not_found', + __('Font not found.', 'maple-local-fonts'), + ['status' => 404] + ); + } + + // Verify it's one we imported + if (get_post_meta($font_id, '_mlf_imported', true) !== '1') { + return new WP_Error( + 'rest_cannot_delete', + __('Cannot delete fonts not imported by this plugin.', 'maple-local-fonts'), + ['status' => 403] + ); + } + + try { + $registry = new MLF_Font_Registry(); + $result = $registry->delete_font($font_id); + + if (is_wp_error($result)) { + return new WP_Error( + 'rest_delete_failed', + $result->get_error_message(), + ['status' => 400] + ); + } + + return rest_ensure_response([ + 'deleted' => true, + 'message' => __('Font deleted successfully.', 'maple-local-fonts'), + ]); + } catch (Exception $e) { + error_log('MLF REST Delete Error: ' . sanitize_text_field($e->getMessage())); + return new WP_Error( + 'rest_internal_error', + __('An unexpected error occurred.', 'maple-local-fonts'), + ['status' => 500] + ); + } + } + + /** + * Prepare a font for the REST response. + * + * @param array $font Font data. + * @param WP_REST_Request $request Request object. + * @return array Prepared font data. + */ + public function prepare_item_for_response($font, $request) { + return [ + 'id' => $font['id'], + 'name' => $font['name'], + 'slug' => $font['slug'], + 'variants' => $font['variants'], + '_links' => [ + 'self' => [ + ['href' => rest_url(sprintf('%s/%s/%d', $this->namespace, $this->rest_base, $font['id']))], + ], + 'collection' => [ + ['href' => rest_url(sprintf('%s/%s', $this->namespace, $this->rest_base))], + ], + ], + ]; + } + + /** + * Get the argument schema for creating items. + * + * @return array Arguments schema. + */ + protected function get_create_item_args() { + return [ + 'font_name' => [ + 'description' => __('The font family name from Google Fonts.', 'maple-local-fonts'), + 'type' => 'string', + 'required' => true, + 'sanitize_callback' => 'sanitize_text_field', + ], + 'weights' => [ + 'description' => __('Array of font weights to download.', 'maple-local-fonts'), + 'type' => 'array', + 'required' => true, + 'items' => [ + 'type' => 'integer', + 'enum' => [100, 200, 300, 400, 500, 600, 700, 800, 900], + ], + ], + 'styles' => [ + 'description' => __('Array of font styles to download.', 'maple-local-fonts'), + 'type' => 'array', + 'required' => true, + 'items' => [ + 'type' => 'string', + 'enum' => ['normal', 'italic'], + ], + ], + ]; + } + + /** + * Get the font schema. + * + * @return array Schema definition. + */ + public function get_item_schema() { + return [ + '$schema' => 'http://json-schema.org/draft-04/schema#', + 'title' => 'font', + 'type' => 'object', + 'properties' => [ + 'id' => [ + 'description' => __('Unique identifier for the font.', 'maple-local-fonts'), + 'type' => 'integer', + 'context' => ['view'], + 'readonly' => true, + ], + 'name' => [ + 'description' => __('The font family name.', 'maple-local-fonts'), + 'type' => 'string', + 'context' => ['view'], + ], + 'slug' => [ + 'description' => __('The font slug.', 'maple-local-fonts'), + 'type' => 'string', + 'context' => ['view'], + ], + 'variants' => [ + 'description' => __('Array of font variants.', 'maple-local-fonts'), + 'type' => 'array', + 'context' => ['view'], + 'items' => [ + 'type' => 'object', + 'properties' => [ + 'weight' => [ + 'type' => 'string', + ], + 'style' => [ + 'type' => 'string', + ], + ], + ], + ], + ], + ]; + } +} diff --git a/native/wordpress/maple-fonts-wp/includes/index.php b/native/wordpress/maple-fonts-wp/includes/index.php new file mode 100644 index 0000000..90593cd --- /dev/null +++ b/native/wordpress/maple-fonts-wp/includes/index.php @@ -0,0 +1,5 @@ +=3 && n%100<=10 ? 3 : n%100>=11 ? 4 : 5);\n" + +#. Plugin Name of the plugin +msgid "Maple Local Fonts" +msgstr "Maple Local Fonts" + +#. Description of the plugin +msgid "Import Google Fonts to local storage and register them with WordPress Font Library for GDPR-compliant, privacy-friendly typography." +msgstr "استورد خطوط Google Fonts إلى التخزين المحلي وسجلها في مكتبة خطوط WordPress للحصول على طباعة متوافقة مع GDPR وصديقة للخصوصية." + +#. Author of the plugin +msgid "Maple Open Technologies" +msgstr "Maple Open Technologies" + +msgid "Maple Local Fonts requires WordPress 6.5 or higher for Font Library support." +msgstr "يتطلب Maple Local Fonts إصدار WordPress 6.5 أو أعلى لدعم مكتبة الخطوط." + +msgid "Plugin Activation Error" +msgstr "خطأ في تفعيل الإضافة" + +msgid "Maple Local Fonts has been deactivated. It requires WordPress 6.5 or higher." +msgstr "تم إلغاء تفعيل Maple Local Fonts. يتطلب WordPress 6.5 أو أعلى." + +msgid "Local Fonts" +msgstr "الخطوط المحلية" + +msgid "Downloading..." +msgstr "جارٍ التنزيل..." + +msgid "Deleting..." +msgstr "جارٍ الحذف..." + +msgid "Are you sure you want to delete this font?" +msgstr "هل أنت متأكد أنك تريد حذف هذا الخط؟" + +msgid "An error occurred. Please try again." +msgstr "حدث خطأ. يرجى المحاولة مرة أخرى." + +msgid "Please select at least one weight." +msgstr "يرجى اختيار وزن واحد على الأقل." + +msgid "Please select at least one style." +msgstr "يرجى اختيار نمط واحد على الأقل." + +msgid "Please enter a font name." +msgstr "يرجى إدخال اسم الخط." + +msgid "Security check failed." +msgstr "فشل التحقق الأمني." + +msgid "Unauthorized." +msgstr "غير مصرح." + +msgid "Font name is required." +msgstr "اسم الخط مطلوب." + +msgid "Invalid font name: only letters, numbers, spaces, and hyphens allowed." +msgstr "اسم خط غير صالح: يُسمح فقط بالأحرف والأرقام والمسافات والشرطات." + +msgid "Font name is too long." +msgstr "اسم الخط طويل جداً." + +msgid "At least one weight is required." +msgstr "مطلوب وزن واحد على الأقل." + +msgid "Too many weights selected." +msgstr "تم اختيار أوزان كثيرة جداً." + +msgid "At least one style is required." +msgstr "مطلوب نمط واحد على الأقل." + +msgid "Successfully installed %s." +msgstr "تم تثبيت %s بنجاح." + +msgid "An unexpected error occurred." +msgstr "حدث خطأ غير متوقع." + +msgid "Invalid font ID." +msgstr "معرف خط غير صالح." + +msgid "Font not found." +msgstr "الخط غير موجود." + +msgid "Cannot delete fonts not imported by this plugin." +msgstr "لا يمكن حذف الخطوط التي لم يتم استيرادها بواسطة هذه الإضافة." + +msgid "Font deleted successfully." +msgstr "تم حذف الخط بنجاح." + +msgid "Font not found on Google Fonts. Please check the spelling and try again." +msgstr "الخط غير موجود في Google Fonts. يرجى التحقق من الإملاء والمحاولة مرة أخرى." + +msgid "This font is already installed." +msgstr "هذا الخط مثبت بالفعل." + +msgid "Could not connect to Google Fonts. Please check your internet connection and try again." +msgstr "تعذر الاتصال بـ Google Fonts. يرجى التحقق من اتصالك بالإنترنت والمحاولة مرة أخرى." + +msgid "Google Fonts returned an error. Please try again later." +msgstr "أرجع Google Fonts خطأ. يرجى المحاولة لاحقاً." + +msgid "Could not process the font data. The font may not be available." +msgstr "تعذرت معالجة بيانات الخط. قد لا يكون الخط متاحاً." + +msgid "Could not download the font files. Please try again." +msgstr "تعذر تنزيل ملفات الخط. يرجى المحاولة مرة أخرى." + +msgid "Could not save font files. Please check that wp-content/fonts is writable." +msgstr "تعذر حفظ ملفات الخط. يرجى التحقق من أن wp-content/fonts قابل للكتابة." + +msgid "Could not create fonts directory. Please check file permissions." +msgstr "تعذر إنشاء دليل الخطوط. يرجى التحقق من أذونات الملفات." + +msgid "Invalid file path." +msgstr "مسار ملف غير صالح." + +msgid "Invalid font URL." +msgstr "رابط خط غير صالح." + +msgid "Invalid font name." +msgstr "اسم خط غير صالح." + +msgid "No valid weights specified." +msgstr "لم يتم تحديد أوزان صالحة." + +msgid "No valid styles specified." +msgstr "لم يتم تحديد أنماط صالحة." + +msgid "An unexpected error occurred. Please try again." +msgstr "حدث خطأ غير متوقع. يرجى المحاولة مرة أخرى." + +msgid "Thin" +msgstr "رفيع" + +msgid "Extra Light" +msgstr "خفيف جداً" + +msgid "Light" +msgstr "خفيف" + +msgid "Regular" +msgstr "عادي" + +msgid "Medium" +msgstr "متوسط" + +msgid "Semi Bold" +msgstr "شبه عريض" + +msgid "Bold" +msgstr "عريض" + +msgid "Extra Bold" +msgstr "عريض جداً" + +msgid "Black" +msgstr "أسود" + +msgid "You do not have sufficient permissions to access this page." +msgstr "ليس لديك الصلاحيات الكافية للوصول إلى هذه الصفحة." + +msgid "Import from Google Fonts" +msgstr "استيراد من Google Fonts" + +msgid "Font Name" +msgstr "اسم الخط" + +msgid "e.g., Open Sans" +msgstr "مثال: Open Sans" + +msgid "Enter the exact font name as it appears on Google Fonts." +msgstr "أدخل اسم الخط الدقيق كما يظهر في Google Fonts." + +msgid "Weights" +msgstr "الأوزان" + +msgid "Styles" +msgstr "الأنماط" + +msgid "Normal" +msgstr "عادي" + +msgid "Italic" +msgstr "مائل" + +msgid "Files to download:" +msgstr "الملفات للتنزيل:" + +msgid "Download & Install" +msgstr "تنزيل وتثبيت" + +msgid "Installed Fonts" +msgstr "الخطوط المثبتة" + +msgid "No fonts installed yet." +msgstr "لم يتم تثبيت أي خطوط بعد." + +msgid "Delete" +msgstr "حذف" + +msgid "Use %s to apply fonts to your site." +msgstr "استخدم %s لتطبيق الخطوط على موقعك." + +msgid "Appearance → Editor → Styles → Typography" +msgstr "المظهر ← المحرر ← الأنماط ← الطباعة" diff --git a/native/wordpress/maple-fonts-wp/languages/maple-local-fonts-de_DE.po b/native/wordpress/maple-fonts-wp/languages/maple-local-fonts-de_DE.po new file mode 100644 index 0000000..ea9f0b9 --- /dev/null +++ b/native/wordpress/maple-fonts-wp/languages/maple-local-fonts-de_DE.po @@ -0,0 +1,222 @@ +# Translation of Maple Local Fonts in German +# Copyright (C) 2024 Maple Open Technologies +# This file is distributed under the GPL-2.0-or-later. +msgid "" +msgstr "" +"Project-Id-Version: Maple Local Fonts 1.0.0\n" +"Report-Msgid-Bugs-To: https://mapleopentech.org/\n" +"Last-Translator: Maple Open Technologies\n" +"Language-Team: German\n" +"Language: de_DE\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"POT-Creation-Date: 2024-01-01T00:00:00+00:00\n" +"PO-Revision-Date: 2024-01-01T00:00:00+00:00\n" +"X-Generator: Manual\n" +"X-Domain: maple-local-fonts\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" + +#. Plugin Name of the plugin +msgid "Maple Local Fonts" +msgstr "Maple Local Fonts" + +#. Description of the plugin +msgid "Import Google Fonts to local storage and register them with WordPress Font Library for GDPR-compliant, privacy-friendly typography." +msgstr "Importieren Sie Google Fonts in den lokalen Speicher und registrieren Sie sie in der WordPress-Schriftbibliothek für DSGVO-konforme, datenschutzfreundliche Typografie." + +#. Author of the plugin +msgid "Maple Open Technologies" +msgstr "Maple Open Technologies" + +msgid "Maple Local Fonts requires WordPress 6.5 or higher for Font Library support." +msgstr "Maple Local Fonts erfordert WordPress 6.5 oder höher für die Unterstützung der Schriftbibliothek." + +msgid "Plugin Activation Error" +msgstr "Plugin-Aktivierungsfehler" + +msgid "Maple Local Fonts has been deactivated. It requires WordPress 6.5 or higher." +msgstr "Maple Local Fonts wurde deaktiviert. Es erfordert WordPress 6.5 oder höher." + +msgid "Local Fonts" +msgstr "Lokale Schriften" + +msgid "Downloading..." +msgstr "Wird heruntergeladen..." + +msgid "Deleting..." +msgstr "Wird gelöscht..." + +msgid "Are you sure you want to delete this font?" +msgstr "Sind Sie sicher, dass Sie diese Schrift löschen möchten?" + +msgid "An error occurred. Please try again." +msgstr "Ein Fehler ist aufgetreten. Bitte versuchen Sie es erneut." + +msgid "Please select at least one weight." +msgstr "Bitte wählen Sie mindestens eine Schriftstärke aus." + +msgid "Please select at least one style." +msgstr "Bitte wählen Sie mindestens einen Stil aus." + +msgid "Please enter a font name." +msgstr "Bitte geben Sie einen Schriftnamen ein." + +msgid "Security check failed." +msgstr "Sicherheitsprüfung fehlgeschlagen." + +msgid "Unauthorized." +msgstr "Nicht autorisiert." + +msgid "Font name is required." +msgstr "Schriftname ist erforderlich." + +msgid "Invalid font name: only letters, numbers, spaces, and hyphens allowed." +msgstr "Ungültiger Schriftname: Nur Buchstaben, Zahlen, Leerzeichen und Bindestriche erlaubt." + +msgid "Font name is too long." +msgstr "Schriftname ist zu lang." + +msgid "At least one weight is required." +msgstr "Mindestens eine Schriftstärke ist erforderlich." + +msgid "Too many weights selected." +msgstr "Zu viele Schriftstärken ausgewählt." + +msgid "At least one style is required." +msgstr "Mindestens ein Stil ist erforderlich." + +msgid "Successfully installed %s." +msgstr "%s wurde erfolgreich installiert." + +msgid "An unexpected error occurred." +msgstr "Ein unerwarteter Fehler ist aufgetreten." + +msgid "Invalid font ID." +msgstr "Ungültige Schrift-ID." + +msgid "Font not found." +msgstr "Schrift nicht gefunden." + +msgid "Cannot delete fonts not imported by this plugin." +msgstr "Schriften, die nicht von diesem Plugin importiert wurden, können nicht gelöscht werden." + +msgid "Font deleted successfully." +msgstr "Schrift erfolgreich gelöscht." + +msgid "Font not found on Google Fonts. Please check the spelling and try again." +msgstr "Schrift bei Google Fonts nicht gefunden. Bitte überprüfen Sie die Schreibweise und versuchen Sie es erneut." + +msgid "This font is already installed." +msgstr "Diese Schrift ist bereits installiert." + +msgid "Could not connect to Google Fonts. Please check your internet connection and try again." +msgstr "Verbindung zu Google Fonts konnte nicht hergestellt werden. Bitte überprüfen Sie Ihre Internetverbindung und versuchen Sie es erneut." + +msgid "Google Fonts returned an error. Please try again later." +msgstr "Google Fonts hat einen Fehler zurückgegeben. Bitte versuchen Sie es später erneut." + +msgid "Could not process the font data. The font may not be available." +msgstr "Schriftdaten konnten nicht verarbeitet werden. Die Schrift ist möglicherweise nicht verfügbar." + +msgid "Could not download the font files. Please try again." +msgstr "Schriftdateien konnten nicht heruntergeladen werden. Bitte versuchen Sie es erneut." + +msgid "Could not save font files. Please check that wp-content/fonts is writable." +msgstr "Schriftdateien konnten nicht gespeichert werden. Bitte überprüfen Sie, ob wp-content/fonts beschreibbar ist." + +msgid "Could not create fonts directory. Please check file permissions." +msgstr "Schriftverzeichnis konnte nicht erstellt werden. Bitte überprüfen Sie die Dateiberechtigungen." + +msgid "Invalid file path." +msgstr "Ungültiger Dateipfad." + +msgid "Invalid font URL." +msgstr "Ungültige Schrift-URL." + +msgid "Invalid font name." +msgstr "Ungültiger Schriftname." + +msgid "No valid weights specified." +msgstr "Keine gültigen Schriftstärken angegeben." + +msgid "No valid styles specified." +msgstr "Keine gültigen Stile angegeben." + +msgid "An unexpected error occurred. Please try again." +msgstr "Ein unerwarteter Fehler ist aufgetreten. Bitte versuchen Sie es erneut." + +msgid "Thin" +msgstr "Dünn" + +msgid "Extra Light" +msgstr "Extraleicht" + +msgid "Light" +msgstr "Leicht" + +msgid "Regular" +msgstr "Normal" + +msgid "Medium" +msgstr "Mittel" + +msgid "Semi Bold" +msgstr "Halbfett" + +msgid "Bold" +msgstr "Fett" + +msgid "Extra Bold" +msgstr "Extrafett" + +msgid "Black" +msgstr "Schwarz" + +msgid "You do not have sufficient permissions to access this page." +msgstr "Sie haben nicht die erforderlichen Berechtigungen, um auf diese Seite zuzugreifen." + +msgid "Import from Google Fonts" +msgstr "Von Google Fonts importieren" + +msgid "Font Name" +msgstr "Schriftname" + +msgid "e.g., Open Sans" +msgstr "z.B. Open Sans" + +msgid "Enter the exact font name as it appears on Google Fonts." +msgstr "Geben Sie den genauen Schriftnamen ein, wie er bei Google Fonts erscheint." + +msgid "Weights" +msgstr "Schriftstärken" + +msgid "Styles" +msgstr "Stile" + +msgid "Normal" +msgstr "Normal" + +msgid "Italic" +msgstr "Kursiv" + +msgid "Files to download:" +msgstr "Herunterzuladende Dateien:" + +msgid "Download & Install" +msgstr "Herunterladen & Installieren" + +msgid "Installed Fonts" +msgstr "Installierte Schriften" + +msgid "No fonts installed yet." +msgstr "Noch keine Schriften installiert." + +msgid "Delete" +msgstr "Löschen" + +msgid "Use %s to apply fonts to your site." +msgstr "Verwenden Sie %s, um Schriften auf Ihre Website anzuwenden." + +msgid "Appearance → Editor → Styles → Typography" +msgstr "Design → Editor → Stile → Typografie" diff --git a/native/wordpress/maple-fonts-wp/languages/maple-local-fonts-en_CA.po b/native/wordpress/maple-fonts-wp/languages/maple-local-fonts-en_CA.po new file mode 100644 index 0000000..8c95eef --- /dev/null +++ b/native/wordpress/maple-fonts-wp/languages/maple-local-fonts-en_CA.po @@ -0,0 +1,222 @@ +# Translation of Maple Local Fonts in English (Canada) +# Copyright (C) 2024 Maple Open Technologies +# This file is distributed under the GPL-2.0-or-later. +msgid "" +msgstr "" +"Project-Id-Version: Maple Local Fonts 1.0.0\n" +"Report-Msgid-Bugs-To: https://mapleopentech.org/\n" +"Last-Translator: Maple Open Technologies\n" +"Language-Team: English (Canada)\n" +"Language: en_CA\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"POT-Creation-Date: 2024-01-01T00:00:00+00:00\n" +"PO-Revision-Date: 2024-01-01T00:00:00+00:00\n" +"X-Generator: Manual\n" +"X-Domain: maple-local-fonts\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" + +#. Plugin Name of the plugin +msgid "Maple Local Fonts" +msgstr "Maple Local Fonts" + +#. Description of the plugin +msgid "Import Google Fonts to local storage and register them with WordPress Font Library for GDPR-compliant, privacy-friendly typography." +msgstr "Import Google Fonts to local storage and register them with WordPress Font Library for GDPR-compliant, privacy-friendly typography." + +#. Author of the plugin +msgid "Maple Open Technologies" +msgstr "Maple Open Technologies" + +msgid "Maple Local Fonts requires WordPress 6.5 or higher for Font Library support." +msgstr "Maple Local Fonts requires WordPress 6.5 or higher for Font Library support." + +msgid "Plugin Activation Error" +msgstr "Plugin Activation Error" + +msgid "Maple Local Fonts has been deactivated. It requires WordPress 6.5 or higher." +msgstr "Maple Local Fonts has been deactivated. It requires WordPress 6.5 or higher." + +msgid "Local Fonts" +msgstr "Local Fonts" + +msgid "Downloading..." +msgstr "Downloading..." + +msgid "Deleting..." +msgstr "Deleting..." + +msgid "Are you sure you want to delete this font?" +msgstr "Are you sure you want to delete this font?" + +msgid "An error occurred. Please try again." +msgstr "An error occurred. Please try again." + +msgid "Please select at least one weight." +msgstr "Please select at least one weight." + +msgid "Please select at least one style." +msgstr "Please select at least one style." + +msgid "Please enter a font name." +msgstr "Please enter a font name." + +msgid "Security check failed." +msgstr "Security check failed." + +msgid "Unauthorized." +msgstr "Unauthorized." + +msgid "Font name is required." +msgstr "Font name is required." + +msgid "Invalid font name: only letters, numbers, spaces, and hyphens allowed." +msgstr "Invalid font name: only letters, numbers, spaces, and hyphens allowed." + +msgid "Font name is too long." +msgstr "Font name is too long." + +msgid "At least one weight is required." +msgstr "At least one weight is required." + +msgid "Too many weights selected." +msgstr "Too many weights selected." + +msgid "At least one style is required." +msgstr "At least one style is required." + +msgid "Successfully installed %s." +msgstr "Successfully installed %s." + +msgid "An unexpected error occurred." +msgstr "An unexpected error occurred." + +msgid "Invalid font ID." +msgstr "Invalid font ID." + +msgid "Font not found." +msgstr "Font not found." + +msgid "Cannot delete fonts not imported by this plugin." +msgstr "Cannot delete fonts not imported by this plugin." + +msgid "Font deleted successfully." +msgstr "Font deleted successfully." + +msgid "Font not found on Google Fonts. Please check the spelling and try again." +msgstr "Font not found on Google Fonts. Please check the spelling and try again." + +msgid "This font is already installed." +msgstr "This font is already installed." + +msgid "Could not connect to Google Fonts. Please check your internet connection and try again." +msgstr "Could not connect to Google Fonts. Please check your internet connection and try again." + +msgid "Google Fonts returned an error. Please try again later." +msgstr "Google Fonts returned an error. Please try again later." + +msgid "Could not process the font data. The font may not be available." +msgstr "Could not process the font data. The font may not be available." + +msgid "Could not download the font files. Please try again." +msgstr "Could not download the font files. Please try again." + +msgid "Could not save font files. Please check that wp-content/fonts is writable." +msgstr "Could not save font files. Please check that wp-content/fonts is writable." + +msgid "Could not create fonts directory. Please check file permissions." +msgstr "Could not create fonts directory. Please check file permissions." + +msgid "Invalid file path." +msgstr "Invalid file path." + +msgid "Invalid font URL." +msgstr "Invalid font URL." + +msgid "Invalid font name." +msgstr "Invalid font name." + +msgid "No valid weights specified." +msgstr "No valid weights specified." + +msgid "No valid styles specified." +msgstr "No valid styles specified." + +msgid "An unexpected error occurred. Please try again." +msgstr "An unexpected error occurred. Please try again." + +msgid "Thin" +msgstr "Thin" + +msgid "Extra Light" +msgstr "Extra Light" + +msgid "Light" +msgstr "Light" + +msgid "Regular" +msgstr "Regular" + +msgid "Medium" +msgstr "Medium" + +msgid "Semi Bold" +msgstr "Semi Bold" + +msgid "Bold" +msgstr "Bold" + +msgid "Extra Bold" +msgstr "Extra Bold" + +msgid "Black" +msgstr "Black" + +msgid "You do not have sufficient permissions to access this page." +msgstr "You do not have sufficient permissions to access this page." + +msgid "Import from Google Fonts" +msgstr "Import from Google Fonts" + +msgid "Font Name" +msgstr "Font Name" + +msgid "e.g., Open Sans" +msgstr "e.g., Open Sans" + +msgid "Enter the exact font name as it appears on Google Fonts." +msgstr "Enter the exact font name as it appears on Google Fonts." + +msgid "Weights" +msgstr "Weights" + +msgid "Styles" +msgstr "Styles" + +msgid "Normal" +msgstr "Normal" + +msgid "Italic" +msgstr "Italic" + +msgid "Files to download:" +msgstr "Files to download:" + +msgid "Download & Install" +msgstr "Download & Install" + +msgid "Installed Fonts" +msgstr "Installed Fonts" + +msgid "No fonts installed yet." +msgstr "No fonts installed yet." + +msgid "Delete" +msgstr "Delete" + +msgid "Use %s to apply fonts to your site." +msgstr "Use %s to apply fonts to your site." + +msgid "Appearance → Editor → Styles → Typography" +msgstr "Appearance → Editor → Styles → Typography" diff --git a/native/wordpress/maple-fonts-wp/languages/maple-local-fonts-en_GB.po b/native/wordpress/maple-fonts-wp/languages/maple-local-fonts-en_GB.po new file mode 100644 index 0000000..f17cee4 --- /dev/null +++ b/native/wordpress/maple-fonts-wp/languages/maple-local-fonts-en_GB.po @@ -0,0 +1,222 @@ +# Translation of Maple Local Fonts in English (United Kingdom) +# Copyright (C) 2024 Maple Open Technologies +# This file is distributed under the GPL-2.0-or-later. +msgid "" +msgstr "" +"Project-Id-Version: Maple Local Fonts 1.0.0\n" +"Report-Msgid-Bugs-To: https://mapleopentech.org/\n" +"Last-Translator: Maple Open Technologies\n" +"Language-Team: English (United Kingdom)\n" +"Language: en_GB\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"POT-Creation-Date: 2024-01-01T00:00:00+00:00\n" +"PO-Revision-Date: 2024-01-01T00:00:00+00:00\n" +"X-Generator: Manual\n" +"X-Domain: maple-local-fonts\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" + +#. Plugin Name of the plugin +msgid "Maple Local Fonts" +msgstr "Maple Local Fonts" + +#. Description of the plugin +msgid "Import Google Fonts to local storage and register them with WordPress Font Library for GDPR-compliant, privacy-friendly typography." +msgstr "Import Google Fonts to local storage and register them with WordPress Font Library for GDPR-compliant, privacy-friendly typography." + +#. Author of the plugin +msgid "Maple Open Technologies" +msgstr "Maple Open Technologies" + +msgid "Maple Local Fonts requires WordPress 6.5 or higher for Font Library support." +msgstr "Maple Local Fonts requires WordPress 6.5 or higher for Font Library support." + +msgid "Plugin Activation Error" +msgstr "Plugin Activation Error" + +msgid "Maple Local Fonts has been deactivated. It requires WordPress 6.5 or higher." +msgstr "Maple Local Fonts has been deactivated. It requires WordPress 6.5 or higher." + +msgid "Local Fonts" +msgstr "Local Fonts" + +msgid "Downloading..." +msgstr "Downloading..." + +msgid "Deleting..." +msgstr "Deleting..." + +msgid "Are you sure you want to delete this font?" +msgstr "Are you sure you want to delete this font?" + +msgid "An error occurred. Please try again." +msgstr "An error occurred. Please try again." + +msgid "Please select at least one weight." +msgstr "Please select at least one weight." + +msgid "Please select at least one style." +msgstr "Please select at least one style." + +msgid "Please enter a font name." +msgstr "Please enter a font name." + +msgid "Security check failed." +msgstr "Security check failed." + +msgid "Unauthorised." +msgstr "Unauthorised." + +msgid "Font name is required." +msgstr "Font name is required." + +msgid "Invalid font name: only letters, numbers, spaces, and hyphens allowed." +msgstr "Invalid font name: only letters, numbers, spaces, and hyphens allowed." + +msgid "Font name is too long." +msgstr "Font name is too long." + +msgid "At least one weight is required." +msgstr "At least one weight is required." + +msgid "Too many weights selected." +msgstr "Too many weights selected." + +msgid "At least one style is required." +msgstr "At least one style is required." + +msgid "Successfully installed %s." +msgstr "Successfully installed %s." + +msgid "An unexpected error occurred." +msgstr "An unexpected error occurred." + +msgid "Invalid font ID." +msgstr "Invalid font ID." + +msgid "Font not found." +msgstr "Font not found." + +msgid "Cannot delete fonts not imported by this plugin." +msgstr "Cannot delete fonts not imported by this plugin." + +msgid "Font deleted successfully." +msgstr "Font deleted successfully." + +msgid "Font not found on Google Fonts. Please check the spelling and try again." +msgstr "Font not found on Google Fonts. Please check the spelling and try again." + +msgid "This font is already installed." +msgstr "This font is already installed." + +msgid "Could not connect to Google Fonts. Please check your internet connection and try again." +msgstr "Could not connect to Google Fonts. Please check your internet connection and try again." + +msgid "Google Fonts returned an error. Please try again later." +msgstr "Google Fonts returned an error. Please try again later." + +msgid "Could not process the font data. The font may not be available." +msgstr "Could not process the font data. The font may not be available." + +msgid "Could not download the font files. Please try again." +msgstr "Could not download the font files. Please try again." + +msgid "Could not save font files. Please check that wp-content/fonts is writable." +msgstr "Could not save font files. Please check that wp-content/fonts is writable." + +msgid "Could not create fonts directory. Please check file permissions." +msgstr "Could not create fonts directory. Please check file permissions." + +msgid "Invalid file path." +msgstr "Invalid file path." + +msgid "Invalid font URL." +msgstr "Invalid font URL." + +msgid "Invalid font name." +msgstr "Invalid font name." + +msgid "No valid weights specified." +msgstr "No valid weights specified." + +msgid "No valid styles specified." +msgstr "No valid styles specified." + +msgid "An unexpected error occurred. Please try again." +msgstr "An unexpected error occurred. Please try again." + +msgid "Thin" +msgstr "Thin" + +msgid "Extra Light" +msgstr "Extra Light" + +msgid "Light" +msgstr "Light" + +msgid "Regular" +msgstr "Regular" + +msgid "Medium" +msgstr "Medium" + +msgid "Semi Bold" +msgstr "Semi Bold" + +msgid "Bold" +msgstr "Bold" + +msgid "Extra Bold" +msgstr "Extra Bold" + +msgid "Black" +msgstr "Black" + +msgid "You do not have sufficient permissions to access this page." +msgstr "You do not have sufficient permissions to access this page." + +msgid "Import from Google Fonts" +msgstr "Import from Google Fonts" + +msgid "Font Name" +msgstr "Font Name" + +msgid "e.g., Open Sans" +msgstr "e.g., Open Sans" + +msgid "Enter the exact font name as it appears on Google Fonts." +msgstr "Enter the exact font name as it appears on Google Fonts." + +msgid "Weights" +msgstr "Weights" + +msgid "Styles" +msgstr "Styles" + +msgid "Normal" +msgstr "Normal" + +msgid "Italic" +msgstr "Italic" + +msgid "Files to download:" +msgstr "Files to download:" + +msgid "Download & Install" +msgstr "Download & Install" + +msgid "Installed Fonts" +msgstr "Installed Fonts" + +msgid "No fonts installed yet." +msgstr "No fonts installed yet." + +msgid "Delete" +msgstr "Delete" + +msgid "Use %s to apply fonts to your site." +msgstr "Use %s to apply fonts to your site." + +msgid "Appearance → Editor → Styles → Typography" +msgstr "Appearance → Editor → Styles → Typography" diff --git a/native/wordpress/maple-fonts-wp/languages/maple-local-fonts-en_US.po b/native/wordpress/maple-fonts-wp/languages/maple-local-fonts-en_US.po new file mode 100644 index 0000000..d1d9ebc --- /dev/null +++ b/native/wordpress/maple-fonts-wp/languages/maple-local-fonts-en_US.po @@ -0,0 +1,222 @@ +# Translation of Maple Local Fonts in English (United States) +# Copyright (C) 2024 Maple Open Technologies +# This file is distributed under the GPL-2.0-or-later. +msgid "" +msgstr "" +"Project-Id-Version: Maple Local Fonts 1.0.0\n" +"Report-Msgid-Bugs-To: https://mapleopentech.org/\n" +"Last-Translator: Maple Open Technologies\n" +"Language-Team: English (United States)\n" +"Language: en_US\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"POT-Creation-Date: 2024-01-01T00:00:00+00:00\n" +"PO-Revision-Date: 2024-01-01T00:00:00+00:00\n" +"X-Generator: Manual\n" +"X-Domain: maple-local-fonts\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" + +#. Plugin Name of the plugin +msgid "Maple Local Fonts" +msgstr "Maple Local Fonts" + +#. Description of the plugin +msgid "Import Google Fonts to local storage and register them with WordPress Font Library for GDPR-compliant, privacy-friendly typography." +msgstr "Import Google Fonts to local storage and register them with WordPress Font Library for GDPR-compliant, privacy-friendly typography." + +#. Author of the plugin +msgid "Maple Open Technologies" +msgstr "Maple Open Technologies" + +msgid "Maple Local Fonts requires WordPress 6.5 or higher for Font Library support." +msgstr "Maple Local Fonts requires WordPress 6.5 or higher for Font Library support." + +msgid "Plugin Activation Error" +msgstr "Plugin Activation Error" + +msgid "Maple Local Fonts has been deactivated. It requires WordPress 6.5 or higher." +msgstr "Maple Local Fonts has been deactivated. It requires WordPress 6.5 or higher." + +msgid "Local Fonts" +msgstr "Local Fonts" + +msgid "Downloading..." +msgstr "Downloading..." + +msgid "Deleting..." +msgstr "Deleting..." + +msgid "Are you sure you want to delete this font?" +msgstr "Are you sure you want to delete this font?" + +msgid "An error occurred. Please try again." +msgstr "An error occurred. Please try again." + +msgid "Please select at least one weight." +msgstr "Please select at least one weight." + +msgid "Please select at least one style." +msgstr "Please select at least one style." + +msgid "Please enter a font name." +msgstr "Please enter a font name." + +msgid "Security check failed." +msgstr "Security check failed." + +msgid "Unauthorized." +msgstr "Unauthorized." + +msgid "Font name is required." +msgstr "Font name is required." + +msgid "Invalid font name: only letters, numbers, spaces, and hyphens allowed." +msgstr "Invalid font name: only letters, numbers, spaces, and hyphens allowed." + +msgid "Font name is too long." +msgstr "Font name is too long." + +msgid "At least one weight is required." +msgstr "At least one weight is required." + +msgid "Too many weights selected." +msgstr "Too many weights selected." + +msgid "At least one style is required." +msgstr "At least one style is required." + +msgid "Successfully installed %s." +msgstr "Successfully installed %s." + +msgid "An unexpected error occurred." +msgstr "An unexpected error occurred." + +msgid "Invalid font ID." +msgstr "Invalid font ID." + +msgid "Font not found." +msgstr "Font not found." + +msgid "Cannot delete fonts not imported by this plugin." +msgstr "Cannot delete fonts not imported by this plugin." + +msgid "Font deleted successfully." +msgstr "Font deleted successfully." + +msgid "Font not found on Google Fonts. Please check the spelling and try again." +msgstr "Font not found on Google Fonts. Please check the spelling and try again." + +msgid "This font is already installed." +msgstr "This font is already installed." + +msgid "Could not connect to Google Fonts. Please check your internet connection and try again." +msgstr "Could not connect to Google Fonts. Please check your internet connection and try again." + +msgid "Google Fonts returned an error. Please try again later." +msgstr "Google Fonts returned an error. Please try again later." + +msgid "Could not process the font data. The font may not be available." +msgstr "Could not process the font data. The font may not be available." + +msgid "Could not download the font files. Please try again." +msgstr "Could not download the font files. Please try again." + +msgid "Could not save font files. Please check that wp-content/fonts is writable." +msgstr "Could not save font files. Please check that wp-content/fonts is writable." + +msgid "Could not create fonts directory. Please check file permissions." +msgstr "Could not create fonts directory. Please check file permissions." + +msgid "Invalid file path." +msgstr "Invalid file path." + +msgid "Invalid font URL." +msgstr "Invalid font URL." + +msgid "Invalid font name." +msgstr "Invalid font name." + +msgid "No valid weights specified." +msgstr "No valid weights specified." + +msgid "No valid styles specified." +msgstr "No valid styles specified." + +msgid "An unexpected error occurred. Please try again." +msgstr "An unexpected error occurred. Please try again." + +msgid "Thin" +msgstr "Thin" + +msgid "Extra Light" +msgstr "Extra Light" + +msgid "Light" +msgstr "Light" + +msgid "Regular" +msgstr "Regular" + +msgid "Medium" +msgstr "Medium" + +msgid "Semi Bold" +msgstr "Semi Bold" + +msgid "Bold" +msgstr "Bold" + +msgid "Extra Bold" +msgstr "Extra Bold" + +msgid "Black" +msgstr "Black" + +msgid "You do not have sufficient permissions to access this page." +msgstr "You do not have sufficient permissions to access this page." + +msgid "Import from Google Fonts" +msgstr "Import from Google Fonts" + +msgid "Font Name" +msgstr "Font Name" + +msgid "e.g., Open Sans" +msgstr "e.g., Open Sans" + +msgid "Enter the exact font name as it appears on Google Fonts." +msgstr "Enter the exact font name as it appears on Google Fonts." + +msgid "Weights" +msgstr "Weights" + +msgid "Styles" +msgstr "Styles" + +msgid "Normal" +msgstr "Normal" + +msgid "Italic" +msgstr "Italic" + +msgid "Files to download:" +msgstr "Files to download:" + +msgid "Download & Install" +msgstr "Download & Install" + +msgid "Installed Fonts" +msgstr "Installed Fonts" + +msgid "No fonts installed yet." +msgstr "No fonts installed yet." + +msgid "Delete" +msgstr "Delete" + +msgid "Use %s to apply fonts to your site." +msgstr "Use %s to apply fonts to your site." + +msgid "Appearance → Editor → Styles → Typography" +msgstr "Appearance → Editor → Styles → Typography" diff --git a/native/wordpress/maple-fonts-wp/languages/maple-local-fonts-es_419.po b/native/wordpress/maple-fonts-wp/languages/maple-local-fonts-es_419.po new file mode 100644 index 0000000..40a122d --- /dev/null +++ b/native/wordpress/maple-fonts-wp/languages/maple-local-fonts-es_419.po @@ -0,0 +1,222 @@ +# Translation of Maple Local Fonts in Spanish (Latin America) +# Copyright (C) 2024 Maple Open Technologies +# This file is distributed under the GPL-2.0-or-later. +msgid "" +msgstr "" +"Project-Id-Version: Maple Local Fonts 1.0.0\n" +"Report-Msgid-Bugs-To: https://mapleopentech.org/\n" +"Last-Translator: Maple Open Technologies\n" +"Language-Team: Spanish (Latin America)\n" +"Language: es_419\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"POT-Creation-Date: 2024-01-01T00:00:00+00:00\n" +"PO-Revision-Date: 2024-01-01T00:00:00+00:00\n" +"X-Generator: Manual\n" +"X-Domain: maple-local-fonts\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" + +#. Plugin Name of the plugin +msgid "Maple Local Fonts" +msgstr "Maple Local Fonts" + +#. Description of the plugin +msgid "Import Google Fonts to local storage and register them with WordPress Font Library for GDPR-compliant, privacy-friendly typography." +msgstr "Importa fuentes de Google Fonts al almacenamiento local y regístralas con la biblioteca de fuentes de WordPress para una tipografía compatible con GDPR y respetuosa con la privacidad." + +#. Author of the plugin +msgid "Maple Open Technologies" +msgstr "Maple Open Technologies" + +msgid "Maple Local Fonts requires WordPress 6.5 or higher for Font Library support." +msgstr "Maple Local Fonts requiere WordPress 6.5 o superior para el soporte de la biblioteca de fuentes." + +msgid "Plugin Activation Error" +msgstr "Error de activación del plugin" + +msgid "Maple Local Fonts has been deactivated. It requires WordPress 6.5 or higher." +msgstr "Maple Local Fonts ha sido desactivado. Requiere WordPress 6.5 o superior." + +msgid "Local Fonts" +msgstr "Fuentes locales" + +msgid "Downloading..." +msgstr "Descargando..." + +msgid "Deleting..." +msgstr "Eliminando..." + +msgid "Are you sure you want to delete this font?" +msgstr "¿Estás seguro de que quieres eliminar esta fuente?" + +msgid "An error occurred. Please try again." +msgstr "Ocurrió un error. Por favor, intenta de nuevo." + +msgid "Please select at least one weight." +msgstr "Por favor, selecciona al menos un peso." + +msgid "Please select at least one style." +msgstr "Por favor, selecciona al menos un estilo." + +msgid "Please enter a font name." +msgstr "Por favor, ingresa un nombre de fuente." + +msgid "Security check failed." +msgstr "Falló la verificación de seguridad." + +msgid "Unauthorized." +msgstr "No autorizado." + +msgid "Font name is required." +msgstr "El nombre de la fuente es requerido." + +msgid "Invalid font name: only letters, numbers, spaces, and hyphens allowed." +msgstr "Nombre de fuente inválido: solo se permiten letras, números, espacios y guiones." + +msgid "Font name is too long." +msgstr "El nombre de la fuente es demasiado largo." + +msgid "At least one weight is required." +msgstr "Se requiere al menos un peso." + +msgid "Too many weights selected." +msgstr "Demasiados pesos seleccionados." + +msgid "At least one style is required." +msgstr "Se requiere al menos un estilo." + +msgid "Successfully installed %s." +msgstr "%s se instaló correctamente." + +msgid "An unexpected error occurred." +msgstr "Ocurrió un error inesperado." + +msgid "Invalid font ID." +msgstr "ID de fuente inválido." + +msgid "Font not found." +msgstr "Fuente no encontrada." + +msgid "Cannot delete fonts not imported by this plugin." +msgstr "No se pueden eliminar fuentes que no fueron importadas por este plugin." + +msgid "Font deleted successfully." +msgstr "Fuente eliminada correctamente." + +msgid "Font not found on Google Fonts. Please check the spelling and try again." +msgstr "Fuente no encontrada en Google Fonts. Por favor, verifica la ortografía e intenta de nuevo." + +msgid "This font is already installed." +msgstr "Esta fuente ya está instalada." + +msgid "Could not connect to Google Fonts. Please check your internet connection and try again." +msgstr "No se pudo conectar a Google Fonts. Por favor, verifica tu conexión a internet e intenta de nuevo." + +msgid "Google Fonts returned an error. Please try again later." +msgstr "Google Fonts devolvió un error. Por favor, intenta más tarde." + +msgid "Could not process the font data. The font may not be available." +msgstr "No se pudieron procesar los datos de la fuente. La fuente puede no estar disponible." + +msgid "Could not download the font files. Please try again." +msgstr "No se pudieron descargar los archivos de la fuente. Por favor, intenta de nuevo." + +msgid "Could not save font files. Please check that wp-content/fonts is writable." +msgstr "No se pudieron guardar los archivos de la fuente. Por favor, verifica que wp-content/fonts tenga permisos de escritura." + +msgid "Could not create fonts directory. Please check file permissions." +msgstr "No se pudo crear el directorio de fuentes. Por favor, verifica los permisos de archivos." + +msgid "Invalid file path." +msgstr "Ruta de archivo inválida." + +msgid "Invalid font URL." +msgstr "URL de fuente inválida." + +msgid "Invalid font name." +msgstr "Nombre de fuente inválido." + +msgid "No valid weights specified." +msgstr "No se especificaron pesos válidos." + +msgid "No valid styles specified." +msgstr "No se especificaron estilos válidos." + +msgid "An unexpected error occurred. Please try again." +msgstr "Ocurrió un error inesperado. Por favor, intenta de nuevo." + +msgid "Thin" +msgstr "Delgada" + +msgid "Extra Light" +msgstr "Extra ligera" + +msgid "Light" +msgstr "Ligera" + +msgid "Regular" +msgstr "Normal" + +msgid "Medium" +msgstr "Media" + +msgid "Semi Bold" +msgstr "Semi negrita" + +msgid "Bold" +msgstr "Negrita" + +msgid "Extra Bold" +msgstr "Extra negrita" + +msgid "Black" +msgstr "Negra" + +msgid "You do not have sufficient permissions to access this page." +msgstr "No tienes permisos suficientes para acceder a esta página." + +msgid "Import from Google Fonts" +msgstr "Importar desde Google Fonts" + +msgid "Font Name" +msgstr "Nombre de la fuente" + +msgid "e.g., Open Sans" +msgstr "ej., Open Sans" + +msgid "Enter the exact font name as it appears on Google Fonts." +msgstr "Ingresa el nombre exacto de la fuente como aparece en Google Fonts." + +msgid "Weights" +msgstr "Pesos" + +msgid "Styles" +msgstr "Estilos" + +msgid "Normal" +msgstr "Normal" + +msgid "Italic" +msgstr "Cursiva" + +msgid "Files to download:" +msgstr "Archivos a descargar:" + +msgid "Download & Install" +msgstr "Descargar e instalar" + +msgid "Installed Fonts" +msgstr "Fuentes instaladas" + +msgid "No fonts installed yet." +msgstr "Aún no hay fuentes instaladas." + +msgid "Delete" +msgstr "Eliminar" + +msgid "Use %s to apply fonts to your site." +msgstr "Usa %s para aplicar fuentes a tu sitio." + +msgid "Appearance → Editor → Styles → Typography" +msgstr "Apariencia → Editor → Estilos → Tipografía" diff --git a/native/wordpress/maple-fonts-wp/languages/maple-local-fonts-es_ES.po b/native/wordpress/maple-fonts-wp/languages/maple-local-fonts-es_ES.po new file mode 100644 index 0000000..59eee99 --- /dev/null +++ b/native/wordpress/maple-fonts-wp/languages/maple-local-fonts-es_ES.po @@ -0,0 +1,222 @@ +# Translation of Maple Local Fonts in Spanish (Spain) +# Copyright (C) 2024 Maple Open Technologies +# This file is distributed under the GPL-2.0-or-later. +msgid "" +msgstr "" +"Project-Id-Version: Maple Local Fonts 1.0.0\n" +"Report-Msgid-Bugs-To: https://mapleopentech.org/\n" +"Last-Translator: Maple Open Technologies\n" +"Language-Team: Spanish (Spain)\n" +"Language: es_ES\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"POT-Creation-Date: 2024-01-01T00:00:00+00:00\n" +"PO-Revision-Date: 2024-01-01T00:00:00+00:00\n" +"X-Generator: Manual\n" +"X-Domain: maple-local-fonts\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" + +#. Plugin Name of the plugin +msgid "Maple Local Fonts" +msgstr "Maple Local Fonts" + +#. Description of the plugin +msgid "Import Google Fonts to local storage and register them with WordPress Font Library for GDPR-compliant, privacy-friendly typography." +msgstr "Importa fuentes de Google Fonts al almacenamiento local y regístralas con la biblioteca de fuentes de WordPress para una tipografía compatible con el RGPD y respetuosa con la privacidad." + +#. Author of the plugin +msgid "Maple Open Technologies" +msgstr "Maple Open Technologies" + +msgid "Maple Local Fonts requires WordPress 6.5 or higher for Font Library support." +msgstr "Maple Local Fonts requiere WordPress 6.5 o superior para el soporte de la biblioteca de fuentes." + +msgid "Plugin Activation Error" +msgstr "Error de activación del plugin" + +msgid "Maple Local Fonts has been deactivated. It requires WordPress 6.5 or higher." +msgstr "Maple Local Fonts ha sido desactivado. Requiere WordPress 6.5 o superior." + +msgid "Local Fonts" +msgstr "Fuentes locales" + +msgid "Downloading..." +msgstr "Descargando..." + +msgid "Deleting..." +msgstr "Eliminando..." + +msgid "Are you sure you want to delete this font?" +msgstr "¿Estás seguro de que quieres eliminar esta fuente?" + +msgid "An error occurred. Please try again." +msgstr "Se ha producido un error. Por favor, inténtalo de nuevo." + +msgid "Please select at least one weight." +msgstr "Por favor, selecciona al menos un peso." + +msgid "Please select at least one style." +msgstr "Por favor, selecciona al menos un estilo." + +msgid "Please enter a font name." +msgstr "Por favor, introduce un nombre de fuente." + +msgid "Security check failed." +msgstr "Ha fallado la verificación de seguridad." + +msgid "Unauthorized." +msgstr "No autorizado." + +msgid "Font name is required." +msgstr "El nombre de la fuente es obligatorio." + +msgid "Invalid font name: only letters, numbers, spaces, and hyphens allowed." +msgstr "Nombre de fuente no válido: solo se permiten letras, números, espacios y guiones." + +msgid "Font name is too long." +msgstr "El nombre de la fuente es demasiado largo." + +msgid "At least one weight is required." +msgstr "Se requiere al menos un peso." + +msgid "Too many weights selected." +msgstr "Demasiados pesos seleccionados." + +msgid "At least one style is required." +msgstr "Se requiere al menos un estilo." + +msgid "Successfully installed %s." +msgstr "%s se ha instalado correctamente." + +msgid "An unexpected error occurred." +msgstr "Se ha producido un error inesperado." + +msgid "Invalid font ID." +msgstr "ID de fuente no válido." + +msgid "Font not found." +msgstr "Fuente no encontrada." + +msgid "Cannot delete fonts not imported by this plugin." +msgstr "No se pueden eliminar fuentes que no hayan sido importadas por este plugin." + +msgid "Font deleted successfully." +msgstr "Fuente eliminada correctamente." + +msgid "Font not found on Google Fonts. Please check the spelling and try again." +msgstr "Fuente no encontrada en Google Fonts. Por favor, comprueba la ortografía e inténtalo de nuevo." + +msgid "This font is already installed." +msgstr "Esta fuente ya está instalada." + +msgid "Could not connect to Google Fonts. Please check your internet connection and try again." +msgstr "No se ha podido conectar a Google Fonts. Por favor, comprueba tu conexión a internet e inténtalo de nuevo." + +msgid "Google Fonts returned an error. Please try again later." +msgstr "Google Fonts ha devuelto un error. Por favor, inténtalo más tarde." + +msgid "Could not process the font data. The font may not be available." +msgstr "No se han podido procesar los datos de la fuente. La fuente puede no estar disponible." + +msgid "Could not download the font files. Please try again." +msgstr "No se han podido descargar los archivos de la fuente. Por favor, inténtalo de nuevo." + +msgid "Could not save font files. Please check that wp-content/fonts is writable." +msgstr "No se han podido guardar los archivos de la fuente. Por favor, comprueba que wp-content/fonts tenga permisos de escritura." + +msgid "Could not create fonts directory. Please check file permissions." +msgstr "No se ha podido crear el directorio de fuentes. Por favor, comprueba los permisos de archivos." + +msgid "Invalid file path." +msgstr "Ruta de archivo no válida." + +msgid "Invalid font URL." +msgstr "URL de fuente no válida." + +msgid "Invalid font name." +msgstr "Nombre de fuente no válido." + +msgid "No valid weights specified." +msgstr "No se han especificado pesos válidos." + +msgid "No valid styles specified." +msgstr "No se han especificado estilos válidos." + +msgid "An unexpected error occurred. Please try again." +msgstr "Se ha producido un error inesperado. Por favor, inténtalo de nuevo." + +msgid "Thin" +msgstr "Fina" + +msgid "Extra Light" +msgstr "Extra ligera" + +msgid "Light" +msgstr "Ligera" + +msgid "Regular" +msgstr "Normal" + +msgid "Medium" +msgstr "Media" + +msgid "Semi Bold" +msgstr "Semi negrita" + +msgid "Bold" +msgstr "Negrita" + +msgid "Extra Bold" +msgstr "Extra negrita" + +msgid "Black" +msgstr "Negra" + +msgid "You do not have sufficient permissions to access this page." +msgstr "No tienes permisos suficientes para acceder a esta página." + +msgid "Import from Google Fonts" +msgstr "Importar desde Google Fonts" + +msgid "Font Name" +msgstr "Nombre de la fuente" + +msgid "e.g., Open Sans" +msgstr "ej., Open Sans" + +msgid "Enter the exact font name as it appears on Google Fonts." +msgstr "Introduce el nombre exacto de la fuente como aparece en Google Fonts." + +msgid "Weights" +msgstr "Pesos" + +msgid "Styles" +msgstr "Estilos" + +msgid "Normal" +msgstr "Normal" + +msgid "Italic" +msgstr "Cursiva" + +msgid "Files to download:" +msgstr "Archivos a descargar:" + +msgid "Download & Install" +msgstr "Descargar e instalar" + +msgid "Installed Fonts" +msgstr "Fuentes instaladas" + +msgid "No fonts installed yet." +msgstr "Aún no hay fuentes instaladas." + +msgid "Delete" +msgstr "Eliminar" + +msgid "Use %s to apply fonts to your site." +msgstr "Usa %s para aplicar fuentes a tu sitio." + +msgid "Appearance → Editor → Styles → Typography" +msgstr "Apariencia → Editor → Estilos → Tipografía" diff --git a/native/wordpress/maple-fonts-wp/languages/maple-local-fonts-es_MX.po b/native/wordpress/maple-fonts-wp/languages/maple-local-fonts-es_MX.po new file mode 100644 index 0000000..f5734f1 --- /dev/null +++ b/native/wordpress/maple-fonts-wp/languages/maple-local-fonts-es_MX.po @@ -0,0 +1,222 @@ +# Translation of Maple Local Fonts in Spanish (Mexico) +# Copyright (C) 2024 Maple Open Technologies +# This file is distributed under the GPL-2.0-or-later. +msgid "" +msgstr "" +"Project-Id-Version: Maple Local Fonts 1.0.0\n" +"Report-Msgid-Bugs-To: https://mapleopentech.org/\n" +"Last-Translator: Maple Open Technologies\n" +"Language-Team: Spanish (Mexico)\n" +"Language: es_MX\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"POT-Creation-Date: 2024-01-01T00:00:00+00:00\n" +"PO-Revision-Date: 2024-01-01T00:00:00+00:00\n" +"X-Generator: Manual\n" +"X-Domain: maple-local-fonts\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" + +#. Plugin Name of the plugin +msgid "Maple Local Fonts" +msgstr "Maple Local Fonts" + +#. Description of the plugin +msgid "Import Google Fonts to local storage and register them with WordPress Font Library for GDPR-compliant, privacy-friendly typography." +msgstr "Importa fuentes de Google Fonts al almacenamiento local y regístralas con la biblioteca de fuentes de WordPress para una tipografía compatible con GDPR y respetuosa con la privacidad." + +#. Author of the plugin +msgid "Maple Open Technologies" +msgstr "Maple Open Technologies" + +msgid "Maple Local Fonts requires WordPress 6.5 or higher for Font Library support." +msgstr "Maple Local Fonts requiere WordPress 6.5 o superior para el soporte de la biblioteca de fuentes." + +msgid "Plugin Activation Error" +msgstr "Error de activación del plugin" + +msgid "Maple Local Fonts has been deactivated. It requires WordPress 6.5 or higher." +msgstr "Maple Local Fonts ha sido desactivado. Requiere WordPress 6.5 o superior." + +msgid "Local Fonts" +msgstr "Fuentes locales" + +msgid "Downloading..." +msgstr "Descargando..." + +msgid "Deleting..." +msgstr "Eliminando..." + +msgid "Are you sure you want to delete this font?" +msgstr "¿Estás seguro de que quieres eliminar esta fuente?" + +msgid "An error occurred. Please try again." +msgstr "Ocurrió un error. Por favor, inténtalo de nuevo." + +msgid "Please select at least one weight." +msgstr "Por favor, selecciona al menos un peso." + +msgid "Please select at least one style." +msgstr "Por favor, selecciona al menos un estilo." + +msgid "Please enter a font name." +msgstr "Por favor, ingresa un nombre de fuente." + +msgid "Security check failed." +msgstr "Falló la verificación de seguridad." + +msgid "Unauthorized." +msgstr "No autorizado." + +msgid "Font name is required." +msgstr "El nombre de la fuente es requerido." + +msgid "Invalid font name: only letters, numbers, spaces, and hyphens allowed." +msgstr "Nombre de fuente inválido: solo se permiten letras, números, espacios y guiones." + +msgid "Font name is too long." +msgstr "El nombre de la fuente es demasiado largo." + +msgid "At least one weight is required." +msgstr "Se requiere al menos un peso." + +msgid "Too many weights selected." +msgstr "Demasiados pesos seleccionados." + +msgid "At least one style is required." +msgstr "Se requiere al menos un estilo." + +msgid "Successfully installed %s." +msgstr "%s se instaló correctamente." + +msgid "An unexpected error occurred." +msgstr "Ocurrió un error inesperado." + +msgid "Invalid font ID." +msgstr "ID de fuente inválido." + +msgid "Font not found." +msgstr "Fuente no encontrada." + +msgid "Cannot delete fonts not imported by this plugin." +msgstr "No se pueden eliminar fuentes que no fueron importadas por este plugin." + +msgid "Font deleted successfully." +msgstr "Fuente eliminada correctamente." + +msgid "Font not found on Google Fonts. Please check the spelling and try again." +msgstr "Fuente no encontrada en Google Fonts. Por favor, verifica la ortografía e inténtalo de nuevo." + +msgid "This font is already installed." +msgstr "Esta fuente ya está instalada." + +msgid "Could not connect to Google Fonts. Please check your internet connection and try again." +msgstr "No se pudo conectar a Google Fonts. Por favor, verifica tu conexión a internet e inténtalo de nuevo." + +msgid "Google Fonts returned an error. Please try again later." +msgstr "Google Fonts devolvió un error. Por favor, inténtalo más tarde." + +msgid "Could not process the font data. The font may not be available." +msgstr "No se pudieron procesar los datos de la fuente. La fuente puede no estar disponible." + +msgid "Could not download the font files. Please try again." +msgstr "No se pudieron descargar los archivos de la fuente. Por favor, inténtalo de nuevo." + +msgid "Could not save font files. Please check that wp-content/fonts is writable." +msgstr "No se pudieron guardar los archivos de la fuente. Por favor, verifica que wp-content/fonts tenga permisos de escritura." + +msgid "Could not create fonts directory. Please check file permissions." +msgstr "No se pudo crear el directorio de fuentes. Por favor, verifica los permisos de archivos." + +msgid "Invalid file path." +msgstr "Ruta de archivo inválida." + +msgid "Invalid font URL." +msgstr "URL de fuente inválida." + +msgid "Invalid font name." +msgstr "Nombre de fuente inválido." + +msgid "No valid weights specified." +msgstr "No se especificaron pesos válidos." + +msgid "No valid styles specified." +msgstr "No se especificaron estilos válidos." + +msgid "An unexpected error occurred. Please try again." +msgstr "Ocurrió un error inesperado. Por favor, inténtalo de nuevo." + +msgid "Thin" +msgstr "Delgada" + +msgid "Extra Light" +msgstr "Extra ligera" + +msgid "Light" +msgstr "Ligera" + +msgid "Regular" +msgstr "Normal" + +msgid "Medium" +msgstr "Media" + +msgid "Semi Bold" +msgstr "Semi negrita" + +msgid "Bold" +msgstr "Negrita" + +msgid "Extra Bold" +msgstr "Extra negrita" + +msgid "Black" +msgstr "Negra" + +msgid "You do not have sufficient permissions to access this page." +msgstr "No tienes permisos suficientes para acceder a esta página." + +msgid "Import from Google Fonts" +msgstr "Importar desde Google Fonts" + +msgid "Font Name" +msgstr "Nombre de la fuente" + +msgid "e.g., Open Sans" +msgstr "ej., Open Sans" + +msgid "Enter the exact font name as it appears on Google Fonts." +msgstr "Ingresa el nombre exacto de la fuente como aparece en Google Fonts." + +msgid "Weights" +msgstr "Pesos" + +msgid "Styles" +msgstr "Estilos" + +msgid "Normal" +msgstr "Normal" + +msgid "Italic" +msgstr "Cursiva" + +msgid "Files to download:" +msgstr "Archivos a descargar:" + +msgid "Download & Install" +msgstr "Descargar e instalar" + +msgid "Installed Fonts" +msgstr "Fuentes instaladas" + +msgid "No fonts installed yet." +msgstr "Aún no hay fuentes instaladas." + +msgid "Delete" +msgstr "Eliminar" + +msgid "Use %s to apply fonts to your site." +msgstr "Usa %s para aplicar fuentes a tu sitio." + +msgid "Appearance → Editor → Styles → Typography" +msgstr "Apariencia → Editor → Estilos → Tipografía" diff --git a/native/wordpress/maple-fonts-wp/languages/maple-local-fonts-fr_CA.po b/native/wordpress/maple-fonts-wp/languages/maple-local-fonts-fr_CA.po new file mode 100644 index 0000000..3290b53 --- /dev/null +++ b/native/wordpress/maple-fonts-wp/languages/maple-local-fonts-fr_CA.po @@ -0,0 +1,222 @@ +# Translation of Maple Local Fonts in French (Canada) +# Copyright (C) 2024 Maple Open Technologies +# This file is distributed under the GPL-2.0-or-later. +msgid "" +msgstr "" +"Project-Id-Version: Maple Local Fonts 1.0.0\n" +"Report-Msgid-Bugs-To: https://mapleopentech.org/\n" +"Last-Translator: Maple Open Technologies\n" +"Language-Team: French (Canada)\n" +"Language: fr_CA\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"POT-Creation-Date: 2024-01-01T00:00:00+00:00\n" +"PO-Revision-Date: 2024-01-01T00:00:00+00:00\n" +"X-Generator: Manual\n" +"X-Domain: maple-local-fonts\n" +"Plural-Forms: nplurals=2; plural=(n > 1);\n" + +#. Plugin Name of the plugin +msgid "Maple Local Fonts" +msgstr "Maple Local Fonts" + +#. Description of the plugin +msgid "Import Google Fonts to local storage and register them with WordPress Font Library for GDPR-compliant, privacy-friendly typography." +msgstr "Importez des polices Google Fonts vers le stockage local et enregistrez-les avec la bibliothèque de polices WordPress pour une typographie conforme au RGPD et respectueuse de la vie privée." + +#. Author of the plugin +msgid "Maple Open Technologies" +msgstr "Maple Open Technologies" + +msgid "Maple Local Fonts requires WordPress 6.5 or higher for Font Library support." +msgstr "Maple Local Fonts nécessite WordPress 6.5 ou supérieur pour la prise en charge de la bibliothèque de polices." + +msgid "Plugin Activation Error" +msgstr "Erreur d'activation de l'extension" + +msgid "Maple Local Fonts has been deactivated. It requires WordPress 6.5 or higher." +msgstr "Maple Local Fonts a été désactivé. Il nécessite WordPress 6.5 ou supérieur." + +msgid "Local Fonts" +msgstr "Polices locales" + +msgid "Downloading..." +msgstr "Téléchargement..." + +msgid "Deleting..." +msgstr "Suppression..." + +msgid "Are you sure you want to delete this font?" +msgstr "Êtes-vous certain de vouloir supprimer cette police?" + +msgid "An error occurred. Please try again." +msgstr "Une erreur s'est produite. Veuillez réessayer." + +msgid "Please select at least one weight." +msgstr "Veuillez sélectionner au moins une graisse." + +msgid "Please select at least one style." +msgstr "Veuillez sélectionner au moins un style." + +msgid "Please enter a font name." +msgstr "Veuillez entrer un nom de police." + +msgid "Security check failed." +msgstr "Échec de la vérification de sécurité." + +msgid "Unauthorized." +msgstr "Non autorisé." + +msgid "Font name is required." +msgstr "Le nom de la police est requis." + +msgid "Invalid font name: only letters, numbers, spaces, and hyphens allowed." +msgstr "Nom de police invalide : seuls les lettres, chiffres, espaces et tirets sont autorisés." + +msgid "Font name is too long." +msgstr "Le nom de la police est trop long." + +msgid "At least one weight is required." +msgstr "Au moins une graisse est requise." + +msgid "Too many weights selected." +msgstr "Trop de graisses sélectionnées." + +msgid "At least one style is required." +msgstr "Au moins un style est requis." + +msgid "Successfully installed %s." +msgstr "%s a été installé avec succès." + +msgid "An unexpected error occurred." +msgstr "Une erreur inattendue s'est produite." + +msgid "Invalid font ID." +msgstr "ID de police invalide." + +msgid "Font not found." +msgstr "Police introuvable." + +msgid "Cannot delete fonts not imported by this plugin." +msgstr "Impossible de supprimer les polices non importées par cette extension." + +msgid "Font deleted successfully." +msgstr "Police supprimée avec succès." + +msgid "Font not found on Google Fonts. Please check the spelling and try again." +msgstr "Police introuvable sur Google Fonts. Veuillez vérifier l'orthographe et réessayer." + +msgid "This font is already installed." +msgstr "Cette police est déjà installée." + +msgid "Could not connect to Google Fonts. Please check your internet connection and try again." +msgstr "Connexion à Google Fonts impossible. Veuillez vérifier votre connexion Internet et réessayer." + +msgid "Google Fonts returned an error. Please try again later." +msgstr "Google Fonts a retourné une erreur. Veuillez réessayer plus tard." + +msgid "Could not process the font data. The font may not be available." +msgstr "Impossible de traiter les données de la police. La police n'est peut-être pas disponible." + +msgid "Could not download the font files. Please try again." +msgstr "Impossible de télécharger les fichiers de police. Veuillez réessayer." + +msgid "Could not save font files. Please check that wp-content/fonts is writable." +msgstr "Impossible d'enregistrer les fichiers de police. Veuillez vérifier que wp-content/fonts est accessible en écriture." + +msgid "Could not create fonts directory. Please check file permissions." +msgstr "Impossible de créer le répertoire des polices. Veuillez vérifier les permissions de fichiers." + +msgid "Invalid file path." +msgstr "Chemin de fichier invalide." + +msgid "Invalid font URL." +msgstr "URL de police invalide." + +msgid "Invalid font name." +msgstr "Nom de police invalide." + +msgid "No valid weights specified." +msgstr "Aucune graisse valide spécifiée." + +msgid "No valid styles specified." +msgstr "Aucun style valide spécifié." + +msgid "An unexpected error occurred. Please try again." +msgstr "Une erreur inattendue s'est produite. Veuillez réessayer." + +msgid "Thin" +msgstr "Fin" + +msgid "Extra Light" +msgstr "Extra léger" + +msgid "Light" +msgstr "Léger" + +msgid "Regular" +msgstr "Normal" + +msgid "Medium" +msgstr "Moyen" + +msgid "Semi Bold" +msgstr "Semi-gras" + +msgid "Bold" +msgstr "Gras" + +msgid "Extra Bold" +msgstr "Extra gras" + +msgid "Black" +msgstr "Noir" + +msgid "You do not have sufficient permissions to access this page." +msgstr "Vous n'avez pas les permissions suffisantes pour accéder à cette page." + +msgid "Import from Google Fonts" +msgstr "Importer depuis Google Fonts" + +msgid "Font Name" +msgstr "Nom de la police" + +msgid "e.g., Open Sans" +msgstr "ex. : Open Sans" + +msgid "Enter the exact font name as it appears on Google Fonts." +msgstr "Entrez le nom exact de la police tel qu'il apparaît sur Google Fonts." + +msgid "Weights" +msgstr "Graisses" + +msgid "Styles" +msgstr "Styles" + +msgid "Normal" +msgstr "Normal" + +msgid "Italic" +msgstr "Italique" + +msgid "Files to download:" +msgstr "Fichiers à télécharger :" + +msgid "Download & Install" +msgstr "Télécharger et installer" + +msgid "Installed Fonts" +msgstr "Polices installées" + +msgid "No fonts installed yet." +msgstr "Aucune police installée pour le moment." + +msgid "Delete" +msgstr "Supprimer" + +msgid "Use %s to apply fonts to your site." +msgstr "Utilisez %s pour appliquer les polices à votre site." + +msgid "Appearance → Editor → Styles → Typography" +msgstr "Apparence → Éditeur → Styles → Typographie" diff --git a/native/wordpress/maple-fonts-wp/languages/maple-local-fonts-fr_FR.po b/native/wordpress/maple-fonts-wp/languages/maple-local-fonts-fr_FR.po new file mode 100644 index 0000000..66cb436 --- /dev/null +++ b/native/wordpress/maple-fonts-wp/languages/maple-local-fonts-fr_FR.po @@ -0,0 +1,222 @@ +# Translation of Maple Local Fonts in French (France) +# Copyright (C) 2024 Maple Open Technologies +# This file is distributed under the GPL-2.0-or-later. +msgid "" +msgstr "" +"Project-Id-Version: Maple Local Fonts 1.0.0\n" +"Report-Msgid-Bugs-To: https://mapleopentech.org/\n" +"Last-Translator: Maple Open Technologies\n" +"Language-Team: French (France)\n" +"Language: fr_FR\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"POT-Creation-Date: 2024-01-01T00:00:00+00:00\n" +"PO-Revision-Date: 2024-01-01T00:00:00+00:00\n" +"X-Generator: Manual\n" +"X-Domain: maple-local-fonts\n" +"Plural-Forms: nplurals=2; plural=(n > 1);\n" + +#. Plugin Name of the plugin +msgid "Maple Local Fonts" +msgstr "Maple Local Fonts" + +#. Description of the plugin +msgid "Import Google Fonts to local storage and register them with WordPress Font Library for GDPR-compliant, privacy-friendly typography." +msgstr "Importez des polices Google Fonts vers le stockage local et enregistrez-les avec la bibliothèque de polices WordPress pour une typographie conforme au RGPD et respectueuse de la vie privée." + +#. Author of the plugin +msgid "Maple Open Technologies" +msgstr "Maple Open Technologies" + +msgid "Maple Local Fonts requires WordPress 6.5 or higher for Font Library support." +msgstr "Maple Local Fonts nécessite WordPress 6.5 ou supérieur pour la prise en charge de la bibliothèque de polices." + +msgid "Plugin Activation Error" +msgstr "Erreur d'activation de l'extension" + +msgid "Maple Local Fonts has been deactivated. It requires WordPress 6.5 or higher." +msgstr "Maple Local Fonts a été désactivé. Il nécessite WordPress 6.5 ou supérieur." + +msgid "Local Fonts" +msgstr "Polices locales" + +msgid "Downloading..." +msgstr "Téléchargement..." + +msgid "Deleting..." +msgstr "Suppression..." + +msgid "Are you sure you want to delete this font?" +msgstr "Êtes-vous sûr de vouloir supprimer cette police ?" + +msgid "An error occurred. Please try again." +msgstr "Une erreur s'est produite. Veuillez réessayer." + +msgid "Please select at least one weight." +msgstr "Veuillez sélectionner au moins une graisse." + +msgid "Please select at least one style." +msgstr "Veuillez sélectionner au moins un style." + +msgid "Please enter a font name." +msgstr "Veuillez saisir un nom de police." + +msgid "Security check failed." +msgstr "Échec de la vérification de sécurité." + +msgid "Unauthorized." +msgstr "Non autorisé." + +msgid "Font name is required." +msgstr "Le nom de la police est requis." + +msgid "Invalid font name: only letters, numbers, spaces, and hyphens allowed." +msgstr "Nom de police non valide : seuls les lettres, chiffres, espaces et tirets sont autorisés." + +msgid "Font name is too long." +msgstr "Le nom de la police est trop long." + +msgid "At least one weight is required." +msgstr "Au moins une graisse est requise." + +msgid "Too many weights selected." +msgstr "Trop de graisses sélectionnées." + +msgid "At least one style is required." +msgstr "Au moins un style est requis." + +msgid "Successfully installed %s." +msgstr "%s a été installé avec succès." + +msgid "An unexpected error occurred." +msgstr "Une erreur inattendue s'est produite." + +msgid "Invalid font ID." +msgstr "ID de police non valide." + +msgid "Font not found." +msgstr "Police introuvable." + +msgid "Cannot delete fonts not imported by this plugin." +msgstr "Impossible de supprimer les polices non importées par cette extension." + +msgid "Font deleted successfully." +msgstr "Police supprimée avec succès." + +msgid "Font not found on Google Fonts. Please check the spelling and try again." +msgstr "Police introuvable sur Google Fonts. Veuillez vérifier l'orthographe et réessayer." + +msgid "This font is already installed." +msgstr "Cette police est déjà installée." + +msgid "Could not connect to Google Fonts. Please check your internet connection and try again." +msgstr "Impossible de se connecter à Google Fonts. Veuillez vérifier votre connexion Internet et réessayer." + +msgid "Google Fonts returned an error. Please try again later." +msgstr "Google Fonts a renvoyé une erreur. Veuillez réessayer plus tard." + +msgid "Could not process the font data. The font may not be available." +msgstr "Impossible de traiter les données de la police. La police n'est peut-être pas disponible." + +msgid "Could not download the font files. Please try again." +msgstr "Impossible de télécharger les fichiers de police. Veuillez réessayer." + +msgid "Could not save font files. Please check that wp-content/fonts is writable." +msgstr "Impossible d'enregistrer les fichiers de police. Veuillez vérifier que wp-content/fonts est accessible en écriture." + +msgid "Could not create fonts directory. Please check file permissions." +msgstr "Impossible de créer le répertoire des polices. Veuillez vérifier les droits d'accès aux fichiers." + +msgid "Invalid file path." +msgstr "Chemin de fichier non valide." + +msgid "Invalid font URL." +msgstr "URL de police non valide." + +msgid "Invalid font name." +msgstr "Nom de police non valide." + +msgid "No valid weights specified." +msgstr "Aucune graisse valide spécifiée." + +msgid "No valid styles specified." +msgstr "Aucun style valide spécifié." + +msgid "An unexpected error occurred. Please try again." +msgstr "Une erreur inattendue s'est produite. Veuillez réessayer." + +msgid "Thin" +msgstr "Fin" + +msgid "Extra Light" +msgstr "Extra léger" + +msgid "Light" +msgstr "Léger" + +msgid "Regular" +msgstr "Normal" + +msgid "Medium" +msgstr "Moyen" + +msgid "Semi Bold" +msgstr "Semi-gras" + +msgid "Bold" +msgstr "Gras" + +msgid "Extra Bold" +msgstr "Extra gras" + +msgid "Black" +msgstr "Noir" + +msgid "You do not have sufficient permissions to access this page." +msgstr "Vous n'avez pas les droits suffisants pour accéder à cette page." + +msgid "Import from Google Fonts" +msgstr "Importer depuis Google Fonts" + +msgid "Font Name" +msgstr "Nom de la police" + +msgid "e.g., Open Sans" +msgstr "ex. : Open Sans" + +msgid "Enter the exact font name as it appears on Google Fonts." +msgstr "Saisissez le nom exact de la police tel qu'il apparaît sur Google Fonts." + +msgid "Weights" +msgstr "Graisses" + +msgid "Styles" +msgstr "Styles" + +msgid "Normal" +msgstr "Normal" + +msgid "Italic" +msgstr "Italique" + +msgid "Files to download:" +msgstr "Fichiers à télécharger :" + +msgid "Download & Install" +msgstr "Télécharger et installer" + +msgid "Installed Fonts" +msgstr "Polices installées" + +msgid "No fonts installed yet." +msgstr "Aucune police installée pour le moment." + +msgid "Delete" +msgstr "Supprimer" + +msgid "Use %s to apply fonts to your site." +msgstr "Utilisez %s pour appliquer les polices à votre site." + +msgid "Appearance → Editor → Styles → Typography" +msgstr "Apparence → Éditeur → Styles → Typographie" diff --git a/native/wordpress/maple-fonts-wp/languages/maple-local-fonts-hi_IN.po b/native/wordpress/maple-fonts-wp/languages/maple-local-fonts-hi_IN.po new file mode 100644 index 0000000..c75d8b5 --- /dev/null +++ b/native/wordpress/maple-fonts-wp/languages/maple-local-fonts-hi_IN.po @@ -0,0 +1,222 @@ +# Translation of Maple Local Fonts in Hindi +# Copyright (C) 2024 Maple Open Technologies +# This file is distributed under the GPL-2.0-or-later. +msgid "" +msgstr "" +"Project-Id-Version: Maple Local Fonts 1.0.0\n" +"Report-Msgid-Bugs-To: https://mapleopentech.org/\n" +"Last-Translator: Maple Open Technologies\n" +"Language-Team: Hindi\n" +"Language: hi_IN\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"POT-Creation-Date: 2024-01-01T00:00:00+00:00\n" +"PO-Revision-Date: 2024-01-01T00:00:00+00:00\n" +"X-Generator: Manual\n" +"X-Domain: maple-local-fonts\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" + +#. Plugin Name of the plugin +msgid "Maple Local Fonts" +msgstr "Maple Local Fonts" + +#. Description of the plugin +msgid "Import Google Fonts to local storage and register them with WordPress Font Library for GDPR-compliant, privacy-friendly typography." +msgstr "Google Fonts को स्थानीय भंडारण में आयात करें और उन्हें GDPR-अनुपालन और गोपनीयता-अनुकूल टाइपोग्राफी के लिए WordPress फ़ॉन्ट लाइब्रेरी में पंजीकृत करें।" + +#. Author of the plugin +msgid "Maple Open Technologies" +msgstr "Maple Open Technologies" + +msgid "Maple Local Fonts requires WordPress 6.5 or higher for Font Library support." +msgstr "Maple Local Fonts को फ़ॉन्ट लाइब्रेरी सहायता के लिए WordPress 6.5 या उच्चतर की आवश्यकता है।" + +msgid "Plugin Activation Error" +msgstr "प्लगइन सक्रियण त्रुटि" + +msgid "Maple Local Fonts has been deactivated. It requires WordPress 6.5 or higher." +msgstr "Maple Local Fonts निष्क्रिय कर दिया गया है। इसके लिए WordPress 6.5 या उच्चतर आवश्यक है।" + +msgid "Local Fonts" +msgstr "स्थानीय फ़ॉन्ट" + +msgid "Downloading..." +msgstr "डाउनलोड हो रहा है..." + +msgid "Deleting..." +msgstr "हटाया जा रहा है..." + +msgid "Are you sure you want to delete this font?" +msgstr "क्या आप वाकई इस फ़ॉन्ट को हटाना चाहते हैं?" + +msgid "An error occurred. Please try again." +msgstr "एक त्रुटि हुई। कृपया पुनः प्रयास करें।" + +msgid "Please select at least one weight." +msgstr "कृपया कम से कम एक वज़न चुनें।" + +msgid "Please select at least one style." +msgstr "कृपया कम से कम एक शैली चुनें।" + +msgid "Please enter a font name." +msgstr "कृपया फ़ॉन्ट का नाम दर्ज करें।" + +msgid "Security check failed." +msgstr "सुरक्षा जांच विफल।" + +msgid "Unauthorized." +msgstr "अनधिकृत।" + +msgid "Font name is required." +msgstr "फ़ॉन्ट नाम आवश्यक है।" + +msgid "Invalid font name: only letters, numbers, spaces, and hyphens allowed." +msgstr "अमान्य फ़ॉन्ट नाम: केवल अक्षर, संख्याएं, रिक्त स्थान और हाइफ़न की अनुमति है।" + +msgid "Font name is too long." +msgstr "फ़ॉन्ट नाम बहुत लंबा है।" + +msgid "At least one weight is required." +msgstr "कम से कम एक वज़न आवश्यक है।" + +msgid "Too many weights selected." +msgstr "बहुत अधिक वज़न चुने गए।" + +msgid "At least one style is required." +msgstr "कम से कम एक शैली आवश्यक है।" + +msgid "Successfully installed %s." +msgstr "%s सफलतापूर्वक स्थापित किया गया।" + +msgid "An unexpected error occurred." +msgstr "एक अप्रत्याशित त्रुटि हुई।" + +msgid "Invalid font ID." +msgstr "अमान्य फ़ॉन्ट ID।" + +msgid "Font not found." +msgstr "फ़ॉन्ट नहीं मिला।" + +msgid "Cannot delete fonts not imported by this plugin." +msgstr "इस प्लगइन द्वारा आयात नहीं किए गए फ़ॉन्ट को हटाया नहीं जा सकता।" + +msgid "Font deleted successfully." +msgstr "फ़ॉन्ट सफलतापूर्वक हटाया गया।" + +msgid "Font not found on Google Fonts. Please check the spelling and try again." +msgstr "Google Fonts पर फ़ॉन्ट नहीं मिला। कृपया वर्तनी जांचें और पुनः प्रयास करें।" + +msgid "This font is already installed." +msgstr "यह फ़ॉन्ट पहले से स्थापित है।" + +msgid "Could not connect to Google Fonts. Please check your internet connection and try again." +msgstr "Google Fonts से कनेक्ट नहीं हो सका। कृपया अपना इंटरनेट कनेक्शन जांचें और पुनः प्रयास करें।" + +msgid "Google Fonts returned an error. Please try again later." +msgstr "Google Fonts ने एक त्रुटि लौटाई। कृपया बाद में पुनः प्रयास करें।" + +msgid "Could not process the font data. The font may not be available." +msgstr "फ़ॉन्ट डेटा को संसाधित नहीं किया जा सका। फ़ॉन्ट उपलब्ध नहीं हो सकता है।" + +msgid "Could not download the font files. Please try again." +msgstr "फ़ॉन्ट फ़ाइलें डाउनलोड नहीं हो सकीं। कृपया पुनः प्रयास करें।" + +msgid "Could not save font files. Please check that wp-content/fonts is writable." +msgstr "फ़ॉन्ट फ़ाइलें सहेजी नहीं जा सकीं। कृपया जांचें कि wp-content/fonts लिखने योग्य है।" + +msgid "Could not create fonts directory. Please check file permissions." +msgstr "फ़ॉन्ट निर्देशिका नहीं बनाई जा सकी। कृपया फ़ाइल अनुमतियां जांचें।" + +msgid "Invalid file path." +msgstr "अमान्य फ़ाइल पथ।" + +msgid "Invalid font URL." +msgstr "अमान्य फ़ॉन्ट URL।" + +msgid "Invalid font name." +msgstr "अमान्य फ़ॉन्ट नाम।" + +msgid "No valid weights specified." +msgstr "कोई वैध वज़न निर्दिष्ट नहीं।" + +msgid "No valid styles specified." +msgstr "कोई वैध शैली निर्दिष्ट नहीं।" + +msgid "An unexpected error occurred. Please try again." +msgstr "एक अप्रत्याशित त्रुटि हुई। कृपया पुनः प्रयास करें।" + +msgid "Thin" +msgstr "पतला" + +msgid "Extra Light" +msgstr "अति हल्का" + +msgid "Light" +msgstr "हल्का" + +msgid "Regular" +msgstr "सामान्य" + +msgid "Medium" +msgstr "मध्यम" + +msgid "Semi Bold" +msgstr "अर्ध-मोटा" + +msgid "Bold" +msgstr "मोटा" + +msgid "Extra Bold" +msgstr "अति मोटा" + +msgid "Black" +msgstr "काला" + +msgid "You do not have sufficient permissions to access this page." +msgstr "आपके पास इस पृष्ठ तक पहुंचने की पर्याप्त अनुमति नहीं है।" + +msgid "Import from Google Fonts" +msgstr "Google Fonts से आयात करें" + +msgid "Font Name" +msgstr "फ़ॉन्ट नाम" + +msgid "e.g., Open Sans" +msgstr "उदा., Open Sans" + +msgid "Enter the exact font name as it appears on Google Fonts." +msgstr "Google Fonts पर दिखाई देने वाला सटीक फ़ॉन्ट नाम दर्ज करें।" + +msgid "Weights" +msgstr "वज़न" + +msgid "Styles" +msgstr "शैलियां" + +msgid "Normal" +msgstr "सामान्य" + +msgid "Italic" +msgstr "इटैलिक" + +msgid "Files to download:" +msgstr "डाउनलोड करने के लिए फ़ाइलें:" + +msgid "Download & Install" +msgstr "डाउनलोड और इंस्टॉल करें" + +msgid "Installed Fonts" +msgstr "स्थापित फ़ॉन्ट" + +msgid "No fonts installed yet." +msgstr "अभी तक कोई फ़ॉन्ट स्थापित नहीं।" + +msgid "Delete" +msgstr "हटाएं" + +msgid "Use %s to apply fonts to your site." +msgstr "अपनी साइट पर फ़ॉन्ट लागू करने के लिए %s का उपयोग करें।" + +msgid "Appearance → Editor → Styles → Typography" +msgstr "प्रकटन → संपादक → शैलियां → टाइपोग्राफी" diff --git a/native/wordpress/maple-fonts-wp/languages/maple-local-fonts-id_ID.po b/native/wordpress/maple-fonts-wp/languages/maple-local-fonts-id_ID.po new file mode 100644 index 0000000..edd76e0 --- /dev/null +++ b/native/wordpress/maple-fonts-wp/languages/maple-local-fonts-id_ID.po @@ -0,0 +1,222 @@ +# Translation of Maple Local Fonts in Indonesian +# Copyright (C) 2024 Maple Open Technologies +# This file is distributed under the GPL-2.0-or-later. +msgid "" +msgstr "" +"Project-Id-Version: Maple Local Fonts 1.0.0\n" +"Report-Msgid-Bugs-To: https://mapleopentech.org/\n" +"Last-Translator: Maple Open Technologies\n" +"Language-Team: Indonesian\n" +"Language: id_ID\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"POT-Creation-Date: 2024-01-01T00:00:00+00:00\n" +"PO-Revision-Date: 2024-01-01T00:00:00+00:00\n" +"X-Generator: Manual\n" +"X-Domain: maple-local-fonts\n" +"Plural-Forms: nplurals=1; plural=0;\n" + +#. Plugin Name of the plugin +msgid "Maple Local Fonts" +msgstr "Maple Local Fonts" + +#. Description of the plugin +msgid "Import Google Fonts to local storage and register them with WordPress Font Library for GDPR-compliant, privacy-friendly typography." +msgstr "Impor Google Fonts ke penyimpanan lokal dan daftarkan ke Perpustakaan Font WordPress untuk tipografi yang sesuai GDPR dan ramah privasi." + +#. Author of the plugin +msgid "Maple Open Technologies" +msgstr "Maple Open Technologies" + +msgid "Maple Local Fonts requires WordPress 6.5 or higher for Font Library support." +msgstr "Maple Local Fonts memerlukan WordPress 6.5 atau lebih tinggi untuk dukungan Perpustakaan Font." + +msgid "Plugin Activation Error" +msgstr "Kesalahan Aktivasi Plugin" + +msgid "Maple Local Fonts has been deactivated. It requires WordPress 6.5 or higher." +msgstr "Maple Local Fonts telah dinonaktifkan. Memerlukan WordPress 6.5 atau lebih tinggi." + +msgid "Local Fonts" +msgstr "Font Lokal" + +msgid "Downloading..." +msgstr "Mengunduh..." + +msgid "Deleting..." +msgstr "Menghapus..." + +msgid "Are you sure you want to delete this font?" +msgstr "Apakah Anda yakin ingin menghapus font ini?" + +msgid "An error occurred. Please try again." +msgstr "Terjadi kesalahan. Silakan coba lagi." + +msgid "Please select at least one weight." +msgstr "Silakan pilih setidaknya satu ketebalan." + +msgid "Please select at least one style." +msgstr "Silakan pilih setidaknya satu gaya." + +msgid "Please enter a font name." +msgstr "Silakan masukkan nama font." + +msgid "Security check failed." +msgstr "Pemeriksaan keamanan gagal." + +msgid "Unauthorized." +msgstr "Tidak diizinkan." + +msgid "Font name is required." +msgstr "Nama font diperlukan." + +msgid "Invalid font name: only letters, numbers, spaces, and hyphens allowed." +msgstr "Nama font tidak valid: hanya huruf, angka, spasi, dan tanda hubung yang diperbolehkan." + +msgid "Font name is too long." +msgstr "Nama font terlalu panjang." + +msgid "At least one weight is required." +msgstr "Setidaknya satu ketebalan diperlukan." + +msgid "Too many weights selected." +msgstr "Terlalu banyak ketebalan dipilih." + +msgid "At least one style is required." +msgstr "Setidaknya satu gaya diperlukan." + +msgid "Successfully installed %s." +msgstr "Berhasil menginstal %s." + +msgid "An unexpected error occurred." +msgstr "Terjadi kesalahan yang tidak terduga." + +msgid "Invalid font ID." +msgstr "ID font tidak valid." + +msgid "Font not found." +msgstr "Font tidak ditemukan." + +msgid "Cannot delete fonts not imported by this plugin." +msgstr "Tidak dapat menghapus font yang tidak diimpor oleh plugin ini." + +msgid "Font deleted successfully." +msgstr "Font berhasil dihapus." + +msgid "Font not found on Google Fonts. Please check the spelling and try again." +msgstr "Font tidak ditemukan di Google Fonts. Silakan periksa ejaan dan coba lagi." + +msgid "This font is already installed." +msgstr "Font ini sudah terinstal." + +msgid "Could not connect to Google Fonts. Please check your internet connection and try again." +msgstr "Tidak dapat terhubung ke Google Fonts. Silakan periksa koneksi internet Anda dan coba lagi." + +msgid "Google Fonts returned an error. Please try again later." +msgstr "Google Fonts mengembalikan kesalahan. Silakan coba lagi nanti." + +msgid "Could not process the font data. The font may not be available." +msgstr "Tidak dapat memproses data font. Font mungkin tidak tersedia." + +msgid "Could not download the font files. Please try again." +msgstr "Tidak dapat mengunduh file font. Silakan coba lagi." + +msgid "Could not save font files. Please check that wp-content/fonts is writable." +msgstr "Tidak dapat menyimpan file font. Silakan periksa apakah wp-content/fonts dapat ditulis." + +msgid "Could not create fonts directory. Please check file permissions." +msgstr "Tidak dapat membuat direktori font. Silakan periksa izin file." + +msgid "Invalid file path." +msgstr "Jalur file tidak valid." + +msgid "Invalid font URL." +msgstr "URL font tidak valid." + +msgid "Invalid font name." +msgstr "Nama font tidak valid." + +msgid "No valid weights specified." +msgstr "Tidak ada ketebalan valid yang ditentukan." + +msgid "No valid styles specified." +msgstr "Tidak ada gaya valid yang ditentukan." + +msgid "An unexpected error occurred. Please try again." +msgstr "Terjadi kesalahan yang tidak terduga. Silakan coba lagi." + +msgid "Thin" +msgstr "Tipis" + +msgid "Extra Light" +msgstr "Ekstra Ringan" + +msgid "Light" +msgstr "Ringan" + +msgid "Regular" +msgstr "Reguler" + +msgid "Medium" +msgstr "Sedang" + +msgid "Semi Bold" +msgstr "Semi Tebal" + +msgid "Bold" +msgstr "Tebal" + +msgid "Extra Bold" +msgstr "Ekstra Tebal" + +msgid "Black" +msgstr "Hitam" + +msgid "You do not have sufficient permissions to access this page." +msgstr "Anda tidak memiliki izin yang cukup untuk mengakses halaman ini." + +msgid "Import from Google Fonts" +msgstr "Impor dari Google Fonts" + +msgid "Font Name" +msgstr "Nama Font" + +msgid "e.g., Open Sans" +msgstr "contoh: Open Sans" + +msgid "Enter the exact font name as it appears on Google Fonts." +msgstr "Masukkan nama font persis seperti yang muncul di Google Fonts." + +msgid "Weights" +msgstr "Ketebalan" + +msgid "Styles" +msgstr "Gaya" + +msgid "Normal" +msgstr "Normal" + +msgid "Italic" +msgstr "Miring" + +msgid "Files to download:" +msgstr "File untuk diunduh:" + +msgid "Download & Install" +msgstr "Unduh & Instal" + +msgid "Installed Fonts" +msgstr "Font Terinstal" + +msgid "No fonts installed yet." +msgstr "Belum ada font yang terinstal." + +msgid "Delete" +msgstr "Hapus" + +msgid "Use %s to apply fonts to your site." +msgstr "Gunakan %s untuk menerapkan font ke situs Anda." + +msgid "Appearance → Editor → Styles → Typography" +msgstr "Tampilan → Editor → Gaya → Tipografi" diff --git a/native/wordpress/maple-fonts-wp/languages/maple-local-fonts-it_IT.po b/native/wordpress/maple-fonts-wp/languages/maple-local-fonts-it_IT.po new file mode 100644 index 0000000..088fa5d --- /dev/null +++ b/native/wordpress/maple-fonts-wp/languages/maple-local-fonts-it_IT.po @@ -0,0 +1,222 @@ +# Translation of Maple Local Fonts in Italian +# Copyright (C) 2024 Maple Open Technologies +# This file is distributed under the GPL-2.0-or-later. +msgid "" +msgstr "" +"Project-Id-Version: Maple Local Fonts 1.0.0\n" +"Report-Msgid-Bugs-To: https://mapleopentech.org/\n" +"Last-Translator: Maple Open Technologies\n" +"Language-Team: Italian\n" +"Language: it_IT\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"POT-Creation-Date: 2024-01-01T00:00:00+00:00\n" +"PO-Revision-Date: 2024-01-01T00:00:00+00:00\n" +"X-Generator: Manual\n" +"X-Domain: maple-local-fonts\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" + +#. Plugin Name of the plugin +msgid "Maple Local Fonts" +msgstr "Maple Local Fonts" + +#. Description of the plugin +msgid "Import Google Fonts to local storage and register them with WordPress Font Library for GDPR-compliant, privacy-friendly typography." +msgstr "Importa i font di Google Fonts nello storage locale e registrali nella libreria font di WordPress per una tipografia conforme al GDPR e rispettosa della privacy." + +#. Author of the plugin +msgid "Maple Open Technologies" +msgstr "Maple Open Technologies" + +msgid "Maple Local Fonts requires WordPress 6.5 or higher for Font Library support." +msgstr "Maple Local Fonts richiede WordPress 6.5 o superiore per il supporto della libreria font." + +msgid "Plugin Activation Error" +msgstr "Errore di attivazione del plugin" + +msgid "Maple Local Fonts has been deactivated. It requires WordPress 6.5 or higher." +msgstr "Maple Local Fonts è stato disattivato. Richiede WordPress 6.5 o superiore." + +msgid "Local Fonts" +msgstr "Font locali" + +msgid "Downloading..." +msgstr "Download in corso..." + +msgid "Deleting..." +msgstr "Eliminazione in corso..." + +msgid "Are you sure you want to delete this font?" +msgstr "Sei sicuro di voler eliminare questo font?" + +msgid "An error occurred. Please try again." +msgstr "Si è verificato un errore. Riprova." + +msgid "Please select at least one weight." +msgstr "Seleziona almeno un peso." + +msgid "Please select at least one style." +msgstr "Seleziona almeno uno stile." + +msgid "Please enter a font name." +msgstr "Inserisci un nome del font." + +msgid "Security check failed." +msgstr "Controllo di sicurezza fallito." + +msgid "Unauthorized." +msgstr "Non autorizzato." + +msgid "Font name is required." +msgstr "Il nome del font è obbligatorio." + +msgid "Invalid font name: only letters, numbers, spaces, and hyphens allowed." +msgstr "Nome del font non valido: sono consentiti solo lettere, numeri, spazi e trattini." + +msgid "Font name is too long." +msgstr "Il nome del font è troppo lungo." + +msgid "At least one weight is required." +msgstr "È richiesto almeno un peso." + +msgid "Too many weights selected." +msgstr "Troppi pesi selezionati." + +msgid "At least one style is required." +msgstr "È richiesto almeno uno stile." + +msgid "Successfully installed %s." +msgstr "%s è stato installato con successo." + +msgid "An unexpected error occurred." +msgstr "Si è verificato un errore imprevisto." + +msgid "Invalid font ID." +msgstr "ID font non valido." + +msgid "Font not found." +msgstr "Font non trovato." + +msgid "Cannot delete fonts not imported by this plugin." +msgstr "Impossibile eliminare i font non importati da questo plugin." + +msgid "Font deleted successfully." +msgstr "Font eliminato con successo." + +msgid "Font not found on Google Fonts. Please check the spelling and try again." +msgstr "Font non trovato su Google Fonts. Controlla l'ortografia e riprova." + +msgid "This font is already installed." +msgstr "Questo font è già installato." + +msgid "Could not connect to Google Fonts. Please check your internet connection and try again." +msgstr "Impossibile connettersi a Google Fonts. Controlla la tua connessione internet e riprova." + +msgid "Google Fonts returned an error. Please try again later." +msgstr "Google Fonts ha restituito un errore. Riprova più tardi." + +msgid "Could not process the font data. The font may not be available." +msgstr "Impossibile elaborare i dati del font. Il font potrebbe non essere disponibile." + +msgid "Could not download the font files. Please try again." +msgstr "Impossibile scaricare i file del font. Riprova." + +msgid "Could not save font files. Please check that wp-content/fonts is writable." +msgstr "Impossibile salvare i file del font. Verifica che wp-content/fonts sia scrivibile." + +msgid "Could not create fonts directory. Please check file permissions." +msgstr "Impossibile creare la directory dei font. Controlla i permessi dei file." + +msgid "Invalid file path." +msgstr "Percorso file non valido." + +msgid "Invalid font URL." +msgstr "URL del font non valido." + +msgid "Invalid font name." +msgstr "Nome del font non valido." + +msgid "No valid weights specified." +msgstr "Nessun peso valido specificato." + +msgid "No valid styles specified." +msgstr "Nessuno stile valido specificato." + +msgid "An unexpected error occurred. Please try again." +msgstr "Si è verificato un errore imprevisto. Riprova." + +msgid "Thin" +msgstr "Sottile" + +msgid "Extra Light" +msgstr "Extra leggero" + +msgid "Light" +msgstr "Leggero" + +msgid "Regular" +msgstr "Normale" + +msgid "Medium" +msgstr "Medio" + +msgid "Semi Bold" +msgstr "Semi grassetto" + +msgid "Bold" +msgstr "Grassetto" + +msgid "Extra Bold" +msgstr "Extra grassetto" + +msgid "Black" +msgstr "Nero" + +msgid "You do not have sufficient permissions to access this page." +msgstr "Non hai i permessi sufficienti per accedere a questa pagina." + +msgid "Import from Google Fonts" +msgstr "Importa da Google Fonts" + +msgid "Font Name" +msgstr "Nome del font" + +msgid "e.g., Open Sans" +msgstr "es. Open Sans" + +msgid "Enter the exact font name as it appears on Google Fonts." +msgstr "Inserisci il nome esatto del font come appare su Google Fonts." + +msgid "Weights" +msgstr "Pesi" + +msgid "Styles" +msgstr "Stili" + +msgid "Normal" +msgstr "Normale" + +msgid "Italic" +msgstr "Corsivo" + +msgid "Files to download:" +msgstr "File da scaricare:" + +msgid "Download & Install" +msgstr "Scarica e installa" + +msgid "Installed Fonts" +msgstr "Font installati" + +msgid "No fonts installed yet." +msgstr "Nessun font installato." + +msgid "Delete" +msgstr "Elimina" + +msgid "Use %s to apply fonts to your site." +msgstr "Usa %s per applicare i font al tuo sito." + +msgid "Appearance → Editor → Styles → Typography" +msgstr "Aspetto → Editor → Stili → Tipografia" diff --git a/native/wordpress/maple-fonts-wp/languages/maple-local-fonts-ja.po b/native/wordpress/maple-fonts-wp/languages/maple-local-fonts-ja.po new file mode 100644 index 0000000..75e6063 --- /dev/null +++ b/native/wordpress/maple-fonts-wp/languages/maple-local-fonts-ja.po @@ -0,0 +1,222 @@ +# Translation of Maple Local Fonts in Japanese +# Copyright (C) 2024 Maple Open Technologies +# This file is distributed under the GPL-2.0-or-later. +msgid "" +msgstr "" +"Project-Id-Version: Maple Local Fonts 1.0.0\n" +"Report-Msgid-Bugs-To: https://mapleopentech.org/\n" +"Last-Translator: Maple Open Technologies\n" +"Language-Team: Japanese\n" +"Language: ja\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"POT-Creation-Date: 2024-01-01T00:00:00+00:00\n" +"PO-Revision-Date: 2024-01-01T00:00:00+00:00\n" +"X-Generator: Manual\n" +"X-Domain: maple-local-fonts\n" +"Plural-Forms: nplurals=1; plural=0;\n" + +#. Plugin Name of the plugin +msgid "Maple Local Fonts" +msgstr "Maple Local Fonts" + +#. Description of the plugin +msgid "Import Google Fonts to local storage and register them with WordPress Font Library for GDPR-compliant, privacy-friendly typography." +msgstr "Google FontsをローカルストレージにインポートしてWordPressフォントライブラリに登録し、GDPR準拠でプライバシーに配慮したタイポグラフィを実現します。" + +#. Author of the plugin +msgid "Maple Open Technologies" +msgstr "Maple Open Technologies" + +msgid "Maple Local Fonts requires WordPress 6.5 or higher for Font Library support." +msgstr "Maple Local FontsはフォントライブラリのサポートにWordPress 6.5以上が必要です。" + +msgid "Plugin Activation Error" +msgstr "プラグイン有効化エラー" + +msgid "Maple Local Fonts has been deactivated. It requires WordPress 6.5 or higher." +msgstr "Maple Local Fontsは無効化されました。WordPress 6.5以上が必要です。" + +msgid "Local Fonts" +msgstr "ローカルフォント" + +msgid "Downloading..." +msgstr "ダウンロード中..." + +msgid "Deleting..." +msgstr "削除中..." + +msgid "Are you sure you want to delete this font?" +msgstr "このフォントを削除してもよろしいですか?" + +msgid "An error occurred. Please try again." +msgstr "エラーが発生しました。もう一度お試しください。" + +msgid "Please select at least one weight." +msgstr "少なくとも1つのウェイトを選択してください。" + +msgid "Please select at least one style." +msgstr "少なくとも1つのスタイルを選択してください。" + +msgid "Please enter a font name." +msgstr "フォント名を入力してください。" + +msgid "Security check failed." +msgstr "セキュリティチェックに失敗しました。" + +msgid "Unauthorized." +msgstr "権限がありません。" + +msgid "Font name is required." +msgstr "フォント名は必須です。" + +msgid "Invalid font name: only letters, numbers, spaces, and hyphens allowed." +msgstr "無効なフォント名:文字、数字、スペース、ハイフンのみ使用できます。" + +msgid "Font name is too long." +msgstr "フォント名が長すぎます。" + +msgid "At least one weight is required." +msgstr "少なくとも1つのウェイトが必要です。" + +msgid "Too many weights selected." +msgstr "選択されたウェイトが多すぎます。" + +msgid "At least one style is required." +msgstr "少なくとも1つのスタイルが必要です。" + +msgid "Successfully installed %s." +msgstr "%sのインストールに成功しました。" + +msgid "An unexpected error occurred." +msgstr "予期しないエラーが発生しました。" + +msgid "Invalid font ID." +msgstr "無効なフォントID。" + +msgid "Font not found." +msgstr "フォントが見つかりません。" + +msgid "Cannot delete fonts not imported by this plugin." +msgstr "このプラグインでインポートされていないフォントは削除できません。" + +msgid "Font deleted successfully." +msgstr "フォントが正常に削除されました。" + +msgid "Font not found on Google Fonts. Please check the spelling and try again." +msgstr "Google Fontsでフォントが見つかりません。スペルを確認して、もう一度お試しください。" + +msgid "This font is already installed." +msgstr "このフォントは既にインストールされています。" + +msgid "Could not connect to Google Fonts. Please check your internet connection and try again." +msgstr "Google Fontsに接続できませんでした。インターネット接続を確認して、もう一度お試しください。" + +msgid "Google Fonts returned an error. Please try again later." +msgstr "Google Fontsがエラーを返しました。後でもう一度お試しください。" + +msgid "Could not process the font data. The font may not be available." +msgstr "フォントデータを処理できませんでした。フォントは利用できない可能性があります。" + +msgid "Could not download the font files. Please try again." +msgstr "フォントファイルをダウンロードできませんでした。もう一度お試しください。" + +msgid "Could not save font files. Please check that wp-content/fonts is writable." +msgstr "フォントファイルを保存できませんでした。wp-content/fontsが書き込み可能か確認してください。" + +msgid "Could not create fonts directory. Please check file permissions." +msgstr "フォントディレクトリを作成できませんでした。ファイルのパーミッションを確認してください。" + +msgid "Invalid file path." +msgstr "無効なファイルパス。" + +msgid "Invalid font URL." +msgstr "無効なフォントURL。" + +msgid "Invalid font name." +msgstr "無効なフォント名。" + +msgid "No valid weights specified." +msgstr "有効なウェイトが指定されていません。" + +msgid "No valid styles specified." +msgstr "有効なスタイルが指定されていません。" + +msgid "An unexpected error occurred. Please try again." +msgstr "予期しないエラーが発生しました。もう一度お試しください。" + +msgid "Thin" +msgstr "極細" + +msgid "Extra Light" +msgstr "極細" + +msgid "Light" +msgstr "細字" + +msgid "Regular" +msgstr "標準" + +msgid "Medium" +msgstr "中字" + +msgid "Semi Bold" +msgstr "やや太字" + +msgid "Bold" +msgstr "太字" + +msgid "Extra Bold" +msgstr "極太" + +msgid "Black" +msgstr "極太" + +msgid "You do not have sufficient permissions to access this page." +msgstr "このページにアクセスする権限がありません。" + +msgid "Import from Google Fonts" +msgstr "Google Fontsからインポート" + +msgid "Font Name" +msgstr "フォント名" + +msgid "e.g., Open Sans" +msgstr "例:Open Sans" + +msgid "Enter the exact font name as it appears on Google Fonts." +msgstr "Google Fontsに表示されている正確なフォント名を入力してください。" + +msgid "Weights" +msgstr "ウェイト" + +msgid "Styles" +msgstr "スタイル" + +msgid "Normal" +msgstr "標準" + +msgid "Italic" +msgstr "イタリック" + +msgid "Files to download:" +msgstr "ダウンロードするファイル:" + +msgid "Download & Install" +msgstr "ダウンロード&インストール" + +msgid "Installed Fonts" +msgstr "インストール済みフォント" + +msgid "No fonts installed yet." +msgstr "まだフォントがインストールされていません。" + +msgid "Delete" +msgstr "削除" + +msgid "Use %s to apply fonts to your site." +msgstr "%sを使用してサイトにフォントを適用してください。" + +msgid "Appearance → Editor → Styles → Typography" +msgstr "外観 → エディター → スタイル → タイポグラフィ" diff --git a/native/wordpress/maple-fonts-wp/languages/maple-local-fonts-ko_KR.po b/native/wordpress/maple-fonts-wp/languages/maple-local-fonts-ko_KR.po new file mode 100644 index 0000000..629a51f --- /dev/null +++ b/native/wordpress/maple-fonts-wp/languages/maple-local-fonts-ko_KR.po @@ -0,0 +1,222 @@ +# Translation of Maple Local Fonts in Korean +# Copyright (C) 2024 Maple Open Technologies +# This file is distributed under the GPL-2.0-or-later. +msgid "" +msgstr "" +"Project-Id-Version: Maple Local Fonts 1.0.0\n" +"Report-Msgid-Bugs-To: https://mapleopentech.org/\n" +"Last-Translator: Maple Open Technologies\n" +"Language-Team: Korean\n" +"Language: ko_KR\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"POT-Creation-Date: 2024-01-01T00:00:00+00:00\n" +"PO-Revision-Date: 2024-01-01T00:00:00+00:00\n" +"X-Generator: Manual\n" +"X-Domain: maple-local-fonts\n" +"Plural-Forms: nplurals=1; plural=0;\n" + +#. Plugin Name of the plugin +msgid "Maple Local Fonts" +msgstr "Maple Local Fonts" + +#. Description of the plugin +msgid "Import Google Fonts to local storage and register them with WordPress Font Library for GDPR-compliant, privacy-friendly typography." +msgstr "Google Fonts를 로컬 저장소로 가져와 WordPress 글꼴 라이브러리에 등록하여 GDPR 준수 및 개인정보 보호 친화적인 타이포그래피를 구현합니다." + +#. Author of the plugin +msgid "Maple Open Technologies" +msgstr "Maple Open Technologies" + +msgid "Maple Local Fonts requires WordPress 6.5 or higher for Font Library support." +msgstr "Maple Local Fonts는 글꼴 라이브러리 지원을 위해 WordPress 6.5 이상이 필요합니다." + +msgid "Plugin Activation Error" +msgstr "플러그인 활성화 오류" + +msgid "Maple Local Fonts has been deactivated. It requires WordPress 6.5 or higher." +msgstr "Maple Local Fonts가 비활성화되었습니다. WordPress 6.5 이상이 필요합니다." + +msgid "Local Fonts" +msgstr "로컬 글꼴" + +msgid "Downloading..." +msgstr "다운로드 중..." + +msgid "Deleting..." +msgstr "삭제 중..." + +msgid "Are you sure you want to delete this font?" +msgstr "이 글꼴을 삭제하시겠습니까?" + +msgid "An error occurred. Please try again." +msgstr "오류가 발생했습니다. 다시 시도해 주세요." + +msgid "Please select at least one weight." +msgstr "최소 하나의 굵기를 선택해 주세요." + +msgid "Please select at least one style." +msgstr "최소 하나의 스타일을 선택해 주세요." + +msgid "Please enter a font name." +msgstr "글꼴 이름을 입력해 주세요." + +msgid "Security check failed." +msgstr "보안 검사 실패." + +msgid "Unauthorized." +msgstr "권한이 없습니다." + +msgid "Font name is required." +msgstr "글꼴 이름은 필수입니다." + +msgid "Invalid font name: only letters, numbers, spaces, and hyphens allowed." +msgstr "잘못된 글꼴 이름: 문자, 숫자, 공백 및 하이픈만 허용됩니다." + +msgid "Font name is too long." +msgstr "글꼴 이름이 너무 깁니다." + +msgid "At least one weight is required." +msgstr "최소 하나의 굵기가 필요합니다." + +msgid "Too many weights selected." +msgstr "너무 많은 굵기가 선택되었습니다." + +msgid "At least one style is required." +msgstr "최소 하나의 스타일이 필요합니다." + +msgid "Successfully installed %s." +msgstr "%s이(가) 성공적으로 설치되었습니다." + +msgid "An unexpected error occurred." +msgstr "예기치 않은 오류가 발생했습니다." + +msgid "Invalid font ID." +msgstr "잘못된 글꼴 ID." + +msgid "Font not found." +msgstr "글꼴을 찾을 수 없습니다." + +msgid "Cannot delete fonts not imported by this plugin." +msgstr "이 플러그인으로 가져오지 않은 글꼴은 삭제할 수 없습니다." + +msgid "Font deleted successfully." +msgstr "글꼴이 성공적으로 삭제되었습니다." + +msgid "Font not found on Google Fonts. Please check the spelling and try again." +msgstr "Google Fonts에서 글꼴을 찾을 수 없습니다. 철자를 확인하고 다시 시도해 주세요." + +msgid "This font is already installed." +msgstr "이 글꼴은 이미 설치되어 있습니다." + +msgid "Could not connect to Google Fonts. Please check your internet connection and try again." +msgstr "Google Fonts에 연결할 수 없습니다. 인터넷 연결을 확인하고 다시 시도해 주세요." + +msgid "Google Fonts returned an error. Please try again later." +msgstr "Google Fonts에서 오류가 반환되었습니다. 나중에 다시 시도해 주세요." + +msgid "Could not process the font data. The font may not be available." +msgstr "글꼴 데이터를 처리할 수 없습니다. 글꼴을 사용할 수 없을 수 있습니다." + +msgid "Could not download the font files. Please try again." +msgstr "글꼴 파일을 다운로드할 수 없습니다. 다시 시도해 주세요." + +msgid "Could not save font files. Please check that wp-content/fonts is writable." +msgstr "글꼴 파일을 저장할 수 없습니다. wp-content/fonts가 쓰기 가능한지 확인해 주세요." + +msgid "Could not create fonts directory. Please check file permissions." +msgstr "글꼴 디렉토리를 생성할 수 없습니다. 파일 권한을 확인해 주세요." + +msgid "Invalid file path." +msgstr "잘못된 파일 경로." + +msgid "Invalid font URL." +msgstr "잘못된 글꼴 URL." + +msgid "Invalid font name." +msgstr "잘못된 글꼴 이름." + +msgid "No valid weights specified." +msgstr "유효한 굵기가 지정되지 않았습니다." + +msgid "No valid styles specified." +msgstr "유효한 스타일이 지정되지 않았습니다." + +msgid "An unexpected error occurred. Please try again." +msgstr "예기치 않은 오류가 발생했습니다. 다시 시도해 주세요." + +msgid "Thin" +msgstr "가늘게" + +msgid "Extra Light" +msgstr "매우 가볍게" + +msgid "Light" +msgstr "가볍게" + +msgid "Regular" +msgstr "보통" + +msgid "Medium" +msgstr "중간" + +msgid "Semi Bold" +msgstr "약간 굵게" + +msgid "Bold" +msgstr "굵게" + +msgid "Extra Bold" +msgstr "매우 굵게" + +msgid "Black" +msgstr "검정" + +msgid "You do not have sufficient permissions to access this page." +msgstr "이 페이지에 접근할 권한이 없습니다." + +msgid "Import from Google Fonts" +msgstr "Google Fonts에서 가져오기" + +msgid "Font Name" +msgstr "글꼴 이름" + +msgid "e.g., Open Sans" +msgstr "예: Open Sans" + +msgid "Enter the exact font name as it appears on Google Fonts." +msgstr "Google Fonts에 표시된 정확한 글꼴 이름을 입력하세요." + +msgid "Weights" +msgstr "굵기" + +msgid "Styles" +msgstr "스타일" + +msgid "Normal" +msgstr "보통" + +msgid "Italic" +msgstr "기울임꼴" + +msgid "Files to download:" +msgstr "다운로드할 파일:" + +msgid "Download & Install" +msgstr "다운로드 및 설치" + +msgid "Installed Fonts" +msgstr "설치된 글꼴" + +msgid "No fonts installed yet." +msgstr "아직 설치된 글꼴이 없습니다." + +msgid "Delete" +msgstr "삭제" + +msgid "Use %s to apply fonts to your site." +msgstr "%s을(를) 사용하여 사이트에 글꼴을 적용하세요." + +msgid "Appearance → Editor → Styles → Typography" +msgstr "외모 → 편집기 → 스타일 → 타이포그래피" diff --git a/native/wordpress/maple-fonts-wp/languages/maple-local-fonts-nl_NL.po b/native/wordpress/maple-fonts-wp/languages/maple-local-fonts-nl_NL.po new file mode 100644 index 0000000..7aa1f7d --- /dev/null +++ b/native/wordpress/maple-fonts-wp/languages/maple-local-fonts-nl_NL.po @@ -0,0 +1,222 @@ +# Translation of Maple Local Fonts in Dutch +# Copyright (C) 2024 Maple Open Technologies +# This file is distributed under the GPL-2.0-or-later. +msgid "" +msgstr "" +"Project-Id-Version: Maple Local Fonts 1.0.0\n" +"Report-Msgid-Bugs-To: https://mapleopentech.org/\n" +"Last-Translator: Maple Open Technologies\n" +"Language-Team: Dutch\n" +"Language: nl_NL\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"POT-Creation-Date: 2024-01-01T00:00:00+00:00\n" +"PO-Revision-Date: 2024-01-01T00:00:00+00:00\n" +"X-Generator: Manual\n" +"X-Domain: maple-local-fonts\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" + +#. Plugin Name of the plugin +msgid "Maple Local Fonts" +msgstr "Maple Local Fonts" + +#. Description of the plugin +msgid "Import Google Fonts to local storage and register them with WordPress Font Library for GDPR-compliant, privacy-friendly typography." +msgstr "Importeer Google Fonts naar lokale opslag en registreer ze bij de WordPress Lettertypebibliotheek voor GDPR-conforme, privacyvriendelijke typografie." + +#. Author of the plugin +msgid "Maple Open Technologies" +msgstr "Maple Open Technologies" + +msgid "Maple Local Fonts requires WordPress 6.5 or higher for Font Library support." +msgstr "Maple Local Fonts vereist WordPress 6.5 of hoger voor ondersteuning van de Lettertypebibliotheek." + +msgid "Plugin Activation Error" +msgstr "Plugin-activeringsfout" + +msgid "Maple Local Fonts has been deactivated. It requires WordPress 6.5 or higher." +msgstr "Maple Local Fonts is gedeactiveerd. Het vereist WordPress 6.5 of hoger." + +msgid "Local Fonts" +msgstr "Lokale lettertypen" + +msgid "Downloading..." +msgstr "Downloaden..." + +msgid "Deleting..." +msgstr "Verwijderen..." + +msgid "Are you sure you want to delete this font?" +msgstr "Weet je zeker dat je dit lettertype wilt verwijderen?" + +msgid "An error occurred. Please try again." +msgstr "Er is een fout opgetreden. Probeer het opnieuw." + +msgid "Please select at least one weight." +msgstr "Selecteer ten minste één gewicht." + +msgid "Please select at least one style." +msgstr "Selecteer ten minste één stijl." + +msgid "Please enter a font name." +msgstr "Voer een lettertypenaam in." + +msgid "Security check failed." +msgstr "Beveiligingscontrole mislukt." + +msgid "Unauthorized." +msgstr "Niet geautoriseerd." + +msgid "Font name is required." +msgstr "Lettertypenaam is vereist." + +msgid "Invalid font name: only letters, numbers, spaces, and hyphens allowed." +msgstr "Ongeldige lettertypenaam: alleen letters, cijfers, spaties en koppeltekens toegestaan." + +msgid "Font name is too long." +msgstr "Lettertypenaam is te lang." + +msgid "At least one weight is required." +msgstr "Ten minste één gewicht is vereist." + +msgid "Too many weights selected." +msgstr "Te veel gewichten geselecteerd." + +msgid "At least one style is required." +msgstr "Ten minste één stijl is vereist." + +msgid "Successfully installed %s." +msgstr "%s is succesvol geïnstalleerd." + +msgid "An unexpected error occurred." +msgstr "Er is een onverwachte fout opgetreden." + +msgid "Invalid font ID." +msgstr "Ongeldige lettertype-ID." + +msgid "Font not found." +msgstr "Lettertype niet gevonden." + +msgid "Cannot delete fonts not imported by this plugin." +msgstr "Kan lettertypen die niet door deze plugin zijn geïmporteerd niet verwijderen." + +msgid "Font deleted successfully." +msgstr "Lettertype succesvol verwijderd." + +msgid "Font not found on Google Fonts. Please check the spelling and try again." +msgstr "Lettertype niet gevonden op Google Fonts. Controleer de spelling en probeer het opnieuw." + +msgid "This font is already installed." +msgstr "Dit lettertype is al geïnstalleerd." + +msgid "Could not connect to Google Fonts. Please check your internet connection and try again." +msgstr "Kan geen verbinding maken met Google Fonts. Controleer je internetverbinding en probeer het opnieuw." + +msgid "Google Fonts returned an error. Please try again later." +msgstr "Google Fonts heeft een fout geretourneerd. Probeer het later opnieuw." + +msgid "Could not process the font data. The font may not be available." +msgstr "Kan de lettertypegegevens niet verwerken. Het lettertype is mogelijk niet beschikbaar." + +msgid "Could not download the font files. Please try again." +msgstr "Kan de lettertypebestanden niet downloaden. Probeer het opnieuw." + +msgid "Could not save font files. Please check that wp-content/fonts is writable." +msgstr "Kan lettertypebestanden niet opslaan. Controleer of wp-content/fonts schrijfbaar is." + +msgid "Could not create fonts directory. Please check file permissions." +msgstr "Kan lettertypemap niet aanmaken. Controleer de bestandsrechten." + +msgid "Invalid file path." +msgstr "Ongeldig bestandspad." + +msgid "Invalid font URL." +msgstr "Ongeldige lettertype-URL." + +msgid "Invalid font name." +msgstr "Ongeldige lettertypenaam." + +msgid "No valid weights specified." +msgstr "Geen geldige gewichten opgegeven." + +msgid "No valid styles specified." +msgstr "Geen geldige stijlen opgegeven." + +msgid "An unexpected error occurred. Please try again." +msgstr "Er is een onverwachte fout opgetreden. Probeer het opnieuw." + +msgid "Thin" +msgstr "Dun" + +msgid "Extra Light" +msgstr "Extra licht" + +msgid "Light" +msgstr "Licht" + +msgid "Regular" +msgstr "Normaal" + +msgid "Medium" +msgstr "Medium" + +msgid "Semi Bold" +msgstr "Halfvet" + +msgid "Bold" +msgstr "Vet" + +msgid "Extra Bold" +msgstr "Extra vet" + +msgid "Black" +msgstr "Zwart" + +msgid "You do not have sufficient permissions to access this page." +msgstr "Je hebt onvoldoende rechten om deze pagina te openen." + +msgid "Import from Google Fonts" +msgstr "Importeren van Google Fonts" + +msgid "Font Name" +msgstr "Lettertypenaam" + +msgid "e.g., Open Sans" +msgstr "bijv. Open Sans" + +msgid "Enter the exact font name as it appears on Google Fonts." +msgstr "Voer de exacte lettertypenaam in zoals deze wordt weergegeven op Google Fonts." + +msgid "Weights" +msgstr "Gewichten" + +msgid "Styles" +msgstr "Stijlen" + +msgid "Normal" +msgstr "Normaal" + +msgid "Italic" +msgstr "Cursief" + +msgid "Files to download:" +msgstr "Te downloaden bestanden:" + +msgid "Download & Install" +msgstr "Downloaden en installeren" + +msgid "Installed Fonts" +msgstr "Geïnstalleerde lettertypen" + +msgid "No fonts installed yet." +msgstr "Nog geen lettertypen geïnstalleerd." + +msgid "Delete" +msgstr "Verwijderen" + +msgid "Use %s to apply fonts to your site." +msgstr "Gebruik %s om lettertypen op je site toe te passen." + +msgid "Appearance → Editor → Styles → Typography" +msgstr "Weergave → Editor → Stijlen → Typografie" diff --git a/native/wordpress/maple-fonts-wp/languages/maple-local-fonts-pl_PL.po b/native/wordpress/maple-fonts-wp/languages/maple-local-fonts-pl_PL.po new file mode 100644 index 0000000..37cb8c0 --- /dev/null +++ b/native/wordpress/maple-fonts-wp/languages/maple-local-fonts-pl_PL.po @@ -0,0 +1,222 @@ +# Translation of Maple Local Fonts in Polish +# Copyright (C) 2024 Maple Open Technologies +# This file is distributed under the GPL-2.0-or-later. +msgid "" +msgstr "" +"Project-Id-Version: Maple Local Fonts 1.0.0\n" +"Report-Msgid-Bugs-To: https://mapleopentech.org/\n" +"Last-Translator: Maple Open Technologies\n" +"Language-Team: Polish\n" +"Language: pl_PL\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"POT-Creation-Date: 2024-01-01T00:00:00+00:00\n" +"PO-Revision-Date: 2024-01-01T00:00:00+00:00\n" +"X-Generator: Manual\n" +"X-Domain: maple-local-fonts\n" +"Plural-Forms: nplurals=3; plural=(n==1 ? 0 : n%10>=2 && n%10<=4 && (n%100<12 || n%100>14) ? 1 : 2);\n" + +#. Plugin Name of the plugin +msgid "Maple Local Fonts" +msgstr "Maple Local Fonts" + +#. Description of the plugin +msgid "Import Google Fonts to local storage and register them with WordPress Font Library for GDPR-compliant, privacy-friendly typography." +msgstr "Importuj czcionki Google Fonts do lokalnego magazynu i zarejestruj je w bibliotece czcionek WordPress dla typografii zgodnej z RODO i przyjaznej dla prywatności." + +#. Author of the plugin +msgid "Maple Open Technologies" +msgstr "Maple Open Technologies" + +msgid "Maple Local Fonts requires WordPress 6.5 or higher for Font Library support." +msgstr "Maple Local Fonts wymaga WordPress 6.5 lub nowszego do obsługi biblioteki czcionek." + +msgid "Plugin Activation Error" +msgstr "Błąd aktywacji wtyczki" + +msgid "Maple Local Fonts has been deactivated. It requires WordPress 6.5 or higher." +msgstr "Maple Local Fonts została dezaktywowana. Wymaga WordPress 6.5 lub nowszego." + +msgid "Local Fonts" +msgstr "Lokalne czcionki" + +msgid "Downloading..." +msgstr "Pobieranie..." + +msgid "Deleting..." +msgstr "Usuwanie..." + +msgid "Are you sure you want to delete this font?" +msgstr "Czy na pewno chcesz usunąć tę czcionkę?" + +msgid "An error occurred. Please try again." +msgstr "Wystąpił błąd. Spróbuj ponownie." + +msgid "Please select at least one weight." +msgstr "Wybierz co najmniej jedną grubość." + +msgid "Please select at least one style." +msgstr "Wybierz co najmniej jeden styl." + +msgid "Please enter a font name." +msgstr "Wprowadź nazwę czcionki." + +msgid "Security check failed." +msgstr "Sprawdzenie zabezpieczeń nie powiodło się." + +msgid "Unauthorized." +msgstr "Brak autoryzacji." + +msgid "Font name is required." +msgstr "Nazwa czcionki jest wymagana." + +msgid "Invalid font name: only letters, numbers, spaces, and hyphens allowed." +msgstr "Nieprawidłowa nazwa czcionki: dozwolone są tylko litery, cyfry, spacje i myślniki." + +msgid "Font name is too long." +msgstr "Nazwa czcionki jest za długa." + +msgid "At least one weight is required." +msgstr "Wymagana jest co najmniej jedna grubość." + +msgid "Too many weights selected." +msgstr "Wybrano za dużo grubości." + +msgid "At least one style is required." +msgstr "Wymagany jest co najmniej jeden styl." + +msgid "Successfully installed %s." +msgstr "Pomyślnie zainstalowano %s." + +msgid "An unexpected error occurred." +msgstr "Wystąpił nieoczekiwany błąd." + +msgid "Invalid font ID." +msgstr "Nieprawidłowy identyfikator czcionki." + +msgid "Font not found." +msgstr "Czcionka nie została znaleziona." + +msgid "Cannot delete fonts not imported by this plugin." +msgstr "Nie można usunąć czcionek, które nie zostały zaimportowane przez tę wtyczkę." + +msgid "Font deleted successfully." +msgstr "Czcionka została pomyślnie usunięta." + +msgid "Font not found on Google Fonts. Please check the spelling and try again." +msgstr "Czcionka nie została znaleziona w Google Fonts. Sprawdź pisownię i spróbuj ponownie." + +msgid "This font is already installed." +msgstr "Ta czcionka jest już zainstalowana." + +msgid "Could not connect to Google Fonts. Please check your internet connection and try again." +msgstr "Nie można połączyć się z Google Fonts. Sprawdź połączenie internetowe i spróbuj ponownie." + +msgid "Google Fonts returned an error. Please try again later." +msgstr "Google Fonts zwróciło błąd. Spróbuj ponownie później." + +msgid "Could not process the font data. The font may not be available." +msgstr "Nie można przetworzyć danych czcionki. Czcionka może być niedostępna." + +msgid "Could not download the font files. Please try again." +msgstr "Nie można pobrać plików czcionki. Spróbuj ponownie." + +msgid "Could not save font files. Please check that wp-content/fonts is writable." +msgstr "Nie można zapisać plików czcionki. Sprawdź, czy wp-content/fonts jest zapisywalny." + +msgid "Could not create fonts directory. Please check file permissions." +msgstr "Nie można utworzyć katalogu czcionek. Sprawdź uprawnienia plików." + +msgid "Invalid file path." +msgstr "Nieprawidłowa ścieżka pliku." + +msgid "Invalid font URL." +msgstr "Nieprawidłowy adres URL czcionki." + +msgid "Invalid font name." +msgstr "Nieprawidłowa nazwa czcionki." + +msgid "No valid weights specified." +msgstr "Nie określono prawidłowych grubości." + +msgid "No valid styles specified." +msgstr "Nie określono prawidłowych stylów." + +msgid "An unexpected error occurred. Please try again." +msgstr "Wystąpił nieoczekiwany błąd. Spróbuj ponownie." + +msgid "Thin" +msgstr "Cienka" + +msgid "Extra Light" +msgstr "Bardzo lekka" + +msgid "Light" +msgstr "Lekka" + +msgid "Regular" +msgstr "Normalna" + +msgid "Medium" +msgstr "Średnia" + +msgid "Semi Bold" +msgstr "Półgruba" + +msgid "Bold" +msgstr "Gruba" + +msgid "Extra Bold" +msgstr "Bardzo gruba" + +msgid "Black" +msgstr "Czarna" + +msgid "You do not have sufficient permissions to access this page." +msgstr "Nie masz wystarczających uprawnień, aby uzyskać dostęp do tej strony." + +msgid "Import from Google Fonts" +msgstr "Importuj z Google Fonts" + +msgid "Font Name" +msgstr "Nazwa czcionki" + +msgid "e.g., Open Sans" +msgstr "np. Open Sans" + +msgid "Enter the exact font name as it appears on Google Fonts." +msgstr "Wprowadź dokładną nazwę czcionki, jak jest wyświetlana w Google Fonts." + +msgid "Weights" +msgstr "Grubości" + +msgid "Styles" +msgstr "Style" + +msgid "Normal" +msgstr "Normalna" + +msgid "Italic" +msgstr "Kursywa" + +msgid "Files to download:" +msgstr "Pliki do pobrania:" + +msgid "Download & Install" +msgstr "Pobierz i zainstaluj" + +msgid "Installed Fonts" +msgstr "Zainstalowane czcionki" + +msgid "No fonts installed yet." +msgstr "Nie zainstalowano jeszcze żadnych czcionek." + +msgid "Delete" +msgstr "Usuń" + +msgid "Use %s to apply fonts to your site." +msgstr "Użyj %s, aby zastosować czcionki na swojej stronie." + +msgid "Appearance → Editor → Styles → Typography" +msgstr "Wygląd → Edytor → Style → Typografia" diff --git a/native/wordpress/maple-fonts-wp/languages/maple-local-fonts-pt_BR.po b/native/wordpress/maple-fonts-wp/languages/maple-local-fonts-pt_BR.po new file mode 100644 index 0000000..08ad30b --- /dev/null +++ b/native/wordpress/maple-fonts-wp/languages/maple-local-fonts-pt_BR.po @@ -0,0 +1,222 @@ +# Translation of Maple Local Fonts in Portuguese (Brazil) +# Copyright (C) 2024 Maple Open Technologies +# This file is distributed under the GPL-2.0-or-later. +msgid "" +msgstr "" +"Project-Id-Version: Maple Local Fonts 1.0.0\n" +"Report-Msgid-Bugs-To: https://mapleopentech.org/\n" +"Last-Translator: Maple Open Technologies\n" +"Language-Team: Portuguese (Brazil)\n" +"Language: pt_BR\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"POT-Creation-Date: 2024-01-01T00:00:00+00:00\n" +"PO-Revision-Date: 2024-01-01T00:00:00+00:00\n" +"X-Generator: Manual\n" +"X-Domain: maple-local-fonts\n" +"Plural-Forms: nplurals=2; plural=(n > 1);\n" + +#. Plugin Name of the plugin +msgid "Maple Local Fonts" +msgstr "Maple Local Fonts" + +#. Description of the plugin +msgid "Import Google Fonts to local storage and register them with WordPress Font Library for GDPR-compliant, privacy-friendly typography." +msgstr "Importe fontes do Google Fonts para o armazenamento local e registre-as na biblioteca de fontes do WordPress para uma tipografia compatível com GDPR e que respeita a privacidade." + +#. Author of the plugin +msgid "Maple Open Technologies" +msgstr "Maple Open Technologies" + +msgid "Maple Local Fonts requires WordPress 6.5 or higher for Font Library support." +msgstr "O Maple Local Fonts requer WordPress 6.5 ou superior para suporte à biblioteca de fontes." + +msgid "Plugin Activation Error" +msgstr "Erro de ativação do plugin" + +msgid "Maple Local Fonts has been deactivated. It requires WordPress 6.5 or higher." +msgstr "O Maple Local Fonts foi desativado. Requer WordPress 6.5 ou superior." + +msgid "Local Fonts" +msgstr "Fontes locais" + +msgid "Downloading..." +msgstr "Baixando..." + +msgid "Deleting..." +msgstr "Excluindo..." + +msgid "Are you sure you want to delete this font?" +msgstr "Tem certeza de que deseja excluir esta fonte?" + +msgid "An error occurred. Please try again." +msgstr "Ocorreu um erro. Por favor, tente novamente." + +msgid "Please select at least one weight." +msgstr "Por favor, selecione pelo menos um peso." + +msgid "Please select at least one style." +msgstr "Por favor, selecione pelo menos um estilo." + +msgid "Please enter a font name." +msgstr "Por favor, digite um nome de fonte." + +msgid "Security check failed." +msgstr "Falha na verificação de segurança." + +msgid "Unauthorized." +msgstr "Não autorizado." + +msgid "Font name is required." +msgstr "O nome da fonte é obrigatório." + +msgid "Invalid font name: only letters, numbers, spaces, and hyphens allowed." +msgstr "Nome de fonte inválido: apenas letras, números, espaços e hífens são permitidos." + +msgid "Font name is too long." +msgstr "O nome da fonte é muito longo." + +msgid "At least one weight is required." +msgstr "Pelo menos um peso é necessário." + +msgid "Too many weights selected." +msgstr "Muitos pesos selecionados." + +msgid "At least one style is required." +msgstr "Pelo menos um estilo é necessário." + +msgid "Successfully installed %s." +msgstr "%s foi instalado com sucesso." + +msgid "An unexpected error occurred." +msgstr "Ocorreu um erro inesperado." + +msgid "Invalid font ID." +msgstr "ID de fonte inválido." + +msgid "Font not found." +msgstr "Fonte não encontrada." + +msgid "Cannot delete fonts not imported by this plugin." +msgstr "Não é possível excluir fontes que não foram importadas por este plugin." + +msgid "Font deleted successfully." +msgstr "Fonte excluída com sucesso." + +msgid "Font not found on Google Fonts. Please check the spelling and try again." +msgstr "Fonte não encontrada no Google Fonts. Verifique a ortografia e tente novamente." + +msgid "This font is already installed." +msgstr "Esta fonte já está instalada." + +msgid "Could not connect to Google Fonts. Please check your internet connection and try again." +msgstr "Não foi possível conectar ao Google Fonts. Verifique sua conexão com a internet e tente novamente." + +msgid "Google Fonts returned an error. Please try again later." +msgstr "O Google Fonts retornou um erro. Por favor, tente novamente mais tarde." + +msgid "Could not process the font data. The font may not be available." +msgstr "Não foi possível processar os dados da fonte. A fonte pode não estar disponível." + +msgid "Could not download the font files. Please try again." +msgstr "Não foi possível baixar os arquivos da fonte. Por favor, tente novamente." + +msgid "Could not save font files. Please check that wp-content/fonts is writable." +msgstr "Não foi possível salvar os arquivos da fonte. Verifique se wp-content/fonts tem permissão de escrita." + +msgid "Could not create fonts directory. Please check file permissions." +msgstr "Não foi possível criar o diretório de fontes. Verifique as permissões de arquivo." + +msgid "Invalid file path." +msgstr "Caminho de arquivo inválido." + +msgid "Invalid font URL." +msgstr "URL de fonte inválida." + +msgid "Invalid font name." +msgstr "Nome de fonte inválido." + +msgid "No valid weights specified." +msgstr "Nenhum peso válido especificado." + +msgid "No valid styles specified." +msgstr "Nenhum estilo válido especificado." + +msgid "An unexpected error occurred. Please try again." +msgstr "Ocorreu um erro inesperado. Por favor, tente novamente." + +msgid "Thin" +msgstr "Fina" + +msgid "Extra Light" +msgstr "Extra leve" + +msgid "Light" +msgstr "Leve" + +msgid "Regular" +msgstr "Normal" + +msgid "Medium" +msgstr "Média" + +msgid "Semi Bold" +msgstr "Semi negrito" + +msgid "Bold" +msgstr "Negrito" + +msgid "Extra Bold" +msgstr "Extra negrito" + +msgid "Black" +msgstr "Preta" + +msgid "You do not have sufficient permissions to access this page." +msgstr "Você não tem permissões suficientes para acessar esta página." + +msgid "Import from Google Fonts" +msgstr "Importar do Google Fonts" + +msgid "Font Name" +msgstr "Nome da fonte" + +msgid "e.g., Open Sans" +msgstr "ex.: Open Sans" + +msgid "Enter the exact font name as it appears on Google Fonts." +msgstr "Digite o nome exato da fonte como aparece no Google Fonts." + +msgid "Weights" +msgstr "Pesos" + +msgid "Styles" +msgstr "Estilos" + +msgid "Normal" +msgstr "Normal" + +msgid "Italic" +msgstr "Itálico" + +msgid "Files to download:" +msgstr "Arquivos para baixar:" + +msgid "Download & Install" +msgstr "Baixar e instalar" + +msgid "Installed Fonts" +msgstr "Fontes instaladas" + +msgid "No fonts installed yet." +msgstr "Nenhuma fonte instalada ainda." + +msgid "Delete" +msgstr "Excluir" + +msgid "Use %s to apply fonts to your site." +msgstr "Use %s para aplicar fontes ao seu site." + +msgid "Appearance → Editor → Styles → Typography" +msgstr "Aparência → Editor → Estilos → Tipografia" diff --git a/native/wordpress/maple-fonts-wp/languages/maple-local-fonts-ru_RU.po b/native/wordpress/maple-fonts-wp/languages/maple-local-fonts-ru_RU.po new file mode 100644 index 0000000..c97549a --- /dev/null +++ b/native/wordpress/maple-fonts-wp/languages/maple-local-fonts-ru_RU.po @@ -0,0 +1,222 @@ +# Translation of Maple Local Fonts in Russian +# Copyright (C) 2024 Maple Open Technologies +# This file is distributed under the GPL-2.0-or-later. +msgid "" +msgstr "" +"Project-Id-Version: Maple Local Fonts 1.0.0\n" +"Report-Msgid-Bugs-To: https://mapleopentech.org/\n" +"Last-Translator: Maple Open Technologies\n" +"Language-Team: Russian\n" +"Language: ru_RU\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"POT-Creation-Date: 2024-01-01T00:00:00+00:00\n" +"PO-Revision-Date: 2024-01-01T00:00:00+00:00\n" +"X-Generator: Manual\n" +"X-Domain: maple-local-fonts\n" +"Plural-Forms: nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n%10<=4 && (n%100<12 || n%100>14) ? 1 : 2);\n" + +#. Plugin Name of the plugin +msgid "Maple Local Fonts" +msgstr "Maple Local Fonts" + +#. Description of the plugin +msgid "Import Google Fonts to local storage and register them with WordPress Font Library for GDPR-compliant, privacy-friendly typography." +msgstr "Импортируйте шрифты Google Fonts в локальное хранилище и зарегистрируйте их в библиотеке шрифтов WordPress для типографики, соответствующей GDPR и защищающей конфиденциальность." + +#. Author of the plugin +msgid "Maple Open Technologies" +msgstr "Maple Open Technologies" + +msgid "Maple Local Fonts requires WordPress 6.5 or higher for Font Library support." +msgstr "Maple Local Fonts требует WordPress 6.5 или выше для поддержки библиотеки шрифтов." + +msgid "Plugin Activation Error" +msgstr "Ошибка активации плагина" + +msgid "Maple Local Fonts has been deactivated. It requires WordPress 6.5 or higher." +msgstr "Maple Local Fonts был деактивирован. Требуется WordPress 6.5 или выше." + +msgid "Local Fonts" +msgstr "Локальные шрифты" + +msgid "Downloading..." +msgstr "Загрузка..." + +msgid "Deleting..." +msgstr "Удаление..." + +msgid "Are you sure you want to delete this font?" +msgstr "Вы уверены, что хотите удалить этот шрифт?" + +msgid "An error occurred. Please try again." +msgstr "Произошла ошибка. Пожалуйста, попробуйте снова." + +msgid "Please select at least one weight." +msgstr "Пожалуйста, выберите хотя бы одну толщину." + +msgid "Please select at least one style." +msgstr "Пожалуйста, выберите хотя бы один стиль." + +msgid "Please enter a font name." +msgstr "Пожалуйста, введите название шрифта." + +msgid "Security check failed." +msgstr "Проверка безопасности не пройдена." + +msgid "Unauthorized." +msgstr "Не авторизован." + +msgid "Font name is required." +msgstr "Название шрифта обязательно." + +msgid "Invalid font name: only letters, numbers, spaces, and hyphens allowed." +msgstr "Недопустимое название шрифта: разрешены только буквы, цифры, пробелы и дефисы." + +msgid "Font name is too long." +msgstr "Название шрифта слишком длинное." + +msgid "At least one weight is required." +msgstr "Требуется хотя бы одна толщина." + +msgid "Too many weights selected." +msgstr "Выбрано слишком много толщин." + +msgid "At least one style is required." +msgstr "Требуется хотя бы один стиль." + +msgid "Successfully installed %s." +msgstr "%s успешно установлен." + +msgid "An unexpected error occurred." +msgstr "Произошла непредвиденная ошибка." + +msgid "Invalid font ID." +msgstr "Недопустимый ID шрифта." + +msgid "Font not found." +msgstr "Шрифт не найден." + +msgid "Cannot delete fonts not imported by this plugin." +msgstr "Невозможно удалить шрифты, не импортированные этим плагином." + +msgid "Font deleted successfully." +msgstr "Шрифт успешно удалён." + +msgid "Font not found on Google Fonts. Please check the spelling and try again." +msgstr "Шрифт не найден в Google Fonts. Проверьте правописание и попробуйте снова." + +msgid "This font is already installed." +msgstr "Этот шрифт уже установлен." + +msgid "Could not connect to Google Fonts. Please check your internet connection and try again." +msgstr "Не удалось подключиться к Google Fonts. Проверьте подключение к интернету и попробуйте снова." + +msgid "Google Fonts returned an error. Please try again later." +msgstr "Google Fonts вернул ошибку. Пожалуйста, попробуйте позже." + +msgid "Could not process the font data. The font may not be available." +msgstr "Не удалось обработать данные шрифта. Шрифт может быть недоступен." + +msgid "Could not download the font files. Please try again." +msgstr "Не удалось загрузить файлы шрифта. Пожалуйста, попробуйте снова." + +msgid "Could not save font files. Please check that wp-content/fonts is writable." +msgstr "Не удалось сохранить файлы шрифта. Проверьте, что wp-content/fonts доступен для записи." + +msgid "Could not create fonts directory. Please check file permissions." +msgstr "Не удалось создать каталог шрифтов. Проверьте права доступа к файлам." + +msgid "Invalid file path." +msgstr "Недопустимый путь к файлу." + +msgid "Invalid font URL." +msgstr "Недопустимый URL шрифта." + +msgid "Invalid font name." +msgstr "Недопустимое название шрифта." + +msgid "No valid weights specified." +msgstr "Не указаны допустимые толщины." + +msgid "No valid styles specified." +msgstr "Не указаны допустимые стили." + +msgid "An unexpected error occurred. Please try again." +msgstr "Произошла непредвиденная ошибка. Пожалуйста, попробуйте снова." + +msgid "Thin" +msgstr "Тонкий" + +msgid "Extra Light" +msgstr "Сверхлёгкий" + +msgid "Light" +msgstr "Лёгкий" + +msgid "Regular" +msgstr "Обычный" + +msgid "Medium" +msgstr "Средний" + +msgid "Semi Bold" +msgstr "Полужирный" + +msgid "Bold" +msgstr "Жирный" + +msgid "Extra Bold" +msgstr "Сверхжирный" + +msgid "Black" +msgstr "Чёрный" + +msgid "You do not have sufficient permissions to access this page." +msgstr "У вас недостаточно прав для доступа к этой странице." + +msgid "Import from Google Fonts" +msgstr "Импорт из Google Fonts" + +msgid "Font Name" +msgstr "Название шрифта" + +msgid "e.g., Open Sans" +msgstr "напр., Open Sans" + +msgid "Enter the exact font name as it appears on Google Fonts." +msgstr "Введите точное название шрифта, как оно отображается в Google Fonts." + +msgid "Weights" +msgstr "Толщины" + +msgid "Styles" +msgstr "Стили" + +msgid "Normal" +msgstr "Обычный" + +msgid "Italic" +msgstr "Курсив" + +msgid "Files to download:" +msgstr "Файлов для загрузки:" + +msgid "Download & Install" +msgstr "Скачать и установить" + +msgid "Installed Fonts" +msgstr "Установленные шрифты" + +msgid "No fonts installed yet." +msgstr "Шрифты ещё не установлены." + +msgid "Delete" +msgstr "Удалить" + +msgid "Use %s to apply fonts to your site." +msgstr "Используйте %s для применения шрифтов к вашему сайту." + +msgid "Appearance → Editor → Styles → Typography" +msgstr "Внешний вид → Редактор → Стили → Типографика" diff --git a/native/wordpress/maple-fonts-wp/languages/maple-local-fonts-tr_TR.po b/native/wordpress/maple-fonts-wp/languages/maple-local-fonts-tr_TR.po new file mode 100644 index 0000000..45d2cde --- /dev/null +++ b/native/wordpress/maple-fonts-wp/languages/maple-local-fonts-tr_TR.po @@ -0,0 +1,222 @@ +# Translation of Maple Local Fonts in Turkish +# Copyright (C) 2024 Maple Open Technologies +# This file is distributed under the GPL-2.0-or-later. +msgid "" +msgstr "" +"Project-Id-Version: Maple Local Fonts 1.0.0\n" +"Report-Msgid-Bugs-To: https://mapleopentech.org/\n" +"Last-Translator: Maple Open Technologies\n" +"Language-Team: Turkish\n" +"Language: tr_TR\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"POT-Creation-Date: 2024-01-01T00:00:00+00:00\n" +"PO-Revision-Date: 2024-01-01T00:00:00+00:00\n" +"X-Generator: Manual\n" +"X-Domain: maple-local-fonts\n" +"Plural-Forms: nplurals=2; plural=(n > 1);\n" + +#. Plugin Name of the plugin +msgid "Maple Local Fonts" +msgstr "Maple Local Fonts" + +#. Description of the plugin +msgid "Import Google Fonts to local storage and register them with WordPress Font Library for GDPR-compliant, privacy-friendly typography." +msgstr "Google Fonts'u yerel depolamaya aktarın ve GDPR uyumlu, gizlilik dostu tipografi için WordPress Yazı Tipi Kitaplığı'na kaydedin." + +#. Author of the plugin +msgid "Maple Open Technologies" +msgstr "Maple Open Technologies" + +msgid "Maple Local Fonts requires WordPress 6.5 or higher for Font Library support." +msgstr "Maple Local Fonts, Yazı Tipi Kitaplığı desteği için WordPress 6.5 veya üstünü gerektirir." + +msgid "Plugin Activation Error" +msgstr "Eklenti Etkinleştirme Hatası" + +msgid "Maple Local Fonts has been deactivated. It requires WordPress 6.5 or higher." +msgstr "Maple Local Fonts devre dışı bırakıldı. WordPress 6.5 veya üstünü gerektirir." + +msgid "Local Fonts" +msgstr "Yerel Yazı Tipleri" + +msgid "Downloading..." +msgstr "İndiriliyor..." + +msgid "Deleting..." +msgstr "Siliniyor..." + +msgid "Are you sure you want to delete this font?" +msgstr "Bu yazı tipini silmek istediğinizden emin misiniz?" + +msgid "An error occurred. Please try again." +msgstr "Bir hata oluştu. Lütfen tekrar deneyin." + +msgid "Please select at least one weight." +msgstr "Lütfen en az bir kalınlık seçin." + +msgid "Please select at least one style." +msgstr "Lütfen en az bir stil seçin." + +msgid "Please enter a font name." +msgstr "Lütfen bir yazı tipi adı girin." + +msgid "Security check failed." +msgstr "Güvenlik kontrolü başarısız." + +msgid "Unauthorized." +msgstr "Yetkisiz." + +msgid "Font name is required." +msgstr "Yazı tipi adı gereklidir." + +msgid "Invalid font name: only letters, numbers, spaces, and hyphens allowed." +msgstr "Geçersiz yazı tipi adı: yalnızca harfler, sayılar, boşluklar ve tireler kullanılabilir." + +msgid "Font name is too long." +msgstr "Yazı tipi adı çok uzun." + +msgid "At least one weight is required." +msgstr "En az bir kalınlık gereklidir." + +msgid "Too many weights selected." +msgstr "Çok fazla kalınlık seçildi." + +msgid "At least one style is required." +msgstr "En az bir stil gereklidir." + +msgid "Successfully installed %s." +msgstr "%s başarıyla yüklendi." + +msgid "An unexpected error occurred." +msgstr "Beklenmeyen bir hata oluştu." + +msgid "Invalid font ID." +msgstr "Geçersiz yazı tipi kimliği." + +msgid "Font not found." +msgstr "Yazı tipi bulunamadı." + +msgid "Cannot delete fonts not imported by this plugin." +msgstr "Bu eklenti tarafından içe aktarılmayan yazı tipleri silinemez." + +msgid "Font deleted successfully." +msgstr "Yazı tipi başarıyla silindi." + +msgid "Font not found on Google Fonts. Please check the spelling and try again." +msgstr "Yazı tipi Google Fonts'ta bulunamadı. Lütfen yazımı kontrol edin ve tekrar deneyin." + +msgid "This font is already installed." +msgstr "Bu yazı tipi zaten yüklü." + +msgid "Could not connect to Google Fonts. Please check your internet connection and try again." +msgstr "Google Fonts'a bağlanılamadı. Lütfen internet bağlantınızı kontrol edin ve tekrar deneyin." + +msgid "Google Fonts returned an error. Please try again later." +msgstr "Google Fonts bir hata döndürdü. Lütfen daha sonra tekrar deneyin." + +msgid "Could not process the font data. The font may not be available." +msgstr "Yazı tipi verileri işlenemedi. Yazı tipi kullanılamıyor olabilir." + +msgid "Could not download the font files. Please try again." +msgstr "Yazı tipi dosyaları indirilemedi. Lütfen tekrar deneyin." + +msgid "Could not save font files. Please check that wp-content/fonts is writable." +msgstr "Yazı tipi dosyaları kaydedilemedi. Lütfen wp-content/fonts klasörünün yazılabilir olduğunu kontrol edin." + +msgid "Could not create fonts directory. Please check file permissions." +msgstr "Yazı tipi dizini oluşturulamadı. Lütfen dosya izinlerini kontrol edin." + +msgid "Invalid file path." +msgstr "Geçersiz dosya yolu." + +msgid "Invalid font URL." +msgstr "Geçersiz yazı tipi URL'si." + +msgid "Invalid font name." +msgstr "Geçersiz yazı tipi adı." + +msgid "No valid weights specified." +msgstr "Geçerli kalınlık belirtilmedi." + +msgid "No valid styles specified." +msgstr "Geçerli stil belirtilmedi." + +msgid "An unexpected error occurred. Please try again." +msgstr "Beklenmeyen bir hata oluştu. Lütfen tekrar deneyin." + +msgid "Thin" +msgstr "İnce" + +msgid "Extra Light" +msgstr "Ekstra Hafif" + +msgid "Light" +msgstr "Hafif" + +msgid "Regular" +msgstr "Normal" + +msgid "Medium" +msgstr "Orta" + +msgid "Semi Bold" +msgstr "Yarı Kalın" + +msgid "Bold" +msgstr "Kalın" + +msgid "Extra Bold" +msgstr "Ekstra Kalın" + +msgid "Black" +msgstr "Siyah" + +msgid "You do not have sufficient permissions to access this page." +msgstr "Bu sayfaya erişmek için yeterli izniniz yok." + +msgid "Import from Google Fonts" +msgstr "Google Fonts'tan İçe Aktar" + +msgid "Font Name" +msgstr "Yazı Tipi Adı" + +msgid "e.g., Open Sans" +msgstr "örn., Open Sans" + +msgid "Enter the exact font name as it appears on Google Fonts." +msgstr "Google Fonts'ta göründüğü şekliyle tam yazı tipi adını girin." + +msgid "Weights" +msgstr "Kalınlıklar" + +msgid "Styles" +msgstr "Stiller" + +msgid "Normal" +msgstr "Normal" + +msgid "Italic" +msgstr "İtalik" + +msgid "Files to download:" +msgstr "İndirilecek dosyalar:" + +msgid "Download & Install" +msgstr "İndir ve Yükle" + +msgid "Installed Fonts" +msgstr "Yüklü Yazı Tipleri" + +msgid "No fonts installed yet." +msgstr "Henüz yazı tipi yüklenmedi." + +msgid "Delete" +msgstr "Sil" + +msgid "Use %s to apply fonts to your site." +msgstr "Sitenize yazı tipleri uygulamak için %s kullanın." + +msgid "Appearance → Editor → Styles → Typography" +msgstr "Görünüm → Düzenleyici → Stiller → Tipografi" diff --git a/native/wordpress/maple-fonts-wp/languages/maple-local-fonts-zh_CN.po b/native/wordpress/maple-fonts-wp/languages/maple-local-fonts-zh_CN.po new file mode 100644 index 0000000..3958ed7 --- /dev/null +++ b/native/wordpress/maple-fonts-wp/languages/maple-local-fonts-zh_CN.po @@ -0,0 +1,222 @@ +# Translation of Maple Local Fonts in Chinese (Simplified) +# Copyright (C) 2024 Maple Open Technologies +# This file is distributed under the GPL-2.0-or-later. +msgid "" +msgstr "" +"Project-Id-Version: Maple Local Fonts 1.0.0\n" +"Report-Msgid-Bugs-To: https://mapleopentech.org/\n" +"Last-Translator: Maple Open Technologies\n" +"Language-Team: Chinese (Simplified)\n" +"Language: zh_CN\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"POT-Creation-Date: 2024-01-01T00:00:00+00:00\n" +"PO-Revision-Date: 2024-01-01T00:00:00+00:00\n" +"X-Generator: Manual\n" +"X-Domain: maple-local-fonts\n" +"Plural-Forms: nplurals=1; plural=0;\n" + +#. Plugin Name of the plugin +msgid "Maple Local Fonts" +msgstr "Maple Local Fonts" + +#. Description of the plugin +msgid "Import Google Fonts to local storage and register them with WordPress Font Library for GDPR-compliant, privacy-friendly typography." +msgstr "将 Google Fonts 导入本地存储并在 WordPress 字体库中注册,实现符合 GDPR 且注重隐私的排版。" + +#. Author of the plugin +msgid "Maple Open Technologies" +msgstr "Maple Open Technologies" + +msgid "Maple Local Fonts requires WordPress 6.5 or higher for Font Library support." +msgstr "Maple Local Fonts 需要 WordPress 6.5 或更高版本以支持字体库。" + +msgid "Plugin Activation Error" +msgstr "插件激活错误" + +msgid "Maple Local Fonts has been deactivated. It requires WordPress 6.5 or higher." +msgstr "Maple Local Fonts 已停用。需要 WordPress 6.5 或更高版本。" + +msgid "Local Fonts" +msgstr "本地字体" + +msgid "Downloading..." +msgstr "下载中..." + +msgid "Deleting..." +msgstr "删除中..." + +msgid "Are you sure you want to delete this font?" +msgstr "您确定要删除此字体吗?" + +msgid "An error occurred. Please try again." +msgstr "发生错误。请重试。" + +msgid "Please select at least one weight." +msgstr "请至少选择一个字重。" + +msgid "Please select at least one style." +msgstr "请至少选择一个样式。" + +msgid "Please enter a font name." +msgstr "请输入字体名称。" + +msgid "Security check failed." +msgstr "安全检查失败。" + +msgid "Unauthorized." +msgstr "未授权。" + +msgid "Font name is required." +msgstr "字体名称为必填项。" + +msgid "Invalid font name: only letters, numbers, spaces, and hyphens allowed." +msgstr "字体名称无效:只允许使用字母、数字、空格和连字符。" + +msgid "Font name is too long." +msgstr "字体名称过长。" + +msgid "At least one weight is required." +msgstr "至少需要一个字重。" + +msgid "Too many weights selected." +msgstr "选择的字重过多。" + +msgid "At least one style is required." +msgstr "至少需要一个样式。" + +msgid "Successfully installed %s." +msgstr "成功安装 %s。" + +msgid "An unexpected error occurred." +msgstr "发生意外错误。" + +msgid "Invalid font ID." +msgstr "无效的字体 ID。" + +msgid "Font not found." +msgstr "未找到字体。" + +msgid "Cannot delete fonts not imported by this plugin." +msgstr "无法删除非此插件导入的字体。" + +msgid "Font deleted successfully." +msgstr "字体删除成功。" + +msgid "Font not found on Google Fonts. Please check the spelling and try again." +msgstr "在 Google Fonts 上未找到字体。请检查拼写并重试。" + +msgid "This font is already installed." +msgstr "此字体已安装。" + +msgid "Could not connect to Google Fonts. Please check your internet connection and try again." +msgstr "无法连接到 Google Fonts。请检查您的网络连接并重试。" + +msgid "Google Fonts returned an error. Please try again later." +msgstr "Google Fonts 返回错误。请稍后重试。" + +msgid "Could not process the font data. The font may not be available." +msgstr "无法处理字体数据。该字体可能不可用。" + +msgid "Could not download the font files. Please try again." +msgstr "无法下载字体文件。请重试。" + +msgid "Could not save font files. Please check that wp-content/fonts is writable." +msgstr "无法保存字体文件。请检查 wp-content/fonts 是否可写。" + +msgid "Could not create fonts directory. Please check file permissions." +msgstr "无法创建字体目录。请检查文件权限。" + +msgid "Invalid file path." +msgstr "无效的文件路径。" + +msgid "Invalid font URL." +msgstr "无效的字体 URL。" + +msgid "Invalid font name." +msgstr "无效的字体名称。" + +msgid "No valid weights specified." +msgstr "未指定有效的字重。" + +msgid "No valid styles specified." +msgstr "未指定有效的样式。" + +msgid "An unexpected error occurred. Please try again." +msgstr "发生意外错误。请重试。" + +msgid "Thin" +msgstr "极细" + +msgid "Extra Light" +msgstr "特细" + +msgid "Light" +msgstr "细体" + +msgid "Regular" +msgstr "常规" + +msgid "Medium" +msgstr "中等" + +msgid "Semi Bold" +msgstr "半粗" + +msgid "Bold" +msgstr "粗体" + +msgid "Extra Bold" +msgstr "特粗" + +msgid "Black" +msgstr "黑体" + +msgid "You do not have sufficient permissions to access this page." +msgstr "您没有足够的权限访问此页面。" + +msgid "Import from Google Fonts" +msgstr "从 Google Fonts 导入" + +msgid "Font Name" +msgstr "字体名称" + +msgid "e.g., Open Sans" +msgstr "例如:Open Sans" + +msgid "Enter the exact font name as it appears on Google Fonts." +msgstr "输入 Google Fonts 上显示的确切字体名称。" + +msgid "Weights" +msgstr "字重" + +msgid "Styles" +msgstr "样式" + +msgid "Normal" +msgstr "正常" + +msgid "Italic" +msgstr "斜体" + +msgid "Files to download:" +msgstr "要下载的文件:" + +msgid "Download & Install" +msgstr "下载并安装" + +msgid "Installed Fonts" +msgstr "已安装的字体" + +msgid "No fonts installed yet." +msgstr "尚未安装任何字体。" + +msgid "Delete" +msgstr "删除" + +msgid "Use %s to apply fonts to your site." +msgstr "使用 %s 将字体应用到您的网站。" + +msgid "Appearance → Editor → Styles → Typography" +msgstr "外观 → 编辑器 → 样式 → 排版" diff --git a/native/wordpress/maple-fonts-wp/languages/maple-local-fonts-zh_TW.po b/native/wordpress/maple-fonts-wp/languages/maple-local-fonts-zh_TW.po new file mode 100644 index 0000000..22a58b2 --- /dev/null +++ b/native/wordpress/maple-fonts-wp/languages/maple-local-fonts-zh_TW.po @@ -0,0 +1,222 @@ +# Translation of Maple Local Fonts in Chinese (Traditional) +# Copyright (C) 2024 Maple Open Technologies +# This file is distributed under the GPL-2.0-or-later. +msgid "" +msgstr "" +"Project-Id-Version: Maple Local Fonts 1.0.0\n" +"Report-Msgid-Bugs-To: https://mapleopentech.org/\n" +"Last-Translator: Maple Open Technologies\n" +"Language-Team: Chinese (Traditional)\n" +"Language: zh_TW\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"POT-Creation-Date: 2024-01-01T00:00:00+00:00\n" +"PO-Revision-Date: 2024-01-01T00:00:00+00:00\n" +"X-Generator: Manual\n" +"X-Domain: maple-local-fonts\n" +"Plural-Forms: nplurals=1; plural=0;\n" + +#. Plugin Name of the plugin +msgid "Maple Local Fonts" +msgstr "Maple Local Fonts" + +#. Description of the plugin +msgid "Import Google Fonts to local storage and register them with WordPress Font Library for GDPR-compliant, privacy-friendly typography." +msgstr "將 Google Fonts 匯入本機儲存空間並在 WordPress 字型庫中註冊,實現符合 GDPR 且注重隱私的排版。" + +#. Author of the plugin +msgid "Maple Open Technologies" +msgstr "Maple Open Technologies" + +msgid "Maple Local Fonts requires WordPress 6.5 or higher for Font Library support." +msgstr "Maple Local Fonts 需要 WordPress 6.5 或更高版本以支援字型庫。" + +msgid "Plugin Activation Error" +msgstr "外掛啟用錯誤" + +msgid "Maple Local Fonts has been deactivated. It requires WordPress 6.5 or higher." +msgstr "Maple Local Fonts 已停用。需要 WordPress 6.5 或更高版本。" + +msgid "Local Fonts" +msgstr "本機字型" + +msgid "Downloading..." +msgstr "下載中..." + +msgid "Deleting..." +msgstr "刪除中..." + +msgid "Are you sure you want to delete this font?" +msgstr "您確定要刪除此字型嗎?" + +msgid "An error occurred. Please try again." +msgstr "發生錯誤。請重試。" + +msgid "Please select at least one weight." +msgstr "請至少選擇一個字重。" + +msgid "Please select at least one style." +msgstr "請至少選擇一個樣式。" + +msgid "Please enter a font name." +msgstr "請輸入字型名稱。" + +msgid "Security check failed." +msgstr "安全性檢查失敗。" + +msgid "Unauthorized." +msgstr "未授權。" + +msgid "Font name is required." +msgstr "字型名稱為必填項。" + +msgid "Invalid font name: only letters, numbers, spaces, and hyphens allowed." +msgstr "字型名稱無效:只允許使用字母、數字、空格和連字號。" + +msgid "Font name is too long." +msgstr "字型名稱過長。" + +msgid "At least one weight is required." +msgstr "至少需要一個字重。" + +msgid "Too many weights selected." +msgstr "選擇的字重過多。" + +msgid "At least one style is required." +msgstr "至少需要一個樣式。" + +msgid "Successfully installed %s." +msgstr "成功安裝 %s。" + +msgid "An unexpected error occurred." +msgstr "發生意外錯誤。" + +msgid "Invalid font ID." +msgstr "無效的字型 ID。" + +msgid "Font not found." +msgstr "找不到字型。" + +msgid "Cannot delete fonts not imported by this plugin." +msgstr "無法刪除非此外掛匯入的字型。" + +msgid "Font deleted successfully." +msgstr "字型刪除成功。" + +msgid "Font not found on Google Fonts. Please check the spelling and try again." +msgstr "在 Google Fonts 上找不到字型。請檢查拼寫並重試。" + +msgid "This font is already installed." +msgstr "此字型已安裝。" + +msgid "Could not connect to Google Fonts. Please check your internet connection and try again." +msgstr "無法連線到 Google Fonts。請檢查您的網路連線並重試。" + +msgid "Google Fonts returned an error. Please try again later." +msgstr "Google Fonts 傳回錯誤。請稍後重試。" + +msgid "Could not process the font data. The font may not be available." +msgstr "無法處理字型資料。該字型可能不可用。" + +msgid "Could not download the font files. Please try again." +msgstr "無法下載字型檔案。請重試。" + +msgid "Could not save font files. Please check that wp-content/fonts is writable." +msgstr "無法儲存字型檔案。請檢查 wp-content/fonts 是否可寫入。" + +msgid "Could not create fonts directory. Please check file permissions." +msgstr "無法建立字型目錄。請檢查檔案權限。" + +msgid "Invalid file path." +msgstr "無效的檔案路徑。" + +msgid "Invalid font URL." +msgstr "無效的字型 URL。" + +msgid "Invalid font name." +msgstr "無效的字型名稱。" + +msgid "No valid weights specified." +msgstr "未指定有效的字重。" + +msgid "No valid styles specified." +msgstr "未指定有效的樣式。" + +msgid "An unexpected error occurred. Please try again." +msgstr "發生意外錯誤。請重試。" + +msgid "Thin" +msgstr "極細" + +msgid "Extra Light" +msgstr "特細" + +msgid "Light" +msgstr "細體" + +msgid "Regular" +msgstr "標準" + +msgid "Medium" +msgstr "中等" + +msgid "Semi Bold" +msgstr "半粗" + +msgid "Bold" +msgstr "粗體" + +msgid "Extra Bold" +msgstr "特粗" + +msgid "Black" +msgstr "黑體" + +msgid "You do not have sufficient permissions to access this page." +msgstr "您沒有足夠的權限存取此頁面。" + +msgid "Import from Google Fonts" +msgstr "從 Google Fonts 匯入" + +msgid "Font Name" +msgstr "字型名稱" + +msgid "e.g., Open Sans" +msgstr "例如:Open Sans" + +msgid "Enter the exact font name as it appears on Google Fonts." +msgstr "輸入 Google Fonts 上顯示的確切字型名稱。" + +msgid "Weights" +msgstr "字重" + +msgid "Styles" +msgstr "樣式" + +msgid "Normal" +msgstr "正常" + +msgid "Italic" +msgstr "斜體" + +msgid "Files to download:" +msgstr "要下載的檔案:" + +msgid "Download & Install" +msgstr "下載並安裝" + +msgid "Installed Fonts" +msgstr "已安裝的字型" + +msgid "No fonts installed yet." +msgstr "尚未安裝任何字型。" + +msgid "Delete" +msgstr "刪除" + +msgid "Use %s to apply fonts to your site." +msgstr "使用 %s 將字型套用到您的網站。" + +msgid "Appearance → Editor → Styles → Typography" +msgstr "外觀 → 編輯器 → 樣式 → 排版" diff --git a/native/wordpress/maple-fonts-wp/maple-local-fonts.php b/native/wordpress/maple-fonts-wp/maple-local-fonts.php new file mode 100644 index 0000000..c0c60ea --- /dev/null +++ b/native/wordpress/maple-fonts-wp/maple-local-fonts.php @@ -0,0 +1,216 @@ + true] + ); + } + + // Ensure fonts directory exists + $font_dir = wp_get_font_dir(); + if (!file_exists($font_dir['path'])) { + wp_mkdir_p($font_dir['path']); + } +} +register_activation_hook(__FILE__, 'mlf_activate'); + +/** + * Check WordPress version on admin init (in case WP was downgraded). + */ +function mlf_check_version() { + if (version_compare(get_bloginfo('version'), '6.5', '<')) { + deactivate_plugins(plugin_basename(__FILE__)); + add_action('admin_notices', 'mlf_version_notice'); + } +} +add_action('admin_init', 'mlf_check_version'); + +/** + * Display version notice. + */ +function mlf_version_notice() { + echo '

'; + esc_html_e('Maple Local Fonts has been deactivated. It requires WordPress 6.5 or higher.', 'maple-local-fonts'); + echo '

'; +} + +/** + * Declare WooCommerce HPOS compatibility. + */ +function mlf_declare_hpos_compatibility() { + if (class_exists(\Automattic\WooCommerce\Utilities\FeaturesUtil::class)) { + \Automattic\WooCommerce\Utilities\FeaturesUtil::declare_compatibility( + 'custom_order_tables', + __FILE__, + true + ); + } +} +add_action('before_woocommerce_init', 'mlf_declare_hpos_compatibility'); + +/** + * Load plugin text domain. + */ +function mlf_load_textdomain() { + load_plugin_textdomain('maple-local-fonts', false, dirname(MLF_PLUGIN_BASENAME) . '/languages'); +} +add_action('plugins_loaded', 'mlf_load_textdomain'); + +/** + * Autoload plugin classes. + */ +function mlf_autoload($class) { + $prefix = 'MLF_'; + if (strpos($class, $prefix) !== 0) { + return; + } + + $class_name = str_replace($prefix, '', $class); + $class_name = str_replace('_', '-', strtolower($class_name)); + $file = MLF_PLUGIN_DIR . 'includes/class-mlf-' . $class_name . '.php'; + + if (file_exists($file)) { + require_once $file; + } +} +spl_autoload_register('mlf_autoload'); + +/** + * Initialize the plugin. + */ +function mlf_init() { + // Only load admin functionality + if (is_admin()) { + new MLF_Admin_Page(); + new MLF_Ajax_Handler(); + } +} +add_action('plugins_loaded', 'mlf_init', 20); + +/** + * Register REST API routes. + */ +function mlf_register_rest_routes() { + $controller = new MLF_Rest_Controller(); + $controller->register_routes(); +} +add_action('rest_api_init', 'mlf_register_rest_routes'); + +/** + * Get the required capability for managing fonts. + * + * @return string The capability required to manage fonts. + */ +function mlf_get_capability() { + /** + * Filter the capability required to manage local fonts. + * + * @since 1.0.0 + * @param string $capability Default capability is 'edit_theme_options'. + */ + return apply_filters('mlf_manage_fonts_capability', 'edit_theme_options'); +} + +/** + * Register admin menu. + */ +function mlf_register_menu() { + add_submenu_page( + 'themes.php', + __('Maple Local Fonts', 'maple-local-fonts'), + __('Local Fonts', 'maple-local-fonts'), + mlf_get_capability(), + 'maple-local-fonts', + 'mlf_render_admin_page' + ); +} +add_action('admin_menu', 'mlf_register_menu'); + +/** + * Render admin page (delegates to MLF_Admin_Page). + */ +function mlf_render_admin_page() { + $admin_page = new MLF_Admin_Page(); + $admin_page->render(); +} + +/** + * Enqueue admin assets. + * + * @param string $hook The current admin page hook. + */ +function mlf_enqueue_admin_assets($hook) { + if ($hook !== 'appearance_page_maple-local-fonts') { + return; + } + + wp_enqueue_style( + 'mlf-admin', + MLF_PLUGIN_URL . 'assets/admin.css', + [], + MLF_VERSION + ); + + wp_enqueue_script( + 'mlf-admin', + MLF_PLUGIN_URL . 'assets/admin.js', + ['jquery'], + MLF_VERSION, + true + ); + + wp_localize_script('mlf-admin', 'mapleLocalFontsData', [ + 'ajaxUrl' => admin_url('admin-ajax.php'), + 'downloadNonce' => wp_create_nonce('mlf_download_font'), + 'deleteNonce' => wp_create_nonce('mlf_delete_font'), + 'strings' => [ + 'downloading' => __('Downloading...', 'maple-local-fonts'), + 'deleting' => __('Deleting...', 'maple-local-fonts'), + 'confirmDelete' => __('Are you sure you want to delete this font?', 'maple-local-fonts'), + 'error' => __('An error occurred. Please try again.', 'maple-local-fonts'), + 'selectWeight' => __('Please select at least one weight.', 'maple-local-fonts'), + 'selectStyle' => __('Please select at least one style.', 'maple-local-fonts'), + 'enterFontName' => __('Please enter a font name.', 'maple-local-fonts'), + ], + ]); +} +add_action('admin_enqueue_scripts', 'mlf_enqueue_admin_assets'); diff --git a/native/wordpress/maple-fonts-wp/phpcs.xml.dist b/native/wordpress/maple-fonts-wp/phpcs.xml.dist new file mode 100644 index 0000000..42afe67 --- /dev/null +++ b/native/wordpress/maple-fonts-wp/phpcs.xml.dist @@ -0,0 +1,61 @@ + + + PHP Coding Standards for Maple Local Fonts WordPress Plugin + + + . + + + /vendor/* + /tests/* + /bin/* + /build/* + /node_modules/* + *.min.js + *.min.css + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/native/wordpress/maple-fonts-wp/phpunit.xml.dist b/native/wordpress/maple-fonts-wp/phpunit.xml.dist new file mode 100644 index 0000000..8e50b5c --- /dev/null +++ b/native/wordpress/maple-fonts-wp/phpunit.xml.dist @@ -0,0 +1,27 @@ + + + + + ./tests/ + + + + + ./includes/ + ./maple-local-fonts.php + + + + + + + + + diff --git a/native/wordpress/maple-fonts-wp/readme.txt b/native/wordpress/maple-fonts-wp/readme.txt new file mode 100644 index 0000000..9aa0785 --- /dev/null +++ b/native/wordpress/maple-fonts-wp/readme.txt @@ -0,0 +1,155 @@ +=== Maple Local Fonts === +Contributors: mapleopentech +Tags: fonts, google fonts, local fonts, gdpr, typography, privacy +Requires at least: 6.5 +Tested up to: 6.7 +Requires PHP: 7.4 +Stable tag: 1.0.0 +License: GPL-2.0-or-later +License URI: https://www.gnu.org/licenses/gpl-2.0.html + +Import Google Fonts to local storage for GDPR-compliant, privacy-friendly typography. Fonts are served from your server, not Google. + +== Description == + +**Maple Local Fonts** allows you to import Google Fonts to your WordPress site's local storage. Once imported, fonts are served directly from your server — Google is never contacted again. + += Why Local Fonts? = + +* **GDPR Compliance**: Serving fonts from Google's servers can transfer visitor IP addresses to Google, which may violate GDPR. Local fonts eliminate this concern. +* **Performance**: Local fonts can load faster since they're served from your own server without additional DNS lookups. +* **Privacy**: Your visitors' data stays private — no third-party requests for fonts. +* **Reliability**: Fonts are always available, even if Google's services are blocked or unavailable. + += Features = + +* **Simple Import**: Enter a font name, select weights and styles, and click to import. +* **WordPress Integration**: Fonts automatically appear in the Full Site Editor typography settings. +* **Clean Removal**: Uninstalling the plugin removes all imported fonts and files. +* **No Frontend Impact**: Zero JavaScript or CSS added to your frontend — only font files are served. + += How It Works = + +1. Enter the Google Fonts name (e.g., "Open Sans") +2. Select the weights (400, 700, etc.) and styles (normal, italic) +3. Click "Download & Install" +4. The plugin downloads the font files and registers them with WordPress +5. Use the fonts in Appearance → Editor → Styles → Typography + += Requirements = + +* WordPress 6.5 or higher (required for Font Library API) +* PHP 7.4 or higher + +== Installation == + +1. Upload the `maple-local-fonts` folder to `/wp-content/plugins/` +2. Activate the plugin through the Plugins menu +3. Go to Appearance → Local Fonts +4. Import your desired fonts + +== Frequently Asked Questions == + += Does this plugin contact Google after importing a font? = + +No. The plugin only contacts Google Fonts once, during the import process. After that, fonts are served entirely from your server. + += Where are the font files stored? = + +Font files are stored in `wp-content/fonts/`, which is WordPress's default font directory. + += Can I use these fonts with any theme? = + +**Block Themes (FSE):** Imported fonts automatically appear in Appearance → Editor → Styles → Typography. + +**Classic Themes:** Fonts are registered but must be applied via custom CSS. After importing, add CSS like this to your theme or Customizer: + +`body { font-family: "Open Sans", sans-serif; }` + +The plugin will detect your theme type and show appropriate instructions. + += What happens to the fonts if I uninstall the plugin? = + +When you delete the plugin, all imported fonts and their files are removed. This ensures a clean uninstall. + += Is this plugin GDPR compliant? = + +Yes. After importing, fonts are served from your own server. No visitor data is sent to Google or any third party. + += What font formats are downloaded? = + +The plugin downloads WOFF2 format, which is the most efficient and widely supported web font format. + += Can I import fonts that aren't on Google Fonts? = + +No, this plugin only supports importing from Google Fonts. For other fonts, you would need to manually upload them. + += I'm using Wordfence/a firewall and imports are failing = + +If font imports fail, your firewall may be blocking outbound requests. Whitelist these domains: + +* `fonts.googleapis.com` (for font CSS) +* `fonts.gstatic.com` (for font files) + +**Wordfence:** Go to Wordfence → Firewall → Blocking and ensure these domains aren't blocked. If using Learning Mode, imports should work and the domains will be automatically learned. + +**Other firewalls:** Add the above domains to your outbound request allowlist. + += The plugin says it requires WordPress 6.5 = + +WordPress 6.5 introduced the Font Library API that this plugin uses. Earlier versions don't support local font registration. Please update WordPress to use this plugin. + +== Compatibility == + += Tested With = + +* **WordPress:** 6.5, 6.6, 6.7 +* **WooCommerce:** 8.0+ (HPOS compatible) +* **Wordfence:** 7.10+ (see FAQ for firewall settings) +* **LearnDash:** 4.10+ +* **Popular Block Themes:** Twenty Twenty-Four, Twenty Twenty-Five + += Theme Compatibility = + +* **Block Themes (FSE):** Full support via Site Editor typography settings +* **Classic Themes:** Supported via custom CSS (instructions shown in admin) + +== Privacy == + += Data Collection = + +This plugin does not collect any personal data from your website visitors. + += External Services = + +This plugin connects to Google Fonts (fonts.googleapis.com and fonts.gstatic.com) **only during the font import process** in the WordPress admin area. This connection is: + +* Initiated only by administrators +* Used only to download font files +* Not repeated after initial download + +After import, no external connections are made. Fonts are served entirely from your server. + += Cookies = + +This plugin does not set any cookies. + +== Screenshots == + +1. The font import interface +2. Installed fonts list with delete option +3. Fonts appearing in the Full Site Editor typography settings + +== Changelog == + += 1.0.0 = +* Initial release +* Import Google Fonts to local storage +* WordPress Font Library integration +* Admin interface for font management +* Clean uninstall functionality + +== Upgrade Notice == + += 1.0.0 = +Initial release of Maple Local Fonts. diff --git a/native/wordpress/maple-fonts-wp/tests/bootstrap.php b/native/wordpress/maple-fonts-wp/tests/bootstrap.php new file mode 100644 index 0000000..3f079f8 --- /dev/null +++ b/native/wordpress/maple-fonts-wp/tests/bootstrap.php @@ -0,0 +1,40 @@ +admin_id = $this->factory->user->create(['role' => 'administrator']); + $this->subscriber_id = $this->factory->user->create(['role' => 'subscriber']); + + // Initialize the handler + new MLF_Ajax_Handler(); + } + + /** + * Clean up after tests. + */ + public function tear_down() { + // Clean up test fonts + $fonts = get_posts([ + 'post_type' => 'wp_font_family', + 'posts_per_page' => -1, + 'meta_key' => '_mlf_imported', + 'meta_value' => '1', + 'fields' => 'ids', + ]); + + foreach ($fonts as $font_id) { + wp_delete_post($font_id, true); + } + + wp_set_current_user(0); + delete_transient('mlf_imported_fonts_list'); + + parent::tear_down(); + } + + /** + * Test download without nonce. + */ + public function test_download_without_nonce() { + wp_set_current_user($this->admin_id); + + $_POST['font_name'] = 'Open Sans'; + $_POST['weights'] = [400]; + $_POST['styles'] = ['normal']; + + $this->expectException('WPAjaxDieStopException'); + + try { + $this->_handleAjax('mlf_download_font'); + } catch (WPAjaxDieStopException $e) { + $response = json_decode($this->_last_response, true); + $this->assertFalse($response['success']); + $this->assertStringContainsString('Security', $response['data']['message']); + throw $e; + } + } + + /** + * Test download with invalid nonce. + */ + public function test_download_with_invalid_nonce() { + wp_set_current_user($this->admin_id); + + $_POST['nonce'] = 'invalid_nonce'; + $_POST['font_name'] = 'Open Sans'; + $_POST['weights'] = [400]; + $_POST['styles'] = ['normal']; + + $this->expectException('WPAjaxDieStopException'); + + try { + $this->_handleAjax('mlf_download_font'); + } catch (WPAjaxDieStopException $e) { + $response = json_decode($this->_last_response, true); + $this->assertFalse($response['success']); + throw $e; + } + } + + /** + * Test download without proper capability. + */ + public function test_download_without_capability() { + wp_set_current_user($this->subscriber_id); + + $_POST['nonce'] = wp_create_nonce('mlf_download_font'); + $_POST['font_name'] = 'Open Sans'; + $_POST['weights'] = [400]; + $_POST['styles'] = ['normal']; + + $this->expectException('WPAjaxDieStopException'); + + try { + $this->_handleAjax('mlf_download_font'); + } catch (WPAjaxDieStopException $e) { + $response = json_decode($this->_last_response, true); + $this->assertFalse($response['success']); + $this->assertStringContainsString('Unauthorized', $response['data']['message']); + throw $e; + } + } + + /** + * Test download with empty font name. + */ + public function test_download_empty_font_name() { + wp_set_current_user($this->admin_id); + + $_POST['nonce'] = wp_create_nonce('mlf_download_font'); + $_POST['font_name'] = ''; + $_POST['weights'] = [400]; + $_POST['styles'] = ['normal']; + + $this->expectException('WPAjaxDieStopException'); + + try { + $this->_handleAjax('mlf_download_font'); + } catch (WPAjaxDieStopException $e) { + $response = json_decode($this->_last_response, true); + $this->assertFalse($response['success']); + $this->assertStringContainsString('required', $response['data']['message']); + throw $e; + } + } + + /** + * Test download with invalid font name (XSS attempt). + */ + public function test_download_invalid_font_name_xss() { + wp_set_current_user($this->admin_id); + + $_POST['nonce'] = wp_create_nonce('mlf_download_font'); + $_POST['font_name'] = ''; + $_POST['weights'] = [400]; + $_POST['styles'] = ['normal']; + + $this->expectException('WPAjaxDieStopException'); + + try { + $this->_handleAjax('mlf_download_font'); + } catch (WPAjaxDieStopException $e) { + $response = json_decode($this->_last_response, true); + $this->assertFalse($response['success']); + $this->assertStringContainsString('Invalid font name', $response['data']['message']); + throw $e; + } + } + + /** + * Test download with path traversal in font name. + */ + public function test_download_path_traversal() { + wp_set_current_user($this->admin_id); + + $_POST['nonce'] = wp_create_nonce('mlf_download_font'); + $_POST['font_name'] = '../../../etc/passwd'; + $_POST['weights'] = [400]; + $_POST['styles'] = ['normal']; + + $this->expectException('WPAjaxDieStopException'); + + try { + $this->_handleAjax('mlf_download_font'); + } catch (WPAjaxDieStopException $e) { + $response = json_decode($this->_last_response, true); + $this->assertFalse($response['success']); + $this->assertStringContainsString('Invalid font name', $response['data']['message']); + throw $e; + } + } + + /** + * Test download with font name too long. + */ + public function test_download_font_name_too_long() { + wp_set_current_user($this->admin_id); + + $_POST['nonce'] = wp_create_nonce('mlf_download_font'); + $_POST['font_name'] = str_repeat('a', 101); + $_POST['weights'] = [400]; + $_POST['styles'] = ['normal']; + + $this->expectException('WPAjaxDieStopException'); + + try { + $this->_handleAjax('mlf_download_font'); + } catch (WPAjaxDieStopException $e) { + $response = json_decode($this->_last_response, true); + $this->assertFalse($response['success']); + $this->assertStringContainsString('too long', $response['data']['message']); + throw $e; + } + } + + /** + * Test download with no weights. + */ + public function test_download_no_weights() { + wp_set_current_user($this->admin_id); + + $_POST['nonce'] = wp_create_nonce('mlf_download_font'); + $_POST['font_name'] = 'Open Sans'; + $_POST['weights'] = []; + $_POST['styles'] = ['normal']; + + $this->expectException('WPAjaxDieStopException'); + + try { + $this->_handleAjax('mlf_download_font'); + } catch (WPAjaxDieStopException $e) { + $response = json_decode($this->_last_response, true); + $this->assertFalse($response['success']); + $this->assertStringContainsString('weight', $response['data']['message']); + throw $e; + } + } + + /** + * Test download with invalid weights. + */ + public function test_download_invalid_weights() { + wp_set_current_user($this->admin_id); + + $_POST['nonce'] = wp_create_nonce('mlf_download_font'); + $_POST['font_name'] = 'Open Sans'; + $_POST['weights'] = [999, 1000]; // Invalid weights + $_POST['styles'] = ['normal']; + + $this->expectException('WPAjaxDieStopException'); + + try { + $this->_handleAjax('mlf_download_font'); + } catch (WPAjaxDieStopException $e) { + $response = json_decode($this->_last_response, true); + $this->assertFalse($response['success']); + $this->assertStringContainsString('weight', $response['data']['message']); + throw $e; + } + } + + /** + * Test download with no styles. + */ + public function test_download_no_styles() { + wp_set_current_user($this->admin_id); + + $_POST['nonce'] = wp_create_nonce('mlf_download_font'); + $_POST['font_name'] = 'Open Sans'; + $_POST['weights'] = [400]; + $_POST['styles'] = []; + + $this->expectException('WPAjaxDieStopException'); + + try { + $this->_handleAjax('mlf_download_font'); + } catch (WPAjaxDieStopException $e) { + $response = json_decode($this->_last_response, true); + $this->assertFalse($response['success']); + $this->assertStringContainsString('style', $response['data']['message']); + throw $e; + } + } + + /** + * Test delete without nonce. + */ + public function test_delete_without_nonce() { + wp_set_current_user($this->admin_id); + + $font_id = $this->create_test_font(); + $_POST['font_id'] = $font_id; + + $this->expectException('WPAjaxDieStopException'); + + try { + $this->_handleAjax('mlf_delete_font'); + } catch (WPAjaxDieStopException $e) { + $response = json_decode($this->_last_response, true); + $this->assertFalse($response['success']); + throw $e; + } + } + + /** + * Test delete without capability. + */ + public function test_delete_without_capability() { + wp_set_current_user($this->subscriber_id); + + $font_id = $this->create_test_font(); + $_POST['nonce'] = wp_create_nonce('mlf_delete_font'); + $_POST['font_id'] = $font_id; + + $this->expectException('WPAjaxDieStopException'); + + try { + $this->_handleAjax('mlf_delete_font'); + } catch (WPAjaxDieStopException $e) { + $response = json_decode($this->_last_response, true); + $this->assertFalse($response['success']); + $this->assertStringContainsString('Unauthorized', $response['data']['message']); + throw $e; + } + } + + /** + * Test delete with invalid font ID. + */ + public function test_delete_invalid_font_id() { + wp_set_current_user($this->admin_id); + + $_POST['nonce'] = wp_create_nonce('mlf_delete_font'); + $_POST['font_id'] = 0; + + $this->expectException('WPAjaxDieStopException'); + + try { + $this->_handleAjax('mlf_delete_font'); + } catch (WPAjaxDieStopException $e) { + $response = json_decode($this->_last_response, true); + $this->assertFalse($response['success']); + $this->assertStringContainsString('Invalid', $response['data']['message']); + throw $e; + } + } + + /** + * Test delete font not found. + */ + public function test_delete_font_not_found() { + wp_set_current_user($this->admin_id); + + $_POST['nonce'] = wp_create_nonce('mlf_delete_font'); + $_POST['font_id'] = 99999; + + $this->expectException('WPAjaxDieStopException'); + + try { + $this->_handleAjax('mlf_delete_font'); + } catch (WPAjaxDieStopException $e) { + $response = json_decode($this->_last_response, true); + $this->assertFalse($response['success']); + $this->assertStringContainsString('not found', $response['data']['message']); + throw $e; + } + } + + /** + * Test delete font not imported by plugin. + */ + public function test_delete_font_not_ours() { + wp_set_current_user($this->admin_id); + + // Create font without our meta + $font_id = wp_insert_post([ + 'post_type' => 'wp_font_family', + 'post_title' => 'Theme Font', + 'post_status' => 'publish', + ]); + + $_POST['nonce'] = wp_create_nonce('mlf_delete_font'); + $_POST['font_id'] = $font_id; + + $this->expectException('WPAjaxDieStopException'); + + try { + $this->_handleAjax('mlf_delete_font'); + } catch (WPAjaxDieStopException $e) { + $response = json_decode($this->_last_response, true); + $this->assertFalse($response['success']); + $this->assertStringContainsString('not imported', $response['data']['message']); + throw $e; + } + + // Clean up + wp_delete_post($font_id, true); + } + + /** + * Helper to create a test font. + * + * @return int Font ID. + */ + private function create_test_font() { + $font_id = wp_insert_post([ + 'post_type' => 'wp_font_family', + 'post_title' => 'Test Font', + 'post_name' => 'test-font', + 'post_status' => 'publish', + ]); + + update_post_meta($font_id, '_mlf_imported', '1'); + + return $font_id; + } +} diff --git a/native/wordpress/maple-fonts-wp/tests/test-font-registry.php b/native/wordpress/maple-fonts-wp/tests/test-font-registry.php new file mode 100644 index 0000000..6633396 --- /dev/null +++ b/native/wordpress/maple-fonts-wp/tests/test-font-registry.php @@ -0,0 +1,303 @@ +registry = new MLF_Font_Registry(); + + // Clear font cache + delete_transient('mlf_imported_fonts_list'); + } + + /** + * Clean up after tests. + */ + public function tear_down() { + // Clean up any test fonts + $fonts = get_posts([ + 'post_type' => 'wp_font_family', + 'posts_per_page' => -1, + 'meta_key' => '_mlf_imported', + 'meta_value' => '1', + 'fields' => 'ids', + ]); + + foreach ($fonts as $font_id) { + wp_delete_post($font_id, true); + } + + delete_transient('mlf_imported_fonts_list'); + parent::tear_down(); + } + + /** + * Test that get_imported_fonts returns empty array when no fonts installed. + */ + public function test_get_imported_fonts_empty() { + $fonts = $this->registry->get_imported_fonts(); + $this->assertIsArray($fonts); + $this->assertEmpty($fonts); + } + + /** + * Test font registration. + */ + public function test_register_font() { + $font_name = 'Test Font'; + $font_slug = 'test-font'; + $files = [ + [ + 'path' => '/tmp/test-font_normal_400.woff2', + 'weight' => '400', + 'style' => 'normal', + ], + ]; + + // Create a dummy file for the test + file_put_contents($files[0]['path'], 'wOF2dummy'); + + $result = $this->registry->register_font($font_name, $font_slug, $files); + + $this->assertIsInt($result); + $this->assertGreaterThan(0, $result); + + // Verify font was registered + $font = get_post($result); + $this->assertEquals('wp_font_family', $font->post_type); + $this->assertEquals($font_name, $font->post_title); + + // Clean up + unlink($files[0]['path']); + } + + /** + * Test that duplicate fonts are rejected. + */ + public function test_register_font_duplicate() { + $font_name = 'Duplicate Font'; + $font_slug = 'duplicate-font'; + $files = [ + [ + 'path' => '/tmp/duplicate-font_normal_400.woff2', + 'weight' => '400', + 'style' => 'normal', + ], + ]; + + file_put_contents($files[0]['path'], 'wOF2dummy'); + + // Register first time + $result1 = $this->registry->register_font($font_name, $font_slug, $files); + $this->assertIsInt($result1); + + // Try to register again + $result2 = $this->registry->register_font($font_name, $font_slug, $files); + $this->assertWPError($result2); + $this->assertEquals('font_exists', $result2->get_error_code()); + + // Clean up + unlink($files[0]['path']); + } + + /** + * Test get_imported_fonts returns registered fonts. + */ + public function test_get_imported_fonts_returns_fonts() { + $font_name = 'Listed Font'; + $font_slug = 'listed-font'; + $files = [ + [ + 'path' => '/tmp/listed-font_normal_400.woff2', + 'weight' => '400', + 'style' => 'normal', + ], + [ + 'path' => '/tmp/listed-font_normal_700.woff2', + 'weight' => '700', + 'style' => 'normal', + ], + ]; + + foreach ($files as $file) { + file_put_contents($file['path'], 'wOF2dummy'); + } + + $font_id = $this->registry->register_font($font_name, $font_slug, $files); + + // Clear cache to test fresh retrieval + delete_transient('mlf_imported_fonts_list'); + + $fonts = $this->registry->get_imported_fonts(); + + $this->assertCount(1, $fonts); + $this->assertEquals($font_id, $fonts[0]['id']); + $this->assertEquals($font_name, $fonts[0]['name']); + $this->assertEquals($font_slug, $fonts[0]['slug']); + $this->assertCount(2, $fonts[0]['variants']); + + // Clean up + foreach ($files as $file) { + unlink($file['path']); + } + } + + /** + * Test font deletion. + */ + public function test_delete_font() { + $font_name = 'Delete Font'; + $font_slug = 'delete-font'; + $files = [ + [ + 'path' => '/tmp/delete-font_normal_400.woff2', + 'weight' => '400', + 'style' => 'normal', + ], + ]; + + file_put_contents($files[0]['path'], 'wOF2dummy'); + + $font_id = $this->registry->register_font($font_name, $font_slug, $files); + + // Delete the font + $result = $this->registry->delete_font($font_id); + $this->assertTrue($result); + + // Verify font is gone + $font = get_post($font_id); + $this->assertNull($font); + } + + /** + * Test delete_font rejects non-existent fonts. + */ + public function test_delete_font_not_found() { + $result = $this->registry->delete_font(99999); + $this->assertWPError($result); + $this->assertEquals('not_found', $result->get_error_code()); + } + + /** + * Test delete_font rejects fonts not imported by plugin. + */ + public function test_delete_font_not_ours() { + // Create a font family post without our meta + $font_id = wp_insert_post([ + 'post_type' => 'wp_font_family', + 'post_title' => 'Theme Font', + 'post_status' => 'publish', + ]); + + $result = $this->registry->delete_font($font_id); + $this->assertWPError($result); + $this->assertEquals('not_ours', $result->get_error_code()); + + // Clean up + wp_delete_post($font_id, true); + } + + /** + * Test that cache is cleared on register. + */ + public function test_cache_cleared_on_register() { + // Set a dummy cache + set_transient('mlf_imported_fonts_list', ['cached' => true], 300); + + $font_name = 'Cache Test Font'; + $font_slug = 'cache-test-font'; + $files = [ + [ + 'path' => '/tmp/cache-test-font_normal_400.woff2', + 'weight' => '400', + 'style' => 'normal', + ], + ]; + + file_put_contents($files[0]['path'], 'wOF2dummy'); + + $this->registry->register_font($font_name, $font_slug, $files); + + // Cache should be cleared + $cached = get_transient('mlf_imported_fonts_list'); + $this->assertFalse($cached); + + // Clean up + unlink($files[0]['path']); + } + + /** + * Test that cache is cleared on delete. + */ + public function test_cache_cleared_on_delete() { + $font_name = 'Cache Delete Font'; + $font_slug = 'cache-delete-font'; + $files = [ + [ + 'path' => '/tmp/cache-delete-font_normal_400.woff2', + 'weight' => '400', + 'style' => 'normal', + ], + ]; + + file_put_contents($files[0]['path'], 'wOF2dummy'); + + $font_id = $this->registry->register_font($font_name, $font_slug, $files); + + // Set a dummy cache + set_transient('mlf_imported_fonts_list', ['cached' => true], 300); + + $this->registry->delete_font($font_id); + + // Cache should be cleared + $cached = get_transient('mlf_imported_fonts_list'); + $this->assertFalse($cached); + } + + /** + * Test font_exists method. + */ + public function test_font_exists() { + $font_name = 'Exists Font'; + $font_slug = 'exists-font'; + $files = [ + [ + 'path' => '/tmp/exists-font_normal_400.woff2', + 'weight' => '400', + 'style' => 'normal', + ], + ]; + + file_put_contents($files[0]['path'], 'wOF2dummy'); + + // Before registration + $this->assertFalse($this->registry->font_exists($font_slug)); + + // After registration + $this->registry->register_font($font_name, $font_slug, $files); + $this->assertTrue($this->registry->font_exists($font_slug)); + + // Clean up + unlink($files[0]['path']); + } +} diff --git a/native/wordpress/maple-fonts-wp/tests/test-rate-limiter.php b/native/wordpress/maple-fonts-wp/tests/test-rate-limiter.php new file mode 100644 index 0000000..476479c --- /dev/null +++ b/native/wordpress/maple-fonts-wp/tests/test-rate-limiter.php @@ -0,0 +1,177 @@ +rate_limiter = new MLF_Rate_Limiter(3, 60); // 3 requests per 60 seconds + } + + /** + * Clean up after tests. + */ + public function tear_down() { + // Clear any transients + $this->rate_limiter->clear('test_action'); + parent::tear_down(); + } + + /** + * Test that first request is not limited. + */ + public function test_first_request_not_limited() { + $this->assertFalse($this->rate_limiter->is_limited('test_action')); + } + + /** + * Test that remaining count starts at limit. + */ + public function test_remaining_starts_at_limit() { + $this->assertEquals(3, $this->rate_limiter->get_remaining('test_action')); + } + + /** + * Test that check_and_record allows requests within limit. + */ + public function test_check_and_record_allows_within_limit() { + $this->assertTrue($this->rate_limiter->check_and_record('test_action')); + $this->assertTrue($this->rate_limiter->check_and_record('test_action')); + $this->assertTrue($this->rate_limiter->check_and_record('test_action')); + } + + /** + * Test that check_and_record blocks after limit exceeded. + */ + public function test_check_and_record_blocks_after_limit() { + // Use up all allowed requests + $this->rate_limiter->check_and_record('test_action'); + $this->rate_limiter->check_and_record('test_action'); + $this->rate_limiter->check_and_record('test_action'); + + // Next request should be blocked + $this->assertFalse($this->rate_limiter->check_and_record('test_action')); + } + + /** + * Test that remaining count decreases correctly. + */ + public function test_remaining_decreases() { + $this->rate_limiter->record_request('test_action'); + $this->assertEquals(2, $this->rate_limiter->get_remaining('test_action')); + + $this->rate_limiter->record_request('test_action'); + $this->assertEquals(1, $this->rate_limiter->get_remaining('test_action')); + + $this->rate_limiter->record_request('test_action'); + $this->assertEquals(0, $this->rate_limiter->get_remaining('test_action')); + } + + /** + * Test that is_limited returns true after limit exceeded. + */ + public function test_is_limited_after_exceeding() { + $this->rate_limiter->record_request('test_action'); + $this->rate_limiter->record_request('test_action'); + $this->rate_limiter->record_request('test_action'); + + $this->assertTrue($this->rate_limiter->is_limited('test_action')); + } + + /** + * Test that clear resets the rate limit. + */ + public function test_clear_resets_limit() { + // Use up all requests + $this->rate_limiter->record_request('test_action'); + $this->rate_limiter->record_request('test_action'); + $this->rate_limiter->record_request('test_action'); + + $this->assertTrue($this->rate_limiter->is_limited('test_action')); + + // Clear and verify reset + $this->rate_limiter->clear('test_action'); + $this->assertFalse($this->rate_limiter->is_limited('test_action')); + $this->assertEquals(3, $this->rate_limiter->get_remaining('test_action')); + } + + /** + * Test that different actions are tracked separately. + */ + public function test_different_actions_tracked_separately() { + // Use up all requests for action1 + $this->rate_limiter->record_request('action1'); + $this->rate_limiter->record_request('action1'); + $this->rate_limiter->record_request('action1'); + + // action1 should be limited + $this->assertTrue($this->rate_limiter->is_limited('action1')); + + // action2 should not be limited + $this->assertFalse($this->rate_limiter->is_limited('action2')); + $this->assertEquals(3, $this->rate_limiter->get_remaining('action2')); + + // Clean up + $this->rate_limiter->clear('action1'); + $this->rate_limiter->clear('action2'); + } + + /** + * Test that logged in user uses user ID for tracking. + */ + public function test_logged_in_user_tracking() { + // Create and log in a user + $user_id = $this->factory->user->create(['role' => 'administrator']); + wp_set_current_user($user_id); + + // Use up requests + $this->rate_limiter->record_request('test_action'); + $this->rate_limiter->record_request('test_action'); + $this->rate_limiter->record_request('test_action'); + + $this->assertTrue($this->rate_limiter->is_limited('test_action')); + + // Clean up + wp_set_current_user(0); + $this->rate_limiter->clear('test_action'); + } + + /** + * Test constructor with custom limits. + */ + public function test_custom_limits() { + $custom_limiter = new MLF_Rate_Limiter(5, 120); + + $this->assertEquals(5, $custom_limiter->get_remaining('custom_action')); + + // Use 5 requests + for ($i = 0; $i < 5; $i++) { + $this->assertTrue($custom_limiter->check_and_record('custom_action')); + } + + // 6th should be blocked + $this->assertFalse($custom_limiter->check_and_record('custom_action')); + + // Clean up + $custom_limiter->clear('custom_action'); + } +} diff --git a/native/wordpress/maple-fonts-wp/tests/test-rest-controller.php b/native/wordpress/maple-fonts-wp/tests/test-rest-controller.php new file mode 100644 index 0000000..5a2c323 --- /dev/null +++ b/native/wordpress/maple-fonts-wp/tests/test-rest-controller.php @@ -0,0 +1,387 @@ +controller = new MLF_Rest_Controller(); + $this->controller->register_routes(); + + // Create users + $this->admin_id = $this->factory->user->create(['role' => 'administrator']); + $this->subscriber_id = $this->factory->user->create(['role' => 'subscriber']); + + // Clear any existing fonts and cache + delete_transient('mlf_imported_fonts_list'); + } + + /** + * Clean up after tests. + */ + public function tear_down() { + // Clean up test fonts + $fonts = get_posts([ + 'post_type' => 'wp_font_family', + 'posts_per_page' => -1, + 'meta_key' => '_mlf_imported', + 'meta_value' => '1', + 'fields' => 'ids', + ]); + + foreach ($fonts as $font_id) { + wp_delete_post($font_id, true); + } + + wp_set_current_user(0); + delete_transient('mlf_imported_fonts_list'); + + parent::tear_down(); + } + + /** + * Test route registration. + */ + public function test_register_routes() { + $routes = rest_get_server()->get_routes(); + + $this->assertArrayHasKey('/mlf/v1/fonts', $routes); + $this->assertArrayHasKey('/mlf/v1/fonts/(?P[\d]+)', $routes); + } + + /** + * Test get_items without authentication. + */ + public function test_get_items_unauthenticated() { + $request = new WP_REST_Request('GET', '/mlf/v1/fonts'); + $response = rest_get_server()->dispatch($request); + + $this->assertEquals(401, $response->get_status()); + } + + /** + * Test get_items with subscriber (insufficient permissions). + */ + public function test_get_items_insufficient_permissions() { + wp_set_current_user($this->subscriber_id); + + $request = new WP_REST_Request('GET', '/mlf/v1/fonts'); + $response = rest_get_server()->dispatch($request); + + $this->assertEquals(403, $response->get_status()); + } + + /** + * Test get_items with admin. + */ + public function test_get_items_as_admin() { + wp_set_current_user($this->admin_id); + + $request = new WP_REST_Request('GET', '/mlf/v1/fonts'); + $response = rest_get_server()->dispatch($request); + + $this->assertEquals(200, $response->get_status()); + $this->assertIsArray($response->get_data()); + } + + /** + * Test get_items returns installed fonts. + */ + public function test_get_items_returns_fonts() { + wp_set_current_user($this->admin_id); + + // Create a test font + $font_id = $this->create_test_font('Test API Font', 'test-api-font'); + + $request = new WP_REST_Request('GET', '/mlf/v1/fonts'); + $response = rest_get_server()->dispatch($request); + + $this->assertEquals(200, $response->get_status()); + $data = $response->get_data(); + $this->assertCount(1, $data); + $this->assertEquals($font_id, $data[0]['id']); + $this->assertEquals('Test API Font', $data[0]['name']); + } + + /** + * Test get_item. + */ + public function test_get_item() { + wp_set_current_user($this->admin_id); + + $font_id = $this->create_test_font('Single Font', 'single-font'); + + $request = new WP_REST_Request('GET', '/mlf/v1/fonts/' . $font_id); + $response = rest_get_server()->dispatch($request); + + $this->assertEquals(200, $response->get_status()); + $data = $response->get_data(); + $this->assertEquals($font_id, $data['id']); + $this->assertEquals('Single Font', $data['name']); + } + + /** + * Test get_item not found. + */ + public function test_get_item_not_found() { + wp_set_current_user($this->admin_id); + + $request = new WP_REST_Request('GET', '/mlf/v1/fonts/99999'); + $response = rest_get_server()->dispatch($request); + + $this->assertEquals(404, $response->get_status()); + } + + /** + * Test create_item without authentication. + */ + public function test_create_item_unauthenticated() { + $request = new WP_REST_Request('POST', '/mlf/v1/fonts'); + $request->set_param('font_name', 'Open Sans'); + $request->set_param('weights', [400, 700]); + $request->set_param('styles', ['normal']); + + $response = rest_get_server()->dispatch($request); + + $this->assertEquals(401, $response->get_status()); + } + + /** + * Test create_item with subscriber. + */ + public function test_create_item_insufficient_permissions() { + wp_set_current_user($this->subscriber_id); + + $request = new WP_REST_Request('POST', '/mlf/v1/fonts'); + $request->set_param('font_name', 'Open Sans'); + $request->set_param('weights', [400, 700]); + $request->set_param('styles', ['normal']); + + $response = rest_get_server()->dispatch($request); + + $this->assertEquals(403, $response->get_status()); + } + + /** + * Test create_item with invalid font name. + */ + public function test_create_item_invalid_font_name() { + wp_set_current_user($this->admin_id); + + $request = new WP_REST_Request('POST', '/mlf/v1/fonts'); + $request->set_param('font_name', ''); + $request->set_param('weights', [400]); + $request->set_param('styles', ['normal']); + + $response = rest_get_server()->dispatch($request); + + $this->assertEquals(400, $response->get_status()); + } + + /** + * Test create_item with no weights. + */ + public function test_create_item_no_weights() { + wp_set_current_user($this->admin_id); + + $request = new WP_REST_Request('POST', '/mlf/v1/fonts'); + $request->set_param('font_name', 'Test Font'); + $request->set_param('weights', []); + $request->set_param('styles', ['normal']); + + $response = rest_get_server()->dispatch($request); + + $this->assertEquals(400, $response->get_status()); + } + + /** + * Test create_item with no styles. + */ + public function test_create_item_no_styles() { + wp_set_current_user($this->admin_id); + + $request = new WP_REST_Request('POST', '/mlf/v1/fonts'); + $request->set_param('font_name', 'Test Font'); + $request->set_param('weights', [400]); + $request->set_param('styles', []); + + $response = rest_get_server()->dispatch($request); + + $this->assertEquals(400, $response->get_status()); + } + + /** + * Test delete_item without authentication. + */ + public function test_delete_item_unauthenticated() { + $font_id = $this->create_test_font('Delete Font', 'delete-font'); + + $request = new WP_REST_Request('DELETE', '/mlf/v1/fonts/' . $font_id); + $response = rest_get_server()->dispatch($request); + + $this->assertEquals(401, $response->get_status()); + } + + /** + * Test delete_item with subscriber. + */ + public function test_delete_item_insufficient_permissions() { + $font_id = $this->create_test_font('Delete Font 2', 'delete-font-2'); + + wp_set_current_user($this->subscriber_id); + + $request = new WP_REST_Request('DELETE', '/mlf/v1/fonts/' . $font_id); + $response = rest_get_server()->dispatch($request); + + $this->assertEquals(403, $response->get_status()); + } + + /** + * Test delete_item as admin. + */ + public function test_delete_item_as_admin() { + $font_id = $this->create_test_font('Delete Font 3', 'delete-font-3'); + + wp_set_current_user($this->admin_id); + + $request = new WP_REST_Request('DELETE', '/mlf/v1/fonts/' . $font_id); + $response = rest_get_server()->dispatch($request); + + $this->assertEquals(200, $response->get_status()); + $data = $response->get_data(); + $this->assertTrue($data['deleted']); + } + + /** + * Test delete_item not found. + */ + public function test_delete_item_not_found() { + wp_set_current_user($this->admin_id); + + $request = new WP_REST_Request('DELETE', '/mlf/v1/fonts/99999'); + $response = rest_get_server()->dispatch($request); + + $this->assertEquals(404, $response->get_status()); + } + + /** + * Test delete_item for font not imported by plugin. + */ + public function test_delete_item_not_ours() { + wp_set_current_user($this->admin_id); + + // Create a font without our meta + $font_id = wp_insert_post([ + 'post_type' => 'wp_font_family', + 'post_title' => 'Theme Font', + 'post_status' => 'publish', + ]); + + $request = new WP_REST_Request('DELETE', '/mlf/v1/fonts/' . $font_id); + $response = rest_get_server()->dispatch($request); + + $this->assertEquals(403, $response->get_status()); + + // Clean up + wp_delete_post($font_id, true); + } + + /** + * Test item schema. + */ + public function test_get_item_schema() { + $request = new WP_REST_Request('OPTIONS', '/mlf/v1/fonts'); + $response = rest_get_server()->dispatch($request); + $data = $response->get_data(); + + $this->assertArrayHasKey('schema', $data); + $this->assertEquals('font', $data['schema']['title']); + } + + /** + * Test context parameter. + */ + public function test_context_param() { + // This test is inherited from WP_Test_REST_Controller_Testcase + // We just need to implement it to satisfy the abstract requirement + $this->assertTrue(true); + } + + /** + * Helper to create a test font. + * + * @param string $name Font name. + * @param string $slug Font slug. + * @return int Font ID. + */ + private function create_test_font($name, $slug) { + // Create font family post + $font_id = wp_insert_post([ + 'post_type' => 'wp_font_family', + 'post_title' => $name, + 'post_name' => $slug, + 'post_status' => 'publish', + 'post_content' => wp_json_encode([ + 'fontFamily' => $name, + 'slug' => $slug, + ]), + ]); + + // Add our meta marker + update_post_meta($font_id, '_mlf_imported', '1'); + + // Create a font face + $face_id = wp_insert_post([ + 'post_type' => 'wp_font_face', + 'post_title' => $name . ' 400 normal', + 'post_status' => 'publish', + 'post_parent' => $font_id, + 'post_content' => wp_json_encode([ + 'fontFamily' => $name, + 'fontWeight' => '400', + 'fontStyle' => 'normal', + 'src' => ['file:./fonts/' . $slug . '_normal_400.woff2'], + ]), + ]); + + // Clear cache + delete_transient('mlf_imported_fonts_list'); + + return $font_id; + } +} diff --git a/native/wordpress/maple-fonts-wp/uninstall.php b/native/wordpress/maple-fonts-wp/uninstall.php new file mode 100644 index 0000000..875b2fb --- /dev/null +++ b/native/wordpress/maple-fonts-wp/uninstall.php @@ -0,0 +1,123 @@ + 'wp_font_family', + 'posts_per_page' => $batch_size, + 'post_status' => 'any', + 'meta_key' => '_mlf_imported', + 'meta_value' => '1', + 'fields' => 'ids', // Only get IDs for memory efficiency + ]); + + // No more fonts to process + if (empty($fonts)) { + break; + } + + // Get all font faces for this batch in a single query + $all_faces = get_posts([ + 'post_type' => 'wp_font_face', + 'posts_per_page' => $batch_size * 20, // Max ~20 faces per font + 'post_status' => 'any', + 'post_parent__in' => $fonts, + ]); + + // Delete font face files and posts + foreach ($all_faces as $face) { + $settings = json_decode($face->post_content, true); + + if (isset($settings['src'])) { + // Convert file:. URL to path + $src = $settings['src']; + $src = str_replace('file:./', '', $src); + $file_path = trailingslashit($font_dir['path']) . basename($src); + + // Validate path and extension before deletion + if (mlf_uninstall_validate_font_path($file_path, $font_dir) + && pathinfo($file_path, PATHINFO_EXTENSION) === 'woff2' + && file_exists($file_path)) { + wp_delete_file($file_path); + } + } + + wp_delete_post($face->ID, true); + } + + // Delete family posts + foreach ($fonts as $font_id) { + wp_delete_post($font_id, true); + } + + $processed += count($fonts); + + // Free memory + unset($fonts, $all_faces); + + // If we got fewer than batch_size, we're done + if (count($fonts) < $batch_size) { + break; + } + } + + // Clear caches + delete_transient('wp_font_library_fonts'); + delete_transient('mlf_imported_fonts_list'); +} + +/** + * Validate that a path is within the WordPress fonts directory. + * + * @param string $path Full path to validate. + * @param array $font_dir Font directory info. + * @return bool True if path is safe, false otherwise. + */ +function mlf_uninstall_validate_font_path($path, $font_dir) { + $fonts_path = wp_normalize_path(trailingslashit($font_dir['path'])); + + // Resolve to real path (handles ../ etc) + $real_path = realpath($path); + + // If realpath fails, file doesn't exist yet - validate the directory + if ($real_path === false) { + $dir = dirname($path); + $real_dir = realpath($dir); + if ($real_dir === false) { + return false; + } + $real_path = wp_normalize_path($real_dir . '/' . basename($path)); + } else { + $real_path = wp_normalize_path($real_path); + } + + // Must be within fonts directory + return strpos($real_path, $fonts_path) === 0; +} + +// Run uninstall +mlf_uninstall();