initial commit

This commit is contained in:
rodolfomartinez 2026-01-30 22:33:40 -05:00
parent d066133bd4
commit e6f71e3706
55 changed files with 11928 additions and 0 deletions

View file

@ -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:*)"
]
}
}

View file

@ -0,0 +1,2 @@
<?php
// Silence is golden.

View file

@ -0,0 +1,191 @@
name: CI
on:
push:
branches: [main, develop]
paths:
- 'native/wordpress/maple-fonts-wp/**'
pull_request:
branches: [main, develop]
paths:
- 'native/wordpress/maple-fonts-wp/**'
defaults:
run:
working-directory: native/wordpress/maple-fonts-wp
jobs:
phpcs:
name: PHP Coding Standards
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, cs2pr
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 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

View file

@ -0,0 +1,2 @@
<?php
// Silence is golden.

View file

@ -0,0 +1,53 @@
when:
- event: [push, pull_request]
path: 'native/wordpress/maple-fonts-wp/**'
steps:
install:
image: composer:2
commands:
- cd native/wordpress/maple-fonts-wp
- composer install --prefer-dist --no-interaction
phpcs:
image: php:8.2-cli
commands:
- cd native/wordpress/maple-fonts-wp
- ./vendor/bin/phpcs --standard=phpcs.xml.dist --report=summary || true
depends_on:
- install
phpcompat:
image: php:8.2-cli
commands:
- cd native/wordpress/maple-fonts-wp
- ./vendor/bin/phpcs -p --standard=PHPCompatibilityWP --runtime-set testVersion 7.4- --extensions=php --ignore=vendor,tests . || true
depends_on:
- install
test-php74:
image: wordpressdevelop/phpunit:9-php-7.4-fpm
commands:
- cd native/wordpress/maple-fonts-wp
- composer install --prefer-dist --no-interaction
- bash bin/install-wp-tests.sh wordpress_test root root database latest true
- ./vendor/bin/phpunit
depends_on:
- install
test-php82:
image: wordpressdevelop/phpunit:9-php-8.2-fpm
commands:
- cd native/wordpress/maple-fonts-wp
- composer install --prefer-dist --no-interaction
- bash bin/install-wp-tests.sh wordpress_test root root database latest true
- ./vendor/bin/phpunit
depends_on:
- install
services:
database:
image: mysql:8.0
environment:
MYSQL_ROOT_PASSWORD: root
MYSQL_DATABASE: wordpress_test

View file

@ -0,0 +1,264 @@
# CLAUDE.md — Maple Local Fonts WordPress Plugin
## Quick Reference
| Document | When to Reference |
|----------|-------------------|
| **CLAUDE.md** (this file) | Architecture, file structure, build order |
| **SECURITY.md** | Writing ANY PHP code |
| **GOOGLE_FONTS_API.md** | Building the font downloader class |
| **WORDPRESS_COMPATIBILITY.md** | Building the font registry, FSE integration |
---
## Project Overview
Build a WordPress plugin called **Maple Local Fonts** that:
1. Imports Google Fonts to local storage (one-time download)
2. Registers them with WordPress's native Font Library API
3. Fonts appear in FSE typography dropdowns alongside theme fonts
**Key principle:** Work WITH WordPress, not around it. We're a font importer — WordPress handles everything else.
---
## Requirements
- **Minimum PHP:** 7.4
- **Minimum WordPress:** 6.5 (required for Font Library API)
- **License:** GPL-2.0-or-later
---
## User Flow
```
Admin enters "Open Sans" → selects weights (400, 700) → selects styles (normal, italic)
Plugin hits Google Fonts CSS2 API (ONE TIME)
Downloads WOFF2 files to wp-content/fonts/
Registers font via WP Font Library API
Font appears in Appearance → Editor → Styles → Typography dropdown
User applies font using standard WordPress FSE controls
GOOGLE NEVER CONTACTED AGAIN — fonts served locally
```
---
## File Structure
```
maple-local-fonts/
├── maple-local-fonts.php # Main plugin file
├── index.php # Silence is golden
├── uninstall.php # Clean removal
├── readme.txt # WordPress.org readme
├── includes/
│ ├── index.php # Silence is golden
│ ├── class-mlf-font-downloader.php
│ ├── class-mlf-font-registry.php
│ ├── class-mlf-admin-page.php
│ └── class-mlf-ajax-handler.php
├── assets/
│ ├── index.php # Silence is golden
│ ├── admin.css
│ └── admin.js
└── languages/
├── index.php # Silence is golden
└── maple-local-fonts.pot
```
---
## Class Responsibilities
### MLF_Font_Downloader
- Build Google Fonts CSS2 URL
- Fetch CSS (with correct user-agent for WOFF2)
- Parse CSS to extract font face data
- Download WOFF2 files to wp-content/fonts/
- **Reference:** GOOGLE_FONTS_API.md
### MLF_Font_Registry
- Register fonts with WP Font Library (wp_font_family, wp_font_face post types)
- Delete fonts (remove posts and files)
- List imported fonts
- **Reference:** WORDPRESS_COMPATIBILITY.md
### MLF_Admin_Page
- Render settings page under Appearance menu
- Font name input, weight checkboxes, style checkboxes
- Display installed fonts with delete buttons
- **Reference:** SECURITY.md for output escaping
### MLF_Ajax_Handler
- Handle download requests
- Handle delete requests
- **Reference:** SECURITY.md for nonce/capability checks
---
## Admin Page UI
```
┌─────────────────────────────────────────────────────────┐
│ Maple Local Fonts │
├─────────────────────────────────────────────────────────┤
│ IMPORT FROM GOOGLE FONTS │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ Font Name: [Open Sans_________________________] │ │
│ │ │ │
│ │ Weights: │ │
│ │ ☑ 400 (Regular) ☐ 300 (Light) ☐ 500 (Medium) │ │
│ │ ☑ 700 (Bold) ☐ 600 (Semi) ☐ 800 (Extra) │ │
│ │ │ │
│ │ Styles: │ │
│ │ ☑ Normal ☑ Italic │ │
│ │ │ │
│ │ Files to download: 4 │ │
│ │ [Download & Install] │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
│ INSTALLED FONTS │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ Open Sans │ │
│ │ 400 normal, 400 italic, 700 normal, 700 italic │ │
│ │ [Delete] │ │
│ ├─────────────────────────────────────────────────────┤ │
│ │ Roboto │ │
│ │ 400 normal, 500 normal, 700 normal │ │
│ │ [Delete] │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
Use Appearance → Editor → Styles → Typography to │
│ apply fonts to your site. │
└─────────────────────────────────────────────────────────┘
```
---
## Build Order
### Phase 1: Foundation
1. `maple-local-fonts.php` — Plugin header, constants, autoloader, activation hook
2. `index.php` files in all directories (silence is golden)
3. `includes/class-mlf-ajax-handler.php` — With full security checks (nonce, capability, validation)
### Phase 2: Core Functionality
4. `includes/class-mlf-font-downloader.php` — Google Fonts fetching/parsing/downloading
5. `includes/class-mlf-font-registry.php` — WP Font Library integration
### Phase 3: Admin Interface
6. `includes/class-mlf-admin-page.php` — Settings page render
7. `assets/admin.css` — Admin page styles
8. `assets/admin.js` — AJAX handling for download/delete
### Phase 4: Cleanup & Polish
9. `uninstall.php` — Clean removal of fonts and data
10. `readme.txt` — WordPress.org readme with privacy section
11. Testing against all compatibility targets
---
## Critical Reminders
### Security (see SECURITY.md)
- ABSPATH check on EVERY PHP file
- index.php in EVERY directory
- Nonce verification FIRST in every AJAX handler
- Capability check SECOND
- Input validation THIRD
- Escape ALL output
### Performance
- Zero JavaScript on frontend
- Zero CSS on frontend
- All plugin code is admin-side only (except lightweight font registration)
- Timeout handling on all external requests
- Maximum limits to prevent infinite loops
### GDPR
- No user data collected
- External requests only during admin import
- Fonts served locally after import
- Document in readme.txt
### Compatibility (see WORDPRESS_COMPATIBILITY.md)
- Declare WooCommerce HPOS compatibility
- Use wp_get_font_dir() not hardcoded paths
- Use Font Library API (post types) not theme.json filtering
- Don't touch frontend, let Global Styles handle everything
---
## Constants
```php
define('MLF_VERSION', '1.0.0');
define('MLF_PLUGIN_DIR', plugin_dir_path(__FILE__));
define('MLF_PLUGIN_URL', plugin_dir_url(__FILE__));
define('MLF_PLUGIN_BASENAME', plugin_basename(__FILE__));
// Limits
define('MLF_MAX_FONTS_PER_REQUEST', 10);
define('MLF_MAX_WEIGHTS_PER_FONT', 9);
define('MLF_REQUEST_TIMEOUT', 30);
```
---
## WordPress Hooks Used
```php
// Activation
register_activation_hook(__FILE__, 'mlf_activate');
// Admin menu
add_action('admin_menu', 'mlf_register_menu');
// Admin assets (our page only)
add_action('admin_enqueue_scripts', 'mlf_enqueue_admin_assets');
// AJAX (admin only, no nopriv)
add_action('wp_ajax_mlf_download_font', 'mlf_ajax_download');
add_action('wp_ajax_mlf_delete_font', 'mlf_ajax_delete');
// WooCommerce HPOS
add_action('before_woocommerce_init', 'mlf_declare_hpos_compatibility');
```
---
## Testing Checklist
### Functional
- [ ] Import Open Sans 400, 700 normal + italic → 4 files created
- [ ] Font appears in FSE typography dropdown
- [ ] Apply font in Global Styles → frontend shows local font
- [ ] Delete font → files and posts removed
- [ ] Re-import same font → appropriate error message
### Security
- [ ] AJAX without nonce → 403
- [ ] AJAX as subscriber → 403
- [ ] Path traversal in font name → rejected
- [ ] XSS in font name → sanitized
- [ ] Direct PHP file access → blank/exit
### Performance
- [ ] No frontend network requests from plugin
- [ ] Import 5 fonts → completes without timeout
- [ ] Admin page load < 500ms added time
### Compatibility
- [ ] WordPress 6.5, 6.6+
- [ ] Twenty Twenty-Five theme
- [ ] WooCommerce (HPOS enabled)
- [ ] Wordfence (enabled)
- [ ] LearnDash
- [ ] WPForms

View file

@ -0,0 +1,707 @@
# GOOGLE_FONTS_API.md — Google Fonts Retrieval Guide
## Overview
This document covers how to retrieve fonts from Google Fonts CSS2 API. This is a **one-time retrieval** during admin import — after download, fonts are served locally and Google is never contacted again.
---
## The Retrieval Flow
```
1. Admin enters "Open Sans" + selects weights/styles
2. Plugin constructs Google Fonts CSS2 URL
3. Plugin fetches CSS (contains @font-face rules with WOFF2 URLs)
4. Plugin parses CSS to extract WOFF2 URLs
5. Plugin downloads each WOFF2 file to wp-content/fonts/
6. Plugin registers font with WordPress Font Library
7. DONE - Google never contacted again
```
---
## Google Fonts CSS2 API
### Base URL
```
https://fonts.googleapis.com/css2
```
### URL Construction
**Pattern:**
```
https://fonts.googleapis.com/css2?family={Font+Name}:ital,wght@{variations}&display=swap
```
**Variations format:**
```
ital,wght@{italic},{weight};{italic},{weight};...
```
Where:
- `ital` = 0 (normal) or 1 (italic)
- `wght` = weight (100-900)
### Examples
**Open Sans 400 normal only:**
```
https://fonts.googleapis.com/css2?family=Open+Sans:wght@400&display=swap
```
**Open Sans 400 + 700 normal:**
```
https://fonts.googleapis.com/css2?family=Open+Sans:wght@400;700&display=swap
```
**Open Sans 400 + 700, both normal and italic:**
```
https://fonts.googleapis.com/css2?family=Open+Sans:ital,wght@0,400;0,700;1,400;1,700&display=swap
```
**Breakdown of `ital,wght@0,400;0,700;1,400;1,700`:**
- `0,400` = normal 400
- `0,700` = normal 700
- `1,400` = italic 400
- `1,700` = italic 700
### URL Builder Function
```php
/**
* Build Google Fonts CSS2 API URL.
*
* @param string $font_name Font family name (e.g., "Open Sans")
* @param array $weights Array of weights (e.g., [400, 700])
* @param array $styles Array of styles (e.g., ['normal', 'italic'])
* @return string Google Fonts CSS2 URL
*/
function mlf_build_google_fonts_url($font_name, $weights, $styles) {
// URL-encode font name (spaces become +)
$family = str_replace(' ', '+', $font_name);
// Build variations
$variations = [];
// Sort for consistent URLs
sort($weights);
sort($styles);
$has_italic = in_array('italic', $styles, true);
$has_normal = in_array('normal', $styles, true);
foreach ($weights as $weight) {
if ($has_normal) {
$variations[] = "0,{$weight}";
}
if ($has_italic) {
$variations[] = "1,{$weight}";
}
}
// 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
$variation_string = implode(';', $variations);
return "https://fonts.googleapis.com/css2?family={$family}:ital,wght@{$variation_string}&display=swap";
}
```
---
## CRITICAL: User-Agent Requirement
Google Fonts returns **different file formats** based on user-agent:
| User-Agent | Format Returned |
|------------|-----------------|
| Old browser | TTF or WOFF |
| Modern browser | WOFF2 |
| curl (no UA) | TTF |
**We need WOFF2** (smallest, best compression, modern browser support).
### Required User-Agent
Use a modern Chrome user-agent:
```php
$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';
```
### Fetch Function
```php
/**
* 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
*/
function mlf_fetch_google_css($font_name, $weights, $styles) {
$url = mlf_build_google_fonts_url($font_name, $weights, $styles);
// Validate URL
if (!mlf_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' => 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
<?php
if (!defined('ABSPATH')) {
exit;
}
class MLF_Font_Downloader {
private $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';
/**
* Download a font from Google Fonts.
*
* @param string $font_name Font family name
* @param array $weights Weights to download
* @param array $styles Styles to download
* @return array|WP_Error Download result or error
*/
public function download($font_name, $weights, $styles) {
// Validate inputs
if (empty($font_name) || !preg_match('/^[a-zA-Z0-9\s\-]+$/', $font_name)) {
return new WP_Error('invalid_name', 'Invalid font name');
}
$weights = array_intersect(array_map('absint', $weights), [100,200,300,400,500,600,700,800,900]);
if (empty($weights)) {
return new WP_Error('invalid_weights', 'No valid weights specified');
}
$styles = array_intersect($styles, ['normal', 'italic']);
if (empty($styles)) {
return new WP_Error('invalid_styles', 'No valid styles specified');
}
// Fetch CSS
$css = $this->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)

View file

@ -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
<?php
if (!defined('ABSPATH')) {
exit;
}
```
---
## Silence is Golden Files
Create `index.php` in EVERY directory:
```php
<?php
// Silence is golden.
if (!defined('ABSPATH')) {
exit;
}
```
**Required locations:**
- `/maple-local-fonts/index.php`
- `/maple-local-fonts/includes/index.php`
- `/maple-local-fonts/assets/index.php`
- `/maple-local-fonts/languages/index.php`
---
## OWASP Compliance
### A1 - Injection Prevention
**SQL Injection:**
```php
// NEVER do this
$wpdb->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 '<input value="' . esc_attr($font_name) . '">';
// URLs
echo '<a href="' . esc_url($url) . '">';
// 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 '<div>' . $_POST['font_name'] . '</div>';
// RIGHT - sanitize input, escape output
$font_name = sanitize_text_field($_POST['font_name']);
echo '<div>' . esc_html($font_name) . '</div>';
```
### 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
<?php
if (!defined('ABSPATH')) {
exit;
}
class MLF_Ajax_Handler {
public function __construct() {
add_action('wp_ajax_mlf_download_font', [$this, 'handle_download']);
add_action('wp_ajax_mlf_delete_font', [$this, 'handle_delete']);
// NEVER add wp_ajax_nopriv_ - admin only functionality
}
/**
* 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'], 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

View file

@ -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 '<div class="error"><p>Maple Local Fonts has been deactivated. It requires WordPress 6.5 or higher.</p></div>';
});
}
});
```
---
## 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

View file

@ -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;
}
}

View file

@ -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 = $('<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 = $('<span>').text('test').css({
'font-family': '"' + fontName + '", monospace',
'position': 'absolute',
'visibility': 'hidden'
}).appendTo('body');
var testWidth = testSpan.width();
var fallbackSpan = $('<span>').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(
'<p class="mlf-no-fonts">No fonts installed yet.</p>'
);
}
});
} 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);

View file

@ -0,0 +1,5 @@
<?php
// Silence is golden.
if (!defined('ABSPATH')) {
exit;
}

View file

@ -0,0 +1,39 @@
{
"name": "mapleopentech/maple-local-fonts",
"description": "Import Google Fonts to local storage for GDPR-compliant, privacy-friendly typography in WordPress.",
"type": "wordpress-plugin",
"license": "GPL-2.0-or-later",
"authors": [
{
"name": "Maple Open Technologies",
"homepage": "https://mapleopentech.org"
}
],
"require": {
"php": ">=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/"
}
}
}

View file

@ -0,0 +1,210 @@
<?php
/**
* Admin Page for Maple Local Fonts.
*
* @package Maple_Local_Fonts
*/
if (!defined('ABSPATH')) {
exit;
}
/**
* Class MLF_Admin_Page
*
* Handles the admin settings page rendering.
*/
class MLF_Admin_Page {
/**
* Available font weights.
*
* @var array
*/
private $weights = [
100 => '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();
?>
<div class="wrap mlf-wrap">
<h1><?php esc_html_e('Maple Local Fonts', 'maple-local-fonts'); ?></h1>
<div class="mlf-container">
<!-- Import Section -->
<div class="mlf-section mlf-import-section">
<h2><?php esc_html_e('Import from Google Fonts', 'maple-local-fonts'); ?></h2>
<form id="mlf-import-form" class="mlf-form">
<div class="mlf-form-row">
<label for="mlf-font-name"><?php esc_html_e('Font Name', 'maple-local-fonts'); ?></label>
<input type="text" id="mlf-font-name" name="font_name" placeholder="<?php esc_attr_e('e.g., Open Sans', 'maple-local-fonts'); ?>" required />
<p class="description"><?php esc_html_e('Enter the exact font name as it appears on Google Fonts.', 'maple-local-fonts'); ?></p>
</div>
<div class="mlf-form-row">
<label><?php esc_html_e('Weights', 'maple-local-fonts'); ?></label>
<div class="mlf-checkbox-grid">
<?php foreach ($this->weights as $weight => $label) : ?>
<label class="mlf-checkbox-label">
<input type="checkbox" name="weights[]" value="<?php echo esc_attr($weight); ?>" <?php checked(in_array($weight, [400, 700], true)); ?> />
<span><?php echo esc_html($weight); ?> (<?php echo esc_html($label); ?>)</span>
</label>
<?php endforeach; ?>
</div>
</div>
<div class="mlf-form-row">
<label><?php esc_html_e('Styles', 'maple-local-fonts'); ?></label>
<div class="mlf-checkbox-grid mlf-checkbox-grid-small">
<label class="mlf-checkbox-label">
<input type="checkbox" name="styles[]" value="normal" checked />
<span><?php esc_html_e('Normal', 'maple-local-fonts'); ?></span>
</label>
<label class="mlf-checkbox-label">
<input type="checkbox" name="styles[]" value="italic" />
<span><?php esc_html_e('Italic', 'maple-local-fonts'); ?></span>
</label>
</div>
</div>
<div class="mlf-form-row mlf-form-row-info">
<span class="mlf-file-count">
<?php esc_html_e('Files to download:', 'maple-local-fonts'); ?>
<strong id="mlf-file-count">2</strong>
</span>
</div>
<!-- Font Preview Section -->
<div class="mlf-form-row mlf-preview-section" id="mlf-preview-section" style="display: none;">
<label><?php esc_html_e('Preview', 'maple-local-fonts'); ?></label>
<div class="mlf-preview-box" id="mlf-preview-box">
<div class="mlf-preview-text" id="mlf-preview-text">
<span class="mlf-preview-sample mlf-preview-heading"><?php esc_html_e('The quick brown fox jumps over the lazy dog', 'maple-local-fonts'); ?></span>
<span class="mlf-preview-sample mlf-preview-paragraph"><?php esc_html_e('ABCDEFGHIJKLMNOPQRSTUVWXYZ abcdefghijklmnopqrstuvwxyz 0123456789', 'maple-local-fonts'); ?></span>
</div>
<div class="mlf-preview-loading" id="mlf-preview-loading" style="display: none;">
<span class="spinner is-active"></span>
<span><?php esc_html_e('Loading preview...', 'maple-local-fonts'); ?></span>
</div>
<div class="mlf-preview-error" id="mlf-preview-error" style="display: none;">
<?php esc_html_e('Could not load font preview. The font may not exist on Google Fonts.', 'maple-local-fonts'); ?>
</div>
</div>
<p class="description"><?php esc_html_e('Preview is loaded directly from Google Fonts. After installation, the font will be served locally.', 'maple-local-fonts'); ?></p>
</div>
<div class="mlf-form-row mlf-form-row-submit">
<button type="submit" class="button button-primary" id="mlf-download-btn">
<?php esc_html_e('Download & Install', 'maple-local-fonts'); ?>
</button>
<span class="spinner" id="mlf-spinner"></span>
</div>
<div id="mlf-message" class="mlf-message" style="display: none;"></div>
</form>
</div>
<!-- Installed Fonts Section -->
<div class="mlf-section mlf-installed-section">
<h2><?php esc_html_e('Installed Fonts', 'maple-local-fonts'); ?></h2>
<?php if (empty($installed_fonts)) : ?>
<p class="mlf-no-fonts"><?php esc_html_e('No fonts installed yet.', 'maple-local-fonts'); ?></p>
<?php else : ?>
<div id="mlf-font-list" class="mlf-font-list">
<?php foreach ($installed_fonts as $font) : ?>
<div class="mlf-font-item" data-font-id="<?php echo esc_attr($font['id']); ?>">
<div class="mlf-font-info">
<h3 class="mlf-font-name"><?php echo esc_html($font['name']); ?></h3>
<p class="mlf-font-variants">
<?php
$variant_strings = [];
foreach ($font['variants'] as $variant) {
$variant_strings[] = sprintf('%s %s', $variant['weight'], $variant['style']);
}
echo esc_html(implode(', ', $variant_strings));
?>
</p>
</div>
<div class="mlf-font-actions">
<button type="button" class="button mlf-delete-btn" data-font-id="<?php echo esc_attr($font['id']); ?>">
<?php esc_html_e('Delete', 'maple-local-fonts'); ?>
</button>
</div>
</div>
<?php endforeach; ?>
</div>
<?php endif; ?>
</div>
<!-- Info Section -->
<div class="mlf-section mlf-info-section">
<?php if (wp_is_block_theme()) : ?>
<div class="mlf-info-box">
<span class="dashicons dashicons-info"></span>
<p>
<?php
printf(
/* translators: %s: link to WordPress Editor */
esc_html__('Use %s to apply fonts to your site.', 'maple-local-fonts'),
'<a href="' . esc_url(admin_url('site-editor.php?path=%2Fwp_global_styles')) . '">' . esc_html__('Appearance → Editor → Styles → Typography', 'maple-local-fonts') . '</a>'
);
?>
</p>
</div>
<?php else : ?>
<div class="mlf-info-box mlf-info-box-classic">
<span class="dashicons dashicons-info"></span>
<div class="mlf-classic-theme-info">
<p><strong><?php esc_html_e('Classic Theme Detected', 'maple-local-fonts'); ?></strong></p>
<p><?php esc_html_e('Your theme does not support the Full Site Editor. To use imported fonts, add custom CSS to your theme:', 'maple-local-fonts'); ?></p>
<pre class="mlf-code-example">body {
font-family: "Open Sans", sans-serif;
}
h1, h2, h3, h4, h5, h6 {
font-family: "Open Sans", sans-serif;
}</pre>
<p class="description">
<?php
printf(
/* translators: %s: link to Customizer */
esc_html__('Add this CSS in %s or your theme\'s style.css file.', 'maple-local-fonts'),
'<a href="' . esc_url(admin_url('customize.php')) . '">' . esc_html__('Appearance → Customize → Additional CSS', 'maple-local-fonts') . '</a>'
);
?>
</p>
</div>
</div>
<?php endif; ?>
</div>
</div>
</div>
<?php
}
}

View file

@ -0,0 +1,227 @@
<?php
/**
* AJAX Handler for Maple Local Fonts.
*
* @package Maple_Local_Fonts
*/
if (!defined('ABSPATH')) {
exit;
}
/**
* Class MLF_Ajax_Handler
*
* Handles all AJAX requests for font download and deletion.
*/
class MLF_Ajax_Handler {
/**
* Rate limiter instance.
*
* @var MLF_Rate_Limiter
*/
private $rate_limiter;
/**
* Constructor.
*/
public function __construct() {
add_action('wp_ajax_mlf_download_font', [$this, 'handle_download']);
add_action('wp_ajax_mlf_delete_font', [$this, 'handle_delete']);
// NEVER add wp_ajax_nopriv_ - admin only functionality
// Initialize rate limiter: 10 requests per minute
$this->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');
}
}

View file

@ -0,0 +1,504 @@
<?php
/**
* Font Downloader for Maple Local Fonts.
*
* @package Maple_Local_Fonts
*/
if (!defined('ABSPATH')) {
exit;
}
/**
* Class MLF_Font_Downloader
*
* Handles downloading fonts from Google Fonts CSS2 API.
*/
class MLF_Font_Downloader {
/**
* User agent to send with requests (needed to get WOFF2 format).
*
* @var string
*/
private $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';
/**
* Download a font from Google Fonts.
*
* @param string $font_name Font family name.
* @param array $weights Weights to download.
* @param array $styles Styles to download.
* @return array|WP_Error Download result or error.
*/
public function download($font_name, $weights, $styles) {
// Validate inputs
if (empty($font_name) || !preg_match('/^[a-zA-Z0-9\s\-]+$/', $font_name)) {
return new WP_Error('invalid_name', 'Invalid font name');
}
$weights = array_intersect(array_map('absint', $weights), [100, 200, 300, 400, 500, 600, 700, 800, 900]);
if (empty($weights)) {
return new WP_Error('invalid_weights', 'No valid weights specified');
}
$styles = array_intersect($styles, ['normal', 'italic']);
if (empty($styles)) {
return new WP_Error('invalid_styles', 'No valid styles specified');
}
// Fetch CSS from Google
$css = $this->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;
}
}

View file

@ -0,0 +1,287 @@
<?php
/**
* Font Registry for Maple Local Fonts.
*
* @package Maple_Local_Fonts
*/
if (!defined('ABSPATH')) {
exit;
}
/**
* Class MLF_Font_Registry
*
* Handles registering fonts with WordPress Font Library API.
*/
class MLF_Font_Registry {
/**
* 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.
*/
public function register_font($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');
}
// 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;
}
}

View file

@ -0,0 +1,188 @@
<?php
/**
* Rate Limiter for Maple Local Fonts.
*
* @package Maple_Local_Fonts
*/
if (!defined('ABSPATH')) {
exit;
}
/**
* Class MLF_Rate_Limiter
*
* Provides rate limiting functionality to prevent abuse of AJAX endpoints.
*/
class MLF_Rate_Limiter {
/**
* Default rate limit (requests per window).
*
* @var int
*/
private $limit = 10;
/**
* Default time window in seconds.
*
* @var int
*/
private $window = 60;
/**
* Constructor.
*
* @param int $limit Maximum requests allowed per window.
* @param int $window Time window in seconds.
*/
public function __construct($limit = 10, $window = 60) {
$this->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);
}
}

View file

@ -0,0 +1,525 @@
<?php
/**
* REST API Controller for Maple Local Fonts.
*
* @package Maple_Local_Fonts
*/
if (!defined('ABSPATH')) {
exit;
}
/**
* Class MLF_Rest_Controller
*
* Provides REST API endpoints for font management.
*/
class MLF_Rest_Controller extends WP_REST_Controller {
/**
* Namespace for the API.
*
* @var string
*/
protected $namespace = 'mlf/v1';
/**
* Resource name.
*
* @var string
*/
protected $rest_base = 'fonts';
/**
* Rate limiter instance.
*
* @var MLF_Rate_Limiter
*/
private $rate_limiter;
/**
* Constructor.
*/
public function __construct() {
$this->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<id>[\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<id>[\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',
],
],
],
],
],
];
}
}

View file

@ -0,0 +1,5 @@
<?php
// Silence is golden.
if (!defined('ABSPATH')) {
exit;
}

View file

@ -0,0 +1,5 @@
<?php
// Silence is golden.
if (!defined('ABSPATH')) {
exit;
}

View file

@ -0,0 +1,5 @@
<?php
// Silence is golden.
if (!defined('ABSPATH')) {
exit;
}

View file

@ -0,0 +1,222 @@
# Translation of Maple Local Fonts in Arabic
# 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: Arabic\n"
"Language: ar\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=6; plural=(n==0 ? 0 : n==1 ? 1 : n==2 ? 2 : n%100>=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 "المظهر ← المحرر ← الأنماط ← الطباعة"

View file

@ -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"

View file

@ -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"

View file

@ -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"

View file

@ -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"

View file

@ -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"

View file

@ -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"

View file

@ -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"

View file

@ -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"

View file

@ -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"

View file

@ -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 "प्रकटन → संपादक → शैलियां → टाइपोग्राफी"

View file

@ -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"

View file

@ -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"

View file

@ -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 "外観 → エディター → スタイル → タイポグラフィ"

View file

@ -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 "외모 → 편집기 → 스타일 → 타이포그래피"

View file

@ -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"

View file

@ -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"

View file

@ -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"

View file

@ -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 "Внешний вид → Редактор → Стили → Типографика"

View file

@ -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"

View file

@ -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 "外观 → 编辑器 → 样式 → 排版"

View file

@ -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 "外觀 → 編輯器 → 樣式 → 排版"

View file

@ -0,0 +1,216 @@
<?php
/**
* Plugin Name: Maple Local Fonts
* Plugin URI: https://mapleopentech.org/plugins/maple-local-fonts
* Description: Import Google Fonts to local storage and register them with WordPress Font Library for GDPR-compliant, privacy-friendly typography.
* Version: 1.0.0
* Requires at least: 6.5
* Requires PHP: 7.4
* Author: Maple Open Technologies
* Author URI: https://mapleopentech.org
* License: GPL-2.0-or-later
* License URI: https://www.gnu.org/licenses/gpl-2.0.html
* Text Domain: maple-local-fonts
* Domain Path: /languages
*/
if (!defined('ABSPATH')) {
exit;
}
// Plugin constants
define('MLF_VERSION', '1.0.0');
define('MLF_PLUGIN_DIR', plugin_dir_path(__FILE__));
define('MLF_PLUGIN_URL', plugin_dir_url(__FILE__));
define('MLF_PLUGIN_BASENAME', plugin_basename(__FILE__));
// Limits
define('MLF_MAX_FONTS_PER_REQUEST', 10);
define('MLF_MAX_WEIGHTS_PER_FONT', 9);
define('MLF_REQUEST_TIMEOUT', 30);
define('MLF_MAX_CSS_SIZE', 512 * 1024); // 512KB max CSS response
define('MLF_MAX_FONT_FILE_SIZE', 5 * 1024 * 1024); // 5MB max font file
define('MLF_MAX_FONT_FACES', 20); // Max font faces per import (9 weights × 2 styles + buffer)
/**
* Check WordPress version on activation.
*/
function mlf_activate() {
if (version_compare(get_bloginfo('version'), '6.5', '<')) {
deactivate_plugins(plugin_basename(__FILE__));
wp_die(
esc_html__('Maple Local Fonts requires WordPress 6.5 or higher for Font Library support.', 'maple-local-fonts'),
esc_html__('Plugin Activation Error', 'maple-local-fonts'),
['back_link' => 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 '<div class="error"><p>';
esc_html_e('Maple Local Fonts has been deactivated. It requires WordPress 6.5 or higher.', 'maple-local-fonts');
echo '</p></div>';
}
/**
* 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');

View file

@ -0,0 +1,61 @@
<?xml version="1.0"?>
<ruleset name="Maple Local Fonts">
<description>PHP Coding Standards for Maple Local Fonts WordPress Plugin</description>
<!-- Scan these files -->
<file>.</file>
<!-- Exclude these files/directories -->
<exclude-pattern>/vendor/*</exclude-pattern>
<exclude-pattern>/tests/*</exclude-pattern>
<exclude-pattern>/bin/*</exclude-pattern>
<exclude-pattern>/build/*</exclude-pattern>
<exclude-pattern>/node_modules/*</exclude-pattern>
<exclude-pattern>*.min.js</exclude-pattern>
<exclude-pattern>*.min.css</exclude-pattern>
<!-- Show progress and sniff codes -->
<arg value="ps"/>
<arg name="colors"/>
<!-- Use WordPress Coding Standards -->
<rule ref="WordPress">
<!-- Allow short array syntax -->
<exclude name="Generic.Arrays.DisallowShortArraySyntax"/>
<!-- Allow file names that don't match class names (for index.php files) -->
<exclude name="WordPress.Files.FileName.InvalidClassFileName"/>
<exclude name="WordPress.Files.FileName.NotHyphenatedLowercase"/>
</rule>
<!-- Use WordPress Extra Standards -->
<rule ref="WordPress-Extra">
<exclude name="WordPress.Files.FileName"/>
</rule>
<!-- Check for PHP cross-version compatibility -->
<config name="testVersion" value="7.4-"/>
<rule ref="PHPCompatibilityWP"/>
<!-- WordPress-specific settings -->
<config name="minimum_supported_wp_version" value="6.5"/>
<!-- Text domain for i18n -->
<rule ref="WordPress.WP.I18n">
<properties>
<property name="text_domain" type="array">
<element value="maple-local-fonts"/>
</property>
</properties>
</rule>
<!-- Prefix all globals -->
<rule ref="WordPress.NamingConventions.PrefixAllGlobals">
<properties>
<property name="prefixes" type="array">
<element value="mlf"/>
<element value="MLF"/>
<element value="maple_local_fonts"/>
</property>
</properties>
</rule>
</ruleset>

View file

@ -0,0 +1,27 @@
<?xml version="1.0"?>
<phpunit
bootstrap="tests/bootstrap.php"
backupGlobals="false"
colors="true"
convertErrorsToExceptions="true"
convertNoticesToExceptions="true"
convertWarningsToExceptions="true"
>
<testsuites>
<testsuite name="Maple Local Fonts Test Suite">
<directory suffix=".php">./tests/</directory>
</testsuite>
</testsuites>
<filter>
<whitelist processUncoveredFilesFromWhitelist="true">
<directory suffix=".php">./includes/</directory>
<file>./maple-local-fonts.php</file>
</whitelist>
</filter>
<php>
<const name="WP_TESTS_DOMAIN" value="example.org"/>
<const name="WP_TESTS_EMAIL" value="admin@example.org"/>
<const name="WP_TESTS_TITLE" value="Test Blog"/>
<const name="WP_PHP_BINARY" value="php"/>
</php>
</phpunit>

View file

@ -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.

View file

@ -0,0 +1,40 @@
<?php
/**
* PHPUnit bootstrap file for Maple Local Fonts.
*
* @package Maple_Local_Fonts
*/
// Composer autoloader.
$autoload = dirname(__DIR__) . '/vendor/autoload.php';
if (file_exists($autoload)) {
require_once $autoload;
}
// Get the tests directory.
$_tests_dir = getenv('WP_TESTS_DIR');
// Try to find the WP test suite.
if (!$_tests_dir) {
$_tests_dir = rtrim(sys_get_temp_dir(), '/\\') . '/wordpress-tests-lib';
}
// Check if the test suite exists.
if (!file_exists($_tests_dir . '/includes/functions.php')) {
echo "Could not find $_tests_dir/includes/functions.php, have you run bin/install-wp-tests.sh?" . PHP_EOL;
exit(1);
}
// Give access to tests_add_filter() function.
require_once $_tests_dir . '/includes/functions.php';
/**
* Manually load the plugin being tested.
*/
function _manually_load_plugin() {
require dirname(__DIR__) . '/maple-local-fonts.php';
}
tests_add_filter('muplugins_loaded', '_manually_load_plugin');
// Start up the WP testing environment.
require $_tests_dir . '/includes/bootstrap.php';

View file

@ -0,0 +1,2 @@
<?php
// Silence is golden.

View file

@ -0,0 +1,426 @@
<?php
/**
* Tests for MLF_Ajax_Handler class.
*
* @package Maple_Local_Fonts
*/
/**
* Class Test_MLF_Ajax_Handler
*
* @covers MLF_Ajax_Handler
*/
class Test_MLF_Ajax_Handler extends WP_Ajax_UnitTestCase {
/**
* Admin user ID.
*
* @var int
*/
private $admin_id;
/**
* Subscriber user ID.
*
* @var int
*/
private $subscriber_id;
/**
* Set up test fixtures.
*/
public function set_up() {
parent::set_up();
// Create users
$this->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'] = '<script>alert("xss")</script>';
$_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;
}
}

View file

@ -0,0 +1,303 @@
<?php
/**
* Tests for MLF_Font_Registry class.
*
* @package Maple_Local_Fonts
*/
/**
* Class Test_MLF_Font_Registry
*
* @covers MLF_Font_Registry
*/
class Test_MLF_Font_Registry extends WP_UnitTestCase {
/**
* Font registry instance.
*
* @var MLF_Font_Registry
*/
private $registry;
/**
* Set up test fixtures.
*/
public function set_up() {
parent::set_up();
$this->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']);
}
}

View file

@ -0,0 +1,177 @@
<?php
/**
* Tests for MLF_Rate_Limiter class.
*
* @package Maple_Local_Fonts
*/
/**
* Class Test_MLF_Rate_Limiter
*
* @covers MLF_Rate_Limiter
*/
class Test_MLF_Rate_Limiter extends WP_UnitTestCase {
/**
* Rate limiter instance.
*
* @var MLF_Rate_Limiter
*/
private $rate_limiter;
/**
* Set up test fixtures.
*/
public function set_up() {
parent::set_up();
$this->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');
}
}

View file

@ -0,0 +1,387 @@
<?php
/**
* Tests for MLF_Rest_Controller class.
*
* @package Maple_Local_Fonts
*/
/**
* Class Test_MLF_Rest_Controller
*
* @covers MLF_Rest_Controller
*/
class Test_MLF_Rest_Controller extends WP_Test_REST_Controller_Testcase {
/**
* REST controller instance.
*
* @var MLF_Rest_Controller
*/
private $controller;
/**
* Admin user ID.
*
* @var int
*/
private $admin_id;
/**
* Subscriber user ID.
*
* @var int
*/
private $subscriber_id;
/**
* Set up test fixtures.
*/
public function set_up() {
parent::set_up();
// Register REST routes
$this->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<id>[\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', '<script>alert("xss")</script>');
$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;
}
}

View file

@ -0,0 +1,123 @@
<?php
/**
* Uninstall script for Maple Local Fonts.
*
* This file is executed when the plugin is deleted from WordPress.
* It removes all fonts imported by this plugin and cleans up any data.
*
* @package Maple_Local_Fonts
*/
// If uninstall not called from WordPress, exit.
if (!defined('WP_UNINSTALL_PLUGIN')) {
exit;
}
/**
* Clean up all plugin data on uninstall.
*
* Uses batched processing to prevent memory exhaustion on sites with many fonts.
*/
function mlf_uninstall() {
$font_dir = wp_get_font_dir();
$batch_size = 50; // Process 50 fonts at a time to prevent memory issues
$processed = 0;
$max_iterations = 100; // Safety limit: max 5000 fonts (100 × 50)
// Process fonts in batches
for ($i = 0; $i < $max_iterations; $i++) {
$fonts = get_posts([
'post_type' => '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();