diff --git a/.architecture/README.md b/.architecture/README.md new file mode 100644 index 000000000..d8f1aa140 --- /dev/null +++ b/.architecture/README.md @@ -0,0 +1,173 @@ +# JEO Plugin — Architecture for AI Agents + +> **Navigation index** — consult the files below based on your current task. + +## Quick Reference + +| Task | File | +|------|------| +| Create/modify Gutenberg blocks | [`blocks/README.md`](blocks/README.md) | +| Work with maps (CPT `map`) | [`maps/README.md`](maps/README.md) | +| Composed Mapbox styles (style composer) | [`composed-styles/README.md`](composed-styles/README.md) | +| Work with layers | [`layers/README.md`](layers/README.md) | +| Geocoding, post geolocation | [`geocoding/README.md`](geocoding/README.md) | +| Storymap (scrollytelling) | [`storymap/README.md`](storymap/README.md) | +| Discovery feature | [`discovery/README.md`](discovery/README.md) | +| Stories Near You block | [`stories-near-you/README.md`](stories-near-you/README.md) | +| AI integration (georef, RAG, bulk, minilayer) | [`ai/README.md`](ai/README.md) | +| Minilayer (AI-generated layers) | [`minilayer/README.md`](minilayer/README.md) | +| AI Context (editorial suggestions) | [`context/README.md`](context/README.md) | +| Minimap (AI-assisted map block) | [`minimap/README.md`](minimap/README.md) | +| REST API endpoints | [`rest-api/README.md`](rest-api/README.md) | +| Settings, admin pages | [`settings/README.md`](settings/README.md) | +| Frontend: MapLibre/Mapbox, React, iframe | [`frontend/README.md`](frontend/README.md) | +| Templates PHP/EJS, shortcodes, embeds | [`templates/README.md`](templates/README.md) | +| Build (webpack), scripts, CI | [`build/README.md`](build/README.md) | +| Deploy to WordPress.org | [`deployment/README.md`](deployment/README.md) | + +## Project Overview + +``` +jeo-plugin/ +├── src/ # WordPress plugin (deployed as-is) +│ ├── jeo.php # Main entry point (PHP 8.2+) +│ ├── includes/ # Backend PHP +│ │ ├── class-jeo.php # Central orchestrator (Singleton) +│ │ ├── loaders.php # PSR-0 autoloader + global functions +│ │ ├── maps/ # CPT `map` +│ │ ├── layers/ # CPT `map-layer` +│ │ ├── layer-types/ # Layer types + JS renderers +│ │ ├── legend-types/ # Legend types + JS renderers +│ │ ├── geocode/ # Geocoders (Nominatim, Mapbox) +│ │ ├── storymap/ # CPT `storymap` +│ │ ├── ai/ # AI: georef, RAG, bulk, providers +│ │ ├── settings/ # Settings page +│ │ ├── sidebars/ # Gutenberg sidebars (assets) +│ │ ├── menu/ # Admin menu +│ │ ├── admin/ # Dashboard + Welcome pages +│ │ ├── cli/ # WP-CLI commands +│ │ └── traits/ # Singleton, Rest_Validate_Meta +│ ├── js/ +│ │ ├── src/ # JS/React source +│ │ │ ├── lib/ # MapLibre/Mapbox abstraction +│ │ │ ├── jeo-map/ # Frontend map rendering +│ │ │ ├── map-blocks/ # Gutenberg blocks +│ │ │ ├── layers-sidebar/ # Layer editor sidebar +│ │ │ ├── maps-sidebar/ # Map editor sidebar +│ │ │ ├── posts-sidebar/ # Geolocation sidebar +│ │ │ ├── posts-selector/ # Related posts selector +│ │ │ ├── jeo-storymap/ # Storymap display +│ │ │ ├── discovery/ # Discovery app +│ │ │ ├── icons/ # SVG/PNG assets +│ │ │ └── shared/ # Hooks, utils, shared components (incl. composed-style-*.js) +│ │ └── build/ # Build output (webpack) +│ ├── css/ # SCSS +│ ├── templates/ # PHP + EJS templates +│ └── languages/ # .pot / translations +├── scripts/ # Build and CI scripts +├── docs/ # Documentation (VitePress → site/docs/) +├── landing-page/ # Landing page (React + Vite → site/) +└── .github/workflows/ # CI/CD +``` + +## Stack + +| Layer | Technology | +|-------|-----------| +| Backend | PHP 8.2+, WordPress 6.x | +| Frontend | React 18, WordPress Element, Gutenberg Data | +| Maps | MapLibre GL JS (default) / Mapbox GL JS (optional) | +| Build | Webpack 5, @wordpress/scripts | +| Lint | PHPCS (WPCS), ESLint (via wp-scripts) | +| Tests | Jest (JS), PHPUnit (PHP) | +| AI | NeuronAI (10 providers: Gemini, OpenAI, DeepSeek, etc.) | + +## Key Conventions + +- **PHP**: PSR-0 autoloading (`Jeo\ClassName` → `class-class-name.php`), WPCS coding standards +- **Singleton**: All main classes use `Jeo\Singleton` trait +- **Global accessors**: `jeo()`, `jeo_maps()`, `jeo_layers()`, `jeo_settings()`, etc. +- **Meta REST**: `_related_point` for geolocation, registered with full REST schema +- **Iframe compat**: Extensive patching for Block API v3 (Gutenberg iframe editor) +- **Composed Mapbox styles**: `mapbox`-type layers are merged into a single cached Mapbox Style JSON by `Map_Style_Composer`, rendered natively by MapLibre (legacy Static Tiles overlay removed in 3.1.0). See [`composed-styles/README.md`](composed-styles/README.md). +- **JS entries**: Each webpack entry point is independent; `dependOn` for shared chunks + +## Architecture Diagram (C4 Container) + +```mermaid +graph TB + User[User/Editor] --> WP[WordPress] + WP --> JEO[JEO Plugin PHP] + JEO --> DB[(WordPress DB)] + JEO --> REST[REST API /jeo/v1/] + JEO --> MSC[Map Style Composer] + MSC --> |cached style.json/manifest.json| CACHE[(Uploads Cache)] + REST --> GB[Gutenberg Blocks/Sidebars] + GB --> ML[MapLibre GL / Mapbox GL] + ML --> MSC + JEO --> AI[AI Providers] + JEO --> GEO[Geocoding APIs] + JEO --> MCP[Mapbox DevKit MCP] + AI --> RAG[RAG Vector Store] + ML --> Tiles[Tile Servers] + MSC --> MBX[Mapbox Styles API] + + subgraph Frontend JS + GB + ML + end + + subgraph External APIs + AI + GEO + Tiles + MBX + end +``` + +## Data Model + +```mermaid +erDiagram + MAP ||--o{ MAP_LAYER : "has layers" + MAP ||--o{ POST : "related posts" + POST ||--o{ RELATED_POINT : "geolocated at" + STORYMAP ||--o{ CHAPTER : "has chapters" + STORYMAP ||--o{ MAP_LAYER : "chapter layers" + MAP_LAYER ||--|| LAYER_TYPE : "type of" + MAP_LAYER ||--|| LEGEND_TYPE : "displayed as" + + MAP { + int ID PK + float center_lat + float center_lon + int initial_zoom + json layers + json pan_limits + json related_posts + string composed_style_hash + json composed_style_warnings + } + MAP_LAYER { + int ID PK + string type + json layer_type_options + string source_url + string legend_type + json legend_type_options + } + POST { + int ID PK + } + RELATED_POINT { + float lat + float lon + string city + string region + string country + int relevance + } + STORYMAP { + int ID PK + } +``` diff --git a/.architecture/ai/README.md b/.architecture/ai/README.md new file mode 100644 index 000000000..390613f12 --- /dev/null +++ b/.architecture/ai/README.md @@ -0,0 +1,310 @@ +# AI Integration + +## Key Files + +| File | Role | +|------|------| +| `src/includes/class-ai-handler.php` | Central orchestrator, 12 REST routes | +| `src/includes/class-ai-adapter.php` | Abstract base class for adapters | +| `src/includes/ai/class-neuron-adapter.php` | Universal adapter → NeuronAI | +| `src/includes/ai/class-neuron-agent.php` | NeuronAI agent (chat + tokens) | +| `src/includes/ai/class-neuron-factory.php` | Provider factory (10 providers) | +| `src/includes/ai/class-ai-logger.php` | Cost tracking (CPT `jeo-ai-log`) | +| `src/includes/ai/class-ai-settings.php` | AI settings page | +| `src/includes/ai/class-bulk-processor.php` | Batch geolocation (WP-Cron) | +| `src/includes/ai/class-rag-agent.php` | RAG pipeline (FileVectorStore), accepts store name | +| `src/includes/ai/class-rag-worker.php` | Unified background worker (posts + layers pipelines) | +| `src/includes/ai/class-rag-backup.php` | Vector store backup/restore | +| `src/includes/ai/class-rag-pipeline-config.php` | Pipeline configuration value object | +| `src/includes/ai/class-wp-post-data-loader.php` | Converts WP_Post → NeuronAI Document | +| `src/includes/ai/class-layer-data-loader.php` | Converts map-layer → NeuronAI Document | +| `src/includes/ai/class-color-describer.php` | HSL color analysis for legend embeddings | +| `src/includes/ai/class-minilayer-agent.php` | Minilayer agent factory (Assistant with native MCP config) | +| `src/includes/ai/class-minilayer-handler.php` | Minilayer REST endpoint, delegates to `Minilayer_Service` | +| `src/includes/ai/class-minilayer-service.php` | Shared service — AI style generation, JSON parsing, layer CPT creation (used by handler and `Generate_Layer_Tool`) | +| `src/includes/ai/class-generate-layer-tool.php` | NeuronAI tool for minimap agent — generates custom Mapbox styles (conditional on Mapbox key) | +| `src/includes/ai/class-minimap-agent.php` | Minimap agent factory (Assistant::configure with sub-agents, tools, structured output) | +| `src/includes/ai/class-jeo-ai-factory.php` | Unified Assistant factory — provider, logger, storage, tools, MCP | +| `src/includes/ai/class-tool-registry.php` | Central registry for all NeuronAI tools | +| `src/includes/ai/class-ai-rest-permissions.php` | Reusable REST permission callbacks | +| `src/includes/ai/class-system-prompt-builder.php` | Shared prompt construction (calibration, schema blocks, sanitization) | +| `src/includes/ai/class-georeferencing-conversation.php` | Multi-turn georeferencing session manager (ConversationStore) | +| `src/includes/ai/class-minimap-output.php` | Structured output DTO (layers, base_layer, center, zoom, pins, messages) | +| `src/includes/ai/class-search-layers-tool.php` | Agent tool — wraps RAG_Worker::find_matching_layers() | +| `src/includes/ai/class-geocode-tool.php` | Agent tool — active geocoder → Mapbox fallback → defaults | +| `src/includes/ai/class-get-post-content-tool.php` | Agent tool — post content + _related_point meta for sub-agent | +| `src/includes/ai/class-retrieve-knowledge-tool.php` | Agent tool — semantic retrieval from `jeo_knowledge` vector store | +| `src/includes/ai/class-context-agent.php` | Context Assistant agent factory (Assistant::configure with retrieve_knowledge tool, sub-agents, structured output). Uses custom `ai_context_prompt` from settings when available. Includes `critical_prompt_rules()` and `engineer_custom_prompt()` for the prompt engineering assistant | +| `src/includes/ai/class-context-handler.php` | Context Assistant REST endpoints (setup, chat, state, clear, engineer-prompt) | +| `src/includes/ai/class-context-generation-output.php` | Structured output DTO for suggested paragraphs and references | +| `src/includes/ai/class-wp-storage.php` | StorageInterface adapter for post_meta / user_meta | +| `src/includes/ai/class-wp-option-storage.php` | StorageInterface adapter for wp_options (global learning storage) | +| `src/includes/ai/data/*.json` | Brazilian geographic dictionaries | +| `src/includes/cli/class-ai-cli.php` | WP-CLI `wp jeo ai vectorize` | + +## Supported Providers (10) + +| Provider | Chat | Embedding | Factory Method | +|----------|------|-----------|----------------| +| Gemini | Yes | Yes | `Neuron_Factory::get_chat_provider()` | +| OpenAI | Yes | Yes | `Neuron_Factory::get_chat_provider()` | +| DeepSeek | Yes | — | `Neuron_Factory::get_chat_provider()` | +| Anthropic | Yes | — | `Neuron_Factory::get_chat_provider()` | +| Ollama | Yes | Yes | `Neuron_Factory::get_chat_provider()` | +| Mistral | Yes | — | `Neuron_Factory::get_chat_provider()` | +| ZAI | Yes | — | `Neuron_Factory::get_chat_provider()` | +| HuggingFace | Yes | — | `Neuron_Factory::get_chat_provider()` | +| Grok | Yes | — | `Neuron_Factory::get_chat_provider()` | +| Cohere | Yes | — | `Neuron_Factory::get_chat_provider()` | + +## REST Routes (12+) + +| Route | Method | Description | +|-------|--------|-------------| +| `/jeo/v1/ai-georeference` | POST | Georeference post content | +| `/jeo/v1/ai-chat-prompt-generator` | POST | Generate chat prompt | +| `/jeo/v1/ai-validate-prompt` | POST | Validate prompt | +| `/jeo/v1/ai-test-key` | POST | Test provider API key | +| `/jeo/v1/ai-get-models` | GET | List provider models | +| `/jeo/v1/ai-test-embedding` | POST | Test embedding | +| `/jeo/v1/ai-test-retrieval` | POST | Test RAG retrieval | +| `/jeo/v1/ai-backup-store` | POST | Backup vector store | +| `/jeo/v1/ai-list-backups` | GET | List backups | +| `/jeo/v1/ai-delete-backup` | DELETE | Delete backup | +| `/jeo/v1/ai-clear-store` | POST | Clear vector store | +| `/jeo/v1/bulk-ai-run` | POST | Start batch geolocation | +| `/jeo/v1/bulk-ai-clear-batch` | POST | Clear batch | +| `/jeo/v1/bulk-ai-clear-all` | POST | Clear all | +| `/jeo/v1/bulk-ai-clear-logs` | POST | Clear logs | +| `/jeo/v1/bulk-ai-preview-approval` | POST | Preview batch approval | +| `/jeo/v1/ai-rag-run-manual` | POST | Manual vectorization trigger (posts) | +| `/jeo/v1/ai-layer-rag-run-manual` | POST | Manual vectorization trigger (layers) | +| `/jeo/v1/ai-clear-layer-store` | POST | Clear layer vector store | +| `/jeo/v1/ai-suggest-layers` | POST | Semantic layer matching (post ID or query) | +| `/jeo/v1/ai-georeference-chat` | POST | Multi-turn georeferencing with conversation history | +| `/jeo/v1/minilayer/generate` | POST | Generate Mapbox style from text prompt (Minilayer) | +| `/jeo/v1/minimap/setup` | POST | Generate minimap from post content (RAG + post geopoints) | +| `/jeo/v1/minimap/setup-prompt` | POST | Generate minimap from text prompt via AI agent | +| `/jeo/v1/minimap/chat` | POST | Multi-turn conversation for map refinement via AI agent | +| `/jeo/v1/context/setup` | POST | Generate initial editorial suggestions from post content | +| `/jeo/v1/context/chat` | POST | Multi-turn conversation for refining editorial suggestions | +| `/jeo/v1/context/state` | GET | Load persisted conversation state (messages, suggestions, conversation_id) | +| `/jeo/v1/context/clear` | POST | Reset conversation and suggestions | +| `/jeo/v1/context/engineer-prompt` | POST | Optimize custom system prompt while enforcing critical rules (requires `manage_options`) | + +## AI Georeferencing + +### Flow + +```mermaid +sequenceDiagram + participant JS as Posts Sidebar + participant REST as /ai-georeference + participant H as AI_Handler + participant A as Neuron_Adapter + participant N as Neuron_Agent + participant LLM as Provider (Gemini/OpenAI/...) + + JS->>REST: POST {post_title, post_content} + REST->>H: georeference() + H->>H: Build system prompt (JSON schema) + H->>A: georeference(prompt, content) + A->>N: chat(system_prompt + user_content) + N->>LLM: API call + LLM-->>N: Response with coordinates + N-->>A: {lat, lon, confidence, ...} + A-->>H: Parsed result + H->>H: Log tokens (AI_Logger) + H-->>REST: {location, confidence} + REST-->>JS: Result +``` + +### System Prompt + +The base adapter (`class-ai-adapter.php`) builds a prompt that: +- Defines the task as georeferencing +- Provides strict JSON schema for the response +- Includes confidence scoring instructions +- Applies aggressive JSON format enforcement +- **Incorporates active calibration settings** (granularity, confidence threshold, title weight, primary/secondary thresholds, primary/secondary point count limits) into the default prompt so users who do not generate a custom prompt via the AI Prompt Assistant still benefit from their calibration configuration +- Adapts the precision instruction in the enforced schema based on whether a minimum confidence calibration is active, avoiding conflicting directives +- Enforces max point limits in both the frontend sidebar and the bulk processor when configured + +## Bulk Processing + +`Bulk_Processor` geolocates legacy posts in batches: + +1. Config: post types, confidence threshold, batch size +2. WP-Cron schedules periodic processing +3. Each batch: selects posts without `_related_point`, sends to AI +4. "JEO AI Status" column in admin post list +5. Approval modal with preview +6. Individual or bulk approval + +## RAG (Retrieval-Augmented Generation) + +### Pipeline + +```mermaid +graph LR + A[WP_Post] -->|WP_Post_Data_Loader| B[NeuronAI Document] + B -->|RAG_Worker| C[Embedding] + C -->|FileVectorStore| D[wp-content/uploads/jeo-ai-store/] + D -->|RAG_Agent| E[Retrieval] + E -->|Neuron_Agent| F[LLM Response] +``` + +### Unified Multi-Pipeline Architecture + +The RAG system uses a **unified pipeline** managed by `RAG_Worker` with a `RAG_Pipeline_Config` value object that parameterizes the indexing process for different content types. + +#### Active Pipelines + +| Pipeline | Store Name | Post Type | Data Loader | Meta Key | +|----------|-----------|-----------|-------------|----------| +| Posts | `jeo_knowledge` | Configurable (`['post']` default) | `WP_Post_Data_Loader` | `_jeo_vectorized_at` | +| Layers | `jeo_layers_knowledge` | `map-layer` | `Layer_Data_Loader` | `_jeo_layer_vectorized_at` | + +#### Pipeline Flow + +```mermaid +graph LR + A[Single Cron Hook] --> B[RAG_Worker] + B --> C1[Posts Pipeline] + B --> C2[Layers Pipeline] + C1 --> D1[WP_Post_Data_Loader] + C2 --> D2[Layer_Data_Loader] + D1 --> E1[jeo_knowledge.store] + D2 --> E2[jeo_layers_knowledge.store] +``` + +### Layer Store + +#### Layer Data Loader (`class-layer-data-loader.php`) + +Converts `map-layer` CPT posts into `NeuronAI\RAG\Document[]`. Composes embedding text from: + +- Title (always) +- Content (optional — future-ready) +- Layer type (human-readable label) +- Attribution +- Source URL/path (only if human-readable keywords detected) +- Source layer (only if human-readable keywords detected) +- Legend labels + color descriptions (via `Color_Describer`) +- Palette summary + +#### Color Describer (`class-color-describer.php`) + +Analyzes hex colors via HSL conversion for semantic embedding: + +- Hue name (red, green, blue, etc.) +- Lightness: very dark / dark / medium / light / very light +- Saturation: gray / muted / vivid +- Temperature: warm / cool / neutral +- Palette summary across all legend colors + +#### Cross-Store Retrieval + +`RAG_Worker::find_matching_layers($text, $topK)` enables semantic matching between post content and layers by embedding the query text and searching the layer store. + +### Components + +- **FileVectorStore**: Stored in `wp-content/uploads/jeo-ai-store/` +- **Model Lock**: Per-store model consistency (`.model_info`) +- **Backup**: ZIP with rotation (max 3 backups), includes both stores +- **WP-CLI**: `wp jeo ai vectorize --store=posts|layers --batch_size=20` +- **WP-CLI Aliases**: `vectorize-posts`, `vectorize-layers` + +### Embedded Dictionaries + +`ai/data/` contains 10 JSONs with Brazilian geographic data: +- Biomes, Conservation Units, Indigenous Lands +- Hydrographic Basins, Settlements, Quilombola territories +- Legal Amazon, Extractive Reserves, etc. + +## RAG Vector Store Configuration + +| Setting | Key | Default | Range | Description | +|---------|-----|---------|-------|-------------| +| Search Results (topK) | `ai_rag_topk` | 10 | 1–50 | Maximum semantic matches returned per search by `FileVectorStore` | + +The `topK` value is read from JEO settings in `RAG_Agent::vectorStore()` and passed to `FileVectorStore`. Previously hardcoded to 3, it is now configurable so callers like `Search_Layers_Tool` and `find_matching_layers()` can receive the full number of results they request. + +### Layer Deduplication + +`Layer_Data_Loader` sets `Document->sourceType = 'layer'` and `Document->sourceName = (string) $post->ID` so each embedding is tagged with its layer ID. When a layer is saved, `RAG_Worker::on_layer_save()` calls `deleteBy('layer', $post_id)` on the vector store before adding the new embedding, preventing duplicate entries that would pollute search results. + +## Unified AI Architecture (Phase 1–2) + +The JEO AI system is converging onto `hacklabr/ai-assistant` as the unified execution layer: + +- **JEO_AI_Factory**: Creates all `Assistant` instances with shared provider, logger, storage, and tool registry. +- **Tool_Registry**: Central singleton registering `search_layers`, `geocode`, `generate_layer`, `get_post_content`. +- **AI_REST_Permissions**: Reusable `permission_callback` closures replacing inline anonymous functions. +- **System_Prompt_Builder**: Extracts calibration-aware prompt construction from `AI_Adapter` into reusable static methods. +- **Georeferencing_Conversation**: Enables multi-turn refinement of georeferencing results via `ConversationStore`. + +## AI Settings + +4 tabs under **Jeo → AI**: +1. **Provider**: Selection + API key + model +2. **Knowledge Base**: Manage vector store (embedding model, auto-indexing, batch size, cron interval, topK) +3. **Embedded Data**: Geographic dictionaries +4. **Bulk Geolocation**: Batch processing config + +## Cost Tracking + +`AI_Logger` records via CPT `jeo-ai-log`: +- Provider, model +- Input/output tokens +- Prompt, response +- Timestamp + +Dashboard at **Jeo → AI Debug Logs** with metrics per model/provider. + +## Minimap (AI-Assisted Map Block) + +See [`minimap/README.md`](../minimap/README.md) for full architecture, agent details, REST endpoints, and data flows. + +The `jeo/ai-minimap` block provides: + +- **Post content mode**: Legacy RAG-based generation via `RAG_Worker::find_matching_layers()` (`/minimap/setup`) +- **Prompt mode**: Full AI agent with structured output, sub-agents, and tools (`/minimap/setup-prompt`) +- **Chat refinement**: Multi-turn conversation for iterative map changes (`/minimap/chat`) + +### Key Agent Files + +| File | Role | +|------|------| +| `src/includes/ai/class-minimap-agent.php` | Agent factory (Assistant::configure with tools, sub-agents, storages). Conditionally includes `Generate_Layer_Tool` when Mapbox key is available | +| `src/includes/ai/class-minimap-output.php` | Structured output DTO | +| `src/includes/ai/class-search-layers-tool.php` | Semantic layer search tool | +| `src/includes/ai/class-geocode-tool.php` | Geocoding tool with fallback chain | +| `src/includes/ai/class-generate-layer-tool.php` | Custom Mapbox style generation tool (conditional, requires Mapbox key + user authorization) | +| `src/includes/ai/class-get-post-content-tool.php` | Post content tool for post_analyzer sub-agent | +| `src/includes/ai/class-wp-storage.php` | Post/user meta storage adapter | +| `src/includes/ai/class-wp-option-storage.php` | WP options storage adapter (global learning) | + +## AI Context Assistant (Editorial Suggestions) + +See [`context/README.md`](../context/README.md) for full architecture, agent details, REST endpoints, and data flows. + +The Context Assistant is a Gutenberg sidebar plugin that suggests new paragraphs and related article references. Key features: + +- **Manual setup**: User clicks "Generate Suggestions" to start; no automatic trigger. +- **Dual conversation storage**: `ConversationStore` for AI context, separate `post_meta` for clean UI messages. +- **Structured output**: `Context_Generation_Output` DTO with paragraphs (inline HTML), references, messages. +- **Retry with backoff**: Up to 3 attempts on transient errors. +- **State persistence**: Conversation survives page refresh via `GET /context/state`. +- **Customizable prompt**: `ai_context_prompt` setting in AI Settings → Context Assistant tab. +- **Multi-theme retrieval**: requests combining N themes use one `retrieve_knowledge` query per theme (via the tool's `queries` array), merged/deduped with per-theme rank interleaving; generation prefers one integrated paragraph, falling back to one per theme with an explicit `assistant_message` note. + +## Minilayer (AI-Generated Layers) + +See [`minilayer/README.md`](../minilayer/README.md). + +The minilayer pipeline is shared between: +- The standalone REST endpoint (`/jeo/v1/minilayer/generate`) +- The minimap agent's `Generate_Layer_Tool` (conditional, requires Mapbox key + user authorization) + +See [`minimap/README.md`](../minimap/README.md) for the minimap-side integration: default style activation, tool error handling, and the authorization gate for layer generation. diff --git a/.architecture/ai/structured-output-plan.md b/.architecture/ai/structured-output-plan.md new file mode 100644 index 000000000..6aa62a4c8 --- /dev/null +++ b/.architecture/ai/structured-output-plan.md @@ -0,0 +1,239 @@ +# Plano de Implementação: NeuronAI Structured Output para Georeferencing + +## Contexto + +Atualmente o JEO força o schema JSON de georeferencing injetando instruções agressivas no system prompt (`AI_Adapter::get_system_prompt()`). O LLM retorna texto livre e o JEO usa regex (`parse_json_from_text()`) para extrair o JSON — uma abordagem frágil que depende da "obediência" do modelo ao prompt. + +O NeuronAI (v3.3+) possui recurso nativo de **Structured Output** que: +- Gera JSON Schema automaticamente a partir de classes PHP tipadas +- Envia o schema ao provider (OpenAI, Gemini, Anthropic, etc.) via API nativa +- Faz parsing, desserialização e validação automática da resposta +- Possui retry automático informando ao LLM quais campos falharam + +## Objetivo + +Migrar o georeferencing do JEO para usar NeuronAI Structured Output como **caminho primário**, mantendo o parser de texto atual como **fallback robusto**, com **zero impacto** no funcionamento atual. + +## Como Funciona o Structured Output do NeuronAI + +### 1. Definição de Schema via PHP + +```php +use NeuronAI\StructuredOutput\SchemaProperty; +use NeuronAI\StructuredOutput\Validation\NotBlank; +use NeuronAI\StructuredOutput\Validation\GreaterThanEqual; +use NeuronAI\StructuredOutput\Validation\LowerThanEqual; + +class LocationOutput { + #[SchemaProperty(description: "The location name")] + #[NotBlank] + public string $name; + + #[SchemaProperty(description: "Latitude as float or string")] + public float $lat; + + #[SchemaProperty(description: "Longitude as float or string")] + public float $lon; + + #[SchemaProperty(description: "Short relevant snippet from the text")] + public string $quote; + + #[SchemaProperty(description: "Confidence score 0-100")] + #[GreaterThanEqual(0)] + #[LowerThanEqual(100)] + public int $confidence; + + #[SchemaProperty(description: "Whether this is the primary geographic focus")] + public bool $is_primary; +} +``` + +### 2. Array de Objetos + +O NeuronAI não suporta array como raiz do schema. Usamos uma **classe wrapper**: + +```php +class GeoreferenceResult { + #[SchemaProperty( + description: "List of geographic locations found in the text", + anyOf: [LocationOutput::class] + )] + public array $locations; +} +``` + +O LLM retorna: `{"locations": [{"name": "...", "lat": -3.13, ...}]}` + +### 3. Execução + +```php +$result = $agent->structured( + messages: new UserMessage($user_text), + class: GeoreferenceResult::class, + maxRetries: 3 +); + +$locations = $result->locations; // array de LocationOutput objects +``` + +### 4. Retry Automático + +Se o LLM omitir um campo obrigatório ou usar tipo errado, o NeuronAI: +1. Captura o erro de validação +2. Reenvia a requisição informando exatamente o que falhou +3. Repete até `maxRetries` (default: 1, recomendado: 3) + +## Arquitetura Proposta + +### Estratégia de Zero Impacto + +**Feature Toggle** (`ai_use_structured_output`) com fallback transparente: + +``` +┌─────────────────────────────────────────┐ +│ Neuron_Adapter::georeference() │ +│ │ +│ IF ai_use_structured_output = true │ +│ TRY structured output │ +│ RETURN locations array │ +│ CATCH any exception │ +│ FALLBACK to parse_json_from_text() │ +│ ELSE │ +│ USE parse_json_from_text() (current) │ +│ │ +└─────────────────────────────────────────┘ +``` + +### Componentes + +#### 1. Classes de Structured Output (novas) + +**Arquivo:** `src/includes/ai/structured/location-output.php` +**Arquivo:** `src/includes/ai/structured/georeference-result.php` + +- `LocationOutput` — representa um ponto de georeferencing +- `GeoreferenceResult` — wrapper com array de `LocationOutput` + +#### 2. Neuron_Agent (modificado) + +**Arquivo:** `src/includes/ai/class-neuron-agent.php` + +Novo método `run_georeference_structured()`: +- Chama `$this->structured()` com `GeoreferenceResult::class` +- Retorna array de arrays (para compatibilidade com `parse_json_from_text()`) +- Remove a necessidade do schema agressivo no prompt + +#### 3. Neuron_Adapter (modificado) + +**Arquivo:** `src/includes/ai/class-neuron-adapter.php` + +Novo fluxo `georeference()`: +1. Se `ai_use_structured_output` estiver ativo: + - Chama `run_georeference_structured()` + - Se sucesso, converte objetos para array e retorna + - Se falha (exceção), loga e cai no fallback +2. Fallback: usa o fluxo atual (`get_system_prompt()` + `parse_json_from_text()`) + +#### 4. AI_Settings (modificado) + +**Arquivo:** `src/includes/ai/settings/tab-general.php` + +Novo toggle: +- **Label:** "Use NeuronAI Structured Output" +- **Descrição:** "Use native schema enforcement instead of prompt-based JSON extraction. Falls back to text parsing if structured output fails." +- **Default:** `false` (inicialmente, para testes graduais) + +#### 5. AI_Adapter (modificado) + +**Arquivo:** `src/includes/class-ai-adapter.php` + +Ajuste em `get_system_prompt()`: +- Quando `ai_use_structured_output` está ativo, o schema agressivo não precisa mais ser injetado +- Ou pode ser mantido como safety net, mas com nota de depreciação + +## Passos de Implementação + +### Fase 1: Infraestrutura (sem impacto no runtime) + +1. **Criar classes de structured output** + - `src/includes/ai/structured/location-output.php` + - `src/includes/ai/structured/georeference-result.php` + - Adicionar autoload no `composer.json` (se necessário) ou usar `require_once` + +2. **Adicionar setting `ai_use_structured_output`** + - `class-settings.php`: default `false`, tipo boolean + - `tab-general.php`: checkbox com descrição + +3. **Criar novo método no Neuron_Agent** + - `run_georeference_structured($system_prompt, $user_text, &$input_tokens, &$output_tokens)` + - Usa `$this->structured()` com `GeoreferenceResult::class, maxRetries: 3` + - Converte `LocationOutput[]` para array associativo (compatibilidade) + +4. **Modificar Neuron_Adapter** + - Adicionar lógica de feature toggle + - Try/catch para structured output com fallback para `parse_json_from_text()` + +### Fase 2: Testes (isolated) + +1. **Testar com provider Gemini** + - Gemini já usa `responseMimeType: application/json` — structured output é nativo + - Verificar se o schema gerado é enviado corretamente + +2. **Testar com provider OpenAI** + - OpenAI suporta `json_schema` no modo structured — verificar compatibilidade + +3. **Testar fallback** + - Desligar structured output e confirmar que o fluxo antigo continua funcionando + - Simular falha no structured output (ex: modelo que não suporta) e verificar fallback + +4. **Smoke test** + - Rodar `scripts/wordpress-smoke.sh` com structured output ligado e desligado + +### Fase 3: Rollout Gradual + +1. **Default false em produção** + - Monitorar logs de erros por 1-2 semanas + +2. **Habilitar para providers específicos** + - Gemini e OpenAI primeiro (melhor suporte a structured output) + - DeepSeek, Anthropic, Mistral em seguida + +3. **Tornar default true** + - Após validação em produção + - Remover o schema agressivo do prompt (reduz tokens de input) + +## Riscos e Mitigações + +| Risco | Probabilidade | Impacto | Mitigação | +|-------|--------------|---------|-----------| +| Provider não suporta JSON Schema nativo | Média | Alto | Fallback automático para `parse_json_from_text()` | +| LLM ignora schema e retorna texto livre | Baixa | Médio | Retry do Neuron (maxRetries: 3) + fallback | +| Schema gerado é muito grande (consumo de tokens) | Baixa | Médio | Monitorar; schema é menor que o prompt atual | +| Quebra de compatibilidade com custom prompts | Média | Alto | Feature toggle; custom prompts continuam usando texto livre | +| `lat`/`lon` como float vs string | Baixa | Médio | Usar `string` no schema e converter no adapter | + +## Benefícios Esperados + +1. **Maior confiabilidade**: schema validado pelo provider, não apenas "sugerido" no prompt +2. **Menos tokens de input**: remove ~300-400 tokens do schema agressivo do prompt +3. **Retry inteligente**: Neuron informa exatamente qual campo falhou ao LLM +4. **Tipagem forte**: propriedades validadas como `float`, `int`, `bool` antes de chegar ao JEO +5. **Extensibilidade**: fácil adicionar novos campos (ex: `country`, `region`) na classe PHP + +## Arquivos Envolvidos + +### Novos +- `src/includes/ai/structured/location-output.php` +- `src/includes/ai/structured/georeference-result.php` + +### Modificados +- `src/includes/ai/class-neuron-agent.php` +- `src/includes/ai/class-neuron-adapter.php` +- `src/includes/class-ai-adapter.php` +- `src/includes/settings/class-settings.php` +- `src/includes/ai/settings/tab-general.php` +- `src/includes/ai/class-ai-settings.php` (se necessário) + +--- + +*Plano criado em 2026-05-06. Revisar após implementação da Fase 1.* diff --git a/.architecture/blocks/README.md b/.architecture/blocks/README.md new file mode 100644 index 000000000..3b6f87c20 --- /dev/null +++ b/.architecture/blocks/README.md @@ -0,0 +1,79 @@ +# Gutenberg Blocks + +## Key Files + +| File | Role | +|------|------| +| `src/includes/class-jeo.php` | Registers most blocks via `register_block_type()` | +| `src/js/src/map-blocks/index.js` | Webpack entry point `mapBlocks` | +| `src/js/src/map-blocks/map-editor.js` | Edit component for `jeo/map-blocks` | +| `src/js/src/map-blocks/onetime-map-display.js` | Save component for one-time map | +| `src/js/src/map-blocks/onetime-map-config.js` | Attributes for one-time map | +| `src/js/src/map-blocks/minimap-editor.js` | Edit component for `jeo/ai-minimap` | +| `src/js/src/map-blocks/minimap-display.js` | Save component for `jeo/ai-minimap` | +| `src/js/src/map-blocks/minimap-config.js` | Attributes for `jeo/ai-minimap` | +| `src/js/src/map-blocks/layers-settings.js` | Layer configuration UI inside block | +| `src/js/src/map-blocks/layers-settings-modal.js` | Modal wrapper for layers settings | +| `src/js/src/map-blocks/map-preview-layer.js` | Renders layer in editor preview | +| `src/js/src/map-blocks/mapbox-style-preview.js` | Composed Mapbox style adapter for editor previews (AbortSignal stale-state guard, token transform) — see [`composed-styles/README.md`](../composed-styles/README.md) | +| `src/js/src/map-blocks/jeo-autosuggest.js` | Autosuggest for searching maps/layers | +| `src/js/src/map-blocks/color-palettes.js` | Color palette (437 colors) | +| `src/includes/minimap/class-minimap.php` | `Jeo\Minimap` — REST endpoint, base layer logic | + +## Registered Blocks + +Registered in `class-jeo.php` on `init` hook: + +| Block | Render | Description | +|-------|--------|-------------| +| `jeo/map-blocks` | Server-side (`render_callback`) | Selectable map from library | +| `jeo/storymap` | Server-side | Scrollytelling storymap with chapters | +| `jeo/embedded-storymap` | Server-side | Embeddable storymap | +| `jeo/map-editor` | Dynamic (iframe) | Inline map preview in editor; also manages zoom controls and pan-limits bridge via `window.parent` | +| `jeo/layer-editor` | Dynamic (iframe) | Inline layer preview in editor; also manages notices and post save locking based on layer load status | +| `jeo/stories-near-you` | Skeleton + JS (self-registered) | Geolocated posts sorted by proximity — see [`stories-near-you/README.md`](../stories-near-you/README.md) | +| `jeo/ai-minimap` | Client-side save (`
`) | AI-assisted contextual map with agent-powered generation and multi-turn chat refinement. See [`minimap/README.md`](../minimap/README.md) | + +## Gutenberg Sidebar Plugins + +| Plugin | Entry Point | Post Type | Description | +|--------|------------|-----------|-------------| +| `jeo-layers-sidebar` | `layers-sidebar/index.js` | `map-layer` | Layer editing forms (settings, attribution, legend). Map preview is in the `jeo/layer-editor` block | +| `jeo-maps-sidebar` | `maps-sidebar/index.js` | `map`, `storymap` | Map/storymap editing forms (layers, related posts, settings). Map preview is in the `jeo/map-editor` block | +| `jeo-posts-sidebar` | `posts-sidebar/index.js` | All | Post geolocation | + +## Block Creation Pattern + +1. Define attributes in JS (`attributes` in `registerBlockType` or config file) +2. Create `Edit` component (React) using `@wordpress/block-editor` +3. Create `Save` component or use `render_callback` PHP (server-side) +4. Register in `class-jeo.php` via `register_block_type()`, or self-register in a dedicated class +5. Add entry point in `webpack.config.js` if needed + +## Block Frontend Dependencies + +- `@wordpress/block-editor` — `useBlockProps`, `InnerBlocks`, `InspectorControls` +- `@wordpress/components` — `Modal`, `PanelBody`, `Button`, etc. +- `@wordpress/data` — `withSelect`, `withDispatch`, `useSelect` +- `../lib/mapgl-react` — `Map`, `Layer`, `Source` (React wrappers) +- `../shared/wp-form-controls` — Wrappers with next-gen props + +## Server-Side Render + +Blocks with `render_callback` in PHP receive `$attributes` and return HTML: +- Map blocks: inject `
` which `JeoMap` JS initializes +- Storymap blocks: inject `
` with chapter data + +## Composed Style Editor Previews + +Map, onetime-map, and storymap editors render `mapbox`-type layers via composed styles +(see [`composed-styles/README.md`](../composed-styles/README.md)). When the loaded layers +include a `mapbox` type (`hasMapboxStyleLayers`), the editor loads the composite style +(`useComposedMapPreviewStyle` / `useComposedPayloadPreviewStyle`) and sets it as the +``. The map `key` includes the composed `metadata.hash` so React remounts +when the artifact changes. The `jeo/layer-editor` block uses `useMapboxStylePreview` +(single raw Mapbox style, not the composer). + +## Block Category + +All blocks are registered under the `jeo` category (added via `block_categories_all` filter). diff --git a/.architecture/build/README.md b/.architecture/build/README.md new file mode 100644 index 000000000..0cd27d44b --- /dev/null +++ b/.architecture/build/README.md @@ -0,0 +1,157 @@ +# Build System & Scripts + +## Commands + +| Command | Description | +|---------|-------------| +| `npm start` | Dev server (webpack watch) | +| `npm run build` | Production build + i18n JSON | +| `npm run build:assets` | Webpack production build | +| `npm run build:report` | Bundle size report | +| `npm run test:unit` | Jest tests | +| `npm run i18n:json` | Generate translation JSON | +| `npm run audit:npm` | npm security audit | +| `npm run audit:composer` | Composer security audit | +| `npm run deps:report` | Dependency report | +| `composer install` | Install PHP deps (phpcs) | + +## Lint + +| Command | Tool | Config | +|---------|------|--------| +| `vendor/bin/phpcs` | PHPCS (WPCS) | `phpcs.xml.dist` | +| (via wp-scripts) | ESLint + Prettier | `@wordpress/scripts` defaults | + +## Webpack Config + +File: `webpack.config.js` + +Extends `@wordpress/scripts/config/webpack.config` with: +- Custom entry points (see [frontend/README.md](../frontend/README.md)) +- Output in `src/js/build/` +- `splitChunks: false` (each entry is independent) +- `chunkIds: 'named'` +- `NormalModuleReplacementPlugin` to strip `node:` prefix +- Fallbacks: `fs: false`, `path: false` + +## Helper Scripts + +| Script | File | Description | +|--------|------|-------------| +| Version sync | `scripts/sync-plugin-version.mjs` | Syncs package.json version → readme.txt/jeo.php | +| Node check | `scripts/check-node-version.mjs` | Validates Node.js >= 24 | +| Build compliance | `scripts/patch-build-compliance.mjs` | Post-build patches for WP.org compliance | +| i18n JSON | `scripts/make-i18n-json.mjs` | Generates translation JSON | +| Bundle report | `scripts/report-bundle-sizes.mjs` | Reports bundle sizes | +| WP.org check | `scripts/check-wporg-compliance.mjs` | Checks WP.org compliance | +| PHP compat | `scripts/check-php-compat.php` | Checks PHP 8.0-8.5 compatibility | +| Smoke test | `scripts/wordpress-smoke.sh` | WordPress smoke test | + +## Local Development Environment (Docker) + +A self-contained Docker environment lives in `.docker/` (gitignored) and is **not** committed to the repository. + +```bash +bash .docker/start.sh # Start WordPress + MariaDB +``` + +| Service | Container Name | Host Port | Credentials | +|---------|---------------|-----------|-------------| +| WordPress | `jeo-dev-wordpress` | `8081` | Setup wizard on first run | +| MariaDB | `jeo-dev-mariadb` | `3307` | `jeo` / `jeo` / `jeo` | + +- `src/` is mounted live into `/var/www/html/wp-content/plugins/jeo` — no reinstall needed after edits. +- Network: `jeo-dev-network` (isolated bridge). +- Stop: `docker compose -f .docker/docker-compose.yml down` +| Dependency review | `scripts/dependency-review.mjs` | License/security review | +| Version validate | `scripts/validate-release-meta.mjs` | Validates release metadata | +| PHP compat | `scripts/check-php-compat.php` | Checks PHP compatibility | +| Smoke test | `scripts/wordpress-smoke.sh` | WordPress smoke test | + +## Release Build Scripts + +All release/packaging logic is consolidated to avoid duplication. Shared +helpers (logging, Node 24 check with nvm fallback, version extraction, ZIP +packaging) live in **`scripts/lib/common.sh`**, which the entry-point scripts +source. + +| Script | Role | +|--------|------| +| `scripts/lib/common.sh` | Shared functions: `step/ok/warn/fail`, `jeo_plugin_version`, `jeo_ensure_node_24`, `jeo_create_zip`. Sourced, not executed. | +| `scripts/build.sh` | **Canonical release builder.** Node check → validate metadata → composer (prod) → assets → i18n → `dist/jeo-{version}.zip`. Flags: `--skip-assets`, `--skip-composer`, `--skip-i18n`, `--skip-build`. | +| `scripts/install-and-build.sh` | Clean reinstall (wipes `node_modules` + `vendor` root/src, `npm ci`, composer), then delegates build+zip to `build.sh --skip-composer`. Flag: `--no-zip`. | +| `scripts/build-wordpress-zip.sh` | Deprecated thin alias that `exec`s `build.sh` (same flags). | + +**Node 24 enforcement:** `jeo_ensure_node_24` loads nvm and switches to Node 24 +*before* any npm step runs (so the build never hits `EBADDEVENGINES`). +`scripts/check-node-version.mjs` is the JS-side assertion of the Node 24.x +requirement, used after the switch. + +## CI/CD (GitHub Actions) + +| Workflow | File | Trigger | +|----------|------|---------| +| Node/Frontend | `node-frontend.yml` | Push/PR | +| PHPCS | `phpcs-wpcs.yml` | Push/PR | +| PHP Compat | `php-compat.yml` | Push/PR | +| WP Smoke | `wordpress-smoke.yml` | Push/PR | +| Deploy WP.org | `deploy-wordpress-org.yml` | Tag push | +| Site (landing + docs) → gh-pages | `deploy-site.yml` | Push master | + +## Project Website (site/) + +Two independent builds published together under one origin (`jeowp.org`): + +- **Landing** — React + Vite, source in `landing-page/`, built to `site/` (Vite `outDir`; `LANDING_OUT_DIR` overrides it). `LANDING_BASE=/subpath` produces a homolog build under a subpath. +- **Docs** — VitePress, source in `docs/`, built to `site/docs/`. + +Cross-navigation: the landing links into the docs via `landing-page/src/links.js` (`DOCS_URL = '/docs/'`); the docs navbar logo links back to the landing (`themeConfig.logoLink`). + +### Site commands + +| Command | Purpose | +|---|---| +| `npm run dev:site` | Both dev servers under one origin: http://localhost:5173 (landing, HMR) + `/docs/` (VitePress dev, proxied by the landing dev server) | +| `npm run build:site` | Installs deps and builds both parts into `site/` (landing, then docs) | +| `npm run preview:site` | Serves the built `site/` at http://localhost:4174 | +| `npm --prefix docs run docs:dev` | Docs dev server only | +| `npm --prefix docs run docs:build` | Docs production build only (into `site/docs/`) | +| `LANDING_BASE=/novo npm --prefix landing-page run build` | Landing build under a subpath (homolog) | + +> Caveat: `preview:site` uses Python's `http.server`, which does not resolve extensionless URLs — `/docs/concepts` 404s locally while GitHub Pages serves it from `concepts.html`. Verify extensionless docs URLs on the deployed site (or request the `.html` locally). + +### Documentation conventions (docs/) + +- **Stack**: VitePress with `base: '/docs/'`, `cleanUrls: true`, `srcExclude: ['README.md']` (repo-only index). Output goes straight to `site/docs/` (`outDir`, `emptyOutDir`). +- **Structure**: flat source pages — `docs/*.md` and `docs/dev/*.md`; the home is `docs/index.md`. No rewrites: every page builds to flat `x.html` and is served at the extensionless URL `/docs/x` (GitHub Pages resolves `/x` → `x.html`). Legacy trailing-slash URLs (`/docs/x/`, MkDocs era) are not served. +- **Internal links**: relative `.md` anchors in natural source-space form — the same path you would use to open the target file from the linking page: + - root → root: `[AI Settings](ai-settings.md)` + - root → dev: `[Geocoders](dev/geocoders.md)` + - dev → dev sibling: `[Geocoders](geocoders.md)` + - dev → root: `[AI Settings](../ai-settings.md)` + - home → page: `[Read documentation](index-plugin.md)` +- **Why relative `.md`**: VitePress fails the build on dead links for `.md`/`.html` targets only — absolute links (`/page/`) bypass the check. Relative anchors also resolve when browsing the repo on GitHub. +- **Images**: shared `docs/img/` directory, referenced relatively (`![alt](img/foo.png)`) — valid because pages stay flat. +- **Publishing**: `deploy-site.yml` builds the complete site (landing + docs) on every push to `master` and deploys it to the `gh-pages` branch. `site/` is gitignored. Builds are deterministic (content-hashed assets are stable across builds). + +## Node.js + +- **Required version**: 24+ (see `.nvmrc`) +- **npm**: >= 10 + +## Output Structure + +``` +src/js/build/ +├── mapglLoader.js +├── mapglReact.js +├── jeoMap.js +├── jeoStorymap.js +├── discovery.js +├── mapBlocks.js +├── layersSidebar.js +├── mapsSidebar.js +├── postsSidebar.js +├── JeoLayer.js +└── JeoLegend.js +``` diff --git a/.architecture/composed-styles/README.md b/.architecture/composed-styles/README.md new file mode 100644 index 000000000..aec545679 --- /dev/null +++ b/.architecture/composed-styles/README.md @@ -0,0 +1,364 @@ +# Composed Mapbox Styles (Map Style Composer) + +The **Map Style Composer** merges one or more remote Mapbox GL styles (plus direct +tile layers) into a single cached **Mapbox Style JSON** artifact that MapLibre GL JS +(the plugin's default runtime) can render natively — without requiring the official +Mapbox GL JS runtime. This replaces the legacy "Mapbox Static Tiles overlay" +rendering path for `mapbox`-type layers (shipped in 3.1.0). + +## Key Files + +| File | Role | +|------|------| +| `src/includes/maps/class-map-style-composer.php` | `Jeo\Map_Style_Composer` — backend engine (Singleton) | +| `src/includes/maps/class-style-token-migrator.php` | `Jeo\Style_Token_Migrator` — converts legacy `{token}` strings to expressions (MapLibre compat) | +| `src/includes/loaders.php` | Accessor `jeo_map_style_composer()` | +| `src/includes/class-jeo.php` | Instantiation (`::init`) + `jeoMapVars` localization | +| `src/js/src/shared/composed-style-data.js` | Frontend/editor data loader (`loadComposedStyleData`) | +| `src/js/src/shared/composed-style-layers.js` | Frontend/editor manifest helpers (visibility, interactions) | +| `src/js/src/map-blocks/mapbox-style-preview.js` | Editor adapter (hooks, token transform, single-style loader) | +| `src/js/src/jeo-map/class-jeo-map.js` | Frontend `JeoMap` composed-style integration | +| `src/js/src/jeo-storymap/storymap-display.js` | Storymap composed-style integration | +| `src/js/src/discovery/blocks/map-layers.js` | Discovery per-map composition + sprite atlas | + +## Accessor + +```php +jeo_map_style_composer() // → Jeo\Map_Style_Composer::get_instance() +``` + +Registered in `Jeo::init()` (`class-jeo.php:49`). + +## Backend Architecture + +### Class & Lifecycle + +`Jeo\Map_Style_Composer` (namespace `Jeo`, `Singleton` trait). `init()` registers: + +| Hook | Callback | Purpose | +|------|----------|---------| +| `rest_api_init` | `register_rest_routes()` | 7 REST routes (see [REST API](#rest-endpoints)) | +| `save_post_map` | `invalidate_map_cache()` | Clears map composition meta on save | +| `save_post_map-layer` | `invalidate_layer_cache()` | Purges the layer's shared style JSON transient (`Jeo::delete_mapbox_style_cache()`), finds all maps using a layer, invalidates each | + +### Constants + +| Constant | Value | Purpose | +|----------|-------|---------| +| `CACHE_DIR` | `jeo-mapbox-composed-styles` | Subdirectory under `wp_upload_dir()['basedir']` | +| `CACHE_VERSION` | `18` | Bump to invalidate all artifacts; embedded in hash & metadata | +| `TOKEN_PLACEHOLDER` | `__JEO_MAPBOX_ACCESS_TOKEN__` | Replaces raw `access_token=...` in stored style JSON | +| `DEFAULT_FALLBACK_SPRITE` | `mapbox://sprites/mapbox/standard` | Used when source sprites lack icons | +| `VIRTUAL_SCOPE_PREVIEW` | `preview` | Editor preview scope (requires `edit_posts`) | +| `VIRTUAL_SCOPE_ONETIME` | `onetime` | Public one-time map scope (open) | + +### Composition Flow + +``` +build_context(map_id) / build_virtual_context(payload) + → build_refs_from_settings() // resolve map-layer posts → refs (type, token, styleId, loadAsStyle, ...) + → compose_context() + ├── Jeo::fetch_mapbox_style() // GET api.mapbox.com/styles/v1/{styleId} per mapbox ref + │ // shared helper, transient-cached (jeo_mapbox_style_json_*) + │ // purged on layer save + forced refresh; TTL filter jeo_mapbox_style_cache_ttl (default 1h) + │ // transient stores the RAW body; decode + legacy {token} normalization + │ // (Style_Token_Migrator) run on every read — cache never depends on the code + ├── migrate_legacy_style_functions() // convert pre-expressions {base, stops} objects → interpolate/step/match expressions (MapLibre compat; see below) + ├── Style_Token_Migrator::migrate_style() // rewrite "{token}" strings in text-field/icon-image → get/concat expressions, + │ // including tokens nested inside expression values (legacy function outputs); MapLibre 3+ + │ // renders unmigrated tokens literally (the "{name_en}" bug) + ├── build_composite_sprite()// GD: merge sprites (1x + @2x), prefix image names, pack 2048px canvas + │ // raw sprite JSON+PNG transient-cached (jeo_sprite_*, TTL filter jeo_sprite_cache_ttl, default 1d); + │ // forced refresh bypasses the sprite cache + ├── select_glyphs() // first bundle with text-font, else first glyphs URL + ├── merge root properties // projection, light, terrain, fog (warns on conflicts) + ├── copy + remap layers // source remap, ref-layer remap, visibility by `default`, image-prefix rewrite + ├── normalize_unsupported_expressions() // replace Mapbox-only pitch/distance-from-center → 0 (MapLibre compat) + ├── is_style_layer_enabled()// honor per-layer style_layers[].show + ├── transform_interactions()// remap click/mouseover targets → composite layer IDs + └── build_direct_layer() // append mapbox-tileset-*, mvt, tilelayer as plain sources+layers + → write_json_file() × 3 // style.json, manifest.json, report.json (pretty-printed); + // write failures are logged ([JEO] prefix) and returned as WP_Error + // (a silent failure would force recomposition on every request) +``` + +### Artifacts (per composition) + +Three JSON files written to disk: + +| File | Contents | +|------|----------| +| **`style.json`** | Mapbox Style v8 document — renderable by MapLibre. Tokens scrubbed to `TOKEN_PLACEHOLDER`. | +| **`manifest.json`** | Mapping doc: `kind`, `id`, `slug`, `title`, `originalUrl`, and per-layer `layers[]` with `layerPostId`, `loadAsStyle`, `use`, `default`, `initialVisible`, `directLayer`, `prefix`, `imagePrefix`, `interactions[]` (remapped), `compositeLayers[]` (`originalId`, `compositeId`, `type`, `visibleWhenLayerOn`). | +| **`report.json`** | Diagnostics: counts, `spriteSummary`, `warnings[]`. | + +The **manifest** maps JEO layer post IDs → composite style-layer IDs. The **style** is the +renderable Mapbox Style JSON. Frontend uses the manifest to drive per-JEO-layer visibility, +interactions, and popups over the composite style layers. + +### Initial Visibility Semantics + +The composer bakes each layer's initial `layout.visibility` into `style.json` using a +rule that mirrors the editor's `shouldDisplayLayerInstance` +(`mapbox-style-preview.js:34-39`): + +- **Non-toggle layers** (`use` not in `swappable`/`switchable`, e.g. `fixed`) → **visible** + by default, regardless of `default`. +- **Toggle layers** (`use` ∈ `swappable`/`switchable`) → visible only when + `default === true`. + +The frontend (`class-jeo-map.js`) does **not** re-derive visibility at runtime — it trusts +the baked value. The editor (`mapbox-style-preview.js::applyComposedVisibilityFromSettings`) +applies the same rule on top of the baked artifact, so editor and frontend stay consistent. +If you change this rule, bump `CACHE_VERSION` so stale artifacts regenerate. + +## REST Endpoints + +All under namespace `jeo/v1`. See also [`rest-api/README.md`](../rest-api/README.md). + +| Route | Method | Permission | Description | +|-------|--------|------------|-------------| +| `/map-style/{id}` | GET | `can_read_map` (published OR `edit_post`) | Metadata (`enabled`, `style`/`manifest` URLs, `warnings`). Arg `refresh` forces regeneration. | +| `/map-style/{id}/style` | GET | `can_read_map` | Serves `style.json`. With a valid `?hash=` matching an existing artifact: `Cache-Control: public, max-age=31536000, immutable` (content-addressed); otherwise current artifact with `max-age=300` | +| `/map-style/{id}/manifest` | GET | `can_read_map` | Serves `manifest.json` with the same hash-aware caching as `/style` | +| `/map-style/compose` | POST | `can_create_virtual` | Compose from a JSON payload (scope/kind/postId/layers/center/zoom). `onetime`=public, `preview`=logged-in + `edit_posts` | +| `/map-style/layer/{id}/refresh` | POST | `can_refresh_layer` (`map-layer` + `edit_post`) | Force-regenerate all maps referencing a layer. Returns `{mapIds[], refreshed, failed}` | +| `/map-style/{scope:preview\|onetime}/{hash}/style` | GET | `can_read_virtual` | Virtual artifact `style.json` (`onetime`=open, `preview`=logged-in + `edit_posts`; `max-age=31536000, immutable`) | +| `/map-style/{scope:preview\|onetime}/{hash}/manifest` | GET | `can_read_virtual` | Virtual artifact `manifest.json` (`max-age=31536000, immutable`) | + +> **Preview nonce (3.1.1):** for `preview` scope with a logged-in user, the metadata +> response appends `_wpnonce=wp_create_nonce('wp_rest')` to style/manifest URLs so +> Mapbox GL can fetch them directly with cookie/nonce auth. + +## Caching + +### File Layout (under uploads) + +- **Saved maps:** `{uploads}/jeo-mapbox-composed-styles/{map_id}/{hash}/` → `style.json`, `manifest.json`, `report.json`, `sprite*.json/.png` +- **Virtual:** `{uploads}/jeo-mapbox-composed-styles/{scope}/{hash}/` + +### Hash / Cache Key + +`calculate_hash()` / `calculate_virtual_hash()`: sha1 (truncated to 16 hex chars) of +`{version, mapId/scope+kind+id, modifiedGmt, center, zoom, mapSettings, +refs[layerId, modifiedGmt, type, styleId, layerTypeOptionsHash]}`. Including +`modifiedGmt` + `CACHE_VERSION` means any layer/map edit or version bump produces a +new hash (additive — old artifacts linger until cleanup). + +### Reuse Logic + +`get_or_create_artifacts()` / `get_or_create_virtual_artifacts()`: if both `style.json` +and `manifest.json` exist and `force_refresh` is false → reuse. Otherwise: `wp_mkdir_p`, +run `compose_context`, write all three JSON files, persist post meta. + +### Post Meta (saved maps) + +| Meta Key | Purpose | +|----------|---------| +| `_jeo_mapbox_composed_style_hash` | Current hash | +| `_jeo_mapbox_composed_style_warnings` | Warnings array | +| `_jeo_mapbox_composed_style_error` | Last error (cleared on success) | + +### Invalidation + +- `save_post_map` → deletes the three meta keys above (next read regenerates). +- `save_post_map-layer` → finds all maps using the layer (`get_map_ids_for_layer`, + serialized-meta LIKE query) and invalidates each. +- Forced refresh (`?refresh=true`, layer refresh endpoint) → purges the shared style JSON + transients (`purge_ref_style_caches`) **and** bypasses the sprite transients so + recomposition goes back to the sources. +- Artifact write failures are surfaced (`_jeo_mapbox_composed_style_error` + `[JEO]` log + line) instead of being silently ignored — a failing write would otherwise make every + request recompose from scratch. + +### Virtual Cache Cleanup + +`maybe_cleanup_virtual_cache()` runs opportunistically, gated by a transient +`jeo_mapbox_composed_{scope}_cleanup_lock` (1 hour). Deletes up to 50 directories older +than the TTL (`jeo_mapbox_composed_virtual_cache_ttl`). + +## Filters + +| Filter | Default | Purpose | +|--------|---------|---------| +| `jeo_mapbox_composed_styles_enabled` | `true` | Master kill-switch | +| `jeo_mapbox_composed_payload_max_bytes` | `65536` (arg: `$scope`) | Max POST payload size | +| `jeo_mapbox_composed_payload_max_layers` | `40` (onetime) / `100` (preview) (arg: `$scope`) | Max layers per payload | +| `jeo_mapbox_composed_style_fallback_sprite` | `mapbox://sprites/mapbox/standard` | Fallback sprite root (`''` disables) | +| `jeo_mapbox_composed_virtual_cache_ttl` | `30 * DAY_IN_SECONDS` (onetime) / `DAY_IN_SECONDS` (preview) (arg: `$scope`) | Virtual cache TTL | +| `jeo_mapbox_composed_style_default_glyphs` | `mapbox://fonts/mapbox/{fontstack}/{range}.pbf` | Default glyphs passed to frontend (in `class-jeo.php`) | +| `jeo_sprite_cache_ttl` | `DAY_IN_SECONDS` (arg: `$url`) | TTL for the raw sprite JSON/PNG transients (`jeo_sprite_*`); sprites are immutable per style version | + +## Frontend Localization (`jeoMapVars`) + +Injected by `class-jeo.php` (both frontend `enqueue_blocks_assets` and editor contexts): + +| Key | Value | +|-----|-------| +| `composedStyleUrlBase` | `rest_url('jeo/v1/map-style/')` | +| `composedStyleComposeUrl` | `rest_url('jeo/v1/map-style/compose')` | +| `composedStyleDefaultGlyphs` | Filterable default glyph endpoint | +| `nonce` | WP REST nonce (for same-origin `X-WP-Nonce` injection) | + +## Frontend Shared Utilities + +### `shared/composed-style-data.js` + +| Export | Purpose | +|--------|---------| +| `fetchJson(url, options)` | `fetch` wrapper; injects `X-WP-Nonce` for same-origin requests | +| `postJson(url, data, options)` | POST variant with `Content-Type: application/json` | +| `loadComposedStyleData({forceRefresh, includeStyle, mapId, payload, signal, ...})` | Core loader. Payload path → POST `/compose`; mapId path → GET metadata. Fetches manifest (+ style if requested) in parallel. Honors `AbortSignal` for stale-state cancellation. Returns `{manifest, metadata, style}`. | + +### `shared/composed-style-layers.js` + +| Export | Purpose | +|--------|---------| +| `hasComposedStyle(metadata, manifest)` | `metadata?.style && manifest?.layers?.length` | +| `findComposedManifestLayer(manifest, layerId)` | Match manifest layer by `slug` or `layerPostId` | +| `getComposedLayerVisibility(map, manifest, layerId)` | `'visible'` / `'none'` from first composite layer with `visibleWhenLayerOn !== false` | +| `setComposedLayerVisibility(map, manifest, layerId, visibility)` | Apply visibility to all composite layers of a manifest layer | +| `buildComposedInteractionPopupHtml(feature, interactions)` | Popup `

` (title property) + `

` rows from `interaction.fields` (deduped) | +| `addComposedInteractions(map, manifestOrLayers, {shouldIgnoreEvent, visibleLayerIds})` | Bind click/mouseover popups; returns cleanup functions. Mouseover popups use class `jeo-popup__mouseover`. | + +## Editor Integration (`mapbox-style-preview.js`) + +Editor-side adapter. Exports consumed by all editor blocks: + +| Export | Used by | Purpose | +|--------|---------|---------| +| `useComposedMapPreviewStyle({enabled, forceRefresh, mapId, refreshKey})` | `map-editor.js`, `storymap-editor.js` | Saved-map preview via metadata endpoint | +| `useComposedPayloadPreviewStyle({enabled, payload, refreshKey})` | `map-editor-preview.js` (CPT), `onetime-map-editor.js` | Payload preview via POST `/compose` | +| `useMapboxStylePreview(styleUrl)` | `layer-editor-preview.js` | Single raw Mapbox style (does NOT use the composer) | +| `useEditorMapboxTransformRequest(layers)` | all composed editors | Memoized `transformRequest`: replaces `TOKEN_PLACEHOLDER` → encoded token, injects per-owner custom tokens, delegates to base `mapgl-loader.transformRequest` | +| `hasMapboxStyleLayers(layers)` | all editors | Detects if any loaded layer is `type === 'mapbox'` | +| `applyComposedVisibilityFromSettings(map, manifest, layerSettings)` | map editors | Visibility from editor layer settings (swappable/switchable rules) | +| `applyComposedVisibilityFromSelection(map, manifest, selectedLayers)` | storymap | Visibility from selected-layer list per slide | +| `isAbortError(error)`, `handleEditorMapPreviewError(event)` | — | Abort-error handling helpers | + +### AbortSignal Stale-State Guard + +The internal `useComposedPreviewStyle` hook creates an `AbortController` per effect run, +passes `signal` into `loadComposedStyleData`, and aborts on cleanup. The `.then`/`.catch` +handlers check `signal.aborted` before committing state. This cancels stale in-flight +requests when layers/settings change rapidly. + +### Editor Pattern + +All editor blocks follow: load layers → `hasMapboxStyleLayers` check → if true, call the +appropriate composed hook → render `` → wire +`onStyleData={applyComposedVisibility}` → fall back to per-layer `renderLayer` when not +using composed preview. The map `key` includes `composedPreview.metadata?.hash` so React +remounts when the composed artifact changes. + +## Rendering Changes (Frontend `JeoMap`) + +Previously `mapbox`-type layers rendered as **Mapbox Static Tiles raster overlays** added +individually via `JeoLayer.addLayer()`. Now, when a composed style is available, the entire +set of mapbox layers renders as a **single composite Mapbox GL style** passed as the map's +base `style`, with layer visibility/interactions driven by the manifest. Non-mapbox layers +(tilelayer, mvt, mapbox-tileset-*) still render via the legacy individual-add path. + +### `JeoMap` State + +| Field | Purpose | +|-------|---------| +| `composedStyleMetadata` | Metadata response (style/manifest URLs) | +| `composedManifest` | Resolved manifest | +| `usingComposedStyle` | Whether composition succeeded | +| `composedStyleError` | Error message (drives warning UI) | +| `composedInteractionCleanups` | Popup cleanup functions | + +### Three Fetch Paths + +| Method | Trigger | Scope | +|--------|---------|-------| +| `fetchPreviewComposedStyleData()` | `data-preview-map` (editor previewing a saved map; gated to `edit_post`) | `preview` | +| `fetchOnetimeComposedStyleData()` | One-time maps (no `map_id`, has `layers`) | `onetime` | +| `fetchComposedStyleData(mapId)` | Called only by `storymap-display.js` (saved-map endpoint) | saved-map | + +### Fallback Behavior + +If composition fails or is unavailable, `addComposedStyleWarningMessage()` injects a +`.jeomap-composed-style-warning` div and mapbox-style layers are skipped (they cannot +render without composition). + +## Token Handling + +All token values are scrubbed to `TOKEN_PLACEHOLDER` before persistence +(`sanitize_tokens_in_value`). The frontend replaces the placeholder at request time: + +| Context | Mechanism | +|---------|-----------| +| **Frontend** (`class-jeo-map.js`) | `transformRequestUrl` replaces placeholder → `encodeURIComponent(mapbox_key)`, then applies per-owner `customTokens` (`checkCustomToken` per layer) | +| **Editor** (`mapbox-style-preview.js`) | `useEditorMapboxTransformRequest` mirrors the same logic on top of `mapgl-loader.transformRequest` | +| **Discovery** (`discovery/index.js`) | `transformRequestUrl` + `registerLayerCustomToken` (owner from `style_id`/`tileset_id`) | + +Per-layer `layer_type_options.access_token` overrides the global Mapbox key. The composer +resolves tokens per-layer in `build_refs_from_settings()`: if the layer has an `access_token`, +it is used; otherwise the global `mapbox_key` is used. Mapbox-type layers with no resolved token +(neither per-layer nor global) are **skipped** during composition rather than blocking the entire +request. Non-mapbox layers (tilelayer, mvt, etc.) do not require tokens and are always included. + +The composer assigns the resolved token per-bundle and the frontend injects it per-owner via +`transformRequest`. + +For **MVT** layers with a per-layer `access_token`, the composer appends the token directly +to the tile URL (via `add_query_arg`) and bypasses `sanitize_tokens_in_value`, so the real +token reaches the client rather than being replaced by `TOKEN_PLACEHOLDER`. When no per-layer +token is set, the existing sanitize-to-placeholder behavior is preserved. + +## Notes + +- **GD extension**: Sprite merging (`build_composite_sprite`) requires PHP GD + (`imagecreatefromstring`). Without it, sprite generation fails gracefully (warning, no + sprite set) but composition still succeeds. +- **MapLibre compatibility**: `normalize_unsupported_expressions` replaces Mapbox-only + expression operators (`pitch`, `distance-from-center`) with `0` so composed styles render + under MapLibre. +- **Legacy token strings**: Pre-expressions Mapbox styles (streets v7/v8-era) declare + `text-field`/`icon-image` as `"{name_en}"` token strings. MapLibre GL JS 3+ removed token + interpolation, so unmigrated values render the literal `{name_en}` text on the map. + `Jeo\Style_Token_Migrator` (also applied in `Jeo::fetch_mapbox_style()` / + `Jeo::fetch_style_json()` before caching) rewrites them: + - a single `"{name}"` → `["get", "name"]` + - mixed text → `["concat", "text ", ["get", "a"], ...]` + - tokens nested inside expression values (e.g. `["step", ["zoom"], "{abbr}", 6, "{name_en}"]`, + produced when legacy stop-function outputs were tokens) are rewritten in place + - `{{`/`}}` literal-brace escapes are honored; strings that only produce literal text are + left untouched so the migration stays idempotent + - sources are never visited — tile URL templates (`{z}/{x}/{y}`) are safe + - **ordering matters**: the migrator never descends into JSON objects (PHP decoded both + objects and lists as arrays; `array_is_list()` distinguishes them). Raw legacy stop + functions (`{stops: [...]}`) are objects whose outputs must stay raw until + `migrate_legacy_style_functions()` converts them — the composer-side token pass then + converts the token strings that survive inside the resulting step/interpolate + expressions. Descending into the object pre-conversion caused the composer to + literal-wrap the migrated expressions (`["literal", ["get","abbr"]]`), which MapLibre + rejects with `Expected formatted but found array instead.` + - **scope invariant**: only strings containing `{}` are ever rewritten — token-free + values (font stacks, offsets, colors) are untouched, and `wrap_function_output()` + keeps its original unconditional literal-wrapping for array outputs. The style + transients cache the raw response body and derive (decode + normalize) on every + read, so changing normalization rules never invalidates cache entries and entries + from the regression window (derived arrays) are simply cache misses + Conversions are counted per style and reported as a composer warning. +- **Legacy stop functions**: Old Mapbox styles (pre-expressions era, e.g. streets v7-based + styles) declare paint/layout values as `{base, stops}` function objects, which MapLibre + rejects with `Bare objects invalid. Use ["literal", {...}]`. Right after fetching each + bundle style, `migrate_legacy_style_functions()` converts them to expressions: + - zoom/property exponential (numeric or color outputs) → + `["interpolate", ["linear"|"exponential", base], ["zoom"|["get", p]], ...]` + - interval, enum, or `array` outputs (e.g. `fill-translate`, `text-offset`, + `line-dasharray`) → `["step", ...]` with `["literal", ...]`-wrapped arrays — the + MapLibre runtime **rejects interpolated array outputs** + (`Type array is not interpolatable`), so arrays must always go through `step` + - categorical property functions → `["match", ["get", p], ...]`; `type: "identity"` → + `["get", p]` + - composite zoom-and-property functions → `interpolate` over `zoom` with per-zoom + property expressions + Only layer `paint`/`layout` sections are walked — sources/GeoJSON are never touched. + Conversions are counted per style and reported as a composer warning. Bumping + `CACHE_VERSION` is required when changing conversion rules so cached artifacts regenerate. +- **`jeo:composed` / `jeo:source` metadata**: Composite layers are tagged with `jeo:composed` + and source-association metadata for traceability. diff --git a/.architecture/context/README.md b/.architecture/context/README.md new file mode 100644 index 000000000..9739b4aca --- /dev/null +++ b/.architecture/context/README.md @@ -0,0 +1,433 @@ +# AI Context Assistant — Editorial Suggestions + +The AI Context Assistant is a Gutenberg sidebar plugin that suggests new paragraphs and related article references for editorial posts. It uses the same Assistant architecture as the minimap: structured output, sub-agents, tools, conversation storage in `post_meta`, and user memory in `user_meta`. + +## Key Files + +### Backend (PHP) + +| File | Role | +|------|------| +| `src/includes/ai/class-context-agent.php` | Agent factory (`Assistant::configure`) with `retrieve_knowledge` and `get_post_content` tools, `post_analyzer` sub-agent, structured output (`Context_Generation_Output`), triple storage (conversation/learning/user memory) | +| `src/includes/ai/class-context-handler.php` | REST handler: `/context/setup`, `/context/chat`, `/context/state`, `/context/clear`. Dual storage: `ConversationStore` for AI context, `_jeo_ai_context_chat_messages` for clean UI messages | +| `src/includes/ai/class-context-generation-output.php` | Structured output DTO: `paragraphs` (with inline HTML), `references`, `message`, `assistant_message` | +| `src/includes/ai/class-retrieve-knowledge-tool.php` | NeuronAI tool — queries `jeo_knowledge` via `RAG_Agent::resolveRetrieval()`. Returns `post_id`, `title`, `excerpt`, `url`, `date` (ISO `Y-m-d`, from `WP_Post_Data_Loader` metadata), and `score` per result | +| `src/includes/ai/class-get-post-content-tool.php` | NeuronAI tool — post content + `_related_point` meta for sub-agent | +| `src/includes/ai/settings/tab-context.php` | Settings tab template — custom prompt textarea, default prompt reference, "Suggest initial prompt" button, and "AI Context Prompt Engineer Assistant" with a separate natural-language description box whose content is saved to localStorage | + +### Frontend (JS) + +The sidebar asset (`jeo-context-sidebar`) is only enqueued when the AI integration is correctly configured — see the gate in `class-jeo.php::enqueue_blocks_assets()` (`AI_Handler::is_configured()`). With no provider/key set, the "AI Context" panel does not exist in the editor at all. + +| File | Role | +|------|------| +| `src/js/src/context-sidebar/index.js` | Entry point — `registerPlugin('jeo-context-sidebar')` with `PluginDocumentSettingPanel` | +| `src/js/src/context-sidebar/context-chat-panel.js` | Chat UI, state management, API calls, expand modal | +| `src/js/src/context-sidebar/suggested-paragraphs.js` | Renders suggested paragraphs with inline HTML support. "Insert" creates `core/paragraph` block; "Copy" uses triple-fallback rich-text clipboard | +| `src/includes/ai/class-neuron-agent.php` | Base `Neuron_Agent` extending `NeuronAI\Agent\Agent`; reused by the prompt engineering assistant | +| `src/js/src/context-sidebar/context-sidebar.css` | Styles for panel, chat, suggestions, modal | +| `src/includes/ai/class-context-validator.php` | Post-generation link/reference validation *(planned)* | + +## Architecture Overview + +```mermaid +graph TB + subgraph "Gutenberg Editor" + PH[Intro State
Generate Suggestions button] -->|Click| API1[/context/setup] + CP[Chat Panel
Document sidebar] -->|Send/Retry| API2[/context/chat] + CP -->|Clear| API3[/context/clear] + EXP[Expand Modal] -->|Same chat UI| API2 + end + + subgraph "Backend — Context_Handler" + API1 --> SETUP[api_setup] + API2 --> CHAT[api_chat] + API3 --> CLEAR[api_clear] + GETST[/context/state GET] --> STATE[api_get_state] + SETUP --> RUN[run_agent] + CHAT --> RUN + end + + subgraph "AI Agent (hacklabr/ai-assistant)" + RUN --> AGENT[Context_Agent::create] + AGENT --> MAIN[Main Agent] + AGENT --> SUB[post_analyzer
Sub-Agent] + MAIN --> T1[Retrieve_Knowledge_Tool
jeo_knowledge] + SUB --> T2[Get_Post_Content_Tool] + MAIN --> OUT[Structured Output
Context_Generation_Output] + end + + subgraph "Storages" + S1[WP_Storage
post_meta
conversations] + S2[WP_Option_Storage
wp_options
learning] + S3[WP_User_Memory_Storage
user_meta
user memory] + end + + AGENT --> S1 + AGENT --> S2 + AGENT --> S3 + + RUN --> PERSIST[persist_history
save_context_state
save_chat_message] + PERSIST --> META[(post_meta
3 meta keys)] + STATE --> META +``` + +## REST Endpoints + +### `POST /jeo/v1/context/setup` — Generate initial editorial suggestions + +Analyzes the current post content via the AI agent. If the post has fewer than 100 characters (after stripping tags), returns immediately without calling the AI, asking the user to write more. + +| Parameter | Type | Required | Description | +|-----------|------|----------|-------------| +| `post_id` | int | Yes | Post to analyze | +| `conversation_id` | string | Yes | UUID for conversation continuity | + +Response: `{ success, paragraphs[], references[], message, assistant_message }` + +### `POST /jeo/v1/context/chat` — Multi-turn conversation for refinement + +Refines suggestions through conversation. The agent maintains context via conversation storage. + +| Parameter | Type | Required | Description | +|-----------|------|----------|-------------| +| `post_id` | int | Yes | Post ID for conversation storage | +| `conversation_id` | string | Yes | UUID for conversation continuity | +| `message` | string | Yes | User message | +| `current_state` | object | No | Live editor state (`paragraphs`, `references`) — injected as context so the AI knows the current suggestions | + +Response: `{ success, paragraphs[], references[], message, assistant_message }` + +### `GET /jeo/v1/context/state` — Load persisted conversation state + +Returns the full conversation state for a post: conversation ID, chat messages, last suggestions, and references. Used on mount to restore a previous session. + +| Parameter | Type | Required | Description | +|-----------|------|----------|-------------| +| `post_id` | int | Yes | Post to load state for | + +Response: `{ success, has_started, conversation_id?, messages[], paragraphs?, references?, message? }` + +Messages include a `username` field for user messages (resolved from `user_id`). + +### `POST /jeo/v1/context/clear` — Reset conversation and suggestions + +Deletes all four meta keys for the post and removes the raw AI conversation thread from `ConversationStore`, resetting the conversation completely. + +| Parameter | Type | Required | Description | +|-----------|------|----------|-------------| +| `post_id` | int | Yes | Post to clear state for | + +Response: `{ success, message }` + +### `POST /jeo/v1/context/engineer-prompt` — Generate a custom system prompt + +Uses the configured AI provider to turn a natural-language description into a highly optimized system prompt while preserving the user's intent and enforcing the plugin's critical rules. Requires `manage_options`. + +| Parameter | Type | Required | Description | +|-----------|------|----------|-------------| +| `prompt` | string | Yes | Natural-language description of the desired assistant behavior | + +Response: `{ success, prompt }` or `{ success: false, message }` + +This endpoint powers the **"Generate Custom Prompt"** button in the AI Context Prompt Engineer Assistant card on the Context Assistant settings tab. + +## Agent Architecture + +### Context_Agent Factory + +`Context_Agent::create()` builds an `Assistant` with: + +```mermaid +graph LR + A[AssistantConfig] --> B[Main Agent] + A --> C[post_analyzer sub-agent] + B --> D[Retrieve_Knowledge_Tool] + B --> E[Get_Post_Content_Tool] + C --> E + B --> F[Context_Generation_Output
structured output] + B --> G[autoLearn: true] + B --> H[autoDelegate: true] + B --> I[requireLearningCheck: true] +``` + +### Configuration + +| Property | Value | Purpose | +|----------|-------|---------| +| `outputClass` | `Context_Generation_Output::class` | Enforces structured output schema | +| `conversationStorage` | `WP_Storage(post_id, 'post')` | Chat history per post | +| `learningStorage` | `WP_Option_Storage` | Global agent self-improvement | +| `userMemoryStorage` | `WP_User_Memory_Storage(user_id)` | Per-user preferences | +| `structuredMaxRetries` | 3 | Retries on schema violation (aligned with `run_agent()` exponential backoff) | +| `contextWindow` | 200000 | Token context window | +| `tools` | `retrieve_knowledge`, `get_post_content` | Registered via `Tool_Registry` | +| `subAgents` | `post_analyzer` | Analyzes post content for topics/gaps | + +### System Prompt + +The system prompt is loaded via `Context_Agent::system_prompt()`: + +1. **Custom prompt** — If `ai_use_context_custom_prompt` is enabled and `ai_context_prompt` is non-empty, uses it as the base. The stored value may be either a legacy plain-text prompt or a structured-output JSON object such as `{"prompt": "..."}`. Always route the stored value through `Context_Agent::extract_prompt_text()` before using it as a system prompt; this keeps the runtime unaffected even if the storage format changes later. +2. **Default prompt** — Otherwise, uses `Context_Agent::default_system_prompt()` which defines the editorial assistant role, workflow, tool usage rules, output schema, editorial guidelines, off-topic handling, and tool error handling. The default prompt always appends `Context_Agent::critical_prompt_rules()`. +3. **Critical Rules** — `Context_Agent::critical_prompt_rules()` contains non-negotiable instructions for inline contextual links, factual grounding (including a no-self-reference rule: the post being edited must never appear as a source, link target, or text reference in generated paragraphs), references array, language, recency tie-breaking (when retrieved articles have similar relevance scores, the more recently published one is preferred; recency is a tie-breaker only), and multi-theme retrieval (one query per theme, integrated paragraph with per-theme fallback). They are automatically included in the default prompt and enforced in custom prompts by the prompt engineering assistant. Additional rules forbid combining facts from multiple references into a single unattributed claim and require retracting terms/facts that the user marks as unsupported. +4. **User Preferences** — Appends `## User Preferences` section from `WP_User_Memory_Storage` (if any preferences exist). +5. **Additional Context** — Appends `## Additional Context` section with post metadata and locale from the caller. + +When editing the prompt, preserve the `## User Preferences` and `## Additional Context` injection points or user memory and live state will be lost. + +> **Storage format note:** The settings UI stores `ai_context_prompt` as structured-output JSON (`{"prompt":"..."}`) while rendering it as plain text. Any code that reads this option for runtime use must call `Context_Agent::extract_prompt_text()` instead of using the raw option value directly. + +### post_analyzer Sub-Agent + +The `post_analyzer` sub-agent uses `Get_Post_Content_Tool` to read the post and returns a JSON analysis: + +| Field | Description | +|-------|-------------| +| `topics` | Main subjects | +| `tone` | Editorial tone | +| `gaps` | Content gaps/angles not explored | +| `key_facts` | Important facts mentioned | +| `target_audience` | Intended audience description | +| `suggested_search_queries` | 3–5 queries for `retrieve_knowledge` | + +### Multi-Theme Retrieval + +When the user asks to combine N themes or angles (N ≥ 2), the prompt (default + critical rule 7) mandates **one targeted `retrieve_knowledge` query per theme, never a single blended query**. A blended multi-theme query dilutes dense vector similarity, so the top results concentrate on the 1–2 strongest themes and the rest lose coverage. + +`Retrieve_Knowledge_Tool` supports this via an optional `queries` array property (1–6 strings; the legacy single `query` string still works). A single call with `queries` runs one retrieval per theme, then merges and deduplicates by `post_id` (best score wins) and interleaves results by per-query rank — all first-ranked docs first — so every theme keeps representation before the global `top_k` cap (default 5, max 20) is applied. + +Generation follows the same contract: **one integrated paragraph when the retrieved references genuinely support the synthesis; one paragraph per theme as fallback, with a plain statement in `assistant_message`**, when the sources do not support combining. The model must never force a cross-theme paragraph without grounded sources. + +## Structured Output (Context_Generation_Output) + +| Property | Type | Description | +|----------|------|-------------| +| `paragraphs` | `array` | Suggested paragraphs. Each has `text` (supports inline HTML: ``, ``, ``) and `relevance_score` (int 0–100). Link anchors must be the specific phrase, name, fact, or number the reference supports — never the full article title | +| `references` | `array` | Related articles from the knowledge base. Each has `post_id` (int), `title` (string), `url` (string), `reason` (string) | +| `message` | `string` | Cumulative summary shown as UI notice. Plain text — no HTML | +| `assistant_message` | `string` | Chat message shown in the panel. Plain text — no HTML | + +## Post-Generation Validation + +`Context_Handler::validate_generated_output()` runs after every AI response in `api_setup()` and `api_chat()`: + +1. Extracts all `anchor` tags from paragraph texts. +2. Removes self-references from the `references` array (entries whose `post_id` or `url` match the post being edited). +3. Strips self-links from paragraph text (links pointing to the post's own permalink are converted to plain text silently). +4. Verifies remaining link URLs exist in the `references` array. +5. Links to URLs not listed in references are converted to plain text; a verification note is appended to `assistant_message`. + +Anchor quality (whether the linked phrase is grounded in the referenced article) is enforced by the system prompt (`Context_Agent::critical_prompt_rules()`) and verified by the human editor. The prompt instructs the AI to prefer short, specific anchors that closely reflect the source, and to never attach a real URL to a phrase the source does not support. + +Self-referencing is also enforced at the prompt level: `critical_prompt_rules()` forbids the AI from referencing the post being edited (as a source, link target, or text mention) in generated paragraphs. The backend validation (steps 2–3 above) serves as a safety net for any self-links that slip through. + +## Dual Conversation Storage + +The system uses two separate storage mechanisms to avoid JSON schema pollution from structured output: + +```mermaid +graph TB + subgraph "AI Context Storage (raw)" + CS[ConversationStore
WP_Storage → post_meta] --> K1[_jeo_ai_conversations_{uuid}] + K1 --> |Schema-injected messages
tool calls/results| AI[Agent reads on next call] + end + + subgraph "UI Display Storage (clean)" + META[_jeo_ai_context_chat_messages] --> |User text + assistant_message
+ user_id + timestamp| UI[Frontend renders messages] + end + + subgraph "State Storage" + CID[_jeo_ai_context_conversation_id] --> |UUID| STATE[api_get_state reads] + LR[_jeo_ai_context_last_response] --> |paragraphs + references| STATE + MSG[_jeo_ai_context_chat_messages] --> |messages with usernames| STATE + end +``` + +**Key rule**: `api_get_state()` reads from `_jeo_ai_context_chat_messages` for messages and `_jeo_ai_context_last_response` for suggestions. It does **not** read from `ConversationStore`. + +## Conversation History Persistence + +```mermaid +sequenceDiagram + participant H as Context_Handler + participant A as Assistant + participant CH as ChatHistory + participant CS as ConversationStore (post_meta) + + H->>CS: loadThread(conversation_id) + CS-->>H: [{role, content}, ...] + H->>A: getChatHistory() + H->>CH: addMessage(UserMessage/AssistantMessage) × N + H->>A: structured(UserMessage) + Note over A: Agent sees full history + new message + A-->>H: Context_Generation_Output + H->>A: getChatHistory()->getMessages() + H->>H: Filter to user/assistant text messages + H->>CS: saveThread(conversation_id, [{role, content}, ...]) +``` + +**Serialization**: Only `user` and `assistant` text messages are stored (tool calls/results excluded). Each message is `{role: string, content: string}`. Stored via `ConversationStore` → `WP_Storage` → `update_post_meta()`. + +## Editor State Machine + +```mermaid +stateDiagram-v2 + [*] --> restoring: Component mount + restoring --> idle: No prior state + restoring --> active: Prior state found + idle --> active: "Generate Suggestions" (setup) + active --> active: Send message (chat) + active --> active: Retry (chat with implicit message) + active --> idle: Clear (deletes all meta) + + state idle { + [*] --> show_intro + note right of show_intro: "Generate Suggestions" button visible + } + + state active { + [*] --> chat_panel + chat_panel --> chat_panel: Send / Retry / Expand + } +``` + +## Storage Strategy + +| Storage | Class | Backend | Key | Purpose | +|---------|-------|---------|-----|---------| +| Conversation | `WP_Storage` → `ConversationStore` | `post_meta` | `_jeo_ai_conversations_{uuid}` | Raw AI history (includes schema messages) | +| UI Messages | Direct `update_post_meta()` | `post_meta` | `_jeo_ai_context_chat_messages` | Clean messages for UI (user text + assistant_message + user_id) | +| Last Response | Direct `update_post_meta()` | `post_meta` | `_jeo_ai_context_last_response` | Latest paragraphs + references + message | +| Conversation ID | Direct `update_post_meta()` | `post_meta` | `_jeo_ai_context_conversation_id` | UUID for thread lookup | +| Learning | `WP_Option_Storage` | `wp_options` | Global | Agent self-improvement across sessions | +| User Memory | `WP_User_Memory_Storage` | `user_meta` | `_jeo_ai_memories_*` | Per-user preferences | + +## Post Meta Keys + +| Meta Key | Type | Registered | Description | +|----------|------|-----------|-------------| +| `_jeo_ai_context_conversation_id` | `string` | Yes (`show_in_rest`) | Active conversation UUID | +| `_jeo_ai_context_last_response` | `object` | Yes (`show_in_rest`) | Last generated `{paragraphs, references, message}` | +| `_jeo_ai_context_chat_messages` | `object` | Yes (`show_in_rest`) | Clean UI messages array `[{role, content, timestamp, user_id?}]` | + +All three are registered per post type via `register_post_meta()` for each type in `enabled_post_types`. Auth callback requires `edit_posts`. + +## Retry Logic + +`run_agent()` implements a retry loop with exponential backoff: + +- **Max attempts**: 3 (initial + 2 retries) +- **Backoff**: `sleep(min(attempt * 2, 8))` seconds between retries +- **Retryable**: Network errors, 5xx, 429 (rate limit), timeouts, empty AI response (`TypeError` on `getJson()`) +- **Non-retryable**: 4xx errors (except 429) — auth failures, invalid config, bad requests. Classification uses `HttpException->response->statusCode` (numeric), not string matching. +- **Exhausted retries**: Throws user-friendly exception: "The AI did not respond after multiple attempts. Please try again or rephrase your request." + +## Error Persistence + +When `run_agent()` exhausts all retries, the user message is still persisted to **both** storage layers before throwing: + +1. **UI messages** (`_jeo_ai_context_chat_messages`) — saved in `api_chat()` **before** calling `run_agent()`, so it survives any AI failure. +2. **ConversationStore** (AI thread) — appended via `appendToThread()` after the retry loop exits as the pair `user` + synthetic `assistant` failure note, so the next successful call sees the unanswered message via `inject_history()`. The synthetic assistant reply is required: a bare `user` append would leave the thread ending in an unmatched `user`, and the AI library rejects the same-role adjacency with "Invalid message sequence" on the next replay (its alternation validator runs on every injected message). + +This ensures the user's message is visible after a page reload and the AI retains context for subsequent turns. `api_setup()` does **not** persist on error (system-generated message, no prior context to lose). Both handlers replay threads through `Jeo\AI\Thread_Normalizer::normalize_thread_messages()`, which merges consecutive same-role messages and drops leading `assistant` / trailing orphan `user` entries — self-healing threads poisoned before the fix. + +## Content Validation + +`api_setup()` checks post content length before calling the AI: + +- **Fewer than 100 characters** (after `wp_strip_all_tags()`): Returns immediately without calling the AI. Saves a synthetic user message and an assistant message asking the user to write more or specify what they want. +- **100+ characters**: Proceeds with AI generation. + +## Frontend Components + +### `index.js` — Plugin Registration + +- Registers `jeo-context-sidebar` plugin with `PluginDocumentSettingPanel` +- Passes `postId`, `title`, `content`, `postType` to `ContextChatPanel` +- Post type gate: Only loads for post types in `enabled_post_types` (enforced by PHP asset enqueue in `class-jeo.php`) + +### `context-chat-panel.js` — Main Chat Panel + +State management and API interaction: + +| State | Type | Purpose | +|-------|------|---------| +| `conversationId` | string | UUID for conversation continuity | +| `messages` | array | Chat messages for display | +| `inputValue` | string | Textarea content | +| `isLoading` | boolean | API call in progress | +| `isRestoring` | boolean | Loading persisted state on mount | +| `error` | string/null | Error notice | +| `suggestedParagraphs` | array | Current paragraph suggestions | +| `references` | array | Current article references | +| `isExpanded` | boolean | Modal open state | +| `hasStarted` | boolean | Whether a conversation exists | + +**Lifecycle**: +1. On mount → `loadState()` calls `GET /context/state` to restore previous session +2. If no prior state → generates new UUID, shows intro with "Generate Suggestions" button +3. If prior state found → restores messages, paragraphs, references, conversation ID + +**Actions**: + +| Action | Endpoint | Method | Behavior | +|--------|----------|--------|----------| +| Generate Suggestions | `/context/setup` | `runSetup()` | Creates initial suggestions from post content | +| Send | `/context/chat` | `sendMessage()` | Submits user message, receives updated suggestions | +| Retry | `/context/chat` | `handleRetry()` | Sends implicit "Generate new editorial suggestions..." message | +| Clear | `/context/clear` | `handleClear()` | Deletes all state, generates new UUID | + +### `suggested-paragraphs.js` — Paragraph Cards + +Renders each suggested paragraph with inline HTML support: + +- **HTML Sanitization**: `sanitizeHtml()` uses DOM parser walk. Whitelist: `strong`, `b`, `em`, `i`, `br`, `a` (with href). All other tags stripped but text preserved. +- **Entity Decoding**: `decodeHtmlEntities()` handles escaped HTML from structured output. +- **Insert**: Creates `core/paragraph` block with `content: sanitizedHtml` via `wp.data.dispatch('core/block-editor').insertBlocks()`. Tracks inserted indices to prevent duplicates. +- **Copy**: Triple-fallback rich-text clipboard: + 1. `navigator.clipboard.write()` with `ClipboardItem` (`text/html` + `text/plain`) + 2. DOM selection + `document.execCommand('copy')` (most reliable in Gutenberg iframe) + 3. `navigator.clipboard.writeText()` (plain text only) + +### User Badge + +User messages display a username badge resolved from `msg.username` (returned by `api_get_state()` from `user_id` via `get_userdata()`). Useful for collaborative editing. + +### Expand Modal + +Compact sidebar panel has an expand button (top-right) that opens a `Modal` component with the same chat UI in a larger viewport. + +## Settings Integration + +The Context Assistant settings are in **JEO → AI Configuration → Context Assistant** tab. + +| Setting | Key | Type | Default | Description | +|---------|-----|------|---------|-------------| +| Use custom prompt | `ai_use_context_custom_prompt` | checkbox | `false` | Toggle between default and custom system prompt | +| Custom prompt | `ai_context_prompt` | textarea (stored as structured-output JSON) | empty | Custom system prompt text. The UI stores it internally as `{"prompt":"..."}`, but displays and edits it as plain text. Empty = use built-in default | +| Default prompt (read-only) | — | textarea (readonly) | Built-in | Shows the default prompt for reference | + +When `ai_use_context_custom_prompt` is unchecked, the tab shows the default prompt in a readonly textarea for reference. When checked, it shows an editable textarea for the custom prompt plus a helper button: + +- **Suggest initial prompt** — copies the built-in default prompt into the custom textarea as a starting point. + +Below the prompt editor, an **AI Context Prompt Engineer Assistant** card mirrors the Provider tab layout: + +- A separate natural-language description textarea where editors describe the desired assistant behavior. The description is automatically saved to `localStorage` (`jeo_context_prompt_description`) so it survives page reloads. +- A **Generate Custom Prompt** button that calls `POST /jeo/v1/context/engineer-prompt` and writes the generated system prompt into the custom prompt textarea above, automatically enabling the custom prompt toggle so the result is visible. + +## Conventions + +- **Manual trigger**: Panel does not auto-generate on open. User must click "Generate Suggestions". +- **Content gate**: Setup returns early if post content < 100 characters. +- **Dual storage**: `ConversationStore` for AI context (may contain schema messages), separate `post_meta` for clean UI messages. Never mix them. +- **Inline HTML**: Paragraph `text` supports ``, ``, ``, `
`. Frontend sanitizes before rendering/inserting/copying. +- **Inline contextual links**: Link anchors must be specific phrases, names, facts, or numbers from the referenced article. Never use the full article title as link text. +- **Factual grounding**: Every factual claim must come from `get_post_content` or `retrieve_knowledge`. The agent must not invent names, dates, statistics, places, or events. +- **Plain text messages**: `message` and `assistant_message` fields must be plain text — no HTML tags. `wp_strip_all_tags()` is applied in the handler. +- **Retry with backoff**: Up to 3 attempts with exponential sleep on transient errors. +- **Post type gate**: Only loads for post types in `enabled_post_types` (same filter as geolocation and minimap). +- **Expand modal**: Compact panel with expand button for better chat UX in a full modal. +- **Insert tracking**: Frontend tracks inserted paragraph indices to prevent duplicate block insertion. +- **State restoration**: On mount, `GET /context/state` restores the previous session so closing/reopening the panel or refreshing the page does not lose context. diff --git a/.architecture/deployment/README.md b/.architecture/deployment/README.md new file mode 100644 index 000000000..73d585e8a --- /dev/null +++ b/.architecture/deployment/README.md @@ -0,0 +1,62 @@ +# WordPress.org Deployment + +## Process + +Deployment is automated via GitHub Actions (`deploy-wordpress-org.yml`) on tag push. + +## .wordpress-org/ Assets + +| File | Description | +|------|-------------| +| `banner-772x250.png` | Plugin page banner | +| `icon-128x128.png` | Plugin icon | +| `screenshot-*.png` | Screenshots (7 screenshots) | + +## WP.org Compliance + +The `check-wporg-compliance.mjs` script verifies: +- No disallowed files/binaries +- GPL-compatible license +- Correct plugin headers +- No phoning home +- Assets in correct paths + +## Versioning + +Version is synced in 3 places via `sync-plugin-version.mjs`: +1. `package.json` → `version` +2. `src/jeo.php` → `JEO_VERSION` constant +3. `src/readme.txt` → `Stable tag` + +## Release Structure + +The plugin is deployed as the contents of `src/` (not repo root): +- `src/` contains the complete WordPress plugin +- `node_modules/`, `scripts/`, `docs/` are not deployed +- `src/js/build/` (compiled) is included +- `src/vendor/` (composer) is included if present +- `src/includes/vendor/` (bundled third-party assets such as Select2) is included + +## Activation/Deactivation + +### Activation (`jeo_activate`) +- Registers CPTs +- Flushes rewrite rules + +### Deactivation (`jeo_deactivate`) +- Clears configured API keys +- Clears debug logs +- Flushes rewrite rules + +### Uninstall (`uninstall.php`) +- Deletes `jeo-settings` option +- Deletes `jeo_bulk_ai_cron_logs` and `jeo_ai_embedding_tokens` options +- Clears scheduled cron hooks (`jeo_bulk_ai_cron_hook`, `jeo_bulk_ai_clear_cron_hook`) +- Deletes all `jeo-ai-log` posts +- Deletes all geolocation post meta (`_related_point`, `_geocode_*`) +- Deletes Nominatim transients +- Removes RAG vector store directory (`wp-content/uploads/jeo-ai-store/`) + +### Uninstall Confirmation Page + +To prevent accidental data loss, the plugin overrides the default **Delete** link on `plugins.php` with a confirmation screen (`admin.php?page=jeo-uninstall-confirm`). This screen lists every type of data that will be removed and requires the user to check an explicit acknowledgment before proceeding.},{ diff --git a/.architecture/discovery/README.md b/.architecture/discovery/README.md new file mode 100644 index 000000000..ec1862095 --- /dev/null +++ b/.architecture/discovery/README.md @@ -0,0 +1,77 @@ +# Discovery (Map Exploration) + +## Key Files + +| File | Role | +|------|------| +| `src/js/src/discovery/index.js` | `Discovery` class — main app (incl. `transformRequestUrl`, `registerLayerCustomToken`, `composedStyleDefaultGlyphs`) | +| `src/js/src/discovery/blocks/sidebar.js` | Tabbed sidebar | +| `src/js/src/discovery/blocks/stories.js` | Stories tab (posts) | +| `src/js/src/discovery/blocks/map-layers.js` | Map layers tab (composed Mapbox style layer support) | +| `src/js/src/discovery/blocks/map-item.js` | Individual map card | +| `src/js/src/discovery/blocks/search.js` | Search input | +| `src/js/src/discovery/blocks/date-range-filter.js` | Date range filter | +| `src/js/src/discovery/blocks/tag-filter-control.js` | Tag filter | +| `src/templates/discovery.php` | Discovery page template | +| `src/templates/embed-discovery.php` | Embed template | + +## Overview + +Discovery is a standalone application (not a Gutenberg block) that renders: +1. Fullscreen map (MapLibre/Mapbox) +2. Sidebar with tabs: **Stories** and **Map Layers** +3. Sharing toolbar + +## DOM Mount + +- Target element: `#discovery` or `.discovery-embed` +- Template: `discovery.php` (page template) or `embed-discovery.php` + +## Tab: Stories + +1. Fetches posts via WP REST API with filters (search, date range, tags) +2. Builds clustered GeoJSON from posts with `_related_point` +3. Renders markers + clusters on map +4. Infinite scroll in sidebar +5. Hover highlights feature via `setFeatureState` +6. Click opens popup with details + +## Tab: Map Layers + +1. Fetches maps via WP REST API +2. Expands each map showing its layers +3. Toggle individual layers (add/remove from map) +4. Drag-to-reorder selected layers (`react-movable`) +5. Applies changes to map: `addSource`/`addLayer`/`removeLayer`/`moveLayer` + +### Composed Mapbox Style Layers + +`mapbox`-type layers in Discovery compose per-map. Key methods in `blocks/map-layers.js`: + +- `fetchComposedMap(mapId)` — cached per-mapId; fetches metadata then style+manifest +- `addComposedMapboxLayer(layer)` — extracts the manifest layer, clones composite layer + definitions, preloads sprites via a sprite atlas pipeline (`loadSpriteAtlas` with `@2x` + fallback, `addImageFromAtlas`, `registerStyleImageMissingHandler`), ensures glyphs + (`ensureMapGlyphs` + `composedStyleDefaultGlyphs`), adds layers before + `unclustered-points`, and binds interactions (`addComposedInteractions`, ignoring events + over story features). State tracked in `composedLayerState`. +- `removeComposedMapboxLayer` — cleans interactions, removes layers and unused sources +- `applyLayersChanges` — sequence-guarded (`applyLayersSequence`) for rapid toggles + +Token handling uses `transformRequestUrl` (placeholder replacement + per-owner custom +tokens via `registerLayerCustomToken`). See [`composed-styles/README.md`](../composed-styles/README.md). + +## Sharing + +The toolbar supports: +- WhatsApp +- Twitter/X +- Email +- Embed iframe (copies URL) +- URL-based state (share link preserves position/zoom/layers) + +## Webpack Entry Point + +| Entry | File | Dependency | +|-------|------|------------| +| `discovery` | `discovery/index.js` | `jeoMap` | diff --git a/.architecture/frontend/README.md b/.architecture/frontend/README.md new file mode 100644 index 000000000..c80ee1f4c --- /dev/null +++ b/.architecture/frontend/README.md @@ -0,0 +1,194 @@ +# Frontend: MapLibre / Mapbox / React + +## Key Files + +| File | Role | +|------|------| +| `src/js/src/lib/mapgl-loader.js` | Runtime selector (MapLibre or Mapbox) | +| `src/js/src/lib/mapgl-react.js` | Re-export React components | +| `src/js/src/lib/maplibregl/maplibregl-loader.js` | MapLibre GL loader | +| `src/js/src/lib/maplibregl/maplibregl-react.js` | MapLibre React wrapper | +| `src/js/src/lib/mapboxgl/mapboxgl-loader.js` | Mapbox GL loader | +| `src/js/src/shared/` | Shared utilities | + +## Dual Runtime: MapLibre vs Mapbox + +### Automatic Selection + +``` +mapgl-loader.js + ├── If map_runtime_requested == 'mapbox' AND mapbox_key exists + │ └── Uses mapboxgl-loader.js (globalThis.mapboxgl) + └── Else + └── Uses maplibregl-loader.js (bundled) +``` + +### MapLibre GL (Default) + +- **Bundled** via npm (`maplibre-gl`) +- Enriched with `map-gl-utils` for convenience +- Default style: OSM raster tiles +- CSS injected programmatically + +### Mapbox GL (Optional) + +- **NOT bundled** — uses `globalThis.mapboxgl` loaded via `wp_enqueue_script` +- Token injected via `transformRequest` +- Patches for missing/deprecated APIs: `getLight()`, `getSky()`, `setSky()` + +## Iframe Compatibility (Block API v3) + +Gutenberg Block API v3 renders blocks in iframes. The loaders apply extensive patches: + +### HTMLElement/MouseEvent instanceof + +```javascript +// Cross-document instanceof fix +HTMLElement[Symbol.hasInstance] = function( obj ) { + return obj instanceof iframeHTMLElement || originalCheck( obj ); +}; +``` + +### FullscreenControl + +Patched to use `document.fullscreenElement` from iframe container. + +### Drag Listeners + +React wrapper duplicates listeners from parent document to iframe document. + +### ResizeObserver + +Used to detect map container size changes. + +## React Components + +### Map (`mapgl-react.js`) + +Wrapper around `react-map-gl/maplibre` with: +- Automatic `FullscreenControl` and `NavigationControl` +- ResizeObserver for auto-resize +- Event forwarding: `onMove`, `onZoom`, `onSourceData`, `onError` + +### Layer & Source + +Re-exports from `react-map-gl/maplibre`: +- `Source`: raster, vector, geojson, image, video +- `Layer`: fill, line, symbol, circle, heatmap, raster, hillshade, background + +## Shared Utilities + +| File | Export | Description | +|------|--------|-------------| +| `shared/rest-records.js` | `useRecordsByIds`, `usePaginatedRecords` | Hooks for WP REST API | +| `shared/schema-form.js` | `SchemaForm` | JSON Schema form (RJSF + WP components) | +| `shared/wp-form-controls.js` | `CheckboxControl`, etc. | WP components with next-gen props | +| `shared/story-geojson.js` | `buildRelatedPostsGeoJson` | GeoJSON builder with FNV-1a hash IDs | +| `shared/async-combobox-control.js` | `AsyncComboboxControl` | Async combobox with loading | +| `shared/direction.js` | `computeInlineStart/End` | LTR/RTL support | +| `shared/intersect.js` | `onFirstIntersection` | Lazy init via IntersectionObserver | +| `shared/wait.js` | `waitMapEvent`, `waitUntil` | Promise-based event waiters | +| `shared/locale.js` | `getDocumentLocale` | Locale utilities | +| `shared/intl.js` | `formatDate`, `formatHour` | Intl API wrappers | +| `shared/styles.js` | `EMPTY_STYLE` | Empty MapLibre style | +| `shared/url-normalization.js` | `normalizeOptionalUrl` | URL helper | +| `shared/composed-style-data.js` | `loadComposedStyleData`, `fetchJson`, `postJson` | Composed style loader (AbortSignal-aware, nonce injection) — see [`composed-styles/README.md`](../composed-styles/README.md) | +| `shared/composed-style-layers.js` | `hasComposedStyle`, `setComposedLayerVisibility`, `addComposedInteractions`, etc. | Manifest-driven visibility/interactions over composite style layers | + +## Editor↔Frontend Layer Rendering Parity + +The editor (`map-blocks/`) and frontend (`jeo-map/`) use **separate rendering code** but must produce visually consistent results. The key divergence point is `load_as_style` for `mapbox`-type layers. + +### `load_as_style` (Mapbox Style as Base) + +When a `mapbox` layer instance has `load_as_style: true`, the full Mapbox vector style replaces the map's base: + +| Context | Mechanism | +|---------|-----------| +| **Frontend** | `class-jeo-map.js::getStyleLayer()` returns the style URL as the map's initial `mapStyle`. `mapbox.js::addStyle()` calls `map.setStyle()`. `style_layers` filtering applied after load. | +| **Editor** | `use-style-layer.js::findStyleLayer()` detects the style layer, passes the URL as ``. `map-preview-layer.js::renderLayer()` returns `null` for style layers. `applyStyleLayerFiltering()` applies `style_layers` filtering in `onStyleData`. | + +### Shared Utility: `use-style-layer.js` + +| Export | Purpose | +|--------|---------| +| `getMapboxStyleUrl(layer, instance)` | Builds the Mapbox Styles API URL (mirrors `mapbox.js::getStyleUrl()`) | +| `findStyleLayer(loadedLayers, instances)` | Finds the first instance with `load_as_style`, returns `{ instance, url, transformRequest }` | +| `styleLayerMapProps(styleBase)` | Returns `{ mapStyle, transformRequest? }` ready to spread on `` | +| `applyStyleLayerFiltering(map, instance)` | Removes/hides sub-layers per `style_layers` config (mirrors `class-jeo-map.js` lines 208-231) | + +### Editors with `load_as_style` Support + +| Editor | File | Status | +|--------|------|--------| +| Minimap | `minimap-editor.js` | ✅ Supported | +| Map block | `map-editor.js` | ✅ Supported | +| Onetime map | `onetime-map-editor.js` | ✅ Supported | +| Storymap | `storymap-editor.js` | ✅ Supported | + +### Token Handling + +The Mapbox token (`mapbox_key`) is localized identically in both contexts via the shared `mapgl` script. Per-layer `access_token` overrides are fully supported in both editor and frontend for all Mapbox-dependent layer types (`mapbox`, `mvt`, `mapbox-tileset-raster`, `mapbox-tileset-vector`): + +| Context | `load_as_style: false` | `load_as_style: true` | +|---------|------------------------|-----------------------| +| **Frontend** | Inline `access_token` in raster URL (`mapbox.js:25`) | Inline in style URL (`mapbox.js:177`) + `checkCustomToken`/`transformRequestUrl` re-signs derived requests (`class-jeo-map.js`) | +| **Editor** | Inline `access_token` in raster URL (`map-preview-layer.js:31`) | Inline in style URL + `createTokenAwareTransformRequest` re-signs derived requests (composed on top of runtime's `mapboxTransformRequest`/native `accessToken`) | + +For **tileset** types, `checkCustomToken` extracts the Mapbox owner from `tileset_id` (format `username.tilesetid`) and registers it in `customTokens`. The `transformRequestUrl` function then matches `${owner}.` in request URLs and overrides the token. The editor (`mapbox-style-preview.js::getMapboxOwner`) and discovery widget (`discovery/index.js::registerLayerCustomToken`) already handle `tileset_id` extraction. + +For **MVT** layers, the per-layer `access_token` is appended directly to the tile URL as a `?access_token=` query parameter (via `mvt.js::_resolveUrl`). The old pattern of embedding the token directly in the URL string is preserved when the `access_token` field is empty. In composed styles, the PHP `build_direct_layer` appends the per-layer token and bypasses placeholder sanitization. + +The editor's token-aware transform (`use-style-layer.js::createTokenAwareTransformRequest()`) mirrors the frontend's `transformRequestUrl` logic. It matches the Mapbox username extracted from `style_id` and replaces `access_token` on any matching request, then delegates to the runtime's default transformRequest. Works for both MapLibre and Mapbox runtimes. + +## Composed Mapbox Styles + +`mapbox`-type layers are no longer rendered as individual Static Tiles overlays. The +**Map Style Composer** merges all mapbox layers into a single cached composite Mapbox +Style JSON, set as the map's base `style`, with per-JEO-layer visibility/interactions +driven by a **manifest**. Full details in [`composed-styles/README.md`](../composed-styles/README.md). + +### Token Placeholder Handling + +Tokens are scrubbed to `__JEO_MAPBOX_ACCESS_TOKEN__` before persistence. Each rendering +context replaces the placeholder at request time and applies per-owner custom tokens: + +| Context | Mechanism | +|---------|-----------| +| **Frontend** (`class-jeo-map.js::transformRequestUrl`) | Placeholder → `encodeURIComponent(mapbox_key)` + per-owner `customTokens` | +| **Editor** (`mapbox-style-preview.js::useEditorMapboxTransformRequest`) | Mirrors frontend on top of `mapgl-loader.transformRequest` | +| **Discovery** (`discovery/index.js::transformRequestUrl`) | Same logic + `registerLayerCustomToken` (owner from `style_id`/`tileset_id`) | + +### Editor Adapter (`mapbox-style-preview.js`) + +| Hook | Used by | +|------|---------| +| `useComposedMapPreviewStyle` | `map-editor.js`, `storymap-editor.js` (saved-map preview) | +| `useComposedPayloadPreviewStyle` | `map-editor-preview.js`, `onetime-map-editor.js` (payload preview via POST `/compose`) | +| `useMapboxStylePreview` | `layer-editor-preview.js` (single raw style, NOT the composer) | + +All composed editor hooks use an `AbortController` per effect run to cancel stale in-flight +requests when layers/settings change rapidly. + +## Webpack Entry Points + +| Entry | File | `dependOn` | +|-------|------|------------| +| `mapglLoader` | `lib/mapgl-loader.js` | — | +| `mapglReact` | `lib/mapgl-react.js` | `mapglLoader` | +| `jeoMap` | `jeo-map/index.js` | `mapglLoader` | +| `jeoStorymap` | `jeo-storymap/storymap-display.js` | `jeoMap` | +| `discovery` | `discovery/index.js` | `jeoMap` | +| `mapBlocks` | `map-blocks/index.js` | `mapglReact` | +| `layersSidebar` | `layers-sidebar/index.js` | `mapglReact` | +| `mapsSidebar` | `maps-sidebar/index.js` | `mapglReact` | +| `postsSidebar` | `posts-sidebar/index.js` | — | +| `JeoLayer` | `includes/layer-types/JeoLayer.js` | — | +| `JeoLegend` | `includes/legend-types/JeoLegend.js` | — | + +## Development Notes + +1. **Never** import `maplibre-gl` or `mapbox-gl` directly — always use `lib/mapgl-loader.js` +2. For React map components, use `lib/mapgl-react.js` +3. WP form controls must come from `shared/wp-form-controls.js` (not `@wordpress/components` directly) +4. `splitChunks: false` — each entry is self-contained diff --git a/.architecture/geocoding/README.md b/.architecture/geocoding/README.md new file mode 100644 index 000000000..f5062a93b --- /dev/null +++ b/.architecture/geocoding/README.md @@ -0,0 +1,125 @@ +# Geocoding & Geolocation + +## Key Files + +| File | Role | +|------|------| +| `src/includes/geocode/class-geocode-handler.php` | Orchestrator: meta, AJAX, geocoders | +| `src/includes/geocode/class-geocoder.php` | Abstract base class | +| `src/includes/geocode/geocoders/class-mapbox.php` | Mapbox geocoder | +| `src/includes/geocode/geocoders/class-nominatim.php` | Nominatim/OSM geocoder | +| `src/js/src/posts-sidebar/index.js` | Gutenberg geolocation sidebar | +| `src/js/src/posts-sidebar/geo-posts.js` | Map point editing UI | +| `src/js/src/posts-sidebar/geo-auto-complete.js` | Address autocomplete | +| `src/js/src/posts-sidebar/geo-posts-ai.js` | AI geolocation UI | + +## Meta `_related_point` + +Registered on all enabled post types (configurable in Settings). + +### REST Schema + +```json +{ + "lat": "float", + "lon": "float", + "city": "string", + "region": "string", + "country": "string", + "full_address": "string", + "neighborhood": "string", + "postcode": "string", + "relevance": "int (1=primary, 2=secondary)" +} +``` + +### Geographic Indexes + +Derived metadata computed automatically from `_related_point`: +- `_geocode_lat_p` / `_geocode_lon_p` — Primary point coordinates +- `_geocode_lat_s` / `_geocode_lon_s` — Secondary point coordinates + +These are **read-only** — direct updates are blocked via `update_post_metadata` filter. + +## Available Geocoders + +| Geocoder | Class | Cache | API | +|----------|-------|-------|-----| +| Nominatim | `Jeo\Geocoders\Nominatim` | Transient 6h TTL | OSM Nominatim (free) | +| Mapbox | `Jeo\Geocoders\Mapbox` | None | Mapbox Geocoding API v5 | + +### Active Geocoder + +Selected in **Settings → Geocoders**. The slug is stored in `jeo-settings['geocoding_service']`. + +### Registering a New Geocoder + +```php +add_action( 'jeo_register_geocoders', function( $handler ) { + require_once '/path/to/class-my-geocoder.php'; + $handler->register_geocoder( new \Jeo\Geocoders\MyGeocoder() ); +} ); +``` + +The class must extend `Jeo\Geocoder` and implement: +- `geocode( $query )` — Returns array of results +- `reverse_geocode( $lat, $lon )` — Returns address from coordinates + +## AJAX Endpoints + +| Action | Direction | Description | +|--------|-----------|-------------| +| `wp_ajax_jeo_geocode` | Forward | Text → coordinates | +| `wp_ajax_jeo_reverse_geocode` | Reverse | Coordinates → address | + +Both require nonce `jeo_geocode_nonce` and `current_user_can('edit_posts')`. + +## Frontend: Geolocation Sidebar + +### Manual Flow + +```mermaid +sequenceDiagram + participant E as Gutenberg Editor + participant S as Posts Sidebar + participant L as Leaflet Map + participant AJAX as WP AJAX + + E->>S: Opens "Geolocation" panel + S->>L: Renders Leaflet map + S->>AJAX: Fetches existing points (_related_point) + L-->>S: Shows existing markers + + Note over S: User clicks on map + L->>AJAX: reverse_geocode(lat, lon) + AJAX-->>L: Full address + L->>S: Updates form + S->>E: Saves meta via wp.data +``` + +### AI Flow + +```mermaid +sequenceDiagram + participant E as Editor + participant S as Posts Sidebar + participant REST as /jeo/v1/ai-georeference + participant AI as AI Provider + + E->>S: Clicks "Geolocate with AI" + S->>REST: POST {post_content, post_title} + REST->>AI: Georeferencing prompt + AI-->>REST: {lat, lon, confidence, ...} + REST-->>S: Result with confidence + S->>E: Approval modal + Note over E: User approves/rejects +``` + +### Magnetic Markers + +When enabled, markers "snap" to the nearest address when dragged, via automatic reverse geocoding. + +### Relevance + +- **Primary** (relevance=1): Main post location +- **Secondary** (relevance=2): Additional location diff --git a/.architecture/layers/README.md b/.architecture/layers/README.md new file mode 100644 index 000000000..fe0500bcc --- /dev/null +++ b/.architecture/layers/README.md @@ -0,0 +1,309 @@ +# Layers (CPT `map-layer`) + +## Key Files + +| File | Role | +|------|------| +| `src/includes/layers/class-layers.php` | `Jeo\Layers` class — CPT, meta, capabilities | +| `src/includes/layer-types/class-layer-types.php` | Layer type registry | +| `src/includes/layer-types/*.js` | JS renderers per type | +| `src/includes/legend-types/class-legend-types.php` | Legend type registry | +| `src/includes/legend-types/*.js` | JS legend renderers | +| `src/js/src/layers-sidebar/` | Gutenberg sidebar for editing | +| `src/js/src/map-blocks/map-preview-layer.js` | Renders layer in preview | + +## Custom Post Type: `map-layer` + +### Meta Fields + +| Meta Key | Type | Description | +|----------|------|-------------| +| `type` | string | Layer type (mapbox, tilelayer, mvt, etc.) | +| `attribution` | string | Attribution text | +| `source_url` | string | Source URL | +| `excerpt` | string | Short description shown in layer selectors (CPT supports `excerpt`) | +| `layer_type_options` | object | Type-specific options | +| `legend_type` | string | Legend type | +| `legend_type_options` | object | Legend options | +| `interactions` | array | Configured interactions | + +## Layer Types + +### Registration + +Types are registered via `Jeo\Layer_Types::register_layer_type()` on the `jeo_register_layer_types` hook. + +### Core Types + +| Type | Description | JS File | +|------|-------------|---------| +| `mapbox` | Mapbox style (composed via `Map_Style_Composer` — see [`composed-styles/README.md`](../composed-styles/README.md)) | `mapbox.js` | +| `style-json` | Keyless MapLibre style JSON by URL or inline (OpenFreeMap, VersaTiles, self-hosted) | `style-json.js` | +| `tilelayer` | Generic tiled raster | `tilelayer.js` | +| `mvt` | Mapbox Vector Tiles | `mvt.js` | +| `mapbox-tileset-raster` | Mapbox raster tileset | `mapbox-tileset-raster.js` | +| `mapbox-tileset-vector` | Mapbox vector tileset | `mapbox-tileset-vector.js` | + +> **Note (3.1.0):** The legacy Static Tiles raster-overlay rendering path for `mapbox` +> layers was removed. `mapbox`-type layers are now merged into a single composite Mapbox +> Style JSON by the backend `Map_Style_Composer` and rendered natively by MapLibre. The +> other types (`tilelayer`, `mvt`, `mapbox-tileset-*`) still render via the legacy +> individual-add path. + +### Style Types (`isStyle`) + +`mapbox` and `style-json` are **style types**: instead of being added as individual +GL layers, they load a whole MapLibre GL style that becomes the map's **base style**. +The contract is declared on the type itself, in both registries: + +- **JS**: `JeoLayerTypes.registerLayerType( slug, { isStyle: true, getStyleUrl?, getInlineStyle?, getStyle? } )` + (`JeoLayerTypes.isStyle( slug )`). The `JeoLayer` bridge exposes `isStyle` and + `getStyle()` (inline object takes precedence over URL; both are valid + `map.setStyle()` inputs). +- **PHP**: `Layer_Types::register_layer_type( $slug, [ 'is_style' => true ] )` + (`Layer_Types::is_style( $slug )`, filterable via `jeo_layer_type_is_style` so + JS-only types can opt in). + +Consumers key off the flag instead of the type slug: editor hoisting +(`use-style-layer.js::findStyleLayer`), frontend base handling +(`class-jeo-map.js::applyBaseStyleLayer`, `storymap-display.js`), minimap +`load_as_style` (`build_base_layer_response`), and composer bundle eligibility +(any `is_style` type with `style_url`/`inline_style` in `layer_type_options` is +fetched via `Jeo::fetch_style_json()` and merged as a bottom bundle; `mapbox` +keeps its legacy `style_id` + token path). A future provider only needs the two +registrations — no core changes. + +Editor-side detection goes through `shared/style-layer-types.js::isStyleLayerType()`, +which reads the frontend registry. The registry itself is loaded inside the +block-editor iframe by `Layer_Types::enqueue_iframe_assets()` +(`enqueue_block_assets`, admin-only) — so `window.JeoLayerTypes` (with `isStyle` +and `getSchema`) is available to editor bundles; without a registry the helper +returns false instead of guessing from a hardcoded list. + +The `style-json` schema mirrors `geojson`: `style_url` (public style JSON URL) +or `inline_style` (raw style JSON text, takes precedence). It requires no API +key. Non-base `style-json` layers are skipped with a console warning — the type +only supports the base layer position. + +### Schema per Type (JSON Schema for @rjsf/core) + +Defined in `layers-sidebar/layer-type-definitions.js`: + +| Type | Fields | +|------|--------| +| `mapbox` | `style_id`, `access_token` | +| `style-json` | `style_url`, `inline_style` | +| `tilelayer` | `url`, `scheme` | +| `mvt` | `url`, `source_layer`, `type`, `style_source_type`, `access_token` | +| `mapbox-tileset-raster` | `tileset_id`, `style_source_type`, `type`, `access_token` | +| `mapbox-tileset-vector` | `tileset_id`, `source_layer`, `type`, `style_source_type`, `access_token` | +| `geojson` | `data`, `inline_geojson`, `type` | + +The `geojson` type renders a public GeoJSON URL client-side as a single `fill` +GL layer (used by AI-generated boundary layers). It needs no +Mapbox token and is supported by the frontend registry (`layer-types/geojson.js`), +the editor preview (`map-preview-layer.js`), and the Mapbox style composer +(direct-type inlining with one manifest entry per GL layer). Source data can be +a URL (`data`) or raw inline JSON (`inline_geojson`, takes precedence). The +render type is keyed by `type` (`fill` only in the schema for now; the +renderers are switches ready for future types). Styling lives **on the map +layer instance** (`layers[i].style`, the same flat `{ use_default, paint, layout }` +shape used by `mvt`/`mapbox-tileset-vector`) — never on the layer CPT. When the +instance sets `use_default`, the layer's `default_style` post meta (exposed via +REST, written by AI layer generation) takes over. All renderers resolve the +effective style the same way: editor preview (`map-preview-layer.js`), frontend +runtime (`geojson.js`, fed by `class-jeo-map.js`), and the composer +(`resolve_effective_layer_style()` in `class-map-style-composer.php`). The +outline is the `fill-outline-color` paint prop (~1px antialiasing halo — +no configurable width/opacity; the old `{layer_id}__outline` companion line +layer was replaced, and the legacy toggle in `class-jeo-map.js::changeLayerVisibitly` +remains only for pre-switch layers). Paint defaults are shared via +`JeoLayerTypes.getFallbackPaint('fill')` and mirrored in the composer's PHP +default paint — keep both in sync. Instance opacity multiplies numeric opacity +props. Uses only style-spec v8 core APIs — +compatible with both MapLibre GL JS and Mapbox GL JS runtimes. + +### Extensibility + +Register new types via: +```php +add_action( 'jeo_register_layer_types', function( $layer_types ) { + $layer_types->register_layer_type( [ + 'slug' => 'my-type', + 'name' => 'My Custom Layer', + 'js_handler' => 'my-type-handler', + ] ); +} ); +``` + +The JS handler must be registered as a WordPress script and follow the `window.JeoLayerTypes` pattern. + +## Legend Types + +### Core Legend Types + +| Type | Description | JS File | +|------|-------------|---------| +| `barscale` | Continuous color scale | `barscale.js` | +| `simple-color` | Color categories | `simple-color.js` | +| `icons` | Icon categories | `icons.js` | +| `circles` | Size-based circles | `circles.js` | + +### Legend Editor + +Located in `posts-sidebar/legends-editor/legend-editor.js`, with type-specific editors in `editors/`. + +## Editing Sidebar + +The `jeo-layers-sidebar` provides: +- Dynamic form via JSON Schema (`@rjsf/core`) with debounced dispatch (500ms) +- Attribution settings +- Legend settings +- Interaction settings (popup on click/hover) +- **Refresh composed style cache** button (`mapbox` layers only) — calls + `POST /jeo/v1/map-style/layer/{id}/refresh` to force-regenerate the composite styles for + all maps referencing this layer, showing a notice with the refreshed/failed map counts + +The live map preview and post save lock / notices are handled by the `jeo/layer-editor` block (`map-blocks/layer-editor-preview.js`), which runs inside the Gutenberg content area (iframe in Block API v3). This block: +- Renders the MapLibre/Mapbox preview with the current layer options +- Manages `renderControl` status: `incomplete_form`, `ready`, `loaded`, `request_error` +- Dispatches notices and locks/unlocks post saving based on layer load status +- Runs in async mode (`AsyncModeProvider`) to batch re-renders +- Uses `loadedRef` guard on `onSourceData` to avoid redundant re-renders during tile loading +- Updates the map in-place (no `key`-based remount) when layer options change + +## Layer Usage Modes + +| Mode | Description | +|------|-------------| +| `fixed` | Always visible | +| `switchable` | Toggle on/off | +| `swappable` | Only one in group visible at a time | + +## Frontend Rendering + +`map-preview-layer.js` (used in both editor and frontend) renders: + +```mermaid +graph LR + A[Layer type] --> B{mapbox?} + B -->|Yes| C[Source: raster URL from style] + B -->|No| D{tilelayer?} + D -->|Yes| E[Source: raster with URL pattern] + D -->|No| F{mvt/tileset?} + F -->|Yes| G[Source: vector + layer type styling] +``` + +## Per-Instance Layer Opacity + +Every layer instance in a map (including raster types) supports an `opacity` property in `[0, 1]`. It is stored per map/block instance, not on the layer CPT, so the same layer can have different opacity in different maps. + +| File | Role | +|------|------| +| `src/js/src/map-blocks/layer-settings.js` | Opacity `RangeControl` per selected layer | +| `src/js/src/map-blocks/layers-settings.js` | `handleUpdateOpacity` callback wired to `layers[i].opacity` | +| `src/js/src/map-blocks/minimap-config.js` / `src/js/src/map-blocks/index.js` | `opacity` property in layer instance schema | +| `src/js/src/map-blocks/map-preview-layer.js` | Applies opacity to vector paints and raster layers in the editor preview | +| `src/includes/layer-types/*.js` | Applies opacity on frontend `map.addLayer()` | +| `src/js/src/jeo-map/class-jeo-map.js` | Forwards `opacity` from `layersDefinitions` to `JeoLayer` attributes | + +### Opacity Application Rules + +- **Vector layers** (`mvt`, `mapbox-tileset-vector`): opacity multiplies existing paint opacity properties (`fill-opacity`, `line-opacity`, `circle-opacity`, `symbol-opacity`, `heatmap-opacity`, `fill-extrusion-opacity`). If no paint exists, opacity has no visible effect unless a fallback paint is applied. +- **Raster layers** (`mapbox`, `mapbox-tileset-raster`, `tilelayer`): sets `raster-opacity` paint property. +- Default opacity is `1` (fully opaque). Existing maps without `opacity` retain previous behavior. + +## Per-Instance Vector Layer Styling + +Vector layers (`mvt`, `mapbox-tileset-vector`) and the client-side `geojson` type support per-instance paint/layout overrides stored on the map's `layers` meta, not on the layer CPT. This allows the same layer to have different styles in different maps. + +### Key Files + +| File | Role | +|------|------| +| `src/js/src/shared/layer-style-editor.js` | Reusable modal component for paint/layout editing | +| `src/js/src/map-blocks/layer-settings.js` | Style button per layer row in layers modal | +| `src/js/src/map-blocks/layers-settings.js` | `updateStyle` callback wired to layers array | +| `src/js/src/map-blocks/map-preview-layer.js` | Spreads `instance.style.paint/layout` into `` | +| `src/includes/layer-types/mvt.js` | Merges `attributes.style` into `map.addLayer()` | +| `src/includes/layer-types/mapbox-tileset-vector.js` | Merges `attributes.style` into `map.addLayer()` | +| `src/includes/layer-types/geojson.js` | Resolves `attributes.style`/`attributes.default_style` into the GL layer paint | +| `src/js/src/jeo-map/class-jeo-map.js` | Forwards `style`/`default_style` from `layersDefinitions`/layer meta to `JeoLayer` | +| `src/includes/maps/class-map-style-composer.php` | `resolve_effective_layer_style()` mirrors the same resolution in composed styles | +| `src/includes/layers/class-layers.php` | Registers the `default_style` post meta (show_in_rest) | + +### Editor stability guarantees + +The style modal (`layer-style-editor.js`) keeps its open state in `LayerSettings` local state, so the layers list must never unmount while the user edits. Two guards enforce this: + +- `shared/rest-records.js::useRecordsByIds` memoizes the normalized ID set **by content** (`normalizeRecordIds(...).join(',')`), not by array identity — attribute updates that rebuild the IDs array no longer re-trigger the fetch effect and its transient `isLoading` flip. +- `map-blocks/layers-settings.js` renders the `List` whenever layers exist, even while refetching (`(!loadingLayers || stableLoadedLayers.length > 0)`), so per-item local state survives genuine id-set changes. `stableLoadedLayers` is a ref snapshot of the last non-empty `loadedLayers`, so a re-resolving query (`useEntityRecords` briefly returns `records: undefined, isResolving: true`) neither unmounts the `List` nor drops individual items. +- `map-blocks/layers-settings.js` keys each `LayerSettings` by `layer.id` (never by array index): reordering via `react-movable` must remount nothing, or the whole list flashes and drag animations/state reset. +- `map-blocks/layer-settings.js::memo` comparator **must** include `itemProps.style.visibility`/`zIndex`: react-movable signals drag state (hide the source item, lift the selected one) only through `itemProps.style`. A comparator that ignores `itemProps` leaves the dragged item stuck with `visibility: hidden` after a same-position drop, and lets the drag ghost overlap the source item during the drag. +- `shared/layer-style-editor.js` keeps edits in a local draft and debounces the upstream `onChange` (300ms, flushing on unmount): every committed style change re-renders the host map editor and, when Mapbox layers are present, recomposes the style and remounts the ``. One dispatch per keystroke/slider-tick froze the main thread for seconds (measured ~3.2s of long tasks for 8 slider changes); with the debounce the same burst costs a single commit. + +### `style` Object Shape + +Stored as a property on each `layers[]` item in the map's `layers` meta: + +```json +{ + "id": 123, + "use": "fixed", + "default": true, + "style": { + "paint": { + "fill-color": "#ff0000", + "fill-opacity": 0.6, + "fill-outline-color": "#990000" + } + } +} +``` + +### Supported Paint Properties (by layer type) + +| Layer `type` | Paint properties | +|---|---| +| `fill` | `fill-color`, `fill-opacity`, `fill-outline-color` | +| `line` | `line-color`, `line-opacity`, `line-width` | +| `circle` | `circle-color`, `circle-opacity`, `circle-radius`, `circle-stroke-color`, `circle-stroke-width` | + +### Data Flow + +``` +Layers modal → Style button → LayerStyleEditor modal + → onChange updates layers[i].style in map meta / block attrs + → Preview: map-preview-layer.js reads instance.style (use_default → layer.default_style) → + → Frontend: class-jeo-map.js reads layersDefinitions[i].style → JeoLayer attributes + → mvt.js / mapbox-tileset-vector.js / geojson.js merge style.paint into map.addLayer() + → Composer: class-map-style-composer.php resolve_effective_layer_style() merges the same style into composed styles +``` + +### Backwards Compatibility + +- Existing `layers[]` items without `style` → renderer defaults (unchanged behavior) +- REST schema allows additional properties → no breaking change +- `style` is optional everywhere; renderers check `if (attributes.style?.paint)` before merging + +## Per-Layer Access Token + +All Mapbox-dependent layer types (`mapbox`, `mvt`, `mapbox-tileset-raster`, `mapbox-tileset-vector`) support an optional `access_token` field in `layer_type_options`. When set, it overrides the global `mapbox_key` setting for that layer only. + +| Type | Token Mechanism | +|------|----------------| +| `mapbox` | Inline in style URL; `checkCustomToken` registers per-owner token for derived requests | +| `mapbox-tileset-raster` | `checkCustomToken` extracts owner from `tileset_id` (`username.tilesetid`); `transformRequestUrl` re-signs requests | +| `mapbox-tileset-vector` | Same as raster tileset | +| `mvt` | Token appended to the tile URL as `?access_token=` query param (replaces any existing token); old pattern of embedding token directly in URL is preserved when field is empty | + +### Key Files + +| File | Role | +|------|------| +| `src/js/src/jeo-map/class-jeo-map.js` (`checkCustomToken`) | Registers per-owner custom tokens from `access_token` + `style_id` or `tileset_id` | +| `src/js/src/jeo-map/class-jeo-map.js` (`transformRequestUrl`) | Overrides `access_token` query param for requests matching registered owners | +| `src/includes/layer-types/mvt.js` (`_resolveUrl`) | Appends per-layer token to MVT tile URL | +| `src/includes/maps/class-map-style-composer.php` (`build_direct_layer`) | Appends per-layer token for MVT in composed styles (bypasses placeholder sanitization) | +| `src/js/src/map-blocks/mapbox-style-preview.js` (`getMapboxOwner`) | Editor: extracts owner from `style_id` or `tileset_id` — already tileset-aware | +| `src/js/src/discovery/index.js` (`registerLayerCustomToken`) | Discovery: already tileset-aware | diff --git a/.architecture/maps/README.md b/.architecture/maps/README.md new file mode 100644 index 000000000..2bcf50373 --- /dev/null +++ b/.architecture/maps/README.md @@ -0,0 +1,124 @@ +# Maps (CPT `map`) + +## Key Files + +| File | Role | +|------|------| +| `src/includes/maps/class-maps.php` | `Jeo\Maps` class — CPT, meta, shortcode | +| `src/includes/maps/class-map-style-composer.php` | `Jeo\Map_Style_Composer` — merges Mapbox styles into cached composite Style JSON (see [`composed-styles/README.md`](../composed-styles/README.md)) | +| `src/includes/maps/class-style-token-migrator.php` | `Jeo\Style_Token_Migrator` — converts legacy `{token}` strings to style-spec expressions (MapLibre compat) | +| `src/js/src/jeo-map/class-jeo-map.js` | `JeoMap` class — frontend rendering | +| `src/js/src/jeo-map/index.js` | Entry point, DOM scan | +| `src/js/src/maps-sidebar/` | Gutenberg sidebar for editing | +| `src/templates/single-map.php` | Single map template | +| `src/templates/map-content-layers-list.php` | Layer list below map | + +## Custom Post Type: `map` + +### Meta Fields (15 fields) + +| Meta Key | Type | Description | +|----------|------|-------------| +| `initial_zoom` | int (1-14) | Initial zoom level | +| `center_lat` | float | Center latitude | +| `center_lon` | float | Center longitude | +| `layers` | array | Layer IDs with configuration | +| `pan_limits` | array | Navigation bounds (min/max lat/lon) | +| `related_posts` | array | Related posts config | +| `disable_scroll_zoom` | boolean | Disable scroll zoom | +| `show_posts_sidebar` | boolean | Show posts sidebar | +| `use_native_clusters` | boolean | Use MapLibre native clusters | +| `posts_per_page` | int | Posts per page in popup | +| `max_cluster_radius` | int | Max cluster radius | +| `cluster_radius` | int | Cluster radius | +| `show_markers` | boolean | Show markers | +| `map_layers_load_as_style` | boolean | Load layers as style | + +#### Composed Style Meta (managed by `Map_Style_Composer`) + +| Meta Key | Type | Description | +|----------|------|-------------| +| `_jeo_mapbox_composed_style_hash` | string (16 hex) | Current cache hash; changes on any map/layer edit | +| `_jeo_mapbox_composed_style_warnings` | array | Composition warnings (sprite conflicts, unsupported expressions, etc.) | +| `_jeo_mapbox_composed_style_error` | string | Last composition error (cleared on success) | + +### Shortcode + +``` +[jeo-map id="{map_id}"] +``` + +### REST API + +- `GET /wp-json/wp/v2/map` — List (standard WP CPT) +- `GET /wp-json/jeo/v1/map/{id}` — Full data for frontend rendering +- `GET /wp-json/jeo/v1/map-style/{id}` — Composed style metadata (style/manifest URLs) — see [`composed-styles/README.md`](../composed-styles/README.md) + +## Frontend Rendering + +1. DOM scan: `index.js` finds `.jeomap:not(.storymap)` elements +2. Each `

` becomes a `JeoMap` instance +3. `JeoMap` lazy-initializes via `IntersectionObserver` +4. Fetches data via REST, creates MapLibre/Mapbox map +5. Adds layers, controls, legends +6. Fetches related posts → GeoJSON clustering → markers/popups + +## Template Rendering + +Single map content (`the_content` filter) injects: +1. `
` +2. `map-content-layers-list.php` template with layer list + +## Sidebar Editor (Gutenberg) + +The `jeo-maps-sidebar` provides: +- Layer selector with drag-and-drop +- Related posts selector +- Copyable embed URL +- Map settings panel (center, zoom, pan limits, etc.) + +The live map preview with zoom controls (initial, min, max) is handled by the `jeo/map-editor` block (`map-blocks/map-editor-preview.js`), which runs inside the Gutenberg content area (iframe in Block API v3). This block: +- Renders the MapLibre/Mapbox preview with all selected layers +- Provides zoom mode buttons (initial/min/max) +- Uses local `viewState` for responsive dragging (no `editPost` per pixel) +- Persists center coordinates on `onMoveEnd` and zoom on `onZoomEnd` only +- Updates the map in-place (no `key`-based remount) +- Bridges pan-limits to the sidebar via `window.parent.__jeoSetPanLimitsFromMap` + +## Composed Mapbox Styles + +When a map contains `mapbox`-type layers, the **Map Style Composer** +(`Jeo\Map_Style_Composer`) merges them into a single cached composite Mapbox Style JSON +renderable by MapLibre. This replaces the legacy Static Tiles overlay path (removed 3.1.0). + +- **Backend**: see [`composed-styles/README.md`](../composed-styles/README.md) for the + full architecture (composition, caching, REST endpoints, filters). +- **Frontend** (`JeoMap`): when composed style is available, the composite style is set as + the map's base `style`; per-JEO-layer visibility and interactions are driven by the + manifest. If composition fails, a `.jeomap-composed-style-warning` is shown and mapbox + layers are omitted. Non-mapbox layers still render via the legacy individual-add path. +- **Editor**: the `jeo/map-editor` block and CPT preview use + `mapbox-style-preview.js` hooks (`useComposedMapPreviewStyle` / `useComposedPayloadPreviewStyle`) + to load composed styles with an AbortSignal stale-state guard. + +## Data Flow + +```mermaid +sequenceDiagram + participant U as User + participant DOM as Frontend DOM + participant JM as JeoMap class + participant REST as WP REST API + participant WP as WordPress + + U->>DOM: Loads page + DOM->>JM: IntersectionObserver detects .jeomap + JM->>REST: GET /jeo/v1/map/{id} + REST->>WP: Fetch map meta + layers + REST-->>JM: {layers, center, zoom, ...} + JM->>JM: Create MapLibre map + JM->>JM: Add layers + JM->>REST: Fetch related posts + REST-->>JM: GeoJSON with _related_point + JM->>JM: Clustering + markers + popups +``` diff --git a/.architecture/minilayer/README.md b/.architecture/minilayer/README.md new file mode 100644 index 000000000..84ab86c2f --- /dev/null +++ b/.architecture/minilayer/README.md @@ -0,0 +1,277 @@ +# Minilayer (AI-Generated Layers) + +Creates Mapbox-compatible map layers from natural-language prompts and auto-creates a JEO layer (CPT `map-layer`). + +The pipeline is now deterministic: a single structured-output classifier decides whether the prompt can be approximated with built-in Mapbox tilesets or requires a composed style. The Mapbox DevKit MCP has been removed. + +## Key Files + +| File | Class | Role | +|------|-------|------| +| `src/includes/ai/class-minilayer-classifier.php` | `Minilayer_Classifier` | Single structured-output classifier — prompt → `Layer_Spec_Output` | +| `src/includes/ai/class-layer-spec-output.php` | `Layer_Spec_Output` | Structured output DTO with `#[SchemaProperty]` attributes | +| `src/includes/ai/class-minilayer-handler.php` | `Minilayer_Handler` | REST endpoint, delegates to `Minilayer_Service` | +| `src/includes/ai/class-minilayer-service.php` | `Minilayer_Service` | Shared service — runs classifier, creates `mapbox-tileset-vector` or composed `mapbox` layer CPTs, stores `default_style`, attribution, theme and legend | +| `src/includes/ai/class-minilayer-metadata.php` | `Minilayer_Metadata` | Helpers for post description, `layer-theme` taxonomy, and `simple-color` legend generation from layer paint | +| `src/includes/ai/class-generate-layer-tool.php` | `Generate_Layer_Tool` | NeuronAI tool for the minimap agent — conditionally registered when a Mapbox API key is available | +| `src/includes/ai/class-mapbox-style-builder.php` | `Mapbox_Style_Builder` | Deterministic builder that publishes composed styles to the Mapbox Styles API | +| `src/js/src/shared/layer-style-editor.js` | `LayerStyleEditor` | Modal UI for layer styling — includes "Use AI default style" checkbox | +| `src/js/src/map-blocks/map-preview-layer.js` | `renderLayer` | Gutenberg preview — resolves effective style from `use_default` flag | +| `src/includes/layer-types/mapbox-tileset-vector.js` | layer type | Front-end rendering — applies `default_style` or instance style with filter support | + +## Flow (REST Endpoint) + +```mermaid +sequenceDiagram + participant Client + participant REST as /minilayer/generate + participant H as Minilayer_Handler + participant S as Minilayer_Service + participant C as Minilayer_Classifier + participant F as JEO_AI_Factory + participant LLM as AI Provider + participant B as Mapbox_Style_Builder + participant M as Mapbox Styles API + + Client->>REST: POST { prompt } + REST->>H: api_generate() + H->>H: Validate prompt + H->>S: generate_and_create(prompt) + S->>S: Validate Mapbox key + AI provider + S->>C: classify(prompt) + C->>F: create_assistant(outputClass: Layer_Spec_Output) + F->>LLM: structured(prompt) + LLM-->>C: Layer_Spec_Output + C-->>S: spec + alt layer_type == "mapbox-tileset-vector" + S->>S: create_tileset_vector_layer(spec) + else layer_type == "mapbox" + S->>B: build_from_spec(spec) + B->>B: publish_style(style_json, name, token) + B->>M: POST /styles/v1/{username} + M-->>B: style_id + S->>S: create_composed_layer(spec, style_id) + end + S->>MD: Minilayer_Metadata (description, theme, legend) + S-->>H: Layer info array + H-->>REST: { success, layer } + REST-->>Client: 200 OK +``` + +## Reuse by Generate_Layer_Tool + +The minimap agent's `Generate_Layer_Tool` delegates to `Minilayer_Service::generate_and_create()` — the same pipeline as the REST endpoint. The tool is conditionally registered when a Mapbox API key is available. See [`.architecture/ai/README.md`](ai/README.md) for the minimap-side integration details. + +## REST Endpoint + +``` +POST /jeo/v1/minilayer/generate +Permission: edit_posts +Body: { "prompt": string, "layer_name"?: string } +Response: { "success": true, "layer": { "id", "title", "type", "style_id", "edit_url", ... } } +Error: { "success": false, "error": "Human-readable message" } +``` + +## Classifier Output (`Layer_Spec_Output`) + +The classifier returns a structured object with the following fields: + +| Field | Type | Description | +|-------|------|-------------| +| `can_approximate` | bool | False when the prompt asks for data not available in any tileset or external source | +| `layer_type` | string | `"mapbox-tileset-vector"` or `"mapbox"` | +| `layer_title` | string | Human-readable title | +| `tileset_id` | ?string | Mapbox tileset ID (tileset-vector only) | +| `source_layer` | ?string | Source layer within the tileset (tileset-vector only) | +| `layer_geometry_type` | ?string | `fill`, `line`, `symbol`, `circle`, `fill-extrusion`, `heatmap` | +| `suggested_filter` | ?array | MapLibre filter expression | +| `suggested_paint` | ?array | Default paint properties | +| `style_json` | ?array | Complete Mapbox GL style JSON (composed mapbox only) | +| `external_sources` | ?array | Map of source IDs to URLs | +| `limitations` | string | Honest description of what the layer cannot show | +| `theme` | ?string | Best-matching `layer-theme` taxonomy term for catalog/minimap disambiguation | + +## Layer Type Decision + +The classifier uses a decision tree, preferring `mapbox-tileset-vector` whenever possible: + +| Condition | `layer_type` | CPT `type` | +|-----------|-------------|------------| +| Single built-in tileset + single source-layer + vector geometry + solid colors sufficient | `mapbox-tileset-vector` | `mapbox-tileset-vector` | +| External sources, multiple tilesets, data-driven styling, raster overlays, or complex composition | `mapbox` | `mapbox` | +| Data not available in any source | — | Error (`can_approximate: false`) | + +### `mapbox-tileset-vector` additional fields + +`tileset_id`, `source_layer`, `layer_geometry_type`, `suggested_filter`, `suggested_paint` + +### Validation + +Requires all three tileset fields present and `layer_geometry_type` to be one of: `fill`, `line`, `symbol`, `circle`, `fill-extrusion`, `heatmap`. + +## Default Style (CPT Meta `default_style`) + +When the classifier returns `suggested_filter` and/or `suggested_paint`, these are saved as the `default_style` meta on the layer CPT: + +``` +meta: { + default_style: { + filter: ["==", "class", "wood"], + paint: { "fill-color": "#2d5a27", "fill-opacity": 0.6 } + } +} +``` + +### How default_style is used + +The `default_style` is exposed via REST and consumed by both the Gutenberg preview and the front-end: + +1. **Gutenberg preview** (`map-preview-layer.js`): `resolveStyle()` checks `instance.style.use_default`. When true, uses `layer.default_style` from CPT meta instead of `instance.style`. + +2. **Front-end** (`mapbox-tileset-vector.js`): Same logic in `addLayer()` — if `attributes.style.use_default` is true, applies `attributes.default_style` (filter + paint). + +3. **Style editor modal** (`layer-style-editor.js`): Shows a "Use AI Default Style" checkbox when `default_style` exists. When checked, sets `style.use_default: true` and disables manual paint controls. When unchecked, user can edit paint normally. + +### Style resolution logic + +``` +instance.style.use_default === true && layer.default_style exists + → effectiveStyle = layer.default_style (from CPT, may contain filter + data-driven paint) + +instance.style.use_default === false (or no default_style) + → effectiveStyle = instance.style (manual paint/layout, no filter) +``` + +## Layer CPT Creation + +Creates a `map-layer` post. The type depends on the classifier output: + +### mapbox-tileset-vector + +- `type` = `mapbox-tileset-vector` +- `layer_type_options` = `{ tileset_id, source_layer, type, style_source_type: "vector" }` +- `default_style` = `{ filter, paint }` (when suggested by AI) +- No Mapbox Styles API call is made — creation is instant + +### mapbox (fallback) + +- `type` = `mapbox` +- `layer_type_options` = `{ style_id: "username/style_id" }` +- `Mapbox_Style_Builder::publish_style()` validates the published style via `Jeo::fetch_mapbox_style()` + +Title: from `layer_name` param > `layer_title` (AI-derived) > fallback + +## Metadata and Legend + +Every AI-generated layer is enriched with: + +| Meta / Field | Source | Purpose | +|--------------|--------|---------| +| `post_excerpt` | `Minilayer_Metadata::build_description()` — classifier `description` field or deterministic boundary template | Description paragraph shown in layer library / minimap disambiguation; no disclaimers (no "Generated by AI", "simplified geometry", etc.) | +| `attribution` | Deterministic source credit (e.g. `IBGE`, `OpenStreetMap contributors`, `Mapbox tileset %s`) | Credit only — the UI already labels the field ("Source:"/"Fonte:"), so the stored text must not carry its own prefix; limitations are agent-facing only and never copied into attribution | +| `layer-theme` taxonomy | Classifier `theme` field (tileset-vector) or source adapter (boundary) | Thematic grouping in the layer library | +| `use_legend`, `legend_title`, `legend_type`, `legend_type_options` | Derived from paint color | Automatic `simple-color` legend when a representative color exists | + +Legend generation: + +- Tileset-vector layers extract the first color from `suggested_paint` (`fill-color`, `line-color`, `circle-color`) and create a `simple-color` legend entry using the layer title. +- Boundary layers use the boundary outline/fill color. +- Composed `mapbox` styles attempt to extract the first usable color from `style_json.layers`; if none is found, no legend is generated. + +## WP-CLI Commands + +The `AI_CLI` class registers commands under `wp jeo ai` for local testing and observability: + +| Command | Purpose | +|---------|---------| +| `wp jeo ai generate-layer [--layer_name=]` | Runs `Minilayer_Service::generate_and_create()` and prints the created layer info. | +| `wp jeo ai generate-boundary [--type=] [--context=] [--layer_name=]` | Runs `Place_Polygon_Service::create_layer()` and prints the boundary layer info. | +| `wp jeo ai test-minilayer ` | Runs `Minilayer_Classifier::classify()` only — no side effects. | + +## Observability + +- Every `Minilayer_Classifier::classify()` call is logged via `AI_Logger::insert_log()` with the active provider, prompt, and resulting spec. +- Structured-output mode reports 0 tokens (provider limitation), but the prompt and response are stored for debugging and cost review. + +## Prompt Architecture + +The `Minilayer_Classifier::instructions()` prompt has 6 sections: + +1. **Workflow** — Step-by-step process with layer type decision before style creation +2. **Available Mapbox Tilesets** — Inventory of source-layers, geometry types, and classes in each tileset +3. **Layer Type Decision** — Explicit decision tree for mapbox-tileset-vector vs mapbox +4. **Capability Boundaries & Approximation Guide** — What tilesets can/cannot show, with mapping table +5. **Theme and Metadata** — `layer_title`, `description`, `theme`, and `limitations` guidance +6. **Response Format** — Structured `Layer_Spec_Output` schema +7. **Design Principles** — Relevance first, visual hierarchy, color palettes by theme, honesty + +## Tileset Inventory + +### mapbox-streets-v8 (mapbox.mapbox-streets-v8) + +Key source-layers available for `mapbox-tileset-vector`: + +| Source Layer | Geometry | Common Use | +|---|---|---| +| `admin` | line, polygon | Boundaries (admin_level 0/1/2) | +| `water` | polygon | Water bodies | +| `waterway` | line | Rivers, streams (class: river, stream, canal) | +| `landuse` | polygon | Land cover (class: wood, agriculture, park, residential, industrial) | +| `landuse_overlay` | polygon | Protected areas (class: national_park, wetland) | +| `road` | line | Roads, railways | +| `building` | polygon | Building footprints (z13+) | +| `place_label` | point | Place labels (has iso_3166_2 for states) | +| `poi_label` | point | Points of interest | + +### mapbox-terrain-v2 (mapbox.mapbox-terrain-v2) + +| Source Layer | Geometry | Use | +|---|---|---| +| `contour` | line | Elevation contour lines | +| `hillshade` | polygon | Terrain shading | + +### Limitations (cannot filter by) + +- No `iso_3166_2` on `admin` polygons (only on `place_label` points) +- No thematic data (deforestation, climate, demographics) +- No time-series or real-time data + +## Design Principles + +The classifier prompt includes these design principles: + +1. **PREFER `mapbox-tileset-vector`** over `mapbox` whenever possible (vector is interactive, lighter, and sharper at all zoom levels) +2. **Base/terrain layers** (background, land, water, roads, labels) may use raster; thematic/data layers should prefer vector +3. **RELEVANCE FIRST**: Match the style to the user's intent — if they ask about "rivers", water/waterway layers should be the PRIMARY visual focus +4. **VISUAL HIERARCHY**: Emphasize relevant layers, de-emphasize irrelevant ones +5. **LAYER MINIMIZATION**: Only include layers that contribute to the user's theme; keep styles performant +6. **COLOR PALETTES** by theme: + - Water / hydrography: blues (#1a5276, #2980b9, #3498db) + - Forest / vegetation: greens (#1a5a27, #27ae60, #2ecc71) + - Urban / infrastructure: grays / oranges (#5d6d7e, #e67e22) + - Administrative: purples / reds (#8e44ad, #c0392b) + - Terrain / elevation: browns (#795548, #a1887f) +7. **HONESTY**: Never claim the style shows data it doesn't contain — use the `limitations` field when approximation is imperfect +8. **GOOD CONTRAST AND READABILITY**: Ensure the style is legible at all zoom levels +9. **EXTERNAL SOURCES**: When the user provides a URL, include it as a source with appropriate layers and styling + +## Error Handling + +The `Minilayer_Handler` returns user-friendly error messages: + +| Error Code | HTTP Status | Message | +|---|---|---| +| `minilayer_no_mapbox_key` | 400 | "Mapbox API key is not configured. Set it in JEO Settings." | +| `minilayer_no_provider` | 400 | "No AI provider configured. Set one in JEO AI Settings." | +| `minilayer_classifier_error` | 400 | "Could not classify the layer request. Please try again." | +| `minilayer_classifier_invalid` | 502 | "The classifier returned an unexpected response. Please try again." | +| `minilayer_not_approximable` | 400 | "The request cannot be approximated with available map data." | +| `minilayer_invalid_type` | 400 | "The AI returned an unsupported layer type." | +| `minilayer_missing_tileset_field` | 400 | "The AI response is missing a required tileset field." | +| `minilayer_invalid_geometry` | 400 | "The AI returned an invalid geometry type." | +| `minilayer_missing_json` | 400 | "The AI response is missing composed style JSON." | +| `minilayer_missing_style_id` | 502 | "The AI did not create a valid style. Please try again." | + +## Migration from MCP + +The previous implementation used `Minilayer_Agent` with the Mapbox DevKit MCP. That class and `JEO_AI_Factory::create_minilayer_assistant()` have been removed. The classifier now performs the same decision logic in a single structured-output call, and `Mapbox_Style_Builder` handles direct Styles API publication. diff --git a/.architecture/minimap/README.md b/.architecture/minimap/README.md new file mode 100644 index 000000000..56a48f630 --- /dev/null +++ b/.architecture/minimap/README.md @@ -0,0 +1,641 @@ +# Minimap — AI-Assisted Map Block + +The `jeo/ai-minimap` block generates interactive contextual maps inside the Gutenberg editor using a combination of RAG-based layer search and a full AI agent for prompt-based generation and multi-turn refinement. + +## Key Files + +| File | Role | +|------|------| +| `src/includes/minimap/class-minimap.php` | `Jeo\Minimap` singleton — REST endpoints, base layer CRUD, geolocation pins, luminance heuristic, conversation history persistence (`inject_history`/`persist_history`) | +| `src/includes/ai/class-minimap-agent.php` | `Jeo\AI\Minimap_Agent` — static factory that builds a configured `Assistant` instance | +| `src/includes/ai/class-minimap-output.php` | `Jeo\AI\Minimap_Output` — structured output DTO with `#[SchemaProperty]` attributes | +| `src/includes/ai/class-search-layers-tool.php` | Agent tool: semantic layer search via `RAG_Worker::find_matching_layers()` | +| `src/includes/ai/class-geocode-tool.php` | Agent tool: geocoding with fallback chain (active geocoder → Mapbox → defaults) | +| `src/includes/ai/class-generate-layer-tool.php` | Agent tool (conditional): generates custom Mapbox styles and creates layer CPTs via `Minilayer_Service`. Only available when a Mapbox API key is configured. | +| `src/includes/ai/class-generate-boundary-layer-tool.php` | Agent tool: resolves a place name into an authoritative boundary polygon and creates a client-side `geojson` layer CPT via `Place_Polygon_Service`. Always available (no Mapbox key required). | +| `src/includes/ai/class-place-polygon-service.php` | `Place_Polygon_Service` + adapters — deterministic polygon resolver using IBGE, FUNAI, and OpenStreetMap. | +| `src/includes/ai/class-mapbox-style-builder.php` | Deterministic Mapbox style builder; publishes minilayer composed styles to the Mapbox Styles API. Boundary layers do not use it — they render client-side from GeoJSON attachments. | +| `src/includes/ai/class-get-post-content-tool.php` | Agent tool: post content + `_related_point` meta (used by post_analyzer sub-agent) | +| `src/includes/ai/class-wp-storage.php` | `StorageInterface` adapter for `post_meta` and `user_meta` | +| `src/includes/ai/class-wp-user-memory-storage.php` | `StorageInterface` adapter for `user_meta` memories — strips redundant user ID from the namespace so preferences are reusable across contexts | +| `src/includes/ai/class-wp-option-storage.php` | `StorageInterface` adapter for `wp_options` (single option per namespace, `autoload=false`) | +| `src/js/src/map-blocks/minimap-editor.js` | Edit component — placeholder, map preview, inspector chat panel (composed Mapbox style support), "Understand chosen layers" link that opens the block sidebar scrolled to the Map layers panel | +| `src/js/src/map-blocks/minimap-display.js` | Save component — renders `
` for frontend JS | +| `src/js/src/map-blocks/index.js` | Block registration with `conversation_id` and `conversation` attributes | +| `src/js/src/map-blocks/minimap-config.js` | Attribute coercion helpers | +| `src/js/src/map-blocks/layers-panel.js` | Sidebar summary of selected layers with metadata | +| `src/js/src/map-blocks/layers-settings.js` | Modal layer library / selected layers editor | +| `src/js/src/map-blocks/layer-settings.js` | Per-layer controls (use, default, legend, opacity) | +| `src/js/src/map-blocks/map-preview-layer.js` | Editor preview rendering for all layer types | +| `src/js/src/map-blocks/mapbox-style-preview.js` | Composed Mapbox style adapter — see [`composed-styles/README.md`](../composed-styles/README.md) | + +## Architecture Overview + +```mermaid +graph TB + subgraph "Gutenberg Editor" + PH[Placeholder
idle/loading state] -->|Generate| API1[/minimap/setup
or /minimap/setup-prompt] + CH[Chat Panel
inspector sidebar] -->|Refine| API2[/minimap/chat] + BV[Base Layer Dropdown] -->|Change variant| API2 + end + + subgraph "Backend — Jeo\Minimap" + API1 --> RUN[run_agent] + API2 --> RUN + RUN --> AGENT[Minimap_Agent::create] + end + + subgraph "AI Agent (hacklabr/ai-assistant)" + AGENT --> MAIN[Main Agent] + AGENT --> SUB[post_analyzer
Sub-Agent] + MAIN --> T1[Search_Layers_Tool] + MAIN --> T2[Geocode_Tool] + MAIN -.->|Mapbox key only| T4[Generate_Layer_Tool] + MAIN -.->|Mapbox key only| T5[Generate_Boundary_Layer_Tool] + SUB --> T3[Get_Post_Content_Tool] + T4 -.-> MLS[Minilayer_Service] + MLS -.-> MLA[Minilayer_Agent] + T5 -.-> PPS[Place_Polygon_Service] + PPS --> IBGE[IBGE Adapter] + PPS --> FUNAI[FUNAI Adapter] + PPS --> OSM[OSM Adapter] + PPS --> MSB[Mapbox_Style_Builder] + MAIN --> OUT[Structured Output
Minimap_Output] + end + + subgraph "Storages" + S1[WP_Storage
post_meta
conversations] + S2[WP_Option_Storage
wp_options
learning] + S3[WP_User_Memory_Storage
user_meta
user memory] + end + + AGENT --> S1 + AGENT --> S2 + AGENT --> S3 + + RUN -->|Apply fallbacks| RES[Base layer + pins
post-processing] + RES -->|REST response| CH +``` + +## REST Endpoints + +All three endpoints are gated by `AI_Handler::is_configured()` (defense in depth — the JS gates this earlier). When no AI provider is correctly configured (valid provider slug **and** its API key, or `ollama_url` for Ollama; filterable via `jeo_ai_is_configured`), they return a `400` with `"No AI provider configured. Set one in JEO AI Settings."`. + +### `POST /jeo/v1/minimap/setup` — Generate from post content + +Legacy RAG-based endpoint (no AI agent). Uses `RAG_Worker::find_matching_layers()` directly. When a `conversation_id` is provided, the generated map state is persisted as a synthetic conversation thread so subsequent chat messages can build on the existing map. + +| Parameter | Type | Required | Description | +|-----------|------|----------|-------------| +| `post_id` | int | Yes | Post to analyze | +| `top_k` | int | No | Max layers (default 5) | +| `conversation_id` | string | No | UUID — if provided, persists initial context for chat continuity | + +Response: `{ success, layers[], base_layer, center_lat, center_lon, initial_zoom, pins[], message }` + +### `POST /jeo/v1/minimap/setup-prompt` — Generate from prompt (AI agent) + +Uses the full AI agent with structured output. The agent receives context about the post and may delegate to the `post_analyzer` sub-agent to extract geographic information from the post content — useful when the prompt references specific sections of the post (e.g. "map based on the Amazon section"). + +| Parameter | Type | Required | Description | +|-----------|------|----------|-------------| +| `prompt` | string | Yes | Text description of desired map | +| `post_id` | int | Yes | Post ID — enables post content analysis via post_analyzer and conversation storage | +| `conversation_id` | string | No | UUID for conversation continuity | + +Response: `{ success, layers[], base_layer, center_lat, center_lon, initial_zoom, pins[], message, assistant_message, base_variant? }` + +### `POST /jeo/v1/minimap/chat` — Multi-turn refinement (AI agent) + +Refines an existing map through conversation. The agent maintains context via conversation storage. + +| Parameter | Type | Required | Description | +|-----------|------|----------|-------------| +| `conversation_id` | string | Yes | Block conversation UUID | +| `message` | string | No* | User message (*required unless `type=regenerate`) | +| `type` | string | No | `text` (default), `base_variant`, `add_layers`, `regenerate` | +| `post_id` | int | Yes | Post ID for conversation storage and pin extraction | +| `payload` | object | No | Structured data (e.g. `{ variant: "satellite" }`) | +| `current_map_state` | object | No | Live block attributes (`layers`, `base_layer`, `center_lat`, `center_lon`, `initial_zoom`, `pins`) — injected as system prompt context so the AI always knows the current map regardless of conversation history | + +Structured types are resolved to natural language by `resolve_structured_message()` before being sent to the agent: + +| Type | Resolved Prompt | +|------|----------------| +| `base_variant` | `"Change the base layer variant to {variant}."` | +| `add_layers` | `"Search for and add additional map layers about: {topic}"` | +| `regenerate` | `"Generate a completely new map suggestion, ignoring previous choices."` | +| `text` | Passed through as-is | + +## Agent Architecture + +### Minimap_Agent Factory + +`Minimap_Agent::create()` builds an `Assistant` with: + +```mermaid +graph LR + A[AssistantConfig] --> B[Main Agent] + A --> C[post_analyzer sub-agent] + B --> D[Search_Layers_Tool] + B --> E[Geocode_Tool] + B -.->|Mapbox key only| F[Generate_Layer_Tool] + C --> G[Get_Post_Content_Tool] + F -.-> H[Minilayer_Service] + B --> I[Minimap_Output
structured output] + B --> J[autoLearn: true] + B --> K[autoDelegate: true] +``` + +### Optional Minilayer Integration + +`Minimap_Agent::create()` always registers `Generate_Boundary_Layer_Tool` alongside the standard tools (it needs no Mapbox key). When a Mapbox API key is configured, `Generate_Layer_Tool` is also registered: + +- `Generate_Boundary_Layer_Tool` — resolves a place name into an authoritative boundary polygon via `Place_Polygon_Service` and creates a client-side `geojson` layer CPT (GeoJSON published as a WordPress attachment). +- `Generate_Layer_Tool` — creates custom Mapbox styles from a text prompt via `Minilayer_Service`. Only available when a Mapbox API key is configured. + +**Authorization gates:** +- `generate_layer` requires explicit user authorization via chat for non-boundary layers. +- `generate_boundary_layer` may be called proactively for administrative boundaries (municipality, state, department) and indigenous lands when `search_layers` finds no suitable existing layer. + +When no Mapbox key is configured, `generate_layer` is omitted and the prompt includes a "Layer Limitations" section instructing the agent to suggest connecting a Mapbox key. + +### Layer Metadata in the Editor + +The layer library and selected-layer panels show friendly metadata to help editors understand why a layer was picked: + +- **Title + type** (existing) +- **Excerpt / content** — `excerpt.rendered` falls back to `content.rendered`, stripped of HTML +- **Attribution / source** +- **Layer themes** (`layer-theme` taxonomy terms) + +The CPT `map-layer` registers `'excerpt'` in `supports` so editors can write a dedicated short description. + +### Per-Layer Opacity + +Each selected layer has an opacity slider in `layer-settings.js`. The value is persisted as `layers[i].opacity` (number 0–1) and applied in both the editor preview (`map-preview-layer.js`) and the frontend layer type handlers (`src/includes/layer-types/*.js`). + +### Manual Search Disambiguation + +The layer library search results display type, source, themes, and excerpt so editors can distinguish similarly named layers (e.g. multiple "river" layers). + +### Default Style from Generated Layers + +When `Generate_Layer_Tool` creates a `mapbox-tileset-vector` layer that includes `suggested_filter` and/or `suggested_paint` from the AI, these are stored as `default_style` CPT meta on the layer. The minimap agent then sets `style.use_default: true` on the layer instance in its structured output, activating the AI-suggested filter/paint without the user needing to configure it manually. + +The user can later toggle `use_default` off in the `LayerStyleEditor` modal (Gutenberg layer settings) to override with manual paint values. See [`.architecture/minilayer/README.md`](../minilayer/README.md) for the full `default_style` storage and resolution flow. + +### Tool Error Handling + +The minimap agent's system prompt includes explicit instructions for handling tool failures gracefully: + +- **`search_layers` failure** → informational `assistant_message`, map renders with base layer + pins only +- **`generate_layer` / `generate_boundary_layer` failure** → `assistant_message` must **name the failing service** (IBGE, Mapbox, OpenStreetMap/Overpass) and **repeat the actionable guidance** from the tool's `error` field (e.g. the Mapbox token missing the `styles:write` scope) — the `error` sentences are user-facing copy, not technical internals +- The AI must **never guess a cause**: "boundary not found" may only be reported when the tool error actually says no boundary was found +- **Raw internals remain forbidden** (stack traces, PHP/WP_Error codes like `jeo_mapbox_style_publish_http`, file paths, class names) +- The map is always rendered (with base layer + pins at minimum) even when tools fail +- Tool failures are logged server-side via `error_log()` (prefixed `[JEO]`) in both generation tools and in `Mapbox_Style_Builder::publish_style()`, so admins can diagnose the exact `WP_Error` code and message from the PHP error log + +**Mapbox token requirement:** publishing boundary/minilayer styles calls `POST /styles/v1/{user}`, which needs a token with the **`styles:write` scope**. The default public token (`pk.…`) usually lacks it and fails with HTTP 403 — `publish_style()` translates 403/401 into actionable messages (create a secret token `sk.…` or add the scope at mapbox.com/account/access-tokens). Both AI pipelines resolve the token via `jeo_settings()->get_mapbox_publish_token()`, which prefers the server-only `mapbox_secret_key` setting (never exposed to the frontend) and falls back to the public `mapbox_key`. + +**Flow: Layer generation via chat** + +```mermaid +sequenceDiagram + participant U as User + participant E as minimap-editor.js + participant R as /minimap/chat + participant M as Minimap::run_agent() + participant A as Minimap_Agent + participant GL as Generate_Layer_Tool + participant S as Minilayer_Service + + U->>E: "No deforestation layer? Can you generate one?" + E->>R: POST {conversation_id, message} + R->>M: run_agent() + M->>A: Minimap_Agent::create() — with Generate_Layer_Tool + M->>A: structured(UserMessage) + A->>A: Evaluate: user authorized generation + A->>GL: generate_layer("deforestation heatmap Amazon") + GL->>S: Minilayer_Service::generate_and_create() + S-->>GL: {success, layer_id, title, type, ...} + GL-->>A: JSON result + A-->>M: Minimap_Output (with new layer_id) + M-->>R: to_rest_response() + R-->>E: {success, layers, ..., assistant_message} + E-->>U: Updated map with generated layer +``` + +### Storage Strategy + +| Storage | Class | Backend | Namespace | Purpose | +|---------|-------|---------|-----------|---------| +| Conversation | `WP_Storage` | `post_meta` | Per post ID | Chat history per block instance | +| Learning | `WP_Option_Storage` | `wp_options` | Global | Agent self-improvement across sessions | +| User Memory | `WP_User_Memory_Storage` | `user_meta` | `memories` (namespace normalised) | User preferences (base variant, layer preferences). The ai-assistant library builds namespaces as `memories/{userId}`; this adapter strips the redundant user ID because the `usermeta.user_id` column already scopes the row. Resulting meta key: `_jeo_ai_memories_{memory_id}` | + +### Structured Output (Minimap_Output) + +The agent always returns a `Minimap_Output` DTO via NeuronAI structured output: + +| Property | Type | Description | +|----------|------|-------------| +| `layers` | `array` | Thematic layer definitions (`id`, `use`, `default`, `show_legend`) | +| `base_layer` | `?array` | Base terrain layer with `variant` (dark/light/satellite) | +| `center_lat` | `float` | Map center latitude | +| `center_lon` | `float` | Map center longitude | +| `initial_zoom` | `int` | Zoom level (0-20) | +| `pins` | `array` | Geolocation pins from post | +| `base_variant` | `?string` | Agent-chosen variant (null = luminance heuristic fallback) | +| `restore_version` | `?int` | 1-based version number to restore — set only when the user explicitly asks to go back to a previous version (see [Version History & Restore](#version-history--restore)) | +| `restored_version` | `?int` | Backend-populated: version actually applied after validation (null = no valid restore) | +| `message` | `string` | Info/warning for the block editor | +| `assistant_message` | `string` | Chat message shown in the inspector panel | + +### Conversation History Persistence + +`run_agent()` injects prior chat history before the agent call and persists the updated history after: + +```mermaid +sequenceDiagram + participant M as Minimap::run_agent() + participant A as Assistant + participant H as ChatHistory + participant S as ConversationStore (post_meta) + + M->>S: loadThread(conversation_id) + S-->>M: [{role, content}, ...] + M->>A: getChatHistory() + M->>H: addMessage(UserMessage/AssistantMessage) × N + M->>A: structured(UserMessage) + Note over A: Agent sees full history + new message + A-->>M: Minimap_Output + M->>A: getChatHistory()->getMessages() + M->>M: Filter to user/assistant text messages + M->>S: saveThread(conversation_id, [{role, content}, ...]) +``` + +**Serialization**: Only `user` and `assistant` text messages are stored (tool calls, tool results are excluded). Each message is a simple `{role: string, content: string}` array. Stored via `ConversationStore` → `WP_Storage` → `update_post_meta()` (PHP `serialize()`). + +**Injection**: Prior messages are loaded and added to the assistant's chat history via `addMessage()` BEFORE `structured()` is called. This works because `resolveState()` is lazy — the state is created once and reused, and `init()` resets `toolRuns`/`steps` but NOT the chat history. + +**Alternation guarantee**: The AI library validates strict user/assistant alternation on every `addMessage()` and throws `Invalid message sequence` on any same-role adjacency — a poisoned stored thread makes every subsequent turn fail. To prevent this, `inject_history()` replays threads through `Jeo\AI\Thread_Normalizer::normalize_thread_messages()` (shared trait, `src/includes/ai/trait-thread-normalizer.php`): it merges consecutive same-role messages, drops leading `assistant` messages and a trailing orphan `user`, and coerces unknown roles to `user`. This also self-heals threads poisoned before the fix (the Context Handler replays through the same normalizer). + +#### Error Persistence + +When `run_agent()` throws (AI error, empty response, etc.), both layers preserve the user message: + +1. **Block attributes** (`conversation[]`) — the frontend (`minimap-editor.js`) includes the user message in `.catch()` and `syncError` handlers alongside the error response, so it is visible in the chat panel and persists with the post. +2. **ConversationStore** (AI thread) — `api_chat()` and `api_setup_prompt()` append the message **pair** `user` + synthetic `assistant` failure note via `appendToThread()` in their `catch` blocks (skipped for `type=regenerate`), so the next successful `inject_history()` includes the unanswered message. The synthetic assistant reply is required: appending a bare `user` message leaves the thread ending in an unmatched `user`, which breaks alternation on the next replay. + +The minimap `run_agent()` does **not** have a retry loop (unlike the Context Handler); a single failure throws immediately. + +#### Content-based generation context (persist_initial_context) + +When a map is generated from post content via `/minimap/setup`, the RAG-based flow does not use the AI agent and therefore produces no conversation history. To ensure chat refinement works, `persist_initial_context()` stores a synthetic thread: + +``` +[user] "Generate a map for this post based on its content." +[assistant] "Map generated from post content with the following configuration: + Layers: - Layer Name (ID: 123), ... + Center: -14.235, -51.925 | Zoom: 8 + Base: dark + Pins: 2 geolocation point(s)" +``` + +This synthetic history is loaded by `inject_history()` on the first chat message, giving the AI agent full context about the existing map. + +#### Live state context (build_state_context) + +In addition to conversation history, `/minimap/chat` also receives the current block attributes as `current_map_state`. `build_state_context()` formats this as a context string passed to `Minimap_Agent::create()` as `initial_context`, which appends it to the system prompt. This ensures the AI always knows the live map state even if conversation history is stale or missing. + +### Post-Processing in `run_agent()` + +After the agent returns, `Minimap::run_agent()` applies two safety nets: + +1. **Base layer fallback**: If `base_layer` is null, creates one using the agent's `base_variant` or the luminance heuristic (`determine_base_variant()`). The AI-emitted `base_layer.id` is coerced with `(int)` before the int-typed `is_valid_layer()` check (LLMs sometimes return it as a non-numeric string, which previously caused an uncaught TypeError / WP critical error); non-numeric or invalid ids reset `base_layer` to null so the fallback runs, and a valid id is normalized to int so it never leaks to block attributes. +2. **Pin fallback**: If the agent returned no pins and a `post_id` exists, fills pins from `_related_point` post meta + +Both AI endpoints (`/minimap/setup-prompt`, `/minimap/chat`) catch `\Throwable` (not just `\Exception`) around `run_agent()` and return a handled 500 REST error — residual TypeErrors from malformed AI output become a chat-visible message instead of a WordPress critical error. + +Before the refinement guards, a declared **version restore** is applied deterministically (`apply_version_restore()`, see [Version History & Restore](#version-history--restore)): a valid `restore_version` replaces layers/base/center/zoom/pins with the stored snapshot; an invalid one keeps the agent's own reconstruction with a notice. Restore turns skip `tag_layer_provenance`, `apply_diff_guard`, and `preserve_manual_layers` — the snapshot is authoritative user intent. + +After the refinement guards (`apply_diff_guard`, `preserve_manual_layers`), two normalizations run: + +3. **`default` normalization** (`normalize_layer_defaults()`): Non-toggle layers (`use` not in `swappable`/`switchable`, e.g. `fixed`) are forced `default: true`. This mirrors the composer's visibility-baking rule and the editor's `shouldDisplayLayerInstance`, and guards against the AI omitting or falsifying `default` on fixed layers — which would otherwise render them hidden on the frontend. + +4. **Render order normalization** (`normalize_layer_render_order()`): Layers are stable-sorted bottom-to-top as **raster → vector** so opaque rasters never hide vector data behind them. The base layer is stored separately and always rendered below everything. Classification comes from the layer CPT `type` meta: + - Raster group (bottom): `tilelayer`, `mapbox-tileset-raster`, and `mapbox` styles whose style JSON contains any `type: "raster"` sub-layer. Styles are fetched via the shared `Jeo::fetch_mapbox_style()` helper (per-layer token with global fallback; style JSON transient-cached 1h, filter `jeo_mapbox_style_cache_ttl`). The raster/vector **classification** is cached separately in a minimap transient — 24h on success, 5min on failure. + - Vector group (top): `mvt`, `mapbox-tileset-vector`, purely vector `mapbox` styles, and unknown types. + - **Conservative fallback**: when a `mapbox` style cannot be fetched/parsed it is treated as raster, so genuine vector layers always stay visible above it. + + All render paths (editor preview, frontend `JeoMap`, and the composed Mapbox style composer) stack layers in the `layers` array order, so this single normalization covers them all. The same normalization is applied in the legacy `/minimap/setup` RAG path. Blocks saved before this rule and manual library additions keep their stored order until the next AI interaction (the diff guard compares IDs only, so reordering never triggers it). + +Finally, the post-normalization state is appended to the version history (`append_version()`) and the summary is persisted. + +## Editor State Machine + +### AI Configuration Gate (editor UI) + +`minimap-editor.js` reads `globalThis.jeoMapVars.ai_configured` (localized in `class-jeo.php` from `AI_Handler::is_configured()`) once at module scope into `AI_CONFIGURED`. When the AI integration is not configured, every AI-only control is hidden — **completely** (no warning/CTA): + +- **idle/error placeholder**: description, generation-mode radio, prompt textarea, and "Generate map" button are hidden (label and error notices remain). +- **ready state**: the "AI Assistant" inspector `PanelBody` is hidden. The map itself stays fully rendered and editable (Map settings, Map layers, Base Layer, Geolocation Pins) — only the chat is removed. + +### States + +```mermaid +stateDiagram-v2 + [*] --> idle + idle --> loading: Generate (content or prompt) + error --> loading: Generate (retry) + loading --> ready: API success + loading --> error: API failure + ready --> ready: Chat message (via /minimap/chat) + ready --> ready: Base variant change (via /minimap/chat) + ready --> ready: Regenerate (via /minimap/chat) + ready --> ready: New prompt (via /minimap/setup-prompt, keeps conversation_id) + + state idle { + [*] --> content_mode + content_mode --> prompt_mode: Radio switch + note right of content_mode: Uses /minimap/setup (RAG) + note right of prompt_mode: Uses /minimap/setup-prompt (agent) + } + + state ready { + [*] --> chat_panel + note right of chat_panel: Auto-assigns conversation_id if missing (legacy blocks) + chat_panel --> chat_panel: sendChat() — text, regenerate, base_variant + chat_panel --> chat_panel: generateFromChatPrompt() — new prompt + } +``` + +### `conversation_id` Lifecycle + +| Event | Behavior | +|-------|----------| +| First generation (content) | Frontend generates UUID, stores as `conversation_id` attribute | +| First generation (prompt) | Frontend generates UUID, passes to `/minimap/setup-prompt`, stores as attribute | +| Legacy block (saved before `conversation_id` existed) | `useEffect` auto-assigns UUID when `status === 'ready'` and `conversation_id` is empty | +| `sendChat()` called without `conversation_id` | Generates UUID on the fly, stores it, proceeds with API call | + +### Chat Panel Actions (Inspector Sidebar) + +The inspector sidebar shows three action types when the block is in `ready` state: + +| Action | Endpoint | Callback | Behavior | +|--------|----------|----------|----------| +| **Send** | `/minimap/chat` | `sendChat(text, 'text')` | Refinement — agent modifies existing map based on user instruction. User message + assistant response appended to conversation | +| **Regenerate** | `/minimap/chat` | `sendChat('', 'regenerate')` | Full replacement — agent generates a completely new map. No user message in conversation, only assistant response | +| **New prompt** | `/minimap/setup-prompt` | `generateFromChatPrompt()` | Fresh generation — starts a new agent call with a fresh prompt. Keeps `conversation_id` for learning continuity. User prompt + assistant response appended to conversation | +| **Base variant change** | `/minimap/chat` | `sendChat(text, 'base_variant', {variant})` | Structured control — backend resolves to natural language. Single API call (replaces old two-call approach) | + +All actions use `chatLoading` state (not `status: 'loading'`) so the map preview stays visible during processing. + +### Sidebar Navigation (Preview → Map Layers Panel) + +The preview area shows an "Understand chosen layers" link button (only when layers are selected) next to "Edit layers settings". Clicking it: + +1. Resolves the top window (`window.parent` — the block preview renders inside the editor iframe, the sidebar in the parent document), mirroring the `map-editor-preview.js` bridge pattern +2. Dispatches `core/edit-post::openGeneralSidebar('edit-post/block')` on the **parent** registry (guaranteed to hit the UI-controlling store, regardless of store sync across the iframe) +3. After ~100 ms, queries the parent document for `.jeo-layers-panel` (the `Map layers` PanelBody) and calls `scrollIntoView({ behavior: 'smooth' })` +4. Fallback: if the panel is not mounted (inspector showing the Styles tab), finds the "Settings" tab inside `.block-editor-block-inspector`, clicks it, and retries the scroll + +## Block Attributes + +| Attribute | Type | Default | Description | +|-----------|------|---------|-------------| +| `layers` | `array` | `[]` | Thematic layer definitions | +| `base_layer` | `object` | `null` | Base terrain layer with variant | +| `center_lat` | `number` | — | Map center latitude | +| `center_lon` | `number` | — | Map center longitude | +| `initial_zoom` | `number` | — | Zoom level | +| `pins` | `array` | `[]` | Geolocation pins (`lat`, `lon`, `relevance`, `address`). Rendered as markers in the editor preview when `show_pins` is enabled (see "Pin preview parity" below) | +| `show_pins` | `boolean` | `true` | Toggle pin visibility (editor preview and frontend) | +| `status` | `string` | `'idle'` | Block state: `idle` → `loading` → `ready` / `error` | +| `message` | `string` | `''` | Info/warning message | +| `prompt` | `string` | `''` | User's text prompt (for initial generation) | +| `conversation_id` | `string` | `''` | UUID assigned on first generation | +| `conversation` | `array` | `[]` | Chat history (`{role, text, ts}[]`) | + +## Flow: First Generation (from prompt) + +```mermaid +sequenceDiagram + participant U as User + participant E as minimap-editor.js + participant R as /minimap/setup-prompt + participant M as Minimap::run_agent() + participant A as Minimap_Agent + participant LLM as AI Provider + + U->>E: Click "Generate map" (prompt mode) + E->>E: Generate UUID (conversation_id) + E->>R: POST {prompt, post_id, conversation_id} + R->>M: run_agent(post_id, conversation_id, user_id, prompt) + M->>A: Minimap_Agent::create(post_id, conversation_id, user_id) + A-->>M: Assistant instance + M->>A: structured(UserMessage(prompt)) + A->>A: Delegate to post_analyzer + A->>A: search_layers(query) + A->>A: geocode(location) + A->>LLM: API call (structured output) + LLM-->>A: Minimap_Output JSON + A-->>M: Minimap_Output + M->>M: Apply base layer fallback + M->>M: Apply pins fallback + M-->>R: Minimap_Output.to_rest_response() + R-->>E: {success, layers, base_layer, ..., assistant_message} + E->>E: setAttributes({status: 'ready', conversation_id, conversation: [user_msg, assistant_msg]}) + E-->>U: Map preview + chat panel in inspector +``` + +## Flow: Chat Refinement + +```mermaid +sequenceDiagram + participant U as User + participant E as minimap-editor.js + participant R as /minimap/chat + participant M as Minimap::run_agent() + participant A as Minimap_Agent (with history) + + U->>E: Type "Switch to satellite view" + E->>E: Append assistant typing indicator + E->>R: POST {conversation_id, post_id, message, type: "text"} + R->>R: resolve_structured_message("text", msg, req) → msg + R->>M: run_agent(post_id, conversation_id, user_id, msg) + M->>A: Minimap_Agent::create() + M->>M: inject_history() — load prior thread from post_meta + M->>A: structured(UserMessage) + Note over A: Agent sees full chat context
from post_meta + new user message + A-->>M: Minimap_Output (updated map) + M-->>R: to_rest_response() + R-->>E: {success, layers, base_layer, ..., assistant_message} + E->>E: Append user + assistant messages to conversation + E->>E: Update map attributes + E-->>U: Updated map + new chat messages +``` + +## Base Layer System + +### Luminance Heuristic + +When the agent doesn't choose a `base_variant`, `determine_base_variant()` computes the average luminance of layer legend colors: + +- Average luminance > 0.5 → `dark` base (bright legends need dark background) +- Average luminance ≤ 0.5 → `light` base (dark legends need light background) +- No colors found → `dark` (default) + +### Base Layer CRUD + +Base layers are `map-layer` CPTs tagged with `_jeo_is_base_layer` meta: + +1. **Find existing**: Query by meta key, then by title heuristics (keywords: "dark", "light", "satellite") +2. **Create new**: `wp_insert_post()` with the runtime-aware default config (`get_default_base_layers()`), filtered via `jeo_minimap_base_layers` +3. **Style types**: The default `dark` base for the MapLibre runtime is a `style-json` layer (OpenFreeMap Dark, keyless — CARTO basemaps now require an API key); sites with a Mapbox key get `mapbox` styles instead. `build_base_layer_response()` sets `load_as_style` for any type where `Layer_Types::is_style()` is true, so the editor hoists it as the map's base style and the frontend applies it via `map.setStyle()` when no composed style is in use. +4. **Legacy repair**: `repair_base_layer_if_legacy()` rewrites base CPTs matching a known legacy signature (CARTO dark raster tiles) with the current default config, in place — user-customized bases are untouched. It runs lazily on every `find_existing_base_layer()` and eagerly via `maybe_migrate_base_layers()` on `admin_init` (versioned by `BASE_LAYERS_MIGRATION_VERSION`). + +## Conventions + +- **Stable block identity**: `conversation_id` (UUID) is generated on first generation and stored as a block attribute, ensuring conversation continuity across editor sessions +- **Agent never writes preferences**: User preferences are suggested via agent output and stored by the backend in user_meta, never written directly by the agent +- **Structured controls**: Base variant changes, layer additions, and regeneration are sent as typed messages (`type` field) to the chat endpoint and resolved to natural language server-side +- **Fallback chain**: Agent output is always post-processed with luminance heuristic and pin extraction to ensure complete, usable maps +- **Layer generation authorization**: When Mapbox is available, the agent must always ask for explicit user confirmation via chat before generating custom layers (`generate_layer` tool). Initial auto-generation never creates custom layers +- **Debounced map interaction**: `onMove` and `onZoom` handlers use 300 ms lodash debounce to avoid excessive `setAttributes` calls and re-renders during map pan/zoom +- **Layer render guard**: The map preview only attempts to render layers when `loadedLayers.length > 0`, preventing an empty-map flash while REST metadata is still loading +- **`load_as_style` parity**: When a style-type layer (`mapbox` or `style-json`, see [`.architecture/layers/README.md`](../layers/README.md)) has `load_as_style: true`, the editor preview uses its resolved style (URL or inline object) as the map's base style via `use-style-layer.js::findStyleLayer()` (same mechanism as the frontend's `class-jeo-map.js::applyBaseStyleLayer()`). The style layer is skipped in `renderLayer` (returns `null`), and `style_layers` filtering is applied via `applyStyleLayerFiltering()` in `onStyleData`. See [`.architecture/frontend/README.md`](../frontend/README.md) for the full parity table. +- **Composed Mapbox styles**: When the loaded layers include any `mapbox`-type layer, the editor preview composes all mapbox layers into a single composite style via `useComposedPayloadPreviewStyle` (payload `{scope:'preview', kind:'minimap'}`). The `` uses the composed style as `mapStyle` with `useEditorMapboxTransformRequest` for token handling, and `applyComposedVisibilityFromSettings` drives per-layer visibility from the manifest. Falls back to the single `load_as_style` base when composition is unavailable. An `AbortSignal` cancels stale requests on rapid layer changes. The frontend rides on `JeoMap` (onetime composed-style branch) — no separate rendering path. See [`composed-styles/README.md`](../composed-styles/README.md). +- **Pin preview parity**: When `show_pins` is enabled and `pins` is non-empty, the editor preview renders each pin as a `` child of `` (react-map-gl `Marker`, re-exported from `lib/mapgl-react.js`), mirroring the frontend's `class-jeo-map.js::addOwnPinsAsMarkers()`: primary pins use the `news-marker` icon, secondary ones (`relevance === 'secondary'`) use `news-marker-hover`, 27×36 px, `anchor: bottom`. Icon URLs come from `jeoMapVars.images` — shared by the frontend (`jeo-map`) and editor (`jeo-map-blocks`) localizations via `Jeo::get_map_icon_images()` (filter `jeomap_js_images`); a plain CSS dot is the fallback when icons are unavailable. Markers are non-interactive (`pointerEvents: none`, no popup). The `Marker` component resolves the active GL runtime (MapLibre or Mapbox) from the parent `` context, so pins render under both runtimes. + +## Refinement Stability (Phase 3) + +To prevent the agent from "breaking" an existing map during chat refinement, the backend applies three mechanisms: + +1. **Technical summary persistence** — `Minimap::persist_minimap_summary()` stores a per-conversation snapshot in `post_meta` (`_jeo_minimap_summary_{conversation_id}`) containing: original intent, topics searched, layers found/removed, base variant, center/zoom and pin count. +2. **Enriched state context** — `build_state_context()` sends the live block state to the agent, including layer reasons, full pin coordinates, original intent and searched topics, plus an explicit instruction to make only the requested change. +3. **Diff guard** — `apply_diff_guard()` compares the agent output with the previous block state. If too many layers are removed without an explicit regeneration request, the previous layers are restored and a warning is added to `message`. + +The system prompt also includes **Refinement Rules** that forbid whole-map regeneration unless the user explicitly asks for it. + +### Layer Provenance & Manual Layer Preservation + +Each layer object carries an optional `provenance` field (`'manual'` | `'ai'`) that tracks its origin: + +- **`'manual'`** — Added by the editor via the layer library modal (`layers-settings.js::setLayer()`). Backward-compatible default: layers in saved blocks without `provenance` are treated as `'manual'`. +- **`'ai'`** — Added by the AI agent during generation or refinement. + +Three backend methods work together to protect manual layers during `/minimap/chat` refinements: + +| Method | Runs when | Purpose | +|--------|-----------|---------| +| `tag_layer_provenance()` | Any `run_agent()` with `$previous_state` (skipped on version restore — stored provenance is authoritative) | Propagates `provenance` from previous state to matching AI-returned layers; tags all others as `'ai'` | +| `apply_diff_guard()` | `$is_refinement` only (skipped on version restore) | Existing threshold guard: restores layers when >50% are removed | +| `preserve_manual_layers()` | `$is_refinement` only (skipped on version restore — restores are exact snapshots) | Merges back any `provenance: 'manual'` layers the AI dropped | + +**Explicit removal**: `preserve_manual_layers()` respects intentional removals via `Minimap_Output::$removed_layer_ids` — a structured-output field the AI populates when the user explicitly asks to remove a specific layer. This is language-agnostic (no keyword lists); the AI declares its intent directly. + +**Regenerate**: Manual layers are NOT preserved on regenerate (`type=regenerate`) — the AI gets a fresh start. + +The `build_state_context()` method annotates manual layers with `[manually added]` in the AI prompt context so the agent is aware of them. + +## Version History & Restore + +Conversations store a capped version history so "return to the previous version" works deterministically instead of relying on the model re-reading JSON blobs from the chat thread. + +### Storage + +- **Meta**: `_jeo_minimap_versions_{conversation_id}` (post_meta), capped to `Minimap::VERSIONS_LIMIT = 20` entries (oldest dropped first — same pattern as the Context Assistant's `SUGGESTION_HISTORY_LIMIT`). +- **Snapshot**: taken at the end of every successful turn, after all post-processing (metadata, provenance, defaults, render order) — `label` (truncated triggering message), `timestamp`, `layers`, `base_layer`, `center_lat/lon`, `initial_zoom`, `pins`. +- **Timeline semantics**: a restore appends a new entry (like a git revert), so version numbers always refer to stored history and never shift. +- Appended by `run_agent()` (all AI paths) and by the legacy `api_setup()` (RAG, label "Initial generation from post content"). + +### Restore flow + +```mermaid +sequenceDiagram + participant U as User + participant E as minimap-editor.js + participant R as /minimap/chat + participant M as run_agent() + participant V as _jeo_minimap_versions_* + + U->>E: "volta para a versão anterior" + E->>R: POST {message, current_map_state} + R->>M: run_agent() + M-->>M: build_state_context() includes version list + M->>M: AI sets restore_version (1-based) + alt Valid index + M->>V: load_versions() + M->>M: Snapshot replaces layers/base/center/zoom/pins + M->>M: Skip provenance tagging + diff guard + preserve_manual + M->>M: Notice: "Restored map version N." + else Invalid / legacy (no history) + M->>M: Keep AI reconstruction, skip guards + M->>M: Notice: version not found; fallback applied + end + M->>V: append_version() — restored state becomes new entry + M-->>R: to_rest_response() (includes restored_version) +``` + +### Key properties + +- **Deterministic**: the stored snapshot wins over the model's re-emitted config. The model's output is only a fallback for legacy conversations without history. +- **Restoration-aware guards**: restore turns skip `apply_diff_guard`/`preserve_manual_layers` (option B) — the diff between two snapshots is user intent, not an AI mistake. Declared via the structured field `Minimap_Output::$restore_version` (language-agnostic, same pattern as `removed_layer_ids`). +- **Exact restore**: manual layers added after the restored version are dropped (snapshot semantics); the `message` notice reports the restore. +- **Backward compatible**: old conversations have no version meta — `load_versions()` returns `[]`, the state context shows "(none stored)", the prompt tells the AI not to set `restore_version`, and an invalid declaration falls back gracefully with a notice. Never crashes. +- **Additive API**: `to_rest_response()` includes `restored_version` (int|null); the frontend ignores unknown fields and needed no changes. + +## Layer Themes & Explain Mode (Phase 4) + +- The `layer-theme` taxonomy is registered for the `map-layer` CPT (`src/includes/layers/class-layers.php`). Default terms cover common journalistic themes: Deforestation, Hydrography, Indigenous Lands, Protected Areas, Mining, Oil and Gas, Land Use, Agriculture, Infrastructure, Administrative Boundaries, Socioeconomic, Biodiversity, Fire, Climate. +- `Layer_Data_Loader` indexes theme terms into the RAG embedding text and metadata. +- `RAG_Worker::find_matching_layers()` returns `themes` for each result. +- `Minimap_Agent::system_prompt()` lists the available themes so the agent can prefer theme-matching layers. +- `Minimap::enrich_layer_metadata()` adds `theme`, `themes` and `attribution` to each layer returned to the editor. +- `layers-panel.js` displays reason, themes and source in the inspector. + +### Proactive administrative boundary generation + +The agent may call `generate_boundary_layer` proactively (no Mapbox key required) for: + +- Brazilian municipalities and states (IBGE malhas v3) +- Brazilian indigenous lands (FUNAI WFS) +- International administrative boundaries and protected areas (OpenStreetMap: Nominatim `polygon_geojson` geometry, Overpass outer-ring assembly as fallback) + +`Place_Polygon_Service` tries adapters in an order driven by the optional `entity_type` hint, caches results per place, and publishes the GeoJSON as a WordPress attachment for a stable public URL. The boundary is rendered client-side by the `geojson` layer type — no Mapbox style publishing is involved. The resulting `geojson` layer is returned to the agent with `bbox`, `center_lat`, `center_lon`, `attribution`, `theme`, and an auto-generated `simple-color` legend. + +**Adapter fallback semantics:** an adapter returning `null` means "not applicable" and the next adapter runs. A `WP_Error` is a hard failure of that source (e.g. IBGE API timeout) — the error is remembered, the remaining adapters still run, and the last error is returned only if no adapter produced a polygon. A transient outage of one source therefore does not abort resolution. + +**Adapter contract:** `Place_Polygon_Adapter::resolve( $place_name, $entity_type, $context )` receives the tool's `entity_type` hint. The IBGE adapter honors it (`state` skips the municipality lookup and vice versa) so that, e.g., "França" (France, `state`) does not accent-insensitively match Franca, a city in São Paulo. The IBGE adapter is Brazil-only: when a context is provided and matches no Brazilian state (UF), it returns `null` instead of silently guessing — "Franca, Europa" therefore falls through to OpenStreetMap. Without context or type hint, an accent-insensitive single match (e.g. plain "França") still resolves to the Brazilian city — a known ambiguity; the agent prompt instructs the LLM to pass `entity_type: "state"` for countries. + +**OSM geometry pipeline:** the primary path is a single Nominatim search with `polygon_geojson=1` — the full boundary geometry arrives ready-made (decoded as stdClass, which is markedly lighter in memory than arrays) and the bbox comes from Nominatim's `boundingbox` field. Overpass is only a fallback for relations Nominatim has no outline for; its mirrors are `overpass-api.de` → `overpass.osm.ch` → `maps.mail.ru` with a 25 s per-mirror timeout (the query is sent via POST), and `resolve()` calls `set_time_limit( 120 )` because large relations exceed the PHP 30 s default. Results whose encoded geometry exceeds 1 MB skip the 24 h polygon cache to avoid serializing multi-megabyte structures. + +All non-boundary layer types still require explicit user confirmation before `generate_layer` is called. + +### Boundary resolver configuration + +| Filter | Purpose | +|--------|---------| +| `jeo_place_polygon_adapter_order` | Change the adapter order (`ibge`, `funai`, `osm`) or remove adapters. | +| `jeo_overpass_mirrors` | Replace the default Overpass mirror list. Useful to remove the `.ru` mirror or use a private mirror. | +| `jeo_overpass_request_args` | Inject per-mirror HTTP args (headers, auth tokens, timeouts) for paid/private Overpass instances. | +| `jeo_osm_admin_prefixes` | Customize administrative prefixes stripped from Nominatim queries. | +| `jeo_funai_wfs_name_attribute` | Change the FUNAI feature attribute used for name matching (default: `terrai_nome`; the current FUNAI GeoServer at `geoserver.funai.gov.br` renamed the old `nome` field). | + +### Caching + +* Polygon results (`jeo_polygon__...`): 24 hours — skipped for OSM results whose encoded geometry exceeds 1 MB (e.g. France with overseas territories). +* IBGE municipality/state lists (`jeo_ibge_municipios_list`, `jeo_ibge_estados_list`): 30 days. IBGE cache keys include the `entity_type` hint. +* OSM Nominatim search responses (`jeo_osm_nominatim_`): 6 hours — only responses smaller than 1 MB are cached. +* Overpass relation geometries (`jeo_osm_overpass_relation_`): 24 hours. + +### WP-CLI testing + +The `AI_CLI` class exposes commands for local testing and observability: + +- `wp jeo ai generate-layer "rivers in Brazil"` — runs the full thematic pipeline. +- `wp jeo ai generate-boundary "São Paulo"` — resolves and publishes a boundary layer. +- `wp jeo ai test-minilayer "roads in Rio"` — classifies a prompt without side effects. diff --git a/.architecture/rest-api/README.md b/.architecture/rest-api/README.md new file mode 100644 index 000000000..a960486b3 --- /dev/null +++ b/.architecture/rest-api/README.md @@ -0,0 +1,111 @@ +# REST API + +## Custom Endpoints + +All registered in `class-jeo.php` and `class-ai-handler.php` via `rest_api_init`. + +### Maps & Layers + +| Route | Method | Callback | Description | +|-------|--------|----------|-------------| +| `/jeo/v1/map-layer` | GET | `get_layers` | List layers with search | +| `/jeo/v1/all-pins` | GET | `get_all_pins` | All geolocated points | +| `/jeo/v1/dashboard-stats` | GET | `get_dashboard_stats` | Dashboard statistics | + +### Composed Mapbox Styles + +Registered by `Map_Style_Composer` (`class-map-style-composer.php`). See +[`composed-styles/README.md`](../composed-styles/README.md) for the full architecture. + +| Route | Method | Permission | Description | +|-------|--------|------------|-------------| +| `/jeo/v1/map-style/{id}` | GET | `can_read_map` | Composed style metadata (style/manifest URLs, warnings). Arg `refresh` forces regeneration | +| `/jeo/v1/map-style/{id}/style` | GET | `can_read_map` | Serves `style.json` (composite Mapbox Style v8) | +| `/jeo/v1/map-style/{id}/manifest` | GET | `can_read_map` | Serves `manifest.json` (layer ID → composite layer ID mapping) | +| `/jeo/v1/map-style/compose` | POST | `can_create_virtual` | Compose from payload (scope/kind/postId/layers). `onetime`=public, `preview`=`edit_posts` | +| `/jeo/v1/map-style/layer/{id}/refresh` | POST | `can_refresh_layer` | Force-regenerate all maps referencing a layer | +| `/jeo/v1/map-style/{scope}/{hash}/style` | GET | `can_read_virtual` | Virtual (preview/onetime) style artifact | +| `/jeo/v1/map-style/{scope}/{hash}/manifest` | GET | `can_read_virtual` | Virtual (preview/onetime) manifest artifact | + +### AI (12+ routes) + +See [`ai/README.md`](../ai/README.md) for full details. + +### Minilayer + +| Route | Method | Description | +|-------|--------|-------------| +| `/jeo/v1/minilayer/generate` | POST | Generate Mapbox style from text prompt and create JEO layer | + +### Minimap (AI-Assisted Map) + +| Route | Method | Callback | Description | +|-------|--------|----------|-------------| +| `/jeo/v1/minimap/setup` | POST | `api_setup` | Generate map from post content (RAG + post geopoints) | +| `/jeo/v1/minimap/setup-prompt` | POST | `api_setup_prompt` | Generate map from text prompt via AI agent | +| `/jeo/v1/minimap/chat` | POST | `api_chat` | Multi-turn map refinement via AI agent (requires `conversation_id`) | + +### AI Context (Editorial Suggestions) + +| Route | Method | Callback | Description | +|-------|--------|----------|-------------| +| `/jeo/v1/context/setup` | POST | `api_setup` | Generate initial editorial suggestions from post content | +| `/jeo/v1/context/chat` | POST | `api_chat` | Multi-turn conversation for refining suggestions (requires `conversation_id`) | +| `/jeo/v1/context/state` | GET | `api_get_state` | Load persisted conversation state (messages, suggestions, conversation_id) | +| `/jeo/v1/context/clear` | POST | `api_clear` | Reset conversation and all suggestions | + +### Dashboard + +| Route | Method | Description | +|-------|--------|-------------| +| `/jeo/v1/all-pins` | GET | GeoJSON with all geolocated post points | +| `/jeo/v1/dashboard-stats` | GET | Counts of maps, layers, geolocated posts | +| `/jeo/v1/readme` | GET | README content for welcome page | + +## CPT REST (Standard WordPress) + +| CPT | Base Route | Notes | +|-----|-----------|-------| +| `map` | `/wp/v2/map` | Metafields exposed via REST schema | +| `map-layer` | `/wp/v2/map-layer` | No `per_page` max limit | +| `storymap` | `/wp/v2/storymap` | Enriched with co-author data | +| `jeo-ai-log` | Private (not exposed) | Internal cost tracking | + +## REST Authentication + +- Cookie auth: `rest_request_before_callbacks` filter injects cookie auth +- Nonce: standard `wp_rest` nonce +- Capabilities: Checked via `current_user_can()` on mutations + +## Custom REST Filters + +| Filter | Hook | Description | +|--------|------|-------------| +| `rest_map-layer_query` | `custom_layer_search_filters` | Search by title in layers | +| `rest_map-layer_query` | `order_rest_post_by_post_title` | Order by title | +| `rest_post_tag_query` | `maximum_terms_api_filter` | Limit terms in API | +| `rest_post_query` | `meta_query` passthrough | Filter posts by meta | + +## Response Enrichment + +### Maps + +`rest_prepare_map` → Injects block editor preview HTML into REST response. + +### Layers + +`rest_prepare_map-layer` → Injects block editor preview HTML into REST response. + +### Storymaps + +`rest_prepare_storymap` → Adds co-author data to response. + +## Meta Validation + +The `Rest_Validate_Meta` trait intercepts `rest_pre_insert_{post_type}` and calls `validate_meta_{key}` for each registered meta key. Example: + +```php +protected function validate_meta_initial_zoom( $value ) { + return ( $value >= 1 && $value <= 14 ) ? $value : new WP_Error( ... ); +} +``` diff --git a/.architecture/settings/README.md b/.architecture/settings/README.md new file mode 100644 index 000000000..0c5100517 --- /dev/null +++ b/.architecture/settings/README.md @@ -0,0 +1,124 @@ +# Settings & Admin + +## Key Files + +| File | Role | +|------|------| +| `src/includes/settings/class-settings.php` | `Jeo\Settings` class — 60+ options | +| `src/includes/settings/settings-page.php` | Settings page template | +| `src/includes/settings/settings-page.js` | Interactive JS (Select2, API tests) | +| `src/includes/vendor/select2/` | Locally bundled Select2 assets (no CDN) | +| `src/includes/settings/ai-logs-page.php` | AI cost dashboard | +| `src/includes/menu/class-menu.php` | Admin menu | +| `src/includes/admin/dashboard-page.php` | Geographic dashboard | +| `src/includes/admin/welcome-page.php` | Welcome/onboarding | + +## Settings (Option `jeo-settings`) + +All config in a single array option `jeo-settings`. + +### Tabs + +| Tab | Section | Key Fields | +|-----|---------|------------| +| General | Map runtime, post types | `map_runtime_requested`, `post_types`, `default_zoom`, `default_center`, `mapbox_key`, `mapbox_secret_key` | +| Geocoders | Geocoding service | `geocoding_service` | +| Appearance | Typography, colors | `font_family`, `font_url`, `primary_color`, `secondary_color` | +| Discovery | Discovery page | `discovery_page_id` | + +### AI Settings (Separate tab under Jeo → AI) + +| Tab | Keys | +|-----|------| +| Provider | `ai_provider`, `ai_{provider}_key`, `ai_{provider}_model` | +| Knowledge Base | `rag_auto_index`, `rag_post_types` | +| Context Assistant | `ai_use_context_custom_prompt`, `ai_context_prompt` | +| Bulk | `bulk_ai_enabled`, `bulk_ai_post_types`, `bulk_confidence_threshold` | +| Embedded Data | Reference to JSONs in `ai/data/` | + +### Sanitization + +- API keys: partial masking on display +- Checkboxes: explicit handling per tab (unchecked = absent) +- Colors: hex validation +- Sensitive keys: preserved when input is empty + +### Array-Typed Options Read Coercion + +`Settings::ARRAY_OPTION_KEYS` (`enabled_post_types`, `jeo_bulk_post_types`) lists +settings that must always be arrays. `Settings::get_option()` runs every stored +value for these keys through `normalize_array_option()`, which converts legacy +comma-separated strings (e.g. `'post,storymap'` written by older plugin versions +or imported databases) into arrays and falls back to the default for empty +strings. This self-heals reads without a DB migration — `foreach`/`in_array` +consumers (meta registration, asset enqueue, bulk processor columns, context +handler, privacy exporters, Stories Near You, RAG worker) always receive an +array. + +## Admin Menu + +``` +Jeo +├── Welcome (submenu: welcome) +├── Dashboard (submenu: dashboard) +├── AI (submenu: ai-settings) +│ └── AI Debug Logs (submenu: ai-logs) +├── Maps (CPT: map) +├── Layers (CPT: map-layer) +└── Storymaps (CPT: storymap) +``` + +## Geographic Dashboard + +Fullscreen page with: +- MapLibre/Mapbox map with all pins clustered +- Filter panel: search, post type, taxonomy, term, timeline range slider +- Fetches `/jeo/v1/all-pins` and `/jeo/v1/dashboard-stats` +- 721 lines of inline template + +## Custom CSS + +Settings allow defining primary/secondary colors and typography, injected via: +- `wp_head` → inline `",d.insertBefore(c.lastChild,d.firstChild)}function d(){var a=t.elements;return"string"==typeof a?a.split(" "):a}function e(a,b){var c=t.elements;"string"!=typeof c&&(c=c.join(" ")),"string"!=typeof a&&(a=a.join(" ")),t.elements=c+" "+a,j(b)}function f(a){var b=s[a[q]];return b||(b={},r++,a[q]=r,s[r]=b),b}function g(a,c,d){if(c||(c=b),l)return c.createElement(a);d||(d=f(c));var e;return e=d.cache[a]?d.cache[a].cloneNode():p.test(a)?(d.cache[a]=d.createElem(a)).cloneNode():d.createElem(a),!e.canHaveChildren||o.test(a)||e.tagUrn?e:d.frag.appendChild(e)}function h(a,c){if(a||(a=b),l)return a.createDocumentFragment();c=c||f(a);for(var e=c.frag.cloneNode(),g=0,h=d(),i=h.length;i>g;g++)e.createElement(h[g]);return e}function i(a,b){b.cache||(b.cache={},b.createElem=a.createElement,b.createFrag=a.createDocumentFragment,b.frag=b.createFrag()),a.createElement=function(c){return t.shivMethods?g(c,a,b):b.createElem(c)},a.createDocumentFragment=Function("h,f","return function(){var n=f.cloneNode(),c=n.createElement;h.shivMethods&&("+d().join().replace(/[\w\-:]+/g,function(a){return b.createElem(a),b.frag.createElement(a),'c("'+a+'")'})+");return n}")(t,b.frag)}function j(a){a||(a=b);var d=f(a);return!t.shivCSS||k||d.hasCSS||(d.hasCSS=!!c(a,"article,aside,dialog,figcaption,figure,footer,header,hgroup,main,nav,section{display:block}mark{background:#FF0;color:#000}template{display:none}")),l||i(a,d),a}var k,l,m="3.7.3",n=a.html5||{},o=/^<|^(?:button|map|select|textarea|object|iframe|option|optgroup)$/i,p=/^(?:a|b|code|div|fieldset|h1|h2|h3|h4|h5|h6|i|label|li|ol|p|q|span|strong|style|table|tbody|td|th|tr|ul)$/i,q="_html5shiv",r=0,s={};!function(){try{var a=b.createElement("a");a.innerHTML="",k="hidden"in a,l=1==a.childNodes.length||function(){b.createElement("a");var a=b.createDocumentFragment();return"undefined"==typeof a.cloneNode||"undefined"==typeof a.createDocumentFragment||"undefined"==typeof a.createElement}()}catch(c){k=!0,l=!0}}();var t={elements:n.elements||"abbr article aside audio bdi canvas data datalist details dialog figcaption figure footer header hgroup main mark meter nav output picture progress section summary template time video",version:m,shivCSS:n.shivCSS!==!1,supportsUnknownElements:l,shivMethods:n.shivMethods!==!1,type:"default",shivDocument:j,createElement:g,createDocumentFragment:h,addElements:e};a.html5=t,j(b),"object"==typeof module&&module.exports&&(module.exports=t)}("undefined"!=typeof window?window:this,document); diff --git a/site/js/jquery-3.6.0.min.js b/site/js/jquery-3.6.0.min.js deleted file mode 100644 index c4c6022f2..000000000 --- a/site/js/jquery-3.6.0.min.js +++ /dev/null @@ -1,2 +0,0 @@ -/*! jQuery v3.6.0 | (c) OpenJS Foundation and other contributors | jquery.org/license */ -!function(e,t){"use strict";"object"==typeof module&&"object"==typeof module.exports?module.exports=e.document?t(e,!0):function(e){if(!e.document)throw new Error("jQuery requires a window with a document");return t(e)}:t(e)}("undefined"!=typeof window?window:this,function(C,e){"use strict";var t=[],r=Object.getPrototypeOf,s=t.slice,g=t.flat?function(e){return t.flat.call(e)}:function(e){return t.concat.apply([],e)},u=t.push,i=t.indexOf,n={},o=n.toString,v=n.hasOwnProperty,a=v.toString,l=a.call(Object),y={},m=function(e){return"function"==typeof e&&"number"!=typeof e.nodeType&&"function"!=typeof e.item},x=function(e){return null!=e&&e===e.window},E=C.document,c={type:!0,src:!0,nonce:!0,noModule:!0};function b(e,t,n){var r,i,o=(n=n||E).createElement("script");if(o.text=e,t)for(r in c)(i=t[r]||t.getAttribute&&t.getAttribute(r))&&o.setAttribute(r,i);n.head.appendChild(o).parentNode.removeChild(o)}function w(e){return null==e?e+"":"object"==typeof e||"function"==typeof e?n[o.call(e)]||"object":typeof e}var f="3.6.0",S=function(e,t){return new S.fn.init(e,t)};function p(e){var t=!!e&&"length"in e&&e.length,n=w(e);return!m(e)&&!x(e)&&("array"===n||0===t||"number"==typeof t&&0+~]|"+M+")"+M+"*"),U=new RegExp(M+"|>"),X=new RegExp(F),V=new RegExp("^"+I+"$"),G={ID:new RegExp("^#("+I+")"),CLASS:new RegExp("^\\.("+I+")"),TAG:new RegExp("^("+I+"|[*])"),ATTR:new RegExp("^"+W),PSEUDO:new RegExp("^"+F),CHILD:new RegExp("^:(only|first|last|nth|nth-last)-(child|of-type)(?:\\("+M+"*(even|odd|(([+-]|)(\\d*)n|)"+M+"*(?:([+-]|)"+M+"*(\\d+)|))"+M+"*\\)|)","i"),bool:new RegExp("^(?:"+R+")$","i"),needsContext:new RegExp("^"+M+"*[>+~]|:(even|odd|eq|gt|lt|nth|first|last)(?:\\("+M+"*((?:-\\d)?\\d*)"+M+"*\\)|)(?=[^-]|$)","i")},Y=/HTML$/i,Q=/^(?:input|select|textarea|button)$/i,J=/^h\d$/i,K=/^[^{]+\{\s*\[native \w/,Z=/^(?:#([\w-]+)|(\w+)|\.([\w-]+))$/,ee=/[+~]/,te=new RegExp("\\\\[\\da-fA-F]{1,6}"+M+"?|\\\\([^\\r\\n\\f])","g"),ne=function(e,t){var n="0x"+e.slice(1)-65536;return t||(n<0?String.fromCharCode(n+65536):String.fromCharCode(n>>10|55296,1023&n|56320))},re=/([\0-\x1f\x7f]|^-?\d)|^-$|[^\0-\x1f\x7f-\uFFFF\w-]/g,ie=function(e,t){return t?"\0"===e?"\ufffd":e.slice(0,-1)+"\\"+e.charCodeAt(e.length-1).toString(16)+" ":"\\"+e},oe=function(){T()},ae=be(function(e){return!0===e.disabled&&"fieldset"===e.nodeName.toLowerCase()},{dir:"parentNode",next:"legend"});try{H.apply(t=O.call(p.childNodes),p.childNodes),t[p.childNodes.length].nodeType}catch(e){H={apply:t.length?function(e,t){L.apply(e,O.call(t))}:function(e,t){var n=e.length,r=0;while(e[n++]=t[r++]);e.length=n-1}}}function se(t,e,n,r){var i,o,a,s,u,l,c,f=e&&e.ownerDocument,p=e?e.nodeType:9;if(n=n||[],"string"!=typeof t||!t||1!==p&&9!==p&&11!==p)return n;if(!r&&(T(e),e=e||C,E)){if(11!==p&&(u=Z.exec(t)))if(i=u[1]){if(9===p){if(!(a=e.getElementById(i)))return n;if(a.id===i)return n.push(a),n}else if(f&&(a=f.getElementById(i))&&y(e,a)&&a.id===i)return n.push(a),n}else{if(u[2])return H.apply(n,e.getElementsByTagName(t)),n;if((i=u[3])&&d.getElementsByClassName&&e.getElementsByClassName)return H.apply(n,e.getElementsByClassName(i)),n}if(d.qsa&&!N[t+" "]&&(!v||!v.test(t))&&(1!==p||"object"!==e.nodeName.toLowerCase())){if(c=t,f=e,1===p&&(U.test(t)||z.test(t))){(f=ee.test(t)&&ye(e.parentNode)||e)===e&&d.scope||((s=e.getAttribute("id"))?s=s.replace(re,ie):e.setAttribute("id",s=S)),o=(l=h(t)).length;while(o--)l[o]=(s?"#"+s:":scope")+" "+xe(l[o]);c=l.join(",")}try{return H.apply(n,f.querySelectorAll(c)),n}catch(e){N(t,!0)}finally{s===S&&e.removeAttribute("id")}}}return g(t.replace($,"$1"),e,n,r)}function ue(){var r=[];return function e(t,n){return r.push(t+" ")>b.cacheLength&&delete e[r.shift()],e[t+" "]=n}}function le(e){return e[S]=!0,e}function ce(e){var t=C.createElement("fieldset");try{return!!e(t)}catch(e){return!1}finally{t.parentNode&&t.parentNode.removeChild(t),t=null}}function fe(e,t){var n=e.split("|"),r=n.length;while(r--)b.attrHandle[n[r]]=t}function pe(e,t){var n=t&&e,r=n&&1===e.nodeType&&1===t.nodeType&&e.sourceIndex-t.sourceIndex;if(r)return r;if(n)while(n=n.nextSibling)if(n===t)return-1;return e?1:-1}function de(t){return function(e){return"input"===e.nodeName.toLowerCase()&&e.type===t}}function he(n){return function(e){var t=e.nodeName.toLowerCase();return("input"===t||"button"===t)&&e.type===n}}function ge(t){return function(e){return"form"in e?e.parentNode&&!1===e.disabled?"label"in e?"label"in e.parentNode?e.parentNode.disabled===t:e.disabled===t:e.isDisabled===t||e.isDisabled!==!t&&ae(e)===t:e.disabled===t:"label"in e&&e.disabled===t}}function ve(a){return le(function(o){return o=+o,le(function(e,t){var n,r=a([],e.length,o),i=r.length;while(i--)e[n=r[i]]&&(e[n]=!(t[n]=e[n]))})})}function ye(e){return e&&"undefined"!=typeof e.getElementsByTagName&&e}for(e in d=se.support={},i=se.isXML=function(e){var t=e&&e.namespaceURI,n=e&&(e.ownerDocument||e).documentElement;return!Y.test(t||n&&n.nodeName||"HTML")},T=se.setDocument=function(e){var t,n,r=e?e.ownerDocument||e:p;return r!=C&&9===r.nodeType&&r.documentElement&&(a=(C=r).documentElement,E=!i(C),p!=C&&(n=C.defaultView)&&n.top!==n&&(n.addEventListener?n.addEventListener("unload",oe,!1):n.attachEvent&&n.attachEvent("onunload",oe)),d.scope=ce(function(e){return a.appendChild(e).appendChild(C.createElement("div")),"undefined"!=typeof e.querySelectorAll&&!e.querySelectorAll(":scope fieldset div").length}),d.attributes=ce(function(e){return e.className="i",!e.getAttribute("className")}),d.getElementsByTagName=ce(function(e){return e.appendChild(C.createComment("")),!e.getElementsByTagName("*").length}),d.getElementsByClassName=K.test(C.getElementsByClassName),d.getById=ce(function(e){return a.appendChild(e).id=S,!C.getElementsByName||!C.getElementsByName(S).length}),d.getById?(b.filter.ID=function(e){var t=e.replace(te,ne);return function(e){return e.getAttribute("id")===t}},b.find.ID=function(e,t){if("undefined"!=typeof t.getElementById&&E){var n=t.getElementById(e);return n?[n]:[]}}):(b.filter.ID=function(e){var n=e.replace(te,ne);return function(e){var t="undefined"!=typeof e.getAttributeNode&&e.getAttributeNode("id");return t&&t.value===n}},b.find.ID=function(e,t){if("undefined"!=typeof t.getElementById&&E){var n,r,i,o=t.getElementById(e);if(o){if((n=o.getAttributeNode("id"))&&n.value===e)return[o];i=t.getElementsByName(e),r=0;while(o=i[r++])if((n=o.getAttributeNode("id"))&&n.value===e)return[o]}return[]}}),b.find.TAG=d.getElementsByTagName?function(e,t){return"undefined"!=typeof t.getElementsByTagName?t.getElementsByTagName(e):d.qsa?t.querySelectorAll(e):void 0}:function(e,t){var n,r=[],i=0,o=t.getElementsByTagName(e);if("*"===e){while(n=o[i++])1===n.nodeType&&r.push(n);return r}return o},b.find.CLASS=d.getElementsByClassName&&function(e,t){if("undefined"!=typeof t.getElementsByClassName&&E)return t.getElementsByClassName(e)},s=[],v=[],(d.qsa=K.test(C.querySelectorAll))&&(ce(function(e){var t;a.appendChild(e).innerHTML="
",e.querySelectorAll("[msallowcapture^='']").length&&v.push("[*^$]="+M+"*(?:''|\"\")"),e.querySelectorAll("[selected]").length||v.push("\\["+M+"*(?:value|"+R+")"),e.querySelectorAll("[id~="+S+"-]").length||v.push("~="),(t=C.createElement("input")).setAttribute("name",""),e.appendChild(t),e.querySelectorAll("[name='']").length||v.push("\\["+M+"*name"+M+"*="+M+"*(?:''|\"\")"),e.querySelectorAll(":checked").length||v.push(":checked"),e.querySelectorAll("a#"+S+"+*").length||v.push(".#.+[+~]"),e.querySelectorAll("\\\f"),v.push("[\\r\\n\\f]")}),ce(function(e){e.innerHTML="";var t=C.createElement("input");t.setAttribute("type","hidden"),e.appendChild(t).setAttribute("name","D"),e.querySelectorAll("[name=d]").length&&v.push("name"+M+"*[*^$|!~]?="),2!==e.querySelectorAll(":enabled").length&&v.push(":enabled",":disabled"),a.appendChild(e).disabled=!0,2!==e.querySelectorAll(":disabled").length&&v.push(":enabled",":disabled"),e.querySelectorAll("*,:x"),v.push(",.*:")})),(d.matchesSelector=K.test(c=a.matches||a.webkitMatchesSelector||a.mozMatchesSelector||a.oMatchesSelector||a.msMatchesSelector))&&ce(function(e){d.disconnectedMatch=c.call(e,"*"),c.call(e,"[s!='']:x"),s.push("!=",F)}),v=v.length&&new RegExp(v.join("|")),s=s.length&&new RegExp(s.join("|")),t=K.test(a.compareDocumentPosition),y=t||K.test(a.contains)?function(e,t){var n=9===e.nodeType?e.documentElement:e,r=t&&t.parentNode;return e===r||!(!r||1!==r.nodeType||!(n.contains?n.contains(r):e.compareDocumentPosition&&16&e.compareDocumentPosition(r)))}:function(e,t){if(t)while(t=t.parentNode)if(t===e)return!0;return!1},j=t?function(e,t){if(e===t)return l=!0,0;var n=!e.compareDocumentPosition-!t.compareDocumentPosition;return n||(1&(n=(e.ownerDocument||e)==(t.ownerDocument||t)?e.compareDocumentPosition(t):1)||!d.sortDetached&&t.compareDocumentPosition(e)===n?e==C||e.ownerDocument==p&&y(p,e)?-1:t==C||t.ownerDocument==p&&y(p,t)?1:u?P(u,e)-P(u,t):0:4&n?-1:1)}:function(e,t){if(e===t)return l=!0,0;var n,r=0,i=e.parentNode,o=t.parentNode,a=[e],s=[t];if(!i||!o)return e==C?-1:t==C?1:i?-1:o?1:u?P(u,e)-P(u,t):0;if(i===o)return pe(e,t);n=e;while(n=n.parentNode)a.unshift(n);n=t;while(n=n.parentNode)s.unshift(n);while(a[r]===s[r])r++;return r?pe(a[r],s[r]):a[r]==p?-1:s[r]==p?1:0}),C},se.matches=function(e,t){return se(e,null,null,t)},se.matchesSelector=function(e,t){if(T(e),d.matchesSelector&&E&&!N[t+" "]&&(!s||!s.test(t))&&(!v||!v.test(t)))try{var n=c.call(e,t);if(n||d.disconnectedMatch||e.document&&11!==e.document.nodeType)return n}catch(e){N(t,!0)}return 0":{dir:"parentNode",first:!0}," ":{dir:"parentNode"},"+":{dir:"previousSibling",first:!0},"~":{dir:"previousSibling"}},preFilter:{ATTR:function(e){return e[1]=e[1].replace(te,ne),e[3]=(e[3]||e[4]||e[5]||"").replace(te,ne),"~="===e[2]&&(e[3]=" "+e[3]+" "),e.slice(0,4)},CHILD:function(e){return e[1]=e[1].toLowerCase(),"nth"===e[1].slice(0,3)?(e[3]||se.error(e[0]),e[4]=+(e[4]?e[5]+(e[6]||1):2*("even"===e[3]||"odd"===e[3])),e[5]=+(e[7]+e[8]||"odd"===e[3])):e[3]&&se.error(e[0]),e},PSEUDO:function(e){var t,n=!e[6]&&e[2];return G.CHILD.test(e[0])?null:(e[3]?e[2]=e[4]||e[5]||"":n&&X.test(n)&&(t=h(n,!0))&&(t=n.indexOf(")",n.length-t)-n.length)&&(e[0]=e[0].slice(0,t),e[2]=n.slice(0,t)),e.slice(0,3))}},filter:{TAG:function(e){var t=e.replace(te,ne).toLowerCase();return"*"===e?function(){return!0}:function(e){return e.nodeName&&e.nodeName.toLowerCase()===t}},CLASS:function(e){var t=m[e+" "];return t||(t=new RegExp("(^|"+M+")"+e+"("+M+"|$)"))&&m(e,function(e){return t.test("string"==typeof e.className&&e.className||"undefined"!=typeof e.getAttribute&&e.getAttribute("class")||"")})},ATTR:function(n,r,i){return function(e){var t=se.attr(e,n);return null==t?"!="===r:!r||(t+="","="===r?t===i:"!="===r?t!==i:"^="===r?i&&0===t.indexOf(i):"*="===r?i&&-1:\x20\t\r\n\f]*)[\x20\t\r\n\f]*\/?>(?:<\/\1>|)$/i;function j(e,n,r){return m(n)?S.grep(e,function(e,t){return!!n.call(e,t,e)!==r}):n.nodeType?S.grep(e,function(e){return e===n!==r}):"string"!=typeof n?S.grep(e,function(e){return-1)[^>]*|#([\w-]+))$/;(S.fn.init=function(e,t,n){var r,i;if(!e)return this;if(n=n||D,"string"==typeof e){if(!(r="<"===e[0]&&">"===e[e.length-1]&&3<=e.length?[null,e,null]:q.exec(e))||!r[1]&&t)return!t||t.jquery?(t||n).find(e):this.constructor(t).find(e);if(r[1]){if(t=t instanceof S?t[0]:t,S.merge(this,S.parseHTML(r[1],t&&t.nodeType?t.ownerDocument||t:E,!0)),N.test(r[1])&&S.isPlainObject(t))for(r in t)m(this[r])?this[r](t[r]):this.attr(r,t[r]);return this}return(i=E.getElementById(r[2]))&&(this[0]=i,this.length=1),this}return e.nodeType?(this[0]=e,this.length=1,this):m(e)?void 0!==n.ready?n.ready(e):e(S):S.makeArray(e,this)}).prototype=S.fn,D=S(E);var L=/^(?:parents|prev(?:Until|All))/,H={children:!0,contents:!0,next:!0,prev:!0};function O(e,t){while((e=e[t])&&1!==e.nodeType);return e}S.fn.extend({has:function(e){var t=S(e,this),n=t.length;return this.filter(function(){for(var e=0;e\x20\t\r\n\f]*)/i,he=/^$|^module$|\/(?:java|ecma)script/i;ce=E.createDocumentFragment().appendChild(E.createElement("div")),(fe=E.createElement("input")).setAttribute("type","radio"),fe.setAttribute("checked","checked"),fe.setAttribute("name","t"),ce.appendChild(fe),y.checkClone=ce.cloneNode(!0).cloneNode(!0).lastChild.checked,ce.innerHTML="",y.noCloneChecked=!!ce.cloneNode(!0).lastChild.defaultValue,ce.innerHTML="",y.option=!!ce.lastChild;var ge={thead:[1,"","
"],col:[2,"","
"],tr:[2,"","
"],td:[3,"","
"],_default:[0,"",""]};function ve(e,t){var n;return n="undefined"!=typeof e.getElementsByTagName?e.getElementsByTagName(t||"*"):"undefined"!=typeof e.querySelectorAll?e.querySelectorAll(t||"*"):[],void 0===t||t&&A(e,t)?S.merge([e],n):n}function ye(e,t){for(var n=0,r=e.length;n",""]);var me=/<|&#?\w+;/;function xe(e,t,n,r,i){for(var o,a,s,u,l,c,f=t.createDocumentFragment(),p=[],d=0,h=e.length;d\s*$/g;function je(e,t){return A(e,"table")&&A(11!==t.nodeType?t:t.firstChild,"tr")&&S(e).children("tbody")[0]||e}function De(e){return e.type=(null!==e.getAttribute("type"))+"/"+e.type,e}function qe(e){return"true/"===(e.type||"").slice(0,5)?e.type=e.type.slice(5):e.removeAttribute("type"),e}function Le(e,t){var n,r,i,o,a,s;if(1===t.nodeType){if(Y.hasData(e)&&(s=Y.get(e).events))for(i in Y.remove(t,"handle events"),s)for(n=0,r=s[i].length;n").attr(n.scriptAttrs||{}).prop({charset:n.scriptCharset,src:n.url}).on("load error",i=function(e){r.remove(),i=null,e&&t("error"===e.type?404:200,e.type)}),E.head.appendChild(r[0])},abort:function(){i&&i()}}});var _t,zt=[],Ut=/(=)\?(?=&|$)|\?\?/;S.ajaxSetup({jsonp:"callback",jsonpCallback:function(){var e=zt.pop()||S.expando+"_"+wt.guid++;return this[e]=!0,e}}),S.ajaxPrefilter("json jsonp",function(e,t,n){var r,i,o,a=!1!==e.jsonp&&(Ut.test(e.url)?"url":"string"==typeof e.data&&0===(e.contentType||"").indexOf("application/x-www-form-urlencoded")&&Ut.test(e.data)&&"data");if(a||"jsonp"===e.dataTypes[0])return r=e.jsonpCallback=m(e.jsonpCallback)?e.jsonpCallback():e.jsonpCallback,a?e[a]=e[a].replace(Ut,"$1"+r):!1!==e.jsonp&&(e.url+=(Tt.test(e.url)?"&":"?")+e.jsonp+"="+r),e.converters["script json"]=function(){return o||S.error(r+" was not called"),o[0]},e.dataTypes[0]="json",i=C[r],C[r]=function(){o=arguments},n.always(function(){void 0===i?S(C).removeProp(r):C[r]=i,e[r]&&(e.jsonpCallback=t.jsonpCallback,zt.push(r)),o&&m(i)&&i(o[0]),o=i=void 0}),"script"}),y.createHTMLDocument=((_t=E.implementation.createHTMLDocument("").body).innerHTML="
",2===_t.childNodes.length),S.parseHTML=function(e,t,n){return"string"!=typeof e?[]:("boolean"==typeof t&&(n=t,t=!1),t||(y.createHTMLDocument?((r=(t=E.implementation.createHTMLDocument("")).createElement("base")).href=E.location.href,t.head.appendChild(r)):t=E),o=!n&&[],(i=N.exec(e))?[t.createElement(i[1])]:(i=xe([e],t,o),o&&o.length&&S(o).remove(),S.merge([],i.childNodes)));var r,i,o},S.fn.load=function(e,t,n){var r,i,o,a=this,s=e.indexOf(" ");return-1").append(S.parseHTML(e)).find(r):e)}).always(n&&function(e,t){a.each(function(){n.apply(this,o||[e.responseText,t,e])})}),this},S.expr.pseudos.animated=function(t){return S.grep(S.timers,function(e){return t===e.elem}).length},S.offset={setOffset:function(e,t,n){var r,i,o,a,s,u,l=S.css(e,"position"),c=S(e),f={};"static"===l&&(e.style.position="relative"),s=c.offset(),o=S.css(e,"top"),u=S.css(e,"left"),("absolute"===l||"fixed"===l)&&-1<(o+u).indexOf("auto")?(a=(r=c.position()).top,i=r.left):(a=parseFloat(o)||0,i=parseFloat(u)||0),m(t)&&(t=t.call(e,n,S.extend({},s))),null!=t.top&&(f.top=t.top-s.top+a),null!=t.left&&(f.left=t.left-s.left+i),"using"in t?t.using.call(e,f):c.css(f)}},S.fn.extend({offset:function(t){if(arguments.length)return void 0===t?this:this.each(function(e){S.offset.setOffset(this,t,e)});var e,n,r=this[0];return r?r.getClientRects().length?(e=r.getBoundingClientRect(),n=r.ownerDocument.defaultView,{top:e.top+n.pageYOffset,left:e.left+n.pageXOffset}):{top:0,left:0}:void 0},position:function(){if(this[0]){var e,t,n,r=this[0],i={top:0,left:0};if("fixed"===S.css(r,"position"))t=r.getBoundingClientRect();else{t=this.offset(),n=r.ownerDocument,e=r.offsetParent||n.documentElement;while(e&&(e===n.body||e===n.documentElement)&&"static"===S.css(e,"position"))e=e.parentNode;e&&e!==r&&1===e.nodeType&&((i=S(e).offset()).top+=S.css(e,"borderTopWidth",!0),i.left+=S.css(e,"borderLeftWidth",!0))}return{top:t.top-i.top-S.css(r,"marginTop",!0),left:t.left-i.left-S.css(r,"marginLeft",!0)}}},offsetParent:function(){return this.map(function(){var e=this.offsetParent;while(e&&"static"===S.css(e,"position"))e=e.offsetParent;return e||re})}}),S.each({scrollLeft:"pageXOffset",scrollTop:"pageYOffset"},function(t,i){var o="pageYOffset"===i;S.fn[t]=function(e){return $(this,function(e,t,n){var r;if(x(e)?r=e:9===e.nodeType&&(r=e.defaultView),void 0===n)return r?r[i]:e[t];r?r.scrollTo(o?r.pageXOffset:n,o?n:r.pageYOffset):e[t]=n},t,e,arguments.length)}}),S.each(["top","left"],function(e,n){S.cssHooks[n]=Fe(y.pixelPosition,function(e,t){if(t)return t=We(e,n),Pe.test(t)?S(e).position()[n]+"px":t})}),S.each({Height:"height",Width:"width"},function(a,s){S.each({padding:"inner"+a,content:s,"":"outer"+a},function(r,o){S.fn[o]=function(e,t){var n=arguments.length&&(r||"boolean"!=typeof e),i=r||(!0===e||!0===t?"margin":"border");return $(this,function(e,t,n){var r;return x(e)?0===o.indexOf("outer")?e["inner"+a]:e.document.documentElement["client"+a]:9===e.nodeType?(r=e.documentElement,Math.max(e.body["scroll"+a],r["scroll"+a],e.body["offset"+a],r["offset"+a],r["client"+a])):void 0===n?S.css(e,t,i):S.style(e,t,n,i)},s,n?e:void 0,n)}})}),S.each(["ajaxStart","ajaxStop","ajaxComplete","ajaxError","ajaxSuccess","ajaxSend"],function(e,t){S.fn[t]=function(e){return this.on(t,e)}}),S.fn.extend({bind:function(e,t,n){return this.on(e,null,t,n)},unbind:function(e,t){return this.off(e,null,t)},delegate:function(e,t,n,r){return this.on(t,e,n,r)},undelegate:function(e,t,n){return 1===arguments.length?this.off(e,"**"):this.off(t,e||"**",n)},hover:function(e,t){return this.mouseenter(e).mouseleave(t||e)}}),S.each("blur focus focusin focusout resize scroll click dblclick mousedown mouseup mousemove mouseover mouseout mouseenter mouseleave change select submit keydown keypress keyup contextmenu".split(" "),function(e,n){S.fn[n]=function(e,t){return 0
"),n("table.docutils.footnote").wrap("
"),n("table.docutils.citation").wrap("
"),n(".wy-menu-vertical ul").not(".simple").siblings("a").each((function(){var t=n(this);expand=n(''),expand.on("click",(function(n){return e.toggleCurrent(t),n.stopPropagation(),!1})),t.prepend(expand)}))},reset:function(){var n=encodeURI(window.location.hash)||"#";try{var e=$(".wy-menu-vertical"),t=e.find('[href="'+n+'"]');if(0===t.length){var i=$('.document [id="'+n.substring(1)+'"]').closest("div.section");0===(t=e.find('[href="#'+i.attr("id")+'"]')).length&&(t=e.find('[href="#"]'))}if(t.length>0){$(".wy-menu-vertical .current").removeClass("current").attr("aria-expanded","false"),t.addClass("current").attr("aria-expanded","true"),t.closest("li.toctree-l1").parent().addClass("current").attr("aria-expanded","true");for(let n=1;n<=10;n++)t.closest("li.toctree-l"+n).addClass("current").attr("aria-expanded","true");t[0].scrollIntoView()}}catch(n){console.log("Error expanding nav for anchor",n)}},onScroll:function(){this.winScroll=!1;var n=this.win.scrollTop(),e=n+this.winHeight,t=this.navBar.scrollTop()+(n-this.winPosition);n<0||e>this.docHeight||(this.navBar.scrollTop(t),this.winPosition=n)},onResize:function(){this.winResize=!1,this.winHeight=this.win.height(),this.docHeight=$(document).height()},hashChange:function(){this.linkScroll=!0,this.win.one("hashchange",(function(){this.linkScroll=!1}))},toggleCurrent:function(n){var e=n.closest("li");e.siblings("li.current").removeClass("current").attr("aria-expanded","false"),e.siblings().find("li.current").removeClass("current").attr("aria-expanded","false");var t=e.find("> ul li");t.length&&(t.removeClass("current").attr("aria-expanded","false"),e.toggleClass("current").attr("aria-expanded",(function(n,e){return"true"==e?"false":"true"})))}},"undefined"!=typeof window&&(window.SphinxRtdTheme={Navigation:n.exports.ThemeNav,StickyNav:n.exports.ThemeNav}),function(){for(var n=0,e=["ms","moz","webkit","o"],t=0;t - - - - - - - Layers - JEO - - - - - - - - - - - - - -
- - -
- -
-
- -
-
-
-
- -

Creating a layer

-

One of the custom post types that JEO plugin provides is Layer. Is in the layer where you will be able to add legend and color to your map. A map may contain one or more layers.

-

Post Type Layers

-

Entering the Layer post editor, you'll see a preview of the current layer (or a default layer if the current layer haven't been edited yet) and three sidebar panels: Settings, Attributions and Legend.

-

Layer Sidebar

-

Layer settings

-

On the Layer settings panel, you can change the layer type.

-

JEO supports four layer types out-of-the-box:

- -

You can also inform an address, following the standard username/id, to compose your map style. If an access token is needed for this layer, you can put it into the Acess token input.

-

Layer settings

-

There's also an Edit interactions button. Here, you can add popups to your layer when specific actions (clicking or hovering the mouse) are made (e.g.: Clicking on a building and displaying its height)

-

Layer interactions

-

Layer legend

-

On the Layer legend panel, you can add legends to your layer (barscale, simple-color, icons or circles and colorize them.

-

Layer legend

-

Attributions

-

Here is where you can give layer credits, setting a link to download it or access more information about it. These attributions will be shown on the bottom of the map and inside the popup that shows up when the INFO button is clicked in a map.

- -
-
- -
-
- -
- -
- -
- - - - GitHub - - - - « Previous - - - Next » - - -
- - - - - - - - - diff --git a/site/map-block/index.html b/site/map-block/index.html deleted file mode 100644 index db7ed2f60..000000000 --- a/site/map-block/index.html +++ /dev/null @@ -1,210 +0,0 @@ - - - - - - - - Map block - JEO - - - - - - - - - - - - - -
- - -
- -
-
- -
-
-
-
- -

Map block

-

After creating maps, it is possible to display them apart or inside a block. This functionality makes possible to group maps, increasing your post organization.

-

Displaying a map apart

-

When creating a new post, note that is available a new block category: JEO.

-

JEO block

-

Selecting JEO Map block, you can search for any map you've created.

-

JEO block - Search Bar

-

With a map selected, you are able to choose an optional alignment (Left, Right, Centre, Wide Width or Full Width). Centre is the standard alignment.

-

Map Alignment

-

Left

-

Map Alignment - Left

- -

Map Alignment - Right

-

Wide Width

-

Map Alignment - Wide Width

-

Full Width

-

Map Alignment - Full Width

-

Displaying grouped maps

-

Besides the alignment option, there's also a group functionality available to arrange maps.

-

map-group

-

Visualizing a map into a post

-

If your map has more than one layer, you can swap them and select which one do you want to see, depending on the map layer settings. Check out more about map layers here

-

Swapping layers - Example 1

-

Swapping layers - Example 2

- -
-
- -
-
- -
- -
- -
- - - - GitHub - - - - « Previous - - - Next » - - -
- - - - - - - - - diff --git a/site/map-embed/index.html b/site/map-embed/index.html deleted file mode 100644 index 5ed8eaa4c..000000000 --- a/site/map-embed/index.html +++ /dev/null @@ -1,182 +0,0 @@ - - - - - - - - Map embed - JEO - - - - - - - - - - - - - -
- - -
- -
-
- -
-
-
-
- -

Embedding a map

-

JEO plugin allows a map to be inserted into a post by pasting a link on the editor. This is what is called Embed map and it's very easy to be done.

-

When editing a Map, one of the setting panels is Status & Visibility. There you can find the embed URL of that specific map.

-

Embed URL

-

Copying this link and pasting on the post editor will result on an embed map.

-

Warning: If your post displays a Not Found error, do the following steps:

-
    -
  • Go to Wordpress Dashboard > Settings > Permalinks
  • -
  • Change the Common Settings (you may change to a different setting and then change back to the original) and click on Save Changes
  • -
-

Permalink Settings

-

Now you should be able to see the embed maps with no problems.

- -
-
- -
-
- -
- -
- -
- - - - GitHub - - - - « Previous - - - Next » - - -
- - - - - - - - - diff --git a/site/map-post/index.html b/site/map-post/index.html deleted file mode 100644 index ca2b8c3b4..000000000 --- a/site/map-post/index.html +++ /dev/null @@ -1,200 +0,0 @@ - - - - - - - - Maps - JEO - - - - - - - - - - - - - -
- - -
- -
-
- -
-
-
-
- -

Creating maps

-

One of the custom post types that JEO plugin provides is Map.

-

Post Type Maps

-

Entering the Map post editor, you'll see a preview of the current map (or a default map, if no layers are selected) and three sidebar panels: Map settings, Map layers, and Related posts.

-

Map Sidebar

-

Map settings

-

On the Map settings panel, you can change the initial center of the map setting a latitude and longitude. You can also change the default zoom settings or even disable it.

-

Map settings

-

When visualizing a map in a post, clicking with right button of the mouse and dragging it makes the map rotate. Also, scrolling the mouse wheel changes the map zoom. Both of these options can be enabled or disabled with the checkboxes.

-

Map layers

-

On the Map layers panel, you can visualize all the layers which are part of the map.

-

Map Layers

-

Clicking on the Edit layers settings, a popup will open. There, you can add, remove and rearrange layers, define their types and whether its legends will be shown on the map. Also, you can selec which specific interactions you want to work with this especific map by clicking on Load interactions.

-

A legend can be of one of these types:

-
    -
  • Fixed: the layer will always be shown in the map;
  • -
  • Switchable: the layer visibility can be toggled on and off;
  • -
  • Swappable: only one swappable layer can be visible at a time.
  • -
-

Layer settings

-

Checkout how to create new layers here

- -

On the Related posts panel, you can set which geolocated posts will be used as markers on the post. You can filter the posts by categories, tags, dates interval or, for advanced uses, meta queries using WordPress post_meta.

-

Related posts

- -
-
- -
-
- -
- -
- -
- - - - GitHub - - - - « Previous - - - Next » - - -
- - - - - - - - - diff --git a/site/map-shortcode/index.html b/site/map-shortcode/index.html deleted file mode 100644 index fc0746453..000000000 --- a/site/map-shortcode/index.html +++ /dev/null @@ -1,184 +0,0 @@ - - - - - - - - Map shortcode - JEO - - - - - - - - - - - - - -
- - -
- -
-
- -
-
-
-
- -

Map shortcode

-

A map can be inserted on any page or post using the jeo-map shortcode.

-

The shortcode accepts three attributes:

-
    -
  • map_id (required): The ID of the map you want to insert;
  • -
  • width (optional): The width of the map. It'll default to 600px or whatever the active theme defines for the 'div.jeomap' CSS class;
  • -
  • height (optional): The height of the map. Will default to 600px or whatever the active theme defines for the 'div.jeomap' CSS class.
  • -
-

Examples:

-
[jeo-map map_id=99]
-
-

You have to inform at least the ID of the Map you want to insert. By default, it will be inserted with a size of 600×600px (or whatever the active theme defines), but you can also change it:

-
[jeo-map map_id=99 width="800px" height="800px"]
-
- -
-
- -
-
- -
- -
- -
- - - - GitHub - - - - « Previous - - - Next » - - -
- - - - - - - - - diff --git a/site/one-time-map-block/index.html b/site/one-time-map-block/index.html deleted file mode 100644 index 210e53b37..000000000 --- a/site/one-time-map-block/index.html +++ /dev/null @@ -1,176 +0,0 @@ - - - - - - - - One-time map block - JEO - - - - - - - - - - - - - -
- - -
- -
-
- -
-
-
-
- -

One-time map block

-

It is possible to use maps into posts without having to create a new map or using an existing one. For this, JEO plugin makes availabe another type of block: One-time map.

-

When creating a new post, note that is available a new block category: JEO.

-

JEO block

-

Selecting JEO One-time Map block, a standard map preview will appear. This type of map allow all the same customizations of a normal JEO Map, such as: modifying coordinates, zoom settings, related posts, alignment options and layers settings. To know more precisely how to customize all these options, check out JEO Map page.

-

One-time Map Example

- -
-
- -
-
- -
- -
- -
- - - - GitHub - - - - « Previous - - - Next » - - -
- - - - - - - - - diff --git a/site/search.html b/site/search.html deleted file mode 100644 index 4e7f21fef..000000000 --- a/site/search.html +++ /dev/null @@ -1,162 +0,0 @@ - - - - - - - - JEO - - - - - - - - - - - -
- - -
- -
-
-
    -
  • -
  • -
  • -
-
-
-
-
- - -

Search Results

- - - -
- Searching... -
- - -
-
- -
-
- -
- -
- -
- - - - GitHub - - - - - -
- - - - - - - - - diff --git a/site/search/lunr.js b/site/search/lunr.js deleted file mode 100644 index aca0a167f..000000000 --- a/site/search/lunr.js +++ /dev/null @@ -1,3475 +0,0 @@ -/** - * lunr - http://lunrjs.com - A bit like Solr, but much smaller and not as bright - 2.3.9 - * Copyright (C) 2020 Oliver Nightingale - * @license MIT - */ - -;(function(){ - -/** - * A convenience function for configuring and constructing - * a new lunr Index. - * - * A lunr.Builder instance is created and the pipeline setup - * with a trimmer, stop word filter and stemmer. - * - * This builder object is yielded to the configuration function - * that is passed as a parameter, allowing the list of fields - * and other builder parameters to be customised. - * - * All documents _must_ be added within the passed config function. - * - * @example - * var idx = lunr(function () { - * this.field('title') - * this.field('body') - * this.ref('id') - * - * documents.forEach(function (doc) { - * this.add(doc) - * }, this) - * }) - * - * @see {@link lunr.Builder} - * @see {@link lunr.Pipeline} - * @see {@link lunr.trimmer} - * @see {@link lunr.stopWordFilter} - * @see {@link lunr.stemmer} - * @namespace {function} lunr - */ -var lunr = function (config) { - var builder = new lunr.Builder - - builder.pipeline.add( - lunr.trimmer, - lunr.stopWordFilter, - lunr.stemmer - ) - - builder.searchPipeline.add( - lunr.stemmer - ) - - config.call(builder, builder) - return builder.build() -} - -lunr.version = "2.3.9" -/*! - * lunr.utils - * Copyright (C) 2020 Oliver Nightingale - */ - -/** - * A namespace containing utils for the rest of the lunr library - * @namespace lunr.utils - */ -lunr.utils = {} - -/** - * Print a warning message to the console. - * - * @param {String} message The message to be printed. - * @memberOf lunr.utils - * @function - */ -lunr.utils.warn = (function (global) { - /* eslint-disable no-console */ - return function (message) { - if (global.console && console.warn) { - console.warn(message) - } - } - /* eslint-enable no-console */ -})(this) - -/** - * Convert an object to a string. - * - * In the case of `null` and `undefined` the function returns - * the empty string, in all other cases the result of calling - * `toString` on the passed object is returned. - * - * @param {Any} obj The object to convert to a string. - * @return {String} string representation of the passed object. - * @memberOf lunr.utils - */ -lunr.utils.asString = function (obj) { - if (obj === void 0 || obj === null) { - return "" - } else { - return obj.toString() - } -} - -/** - * Clones an object. - * - * Will create a copy of an existing object such that any mutations - * on the copy cannot affect the original. - * - * Only shallow objects are supported, passing a nested object to this - * function will cause a TypeError. - * - * Objects with primitives, and arrays of primitives are supported. - * - * @param {Object} obj The object to clone. - * @return {Object} a clone of the passed object. - * @throws {TypeError} when a nested object is passed. - * @memberOf Utils - */ -lunr.utils.clone = function (obj) { - if (obj === null || obj === undefined) { - return obj - } - - var clone = Object.create(null), - keys = Object.keys(obj) - - for (var i = 0; i < keys.length; i++) { - var key = keys[i], - val = obj[key] - - if (Array.isArray(val)) { - clone[key] = val.slice() - continue - } - - if (typeof val === 'string' || - typeof val === 'number' || - typeof val === 'boolean') { - clone[key] = val - continue - } - - throw new TypeError("clone is not deep and does not support nested objects") - } - - return clone -} -lunr.FieldRef = function (docRef, fieldName, stringValue) { - this.docRef = docRef - this.fieldName = fieldName - this._stringValue = stringValue -} - -lunr.FieldRef.joiner = "/" - -lunr.FieldRef.fromString = function (s) { - var n = s.indexOf(lunr.FieldRef.joiner) - - if (n === -1) { - throw "malformed field ref string" - } - - var fieldRef = s.slice(0, n), - docRef = s.slice(n + 1) - - return new lunr.FieldRef (docRef, fieldRef, s) -} - -lunr.FieldRef.prototype.toString = function () { - if (this._stringValue == undefined) { - this._stringValue = this.fieldName + lunr.FieldRef.joiner + this.docRef - } - - return this._stringValue -} -/*! - * lunr.Set - * Copyright (C) 2020 Oliver Nightingale - */ - -/** - * A lunr set. - * - * @constructor - */ -lunr.Set = function (elements) { - this.elements = Object.create(null) - - if (elements) { - this.length = elements.length - - for (var i = 0; i < this.length; i++) { - this.elements[elements[i]] = true - } - } else { - this.length = 0 - } -} - -/** - * A complete set that contains all elements. - * - * @static - * @readonly - * @type {lunr.Set} - */ -lunr.Set.complete = { - intersect: function (other) { - return other - }, - - union: function () { - return this - }, - - contains: function () { - return true - } -} - -/** - * An empty set that contains no elements. - * - * @static - * @readonly - * @type {lunr.Set} - */ -lunr.Set.empty = { - intersect: function () { - return this - }, - - union: function (other) { - return other - }, - - contains: function () { - return false - } -} - -/** - * Returns true if this set contains the specified object. - * - * @param {object} object - Object whose presence in this set is to be tested. - * @returns {boolean} - True if this set contains the specified object. - */ -lunr.Set.prototype.contains = function (object) { - return !!this.elements[object] -} - -/** - * Returns a new set containing only the elements that are present in both - * this set and the specified set. - * - * @param {lunr.Set} other - set to intersect with this set. - * @returns {lunr.Set} a new set that is the intersection of this and the specified set. - */ - -lunr.Set.prototype.intersect = function (other) { - var a, b, elements, intersection = [] - - if (other === lunr.Set.complete) { - return this - } - - if (other === lunr.Set.empty) { - return other - } - - if (this.length < other.length) { - a = this - b = other - } else { - a = other - b = this - } - - elements = Object.keys(a.elements) - - for (var i = 0; i < elements.length; i++) { - var element = elements[i] - if (element in b.elements) { - intersection.push(element) - } - } - - return new lunr.Set (intersection) -} - -/** - * Returns a new set combining the elements of this and the specified set. - * - * @param {lunr.Set} other - set to union with this set. - * @return {lunr.Set} a new set that is the union of this and the specified set. - */ - -lunr.Set.prototype.union = function (other) { - if (other === lunr.Set.complete) { - return lunr.Set.complete - } - - if (other === lunr.Set.empty) { - return this - } - - return new lunr.Set(Object.keys(this.elements).concat(Object.keys(other.elements))) -} -/** - * A function to calculate the inverse document frequency for - * a posting. This is shared between the builder and the index - * - * @private - * @param {object} posting - The posting for a given term - * @param {number} documentCount - The total number of documents. - */ -lunr.idf = function (posting, documentCount) { - var documentsWithTerm = 0 - - for (var fieldName in posting) { - if (fieldName == '_index') continue // Ignore the term index, its not a field - documentsWithTerm += Object.keys(posting[fieldName]).length - } - - var x = (documentCount - documentsWithTerm + 0.5) / (documentsWithTerm + 0.5) - - return Math.log(1 + Math.abs(x)) -} - -/** - * A token wraps a string representation of a token - * as it is passed through the text processing pipeline. - * - * @constructor - * @param {string} [str=''] - The string token being wrapped. - * @param {object} [metadata={}] - Metadata associated with this token. - */ -lunr.Token = function (str, metadata) { - this.str = str || "" - this.metadata = metadata || {} -} - -/** - * Returns the token string that is being wrapped by this object. - * - * @returns {string} - */ -lunr.Token.prototype.toString = function () { - return this.str -} - -/** - * A token update function is used when updating or optionally - * when cloning a token. - * - * @callback lunr.Token~updateFunction - * @param {string} str - The string representation of the token. - * @param {Object} metadata - All metadata associated with this token. - */ - -/** - * Applies the given function to the wrapped string token. - * - * @example - * token.update(function (str, metadata) { - * return str.toUpperCase() - * }) - * - * @param {lunr.Token~updateFunction} fn - A function to apply to the token string. - * @returns {lunr.Token} - */ -lunr.Token.prototype.update = function (fn) { - this.str = fn(this.str, this.metadata) - return this -} - -/** - * Creates a clone of this token. Optionally a function can be - * applied to the cloned token. - * - * @param {lunr.Token~updateFunction} [fn] - An optional function to apply to the cloned token. - * @returns {lunr.Token} - */ -lunr.Token.prototype.clone = function (fn) { - fn = fn || function (s) { return s } - return new lunr.Token (fn(this.str, this.metadata), this.metadata) -} -/*! - * lunr.tokenizer - * Copyright (C) 2020 Oliver Nightingale - */ - -/** - * A function for splitting a string into tokens ready to be inserted into - * the search index. Uses `lunr.tokenizer.separator` to split strings, change - * the value of this property to change how strings are split into tokens. - * - * This tokenizer will convert its parameter to a string by calling `toString` and - * then will split this string on the character in `lunr.tokenizer.separator`. - * Arrays will have their elements converted to strings and wrapped in a lunr.Token. - * - * Optional metadata can be passed to the tokenizer, this metadata will be cloned and - * added as metadata to every token that is created from the object to be tokenized. - * - * @static - * @param {?(string|object|object[])} obj - The object to convert into tokens - * @param {?object} metadata - Optional metadata to associate with every token - * @returns {lunr.Token[]} - * @see {@link lunr.Pipeline} - */ -lunr.tokenizer = function (obj, metadata) { - if (obj == null || obj == undefined) { - return [] - } - - if (Array.isArray(obj)) { - return obj.map(function (t) { - return new lunr.Token( - lunr.utils.asString(t).toLowerCase(), - lunr.utils.clone(metadata) - ) - }) - } - - var str = obj.toString().toLowerCase(), - len = str.length, - tokens = [] - - for (var sliceEnd = 0, sliceStart = 0; sliceEnd <= len; sliceEnd++) { - var char = str.charAt(sliceEnd), - sliceLength = sliceEnd - sliceStart - - if ((char.match(lunr.tokenizer.separator) || sliceEnd == len)) { - - if (sliceLength > 0) { - var tokenMetadata = lunr.utils.clone(metadata) || {} - tokenMetadata["position"] = [sliceStart, sliceLength] - tokenMetadata["index"] = tokens.length - - tokens.push( - new lunr.Token ( - str.slice(sliceStart, sliceEnd), - tokenMetadata - ) - ) - } - - sliceStart = sliceEnd + 1 - } - - } - - return tokens -} - -/** - * The separator used to split a string into tokens. Override this property to change the behaviour of - * `lunr.tokenizer` behaviour when tokenizing strings. By default this splits on whitespace and hyphens. - * - * @static - * @see lunr.tokenizer - */ -lunr.tokenizer.separator = /[\s\-]+/ -/*! - * lunr.Pipeline - * Copyright (C) 2020 Oliver Nightingale - */ - -/** - * lunr.Pipelines maintain an ordered list of functions to be applied to all - * tokens in documents entering the search index and queries being ran against - * the index. - * - * An instance of lunr.Index created with the lunr shortcut will contain a - * pipeline with a stop word filter and an English language stemmer. Extra - * functions can be added before or after either of these functions or these - * default functions can be removed. - * - * When run the pipeline will call each function in turn, passing a token, the - * index of that token in the original list of all tokens and finally a list of - * all the original tokens. - * - * The output of functions in the pipeline will be passed to the next function - * in the pipeline. To exclude a token from entering the index the function - * should return undefined, the rest of the pipeline will not be called with - * this token. - * - * For serialisation of pipelines to work, all functions used in an instance of - * a pipeline should be registered with lunr.Pipeline. Registered functions can - * then be loaded. If trying to load a serialised pipeline that uses functions - * that are not registered an error will be thrown. - * - * If not planning on serialising the pipeline then registering pipeline functions - * is not necessary. - * - * @constructor - */ -lunr.Pipeline = function () { - this._stack = [] -} - -lunr.Pipeline.registeredFunctions = Object.create(null) - -/** - * A pipeline function maps lunr.Token to lunr.Token. A lunr.Token contains the token - * string as well as all known metadata. A pipeline function can mutate the token string - * or mutate (or add) metadata for a given token. - * - * A pipeline function can indicate that the passed token should be discarded by returning - * null, undefined or an empty string. This token will not be passed to any downstream pipeline - * functions and will not be added to the index. - * - * Multiple tokens can be returned by returning an array of tokens. Each token will be passed - * to any downstream pipeline functions and all will returned tokens will be added to the index. - * - * Any number of pipeline functions may be chained together using a lunr.Pipeline. - * - * @interface lunr.PipelineFunction - * @param {lunr.Token} token - A token from the document being processed. - * @param {number} i - The index of this token in the complete list of tokens for this document/field. - * @param {lunr.Token[]} tokens - All tokens for this document/field. - * @returns {(?lunr.Token|lunr.Token[])} - */ - -/** - * Register a function with the pipeline. - * - * Functions that are used in the pipeline should be registered if the pipeline - * needs to be serialised, or a serialised pipeline needs to be loaded. - * - * Registering a function does not add it to a pipeline, functions must still be - * added to instances of the pipeline for them to be used when running a pipeline. - * - * @param {lunr.PipelineFunction} fn - The function to check for. - * @param {String} label - The label to register this function with - */ -lunr.Pipeline.registerFunction = function (fn, label) { - if (label in this.registeredFunctions) { - lunr.utils.warn('Overwriting existing registered function: ' + label) - } - - fn.label = label - lunr.Pipeline.registeredFunctions[fn.label] = fn -} - -/** - * Warns if the function is not registered as a Pipeline function. - * - * @param {lunr.PipelineFunction} fn - The function to check for. - * @private - */ -lunr.Pipeline.warnIfFunctionNotRegistered = function (fn) { - var isRegistered = fn.label && (fn.label in this.registeredFunctions) - - if (!isRegistered) { - lunr.utils.warn('Function is not registered with pipeline. This may cause problems when serialising the index.\n', fn) - } -} - -/** - * Loads a previously serialised pipeline. - * - * All functions to be loaded must already be registered with lunr.Pipeline. - * If any function from the serialised data has not been registered then an - * error will be thrown. - * - * @param {Object} serialised - The serialised pipeline to load. - * @returns {lunr.Pipeline} - */ -lunr.Pipeline.load = function (serialised) { - var pipeline = new lunr.Pipeline - - serialised.forEach(function (fnName) { - var fn = lunr.Pipeline.registeredFunctions[fnName] - - if (fn) { - pipeline.add(fn) - } else { - throw new Error('Cannot load unregistered function: ' + fnName) - } - }) - - return pipeline -} - -/** - * Adds new functions to the end of the pipeline. - * - * Logs a warning if the function has not been registered. - * - * @param {lunr.PipelineFunction[]} functions - Any number of functions to add to the pipeline. - */ -lunr.Pipeline.prototype.add = function () { - var fns = Array.prototype.slice.call(arguments) - - fns.forEach(function (fn) { - lunr.Pipeline.warnIfFunctionNotRegistered(fn) - this._stack.push(fn) - }, this) -} - -/** - * Adds a single function after a function that already exists in the - * pipeline. - * - * Logs a warning if the function has not been registered. - * - * @param {lunr.PipelineFunction} existingFn - A function that already exists in the pipeline. - * @param {lunr.PipelineFunction} newFn - The new function to add to the pipeline. - */ -lunr.Pipeline.prototype.after = function (existingFn, newFn) { - lunr.Pipeline.warnIfFunctionNotRegistered(newFn) - - var pos = this._stack.indexOf(existingFn) - if (pos == -1) { - throw new Error('Cannot find existingFn') - } - - pos = pos + 1 - this._stack.splice(pos, 0, newFn) -} - -/** - * Adds a single function before a function that already exists in the - * pipeline. - * - * Logs a warning if the function has not been registered. - * - * @param {lunr.PipelineFunction} existingFn - A function that already exists in the pipeline. - * @param {lunr.PipelineFunction} newFn - The new function to add to the pipeline. - */ -lunr.Pipeline.prototype.before = function (existingFn, newFn) { - lunr.Pipeline.warnIfFunctionNotRegistered(newFn) - - var pos = this._stack.indexOf(existingFn) - if (pos == -1) { - throw new Error('Cannot find existingFn') - } - - this._stack.splice(pos, 0, newFn) -} - -/** - * Removes a function from the pipeline. - * - * @param {lunr.PipelineFunction} fn The function to remove from the pipeline. - */ -lunr.Pipeline.prototype.remove = function (fn) { - var pos = this._stack.indexOf(fn) - if (pos == -1) { - return - } - - this._stack.splice(pos, 1) -} - -/** - * Runs the current list of functions that make up the pipeline against the - * passed tokens. - * - * @param {Array} tokens The tokens to run through the pipeline. - * @returns {Array} - */ -lunr.Pipeline.prototype.run = function (tokens) { - var stackLength = this._stack.length - - for (var i = 0; i < stackLength; i++) { - var fn = this._stack[i] - var memo = [] - - for (var j = 0; j < tokens.length; j++) { - var result = fn(tokens[j], j, tokens) - - if (result === null || result === void 0 || result === '') continue - - if (Array.isArray(result)) { - for (var k = 0; k < result.length; k++) { - memo.push(result[k]) - } - } else { - memo.push(result) - } - } - - tokens = memo - } - - return tokens -} - -/** - * Convenience method for passing a string through a pipeline and getting - * strings out. This method takes care of wrapping the passed string in a - * token and mapping the resulting tokens back to strings. - * - * @param {string} str - The string to pass through the pipeline. - * @param {?object} metadata - Optional metadata to associate with the token - * passed to the pipeline. - * @returns {string[]} - */ -lunr.Pipeline.prototype.runString = function (str, metadata) { - var token = new lunr.Token (str, metadata) - - return this.run([token]).map(function (t) { - return t.toString() - }) -} - -/** - * Resets the pipeline by removing any existing processors. - * - */ -lunr.Pipeline.prototype.reset = function () { - this._stack = [] -} - -/** - * Returns a representation of the pipeline ready for serialisation. - * - * Logs a warning if the function has not been registered. - * - * @returns {Array} - */ -lunr.Pipeline.prototype.toJSON = function () { - return this._stack.map(function (fn) { - lunr.Pipeline.warnIfFunctionNotRegistered(fn) - - return fn.label - }) -} -/*! - * lunr.Vector - * Copyright (C) 2020 Oliver Nightingale - */ - -/** - * A vector is used to construct the vector space of documents and queries. These - * vectors support operations to determine the similarity between two documents or - * a document and a query. - * - * Normally no parameters are required for initializing a vector, but in the case of - * loading a previously dumped vector the raw elements can be provided to the constructor. - * - * For performance reasons vectors are implemented with a flat array, where an elements - * index is immediately followed by its value. E.g. [index, value, index, value]. This - * allows the underlying array to be as sparse as possible and still offer decent - * performance when being used for vector calculations. - * - * @constructor - * @param {Number[]} [elements] - The flat list of element index and element value pairs. - */ -lunr.Vector = function (elements) { - this._magnitude = 0 - this.elements = elements || [] -} - - -/** - * Calculates the position within the vector to insert a given index. - * - * This is used internally by insert and upsert. If there are duplicate indexes then - * the position is returned as if the value for that index were to be updated, but it - * is the callers responsibility to check whether there is a duplicate at that index - * - * @param {Number} insertIdx - The index at which the element should be inserted. - * @returns {Number} - */ -lunr.Vector.prototype.positionForIndex = function (index) { - // For an empty vector the tuple can be inserted at the beginning - if (this.elements.length == 0) { - return 0 - } - - var start = 0, - end = this.elements.length / 2, - sliceLength = end - start, - pivotPoint = Math.floor(sliceLength / 2), - pivotIndex = this.elements[pivotPoint * 2] - - while (sliceLength > 1) { - if (pivotIndex < index) { - start = pivotPoint - } - - if (pivotIndex > index) { - end = pivotPoint - } - - if (pivotIndex == index) { - break - } - - sliceLength = end - start - pivotPoint = start + Math.floor(sliceLength / 2) - pivotIndex = this.elements[pivotPoint * 2] - } - - if (pivotIndex == index) { - return pivotPoint * 2 - } - - if (pivotIndex > index) { - return pivotPoint * 2 - } - - if (pivotIndex < index) { - return (pivotPoint + 1) * 2 - } -} - -/** - * Inserts an element at an index within the vector. - * - * Does not allow duplicates, will throw an error if there is already an entry - * for this index. - * - * @param {Number} insertIdx - The index at which the element should be inserted. - * @param {Number} val - The value to be inserted into the vector. - */ -lunr.Vector.prototype.insert = function (insertIdx, val) { - this.upsert(insertIdx, val, function () { - throw "duplicate index" - }) -} - -/** - * Inserts or updates an existing index within the vector. - * - * @param {Number} insertIdx - The index at which the element should be inserted. - * @param {Number} val - The value to be inserted into the vector. - * @param {function} fn - A function that is called for updates, the existing value and the - * requested value are passed as arguments - */ -lunr.Vector.prototype.upsert = function (insertIdx, val, fn) { - this._magnitude = 0 - var position = this.positionForIndex(insertIdx) - - if (this.elements[position] == insertIdx) { - this.elements[position + 1] = fn(this.elements[position + 1], val) - } else { - this.elements.splice(position, 0, insertIdx, val) - } -} - -/** - * Calculates the magnitude of this vector. - * - * @returns {Number} - */ -lunr.Vector.prototype.magnitude = function () { - if (this._magnitude) return this._magnitude - - var sumOfSquares = 0, - elementsLength = this.elements.length - - for (var i = 1; i < elementsLength; i += 2) { - var val = this.elements[i] - sumOfSquares += val * val - } - - return this._magnitude = Math.sqrt(sumOfSquares) -} - -/** - * Calculates the dot product of this vector and another vector. - * - * @param {lunr.Vector} otherVector - The vector to compute the dot product with. - * @returns {Number} - */ -lunr.Vector.prototype.dot = function (otherVector) { - var dotProduct = 0, - a = this.elements, b = otherVector.elements, - aLen = a.length, bLen = b.length, - aVal = 0, bVal = 0, - i = 0, j = 0 - - while (i < aLen && j < bLen) { - aVal = a[i], bVal = b[j] - if (aVal < bVal) { - i += 2 - } else if (aVal > bVal) { - j += 2 - } else if (aVal == bVal) { - dotProduct += a[i + 1] * b[j + 1] - i += 2 - j += 2 - } - } - - return dotProduct -} - -/** - * Calculates the similarity between this vector and another vector. - * - * @param {lunr.Vector} otherVector - The other vector to calculate the - * similarity with. - * @returns {Number} - */ -lunr.Vector.prototype.similarity = function (otherVector) { - return this.dot(otherVector) / this.magnitude() || 0 -} - -/** - * Converts the vector to an array of the elements within the vector. - * - * @returns {Number[]} - */ -lunr.Vector.prototype.toArray = function () { - var output = new Array (this.elements.length / 2) - - for (var i = 1, j = 0; i < this.elements.length; i += 2, j++) { - output[j] = this.elements[i] - } - - return output -} - -/** - * A JSON serializable representation of the vector. - * - * @returns {Number[]} - */ -lunr.Vector.prototype.toJSON = function () { - return this.elements -} -/* eslint-disable */ -/*! - * lunr.stemmer - * Copyright (C) 2020 Oliver Nightingale - * Includes code from - http://tartarus.org/~martin/PorterStemmer/js.txt - */ - -/** - * lunr.stemmer is an english language stemmer, this is a JavaScript - * implementation of the PorterStemmer taken from http://tartarus.org/~martin - * - * @static - * @implements {lunr.PipelineFunction} - * @param {lunr.Token} token - The string to stem - * @returns {lunr.Token} - * @see {@link lunr.Pipeline} - * @function - */ -lunr.stemmer = (function(){ - var step2list = { - "ational" : "ate", - "tional" : "tion", - "enci" : "ence", - "anci" : "ance", - "izer" : "ize", - "bli" : "ble", - "alli" : "al", - "entli" : "ent", - "eli" : "e", - "ousli" : "ous", - "ization" : "ize", - "ation" : "ate", - "ator" : "ate", - "alism" : "al", - "iveness" : "ive", - "fulness" : "ful", - "ousness" : "ous", - "aliti" : "al", - "iviti" : "ive", - "biliti" : "ble", - "logi" : "log" - }, - - step3list = { - "icate" : "ic", - "ative" : "", - "alize" : "al", - "iciti" : "ic", - "ical" : "ic", - "ful" : "", - "ness" : "" - }, - - c = "[^aeiou]", // consonant - v = "[aeiouy]", // vowel - C = c + "[^aeiouy]*", // consonant sequence - V = v + "[aeiou]*", // vowel sequence - - mgr0 = "^(" + C + ")?" + V + C, // [C]VC... is m>0 - meq1 = "^(" + C + ")?" + V + C + "(" + V + ")?$", // [C]VC[V] is m=1 - mgr1 = "^(" + C + ")?" + V + C + V + C, // [C]VCVC... is m>1 - s_v = "^(" + C + ")?" + v; // vowel in stem - - var re_mgr0 = new RegExp(mgr0); - var re_mgr1 = new RegExp(mgr1); - var re_meq1 = new RegExp(meq1); - var re_s_v = new RegExp(s_v); - - var re_1a = /^(.+?)(ss|i)es$/; - var re2_1a = /^(.+?)([^s])s$/; - var re_1b = /^(.+?)eed$/; - var re2_1b = /^(.+?)(ed|ing)$/; - var re_1b_2 = /.$/; - var re2_1b_2 = /(at|bl|iz)$/; - var re3_1b_2 = new RegExp("([^aeiouylsz])\\1$"); - var re4_1b_2 = new RegExp("^" + C + v + "[^aeiouwxy]$"); - - var re_1c = /^(.+?[^aeiou])y$/; - var re_2 = /^(.+?)(ational|tional|enci|anci|izer|bli|alli|entli|eli|ousli|ization|ation|ator|alism|iveness|fulness|ousness|aliti|iviti|biliti|logi)$/; - - var re_3 = /^(.+?)(icate|ative|alize|iciti|ical|ful|ness)$/; - - var re_4 = /^(.+?)(al|ance|ence|er|ic|able|ible|ant|ement|ment|ent|ou|ism|ate|iti|ous|ive|ize)$/; - var re2_4 = /^(.+?)(s|t)(ion)$/; - - var re_5 = /^(.+?)e$/; - var re_5_1 = /ll$/; - var re3_5 = new RegExp("^" + C + v + "[^aeiouwxy]$"); - - var porterStemmer = function porterStemmer(w) { - var stem, - suffix, - firstch, - re, - re2, - re3, - re4; - - if (w.length < 3) { return w; } - - firstch = w.substr(0,1); - if (firstch == "y") { - w = firstch.toUpperCase() + w.substr(1); - } - - // Step 1a - re = re_1a - re2 = re2_1a; - - if (re.test(w)) { w = w.replace(re,"$1$2"); } - else if (re2.test(w)) { w = w.replace(re2,"$1$2"); } - - // Step 1b - re = re_1b; - re2 = re2_1b; - if (re.test(w)) { - var fp = re.exec(w); - re = re_mgr0; - if (re.test(fp[1])) { - re = re_1b_2; - w = w.replace(re,""); - } - } else if (re2.test(w)) { - var fp = re2.exec(w); - stem = fp[1]; - re2 = re_s_v; - if (re2.test(stem)) { - w = stem; - re2 = re2_1b_2; - re3 = re3_1b_2; - re4 = re4_1b_2; - if (re2.test(w)) { w = w + "e"; } - else if (re3.test(w)) { re = re_1b_2; w = w.replace(re,""); } - else if (re4.test(w)) { w = w + "e"; } - } - } - - // Step 1c - replace suffix y or Y by i if preceded by a non-vowel which is not the first letter of the word (so cry -> cri, by -> by, say -> say) - re = re_1c; - if (re.test(w)) { - var fp = re.exec(w); - stem = fp[1]; - w = stem + "i"; - } - - // Step 2 - re = re_2; - if (re.test(w)) { - var fp = re.exec(w); - stem = fp[1]; - suffix = fp[2]; - re = re_mgr0; - if (re.test(stem)) { - w = stem + step2list[suffix]; - } - } - - // Step 3 - re = re_3; - if (re.test(w)) { - var fp = re.exec(w); - stem = fp[1]; - suffix = fp[2]; - re = re_mgr0; - if (re.test(stem)) { - w = stem + step3list[suffix]; - } - } - - // Step 4 - re = re_4; - re2 = re2_4; - if (re.test(w)) { - var fp = re.exec(w); - stem = fp[1]; - re = re_mgr1; - if (re.test(stem)) { - w = stem; - } - } else if (re2.test(w)) { - var fp = re2.exec(w); - stem = fp[1] + fp[2]; - re2 = re_mgr1; - if (re2.test(stem)) { - w = stem; - } - } - - // Step 5 - re = re_5; - if (re.test(w)) { - var fp = re.exec(w); - stem = fp[1]; - re = re_mgr1; - re2 = re_meq1; - re3 = re3_5; - if (re.test(stem) || (re2.test(stem) && !(re3.test(stem)))) { - w = stem; - } - } - - re = re_5_1; - re2 = re_mgr1; - if (re.test(w) && re2.test(w)) { - re = re_1b_2; - w = w.replace(re,""); - } - - // and turn initial Y back to y - - if (firstch == "y") { - w = firstch.toLowerCase() + w.substr(1); - } - - return w; - }; - - return function (token) { - return token.update(porterStemmer); - } -})(); - -lunr.Pipeline.registerFunction(lunr.stemmer, 'stemmer') -/*! - * lunr.stopWordFilter - * Copyright (C) 2020 Oliver Nightingale - */ - -/** - * lunr.generateStopWordFilter builds a stopWordFilter function from the provided - * list of stop words. - * - * The built in lunr.stopWordFilter is built using this generator and can be used - * to generate custom stopWordFilters for applications or non English languages. - * - * @function - * @param {Array} token The token to pass through the filter - * @returns {lunr.PipelineFunction} - * @see lunr.Pipeline - * @see lunr.stopWordFilter - */ -lunr.generateStopWordFilter = function (stopWords) { - var words = stopWords.reduce(function (memo, stopWord) { - memo[stopWord] = stopWord - return memo - }, {}) - - return function (token) { - if (token && words[token.toString()] !== token.toString()) return token - } -} - -/** - * lunr.stopWordFilter is an English language stop word list filter, any words - * contained in the list will not be passed through the filter. - * - * This is intended to be used in the Pipeline. If the token does not pass the - * filter then undefined will be returned. - * - * @function - * @implements {lunr.PipelineFunction} - * @params {lunr.Token} token - A token to check for being a stop word. - * @returns {lunr.Token} - * @see {@link lunr.Pipeline} - */ -lunr.stopWordFilter = lunr.generateStopWordFilter([ - 'a', - 'able', - 'about', - 'across', - 'after', - 'all', - 'almost', - 'also', - 'am', - 'among', - 'an', - 'and', - 'any', - 'are', - 'as', - 'at', - 'be', - 'because', - 'been', - 'but', - 'by', - 'can', - 'cannot', - 'could', - 'dear', - 'did', - 'do', - 'does', - 'either', - 'else', - 'ever', - 'every', - 'for', - 'from', - 'get', - 'got', - 'had', - 'has', - 'have', - 'he', - 'her', - 'hers', - 'him', - 'his', - 'how', - 'however', - 'i', - 'if', - 'in', - 'into', - 'is', - 'it', - 'its', - 'just', - 'least', - 'let', - 'like', - 'likely', - 'may', - 'me', - 'might', - 'most', - 'must', - 'my', - 'neither', - 'no', - 'nor', - 'not', - 'of', - 'off', - 'often', - 'on', - 'only', - 'or', - 'other', - 'our', - 'own', - 'rather', - 'said', - 'say', - 'says', - 'she', - 'should', - 'since', - 'so', - 'some', - 'than', - 'that', - 'the', - 'their', - 'them', - 'then', - 'there', - 'these', - 'they', - 'this', - 'tis', - 'to', - 'too', - 'twas', - 'us', - 'wants', - 'was', - 'we', - 'were', - 'what', - 'when', - 'where', - 'which', - 'while', - 'who', - 'whom', - 'why', - 'will', - 'with', - 'would', - 'yet', - 'you', - 'your' -]) - -lunr.Pipeline.registerFunction(lunr.stopWordFilter, 'stopWordFilter') -/*! - * lunr.trimmer - * Copyright (C) 2020 Oliver Nightingale - */ - -/** - * lunr.trimmer is a pipeline function for trimming non word - * characters from the beginning and end of tokens before they - * enter the index. - * - * This implementation may not work correctly for non latin - * characters and should either be removed or adapted for use - * with languages with non-latin characters. - * - * @static - * @implements {lunr.PipelineFunction} - * @param {lunr.Token} token The token to pass through the filter - * @returns {lunr.Token} - * @see lunr.Pipeline - */ -lunr.trimmer = function (token) { - return token.update(function (s) { - return s.replace(/^\W+/, '').replace(/\W+$/, '') - }) -} - -lunr.Pipeline.registerFunction(lunr.trimmer, 'trimmer') -/*! - * lunr.TokenSet - * Copyright (C) 2020 Oliver Nightingale - */ - -/** - * A token set is used to store the unique list of all tokens - * within an index. Token sets are also used to represent an - * incoming query to the index, this query token set and index - * token set are then intersected to find which tokens to look - * up in the inverted index. - * - * A token set can hold multiple tokens, as in the case of the - * index token set, or it can hold a single token as in the - * case of a simple query token set. - * - * Additionally token sets are used to perform wildcard matching. - * Leading, contained and trailing wildcards are supported, and - * from this edit distance matching can also be provided. - * - * Token sets are implemented as a minimal finite state automata, - * where both common prefixes and suffixes are shared between tokens. - * This helps to reduce the space used for storing the token set. - * - * @constructor - */ -lunr.TokenSet = function () { - this.final = false - this.edges = {} - this.id = lunr.TokenSet._nextId - lunr.TokenSet._nextId += 1 -} - -/** - * Keeps track of the next, auto increment, identifier to assign - * to a new tokenSet. - * - * TokenSets require a unique identifier to be correctly minimised. - * - * @private - */ -lunr.TokenSet._nextId = 1 - -/** - * Creates a TokenSet instance from the given sorted array of words. - * - * @param {String[]} arr - A sorted array of strings to create the set from. - * @returns {lunr.TokenSet} - * @throws Will throw an error if the input array is not sorted. - */ -lunr.TokenSet.fromArray = function (arr) { - var builder = new lunr.TokenSet.Builder - - for (var i = 0, len = arr.length; i < len; i++) { - builder.insert(arr[i]) - } - - builder.finish() - return builder.root -} - -/** - * Creates a token set from a query clause. - * - * @private - * @param {Object} clause - A single clause from lunr.Query. - * @param {string} clause.term - The query clause term. - * @param {number} [clause.editDistance] - The optional edit distance for the term. - * @returns {lunr.TokenSet} - */ -lunr.TokenSet.fromClause = function (clause) { - if ('editDistance' in clause) { - return lunr.TokenSet.fromFuzzyString(clause.term, clause.editDistance) - } else { - return lunr.TokenSet.fromString(clause.term) - } -} - -/** - * Creates a token set representing a single string with a specified - * edit distance. - * - * Insertions, deletions, substitutions and transpositions are each - * treated as an edit distance of 1. - * - * Increasing the allowed edit distance will have a dramatic impact - * on the performance of both creating and intersecting these TokenSets. - * It is advised to keep the edit distance less than 3. - * - * @param {string} str - The string to create the token set from. - * @param {number} editDistance - The allowed edit distance to match. - * @returns {lunr.Vector} - */ -lunr.TokenSet.fromFuzzyString = function (str, editDistance) { - var root = new lunr.TokenSet - - var stack = [{ - node: root, - editsRemaining: editDistance, - str: str - }] - - while (stack.length) { - var frame = stack.pop() - - // no edit - if (frame.str.length > 0) { - var char = frame.str.charAt(0), - noEditNode - - if (char in frame.node.edges) { - noEditNode = frame.node.edges[char] - } else { - noEditNode = new lunr.TokenSet - frame.node.edges[char] = noEditNode - } - - if (frame.str.length == 1) { - noEditNode.final = true - } - - stack.push({ - node: noEditNode, - editsRemaining: frame.editsRemaining, - str: frame.str.slice(1) - }) - } - - if (frame.editsRemaining == 0) { - continue - } - - // insertion - if ("*" in frame.node.edges) { - var insertionNode = frame.node.edges["*"] - } else { - var insertionNode = new lunr.TokenSet - frame.node.edges["*"] = insertionNode - } - - if (frame.str.length == 0) { - insertionNode.final = true - } - - stack.push({ - node: insertionNode, - editsRemaining: frame.editsRemaining - 1, - str: frame.str - }) - - // deletion - // can only do a deletion if we have enough edits remaining - // and if there are characters left to delete in the string - if (frame.str.length > 1) { - stack.push({ - node: frame.node, - editsRemaining: frame.editsRemaining - 1, - str: frame.str.slice(1) - }) - } - - // deletion - // just removing the last character from the str - if (frame.str.length == 1) { - frame.node.final = true - } - - // substitution - // can only do a substitution if we have enough edits remaining - // and if there are characters left to substitute - if (frame.str.length >= 1) { - if ("*" in frame.node.edges) { - var substitutionNode = frame.node.edges["*"] - } else { - var substitutionNode = new lunr.TokenSet - frame.node.edges["*"] = substitutionNode - } - - if (frame.str.length == 1) { - substitutionNode.final = true - } - - stack.push({ - node: substitutionNode, - editsRemaining: frame.editsRemaining - 1, - str: frame.str.slice(1) - }) - } - - // transposition - // can only do a transposition if there are edits remaining - // and there are enough characters to transpose - if (frame.str.length > 1) { - var charA = frame.str.charAt(0), - charB = frame.str.charAt(1), - transposeNode - - if (charB in frame.node.edges) { - transposeNode = frame.node.edges[charB] - } else { - transposeNode = new lunr.TokenSet - frame.node.edges[charB] = transposeNode - } - - if (frame.str.length == 1) { - transposeNode.final = true - } - - stack.push({ - node: transposeNode, - editsRemaining: frame.editsRemaining - 1, - str: charA + frame.str.slice(2) - }) - } - } - - return root -} - -/** - * Creates a TokenSet from a string. - * - * The string may contain one or more wildcard characters (*) - * that will allow wildcard matching when intersecting with - * another TokenSet. - * - * @param {string} str - The string to create a TokenSet from. - * @returns {lunr.TokenSet} - */ -lunr.TokenSet.fromString = function (str) { - var node = new lunr.TokenSet, - root = node - - /* - * Iterates through all characters within the passed string - * appending a node for each character. - * - * When a wildcard character is found then a self - * referencing edge is introduced to continually match - * any number of any characters. - */ - for (var i = 0, len = str.length; i < len; i++) { - var char = str[i], - final = (i == len - 1) - - if (char == "*") { - node.edges[char] = node - node.final = final - - } else { - var next = new lunr.TokenSet - next.final = final - - node.edges[char] = next - node = next - } - } - - return root -} - -/** - * Converts this TokenSet into an array of strings - * contained within the TokenSet. - * - * This is not intended to be used on a TokenSet that - * contains wildcards, in these cases the results are - * undefined and are likely to cause an infinite loop. - * - * @returns {string[]} - */ -lunr.TokenSet.prototype.toArray = function () { - var words = [] - - var stack = [{ - prefix: "", - node: this - }] - - while (stack.length) { - var frame = stack.pop(), - edges = Object.keys(frame.node.edges), - len = edges.length - - if (frame.node.final) { - /* In Safari, at this point the prefix is sometimes corrupted, see: - * https://github.com/olivernn/lunr.js/issues/279 Calling any - * String.prototype method forces Safari to "cast" this string to what - * it's supposed to be, fixing the bug. */ - frame.prefix.charAt(0) - words.push(frame.prefix) - } - - for (var i = 0; i < len; i++) { - var edge = edges[i] - - stack.push({ - prefix: frame.prefix.concat(edge), - node: frame.node.edges[edge] - }) - } - } - - return words -} - -/** - * Generates a string representation of a TokenSet. - * - * This is intended to allow TokenSets to be used as keys - * in objects, largely to aid the construction and minimisation - * of a TokenSet. As such it is not designed to be a human - * friendly representation of the TokenSet. - * - * @returns {string} - */ -lunr.TokenSet.prototype.toString = function () { - // NOTE: Using Object.keys here as this.edges is very likely - // to enter 'hash-mode' with many keys being added - // - // avoiding a for-in loop here as it leads to the function - // being de-optimised (at least in V8). From some simple - // benchmarks the performance is comparable, but allowing - // V8 to optimize may mean easy performance wins in the future. - - if (this._str) { - return this._str - } - - var str = this.final ? '1' : '0', - labels = Object.keys(this.edges).sort(), - len = labels.length - - for (var i = 0; i < len; i++) { - var label = labels[i], - node = this.edges[label] - - str = str + label + node.id - } - - return str -} - -/** - * Returns a new TokenSet that is the intersection of - * this TokenSet and the passed TokenSet. - * - * This intersection will take into account any wildcards - * contained within the TokenSet. - * - * @param {lunr.TokenSet} b - An other TokenSet to intersect with. - * @returns {lunr.TokenSet} - */ -lunr.TokenSet.prototype.intersect = function (b) { - var output = new lunr.TokenSet, - frame = undefined - - var stack = [{ - qNode: b, - output: output, - node: this - }] - - while (stack.length) { - frame = stack.pop() - - // NOTE: As with the #toString method, we are using - // Object.keys and a for loop instead of a for-in loop - // as both of these objects enter 'hash' mode, causing - // the function to be de-optimised in V8 - var qEdges = Object.keys(frame.qNode.edges), - qLen = qEdges.length, - nEdges = Object.keys(frame.node.edges), - nLen = nEdges.length - - for (var q = 0; q < qLen; q++) { - var qEdge = qEdges[q] - - for (var n = 0; n < nLen; n++) { - var nEdge = nEdges[n] - - if (nEdge == qEdge || qEdge == '*') { - var node = frame.node.edges[nEdge], - qNode = frame.qNode.edges[qEdge], - final = node.final && qNode.final, - next = undefined - - if (nEdge in frame.output.edges) { - // an edge already exists for this character - // no need to create a new node, just set the finality - // bit unless this node is already final - next = frame.output.edges[nEdge] - next.final = next.final || final - - } else { - // no edge exists yet, must create one - // set the finality bit and insert it - // into the output - next = new lunr.TokenSet - next.final = final - frame.output.edges[nEdge] = next - } - - stack.push({ - qNode: qNode, - output: next, - node: node - }) - } - } - } - } - - return output -} -lunr.TokenSet.Builder = function () { - this.previousWord = "" - this.root = new lunr.TokenSet - this.uncheckedNodes = [] - this.minimizedNodes = {} -} - -lunr.TokenSet.Builder.prototype.insert = function (word) { - var node, - commonPrefix = 0 - - if (word < this.previousWord) { - throw new Error ("Out of order word insertion") - } - - for (var i = 0; i < word.length && i < this.previousWord.length; i++) { - if (word[i] != this.previousWord[i]) break - commonPrefix++ - } - - this.minimize(commonPrefix) - - if (this.uncheckedNodes.length == 0) { - node = this.root - } else { - node = this.uncheckedNodes[this.uncheckedNodes.length - 1].child - } - - for (var i = commonPrefix; i < word.length; i++) { - var nextNode = new lunr.TokenSet, - char = word[i] - - node.edges[char] = nextNode - - this.uncheckedNodes.push({ - parent: node, - char: char, - child: nextNode - }) - - node = nextNode - } - - node.final = true - this.previousWord = word -} - -lunr.TokenSet.Builder.prototype.finish = function () { - this.minimize(0) -} - -lunr.TokenSet.Builder.prototype.minimize = function (downTo) { - for (var i = this.uncheckedNodes.length - 1; i >= downTo; i--) { - var node = this.uncheckedNodes[i], - childKey = node.child.toString() - - if (childKey in this.minimizedNodes) { - node.parent.edges[node.char] = this.minimizedNodes[childKey] - } else { - // Cache the key for this node since - // we know it can't change anymore - node.child._str = childKey - - this.minimizedNodes[childKey] = node.child - } - - this.uncheckedNodes.pop() - } -} -/*! - * lunr.Index - * Copyright (C) 2020 Oliver Nightingale - */ - -/** - * An index contains the built index of all documents and provides a query interface - * to the index. - * - * Usually instances of lunr.Index will not be created using this constructor, instead - * lunr.Builder should be used to construct new indexes, or lunr.Index.load should be - * used to load previously built and serialized indexes. - * - * @constructor - * @param {Object} attrs - The attributes of the built search index. - * @param {Object} attrs.invertedIndex - An index of term/field to document reference. - * @param {Object} attrs.fieldVectors - Field vectors - * @param {lunr.TokenSet} attrs.tokenSet - An set of all corpus tokens. - * @param {string[]} attrs.fields - The names of indexed document fields. - * @param {lunr.Pipeline} attrs.pipeline - The pipeline to use for search terms. - */ -lunr.Index = function (attrs) { - this.invertedIndex = attrs.invertedIndex - this.fieldVectors = attrs.fieldVectors - this.tokenSet = attrs.tokenSet - this.fields = attrs.fields - this.pipeline = attrs.pipeline -} - -/** - * A result contains details of a document matching a search query. - * @typedef {Object} lunr.Index~Result - * @property {string} ref - The reference of the document this result represents. - * @property {number} score - A number between 0 and 1 representing how similar this document is to the query. - * @property {lunr.MatchData} matchData - Contains metadata about this match including which term(s) caused the match. - */ - -/** - * Although lunr provides the ability to create queries using lunr.Query, it also provides a simple - * query language which itself is parsed into an instance of lunr.Query. - * - * For programmatically building queries it is advised to directly use lunr.Query, the query language - * is best used for human entered text rather than program generated text. - * - * At its simplest queries can just be a single term, e.g. `hello`, multiple terms are also supported - * and will be combined with OR, e.g `hello world` will match documents that contain either 'hello' - * or 'world', though those that contain both will rank higher in the results. - * - * Wildcards can be included in terms to match one or more unspecified characters, these wildcards can - * be inserted anywhere within the term, and more than one wildcard can exist in a single term. Adding - * wildcards will increase the number of documents that will be found but can also have a negative - * impact on query performance, especially with wildcards at the beginning of a term. - * - * Terms can be restricted to specific fields, e.g. `title:hello`, only documents with the term - * hello in the title field will match this query. Using a field not present in the index will lead - * to an error being thrown. - * - * Modifiers can also be added to terms, lunr supports edit distance and boost modifiers on terms. A term - * boost will make documents matching that term score higher, e.g. `foo^5`. Edit distance is also supported - * to provide fuzzy matching, e.g. 'hello~2' will match documents with hello with an edit distance of 2. - * Avoid large values for edit distance to improve query performance. - * - * Each term also supports a presence modifier. By default a term's presence in document is optional, however - * this can be changed to either required or prohibited. For a term's presence to be required in a document the - * term should be prefixed with a '+', e.g. `+foo bar` is a search for documents that must contain 'foo' and - * optionally contain 'bar'. Conversely a leading '-' sets the terms presence to prohibited, i.e. it must not - * appear in a document, e.g. `-foo bar` is a search for documents that do not contain 'foo' but may contain 'bar'. - * - * To escape special characters the backslash character '\' can be used, this allows searches to include - * characters that would normally be considered modifiers, e.g. `foo\~2` will search for a term "foo~2" instead - * of attempting to apply a boost of 2 to the search term "foo". - * - * @typedef {string} lunr.Index~QueryString - * @example Simple single term query - * hello - * @example Multiple term query - * hello world - * @example term scoped to a field - * title:hello - * @example term with a boost of 10 - * hello^10 - * @example term with an edit distance of 2 - * hello~2 - * @example terms with presence modifiers - * -foo +bar baz - */ - -/** - * Performs a search against the index using lunr query syntax. - * - * Results will be returned sorted by their score, the most relevant results - * will be returned first. For details on how the score is calculated, please see - * the {@link https://lunrjs.com/guides/searching.html#scoring|guide}. - * - * For more programmatic querying use lunr.Index#query. - * - * @param {lunr.Index~QueryString} queryString - A string containing a lunr query. - * @throws {lunr.QueryParseError} If the passed query string cannot be parsed. - * @returns {lunr.Index~Result[]} - */ -lunr.Index.prototype.search = function (queryString) { - return this.query(function (query) { - var parser = new lunr.QueryParser(queryString, query) - parser.parse() - }) -} - -/** - * A query builder callback provides a query object to be used to express - * the query to perform on the index. - * - * @callback lunr.Index~queryBuilder - * @param {lunr.Query} query - The query object to build up. - * @this lunr.Query - */ - -/** - * Performs a query against the index using the yielded lunr.Query object. - * - * If performing programmatic queries against the index, this method is preferred - * over lunr.Index#search so as to avoid the additional query parsing overhead. - * - * A query object is yielded to the supplied function which should be used to - * express the query to be run against the index. - * - * Note that although this function takes a callback parameter it is _not_ an - * asynchronous operation, the callback is just yielded a query object to be - * customized. - * - * @param {lunr.Index~queryBuilder} fn - A function that is used to build the query. - * @returns {lunr.Index~Result[]} - */ -lunr.Index.prototype.query = function (fn) { - // for each query clause - // * process terms - // * expand terms from token set - // * find matching documents and metadata - // * get document vectors - // * score documents - - var query = new lunr.Query(this.fields), - matchingFields = Object.create(null), - queryVectors = Object.create(null), - termFieldCache = Object.create(null), - requiredMatches = Object.create(null), - prohibitedMatches = Object.create(null) - - /* - * To support field level boosts a query vector is created per - * field. An empty vector is eagerly created to support negated - * queries. - */ - for (var i = 0; i < this.fields.length; i++) { - queryVectors[this.fields[i]] = new lunr.Vector - } - - fn.call(query, query) - - for (var i = 0; i < query.clauses.length; i++) { - /* - * Unless the pipeline has been disabled for this term, which is - * the case for terms with wildcards, we need to pass the clause - * term through the search pipeline. A pipeline returns an array - * of processed terms. Pipeline functions may expand the passed - * term, which means we may end up performing multiple index lookups - * for a single query term. - */ - var clause = query.clauses[i], - terms = null, - clauseMatches = lunr.Set.empty - - if (clause.usePipeline) { - terms = this.pipeline.runString(clause.term, { - fields: clause.fields - }) - } else { - terms = [clause.term] - } - - for (var m = 0; m < terms.length; m++) { - var term = terms[m] - - /* - * Each term returned from the pipeline needs to use the same query - * clause object, e.g. the same boost and or edit distance. The - * simplest way to do this is to re-use the clause object but mutate - * its term property. - */ - clause.term = term - - /* - * From the term in the clause we create a token set which will then - * be used to intersect the indexes token set to get a list of terms - * to lookup in the inverted index - */ - var termTokenSet = lunr.TokenSet.fromClause(clause), - expandedTerms = this.tokenSet.intersect(termTokenSet).toArray() - - /* - * If a term marked as required does not exist in the tokenSet it is - * impossible for the search to return any matches. We set all the field - * scoped required matches set to empty and stop examining any further - * clauses. - */ - if (expandedTerms.length === 0 && clause.presence === lunr.Query.presence.REQUIRED) { - for (var k = 0; k < clause.fields.length; k++) { - var field = clause.fields[k] - requiredMatches[field] = lunr.Set.empty - } - - break - } - - for (var j = 0; j < expandedTerms.length; j++) { - /* - * For each term get the posting and termIndex, this is required for - * building the query vector. - */ - var expandedTerm = expandedTerms[j], - posting = this.invertedIndex[expandedTerm], - termIndex = posting._index - - for (var k = 0; k < clause.fields.length; k++) { - /* - * For each field that this query term is scoped by (by default - * all fields are in scope) we need to get all the document refs - * that have this term in that field. - * - * The posting is the entry in the invertedIndex for the matching - * term from above. - */ - var field = clause.fields[k], - fieldPosting = posting[field], - matchingDocumentRefs = Object.keys(fieldPosting), - termField = expandedTerm + "/" + field, - matchingDocumentsSet = new lunr.Set(matchingDocumentRefs) - - /* - * if the presence of this term is required ensure that the matching - * documents are added to the set of required matches for this clause. - * - */ - if (clause.presence == lunr.Query.presence.REQUIRED) { - clauseMatches = clauseMatches.union(matchingDocumentsSet) - - if (requiredMatches[field] === undefined) { - requiredMatches[field] = lunr.Set.complete - } - } - - /* - * if the presence of this term is prohibited ensure that the matching - * documents are added to the set of prohibited matches for this field, - * creating that set if it does not yet exist. - */ - if (clause.presence == lunr.Query.presence.PROHIBITED) { - if (prohibitedMatches[field] === undefined) { - prohibitedMatches[field] = lunr.Set.empty - } - - prohibitedMatches[field] = prohibitedMatches[field].union(matchingDocumentsSet) - - /* - * Prohibited matches should not be part of the query vector used for - * similarity scoring and no metadata should be extracted so we continue - * to the next field - */ - continue - } - - /* - * The query field vector is populated using the termIndex found for - * the term and a unit value with the appropriate boost applied. - * Using upsert because there could already be an entry in the vector - * for the term we are working with. In that case we just add the scores - * together. - */ - queryVectors[field].upsert(termIndex, clause.boost, function (a, b) { return a + b }) - - /** - * If we've already seen this term, field combo then we've already collected - * the matching documents and metadata, no need to go through all that again - */ - if (termFieldCache[termField]) { - continue - } - - for (var l = 0; l < matchingDocumentRefs.length; l++) { - /* - * All metadata for this term/field/document triple - * are then extracted and collected into an instance - * of lunr.MatchData ready to be returned in the query - * results - */ - var matchingDocumentRef = matchingDocumentRefs[l], - matchingFieldRef = new lunr.FieldRef (matchingDocumentRef, field), - metadata = fieldPosting[matchingDocumentRef], - fieldMatch - - if ((fieldMatch = matchingFields[matchingFieldRef]) === undefined) { - matchingFields[matchingFieldRef] = new lunr.MatchData (expandedTerm, field, metadata) - } else { - fieldMatch.add(expandedTerm, field, metadata) - } - - } - - termFieldCache[termField] = true - } - } - } - - /** - * If the presence was required we need to update the requiredMatches field sets. - * We do this after all fields for the term have collected their matches because - * the clause terms presence is required in _any_ of the fields not _all_ of the - * fields. - */ - if (clause.presence === lunr.Query.presence.REQUIRED) { - for (var k = 0; k < clause.fields.length; k++) { - var field = clause.fields[k] - requiredMatches[field] = requiredMatches[field].intersect(clauseMatches) - } - } - } - - /** - * Need to combine the field scoped required and prohibited - * matching documents into a global set of required and prohibited - * matches - */ - var allRequiredMatches = lunr.Set.complete, - allProhibitedMatches = lunr.Set.empty - - for (var i = 0; i < this.fields.length; i++) { - var field = this.fields[i] - - if (requiredMatches[field]) { - allRequiredMatches = allRequiredMatches.intersect(requiredMatches[field]) - } - - if (prohibitedMatches[field]) { - allProhibitedMatches = allProhibitedMatches.union(prohibitedMatches[field]) - } - } - - var matchingFieldRefs = Object.keys(matchingFields), - results = [], - matches = Object.create(null) - - /* - * If the query is negated (contains only prohibited terms) - * we need to get _all_ fieldRefs currently existing in the - * index. This is only done when we know that the query is - * entirely prohibited terms to avoid any cost of getting all - * fieldRefs unnecessarily. - * - * Additionally, blank MatchData must be created to correctly - * populate the results. - */ - if (query.isNegated()) { - matchingFieldRefs = Object.keys(this.fieldVectors) - - for (var i = 0; i < matchingFieldRefs.length; i++) { - var matchingFieldRef = matchingFieldRefs[i] - var fieldRef = lunr.FieldRef.fromString(matchingFieldRef) - matchingFields[matchingFieldRef] = new lunr.MatchData - } - } - - for (var i = 0; i < matchingFieldRefs.length; i++) { - /* - * Currently we have document fields that match the query, but we - * need to return documents. The matchData and scores are combined - * from multiple fields belonging to the same document. - * - * Scores are calculated by field, using the query vectors created - * above, and combined into a final document score using addition. - */ - var fieldRef = lunr.FieldRef.fromString(matchingFieldRefs[i]), - docRef = fieldRef.docRef - - if (!allRequiredMatches.contains(docRef)) { - continue - } - - if (allProhibitedMatches.contains(docRef)) { - continue - } - - var fieldVector = this.fieldVectors[fieldRef], - score = queryVectors[fieldRef.fieldName].similarity(fieldVector), - docMatch - - if ((docMatch = matches[docRef]) !== undefined) { - docMatch.score += score - docMatch.matchData.combine(matchingFields[fieldRef]) - } else { - var match = { - ref: docRef, - score: score, - matchData: matchingFields[fieldRef] - } - matches[docRef] = match - results.push(match) - } - } - - /* - * Sort the results objects by score, highest first. - */ - return results.sort(function (a, b) { - return b.score - a.score - }) -} - -/** - * Prepares the index for JSON serialization. - * - * The schema for this JSON blob will be described in a - * separate JSON schema file. - * - * @returns {Object} - */ -lunr.Index.prototype.toJSON = function () { - var invertedIndex = Object.keys(this.invertedIndex) - .sort() - .map(function (term) { - return [term, this.invertedIndex[term]] - }, this) - - var fieldVectors = Object.keys(this.fieldVectors) - .map(function (ref) { - return [ref, this.fieldVectors[ref].toJSON()] - }, this) - - return { - version: lunr.version, - fields: this.fields, - fieldVectors: fieldVectors, - invertedIndex: invertedIndex, - pipeline: this.pipeline.toJSON() - } -} - -/** - * Loads a previously serialized lunr.Index - * - * @param {Object} serializedIndex - A previously serialized lunr.Index - * @returns {lunr.Index} - */ -lunr.Index.load = function (serializedIndex) { - var attrs = {}, - fieldVectors = {}, - serializedVectors = serializedIndex.fieldVectors, - invertedIndex = Object.create(null), - serializedInvertedIndex = serializedIndex.invertedIndex, - tokenSetBuilder = new lunr.TokenSet.Builder, - pipeline = lunr.Pipeline.load(serializedIndex.pipeline) - - if (serializedIndex.version != lunr.version) { - lunr.utils.warn("Version mismatch when loading serialised index. Current version of lunr '" + lunr.version + "' does not match serialized index '" + serializedIndex.version + "'") - } - - for (var i = 0; i < serializedVectors.length; i++) { - var tuple = serializedVectors[i], - ref = tuple[0], - elements = tuple[1] - - fieldVectors[ref] = new lunr.Vector(elements) - } - - for (var i = 0; i < serializedInvertedIndex.length; i++) { - var tuple = serializedInvertedIndex[i], - term = tuple[0], - posting = tuple[1] - - tokenSetBuilder.insert(term) - invertedIndex[term] = posting - } - - tokenSetBuilder.finish() - - attrs.fields = serializedIndex.fields - - attrs.fieldVectors = fieldVectors - attrs.invertedIndex = invertedIndex - attrs.tokenSet = tokenSetBuilder.root - attrs.pipeline = pipeline - - return new lunr.Index(attrs) -} -/*! - * lunr.Builder - * Copyright (C) 2020 Oliver Nightingale - */ - -/** - * lunr.Builder performs indexing on a set of documents and - * returns instances of lunr.Index ready for querying. - * - * All configuration of the index is done via the builder, the - * fields to index, the document reference, the text processing - * pipeline and document scoring parameters are all set on the - * builder before indexing. - * - * @constructor - * @property {string} _ref - Internal reference to the document reference field. - * @property {string[]} _fields - Internal reference to the document fields to index. - * @property {object} invertedIndex - The inverted index maps terms to document fields. - * @property {object} documentTermFrequencies - Keeps track of document term frequencies. - * @property {object} documentLengths - Keeps track of the length of documents added to the index. - * @property {lunr.tokenizer} tokenizer - Function for splitting strings into tokens for indexing. - * @property {lunr.Pipeline} pipeline - The pipeline performs text processing on tokens before indexing. - * @property {lunr.Pipeline} searchPipeline - A pipeline for processing search terms before querying the index. - * @property {number} documentCount - Keeps track of the total number of documents indexed. - * @property {number} _b - A parameter to control field length normalization, setting this to 0 disabled normalization, 1 fully normalizes field lengths, the default value is 0.75. - * @property {number} _k1 - A parameter to control how quickly an increase in term frequency results in term frequency saturation, the default value is 1.2. - * @property {number} termIndex - A counter incremented for each unique term, used to identify a terms position in the vector space. - * @property {array} metadataWhitelist - A list of metadata keys that have been whitelisted for entry in the index. - */ -lunr.Builder = function () { - this._ref = "id" - this._fields = Object.create(null) - this._documents = Object.create(null) - this.invertedIndex = Object.create(null) - this.fieldTermFrequencies = {} - this.fieldLengths = {} - this.tokenizer = lunr.tokenizer - this.pipeline = new lunr.Pipeline - this.searchPipeline = new lunr.Pipeline - this.documentCount = 0 - this._b = 0.75 - this._k1 = 1.2 - this.termIndex = 0 - this.metadataWhitelist = [] -} - -/** - * Sets the document field used as the document reference. Every document must have this field. - * The type of this field in the document should be a string, if it is not a string it will be - * coerced into a string by calling toString. - * - * The default ref is 'id'. - * - * The ref should _not_ be changed during indexing, it should be set before any documents are - * added to the index. Changing it during indexing can lead to inconsistent results. - * - * @param {string} ref - The name of the reference field in the document. - */ -lunr.Builder.prototype.ref = function (ref) { - this._ref = ref -} - -/** - * A function that is used to extract a field from a document. - * - * Lunr expects a field to be at the top level of a document, if however the field - * is deeply nested within a document an extractor function can be used to extract - * the right field for indexing. - * - * @callback fieldExtractor - * @param {object} doc - The document being added to the index. - * @returns {?(string|object|object[])} obj - The object that will be indexed for this field. - * @example Extracting a nested field - * function (doc) { return doc.nested.field } - */ - -/** - * Adds a field to the list of document fields that will be indexed. Every document being - * indexed should have this field. Null values for this field in indexed documents will - * not cause errors but will limit the chance of that document being retrieved by searches. - * - * All fields should be added before adding documents to the index. Adding fields after - * a document has been indexed will have no effect on already indexed documents. - * - * Fields can be boosted at build time. This allows terms within that field to have more - * importance when ranking search results. Use a field boost to specify that matches within - * one field are more important than other fields. - * - * @param {string} fieldName - The name of a field to index in all documents. - * @param {object} attributes - Optional attributes associated with this field. - * @param {number} [attributes.boost=1] - Boost applied to all terms within this field. - * @param {fieldExtractor} [attributes.extractor] - Function to extract a field from a document. - * @throws {RangeError} fieldName cannot contain unsupported characters '/' - */ -lunr.Builder.prototype.field = function (fieldName, attributes) { - if (/\//.test(fieldName)) { - throw new RangeError ("Field '" + fieldName + "' contains illegal character '/'") - } - - this._fields[fieldName] = attributes || {} -} - -/** - * A parameter to tune the amount of field length normalisation that is applied when - * calculating relevance scores. A value of 0 will completely disable any normalisation - * and a value of 1 will fully normalise field lengths. The default is 0.75. Values of b - * will be clamped to the range 0 - 1. - * - * @param {number} number - The value to set for this tuning parameter. - */ -lunr.Builder.prototype.b = function (number) { - if (number < 0) { - this._b = 0 - } else if (number > 1) { - this._b = 1 - } else { - this._b = number - } -} - -/** - * A parameter that controls the speed at which a rise in term frequency results in term - * frequency saturation. The default value is 1.2. Setting this to a higher value will give - * slower saturation levels, a lower value will result in quicker saturation. - * - * @param {number} number - The value to set for this tuning parameter. - */ -lunr.Builder.prototype.k1 = function (number) { - this._k1 = number -} - -/** - * Adds a document to the index. - * - * Before adding fields to the index the index should have been fully setup, with the document - * ref and all fields to index already having been specified. - * - * The document must have a field name as specified by the ref (by default this is 'id') and - * it should have all fields defined for indexing, though null or undefined values will not - * cause errors. - * - * Entire documents can be boosted at build time. Applying a boost to a document indicates that - * this document should rank higher in search results than other documents. - * - * @param {object} doc - The document to add to the index. - * @param {object} attributes - Optional attributes associated with this document. - * @param {number} [attributes.boost=1] - Boost applied to all terms within this document. - */ -lunr.Builder.prototype.add = function (doc, attributes) { - var docRef = doc[this._ref], - fields = Object.keys(this._fields) - - this._documents[docRef] = attributes || {} - this.documentCount += 1 - - for (var i = 0; i < fields.length; i++) { - var fieldName = fields[i], - extractor = this._fields[fieldName].extractor, - field = extractor ? extractor(doc) : doc[fieldName], - tokens = this.tokenizer(field, { - fields: [fieldName] - }), - terms = this.pipeline.run(tokens), - fieldRef = new lunr.FieldRef (docRef, fieldName), - fieldTerms = Object.create(null) - - this.fieldTermFrequencies[fieldRef] = fieldTerms - this.fieldLengths[fieldRef] = 0 - - // store the length of this field for this document - this.fieldLengths[fieldRef] += terms.length - - // calculate term frequencies for this field - for (var j = 0; j < terms.length; j++) { - var term = terms[j] - - if (fieldTerms[term] == undefined) { - fieldTerms[term] = 0 - } - - fieldTerms[term] += 1 - - // add to inverted index - // create an initial posting if one doesn't exist - if (this.invertedIndex[term] == undefined) { - var posting = Object.create(null) - posting["_index"] = this.termIndex - this.termIndex += 1 - - for (var k = 0; k < fields.length; k++) { - posting[fields[k]] = Object.create(null) - } - - this.invertedIndex[term] = posting - } - - // add an entry for this term/fieldName/docRef to the invertedIndex - if (this.invertedIndex[term][fieldName][docRef] == undefined) { - this.invertedIndex[term][fieldName][docRef] = Object.create(null) - } - - // store all whitelisted metadata about this token in the - // inverted index - for (var l = 0; l < this.metadataWhitelist.length; l++) { - var metadataKey = this.metadataWhitelist[l], - metadata = term.metadata[metadataKey] - - if (this.invertedIndex[term][fieldName][docRef][metadataKey] == undefined) { - this.invertedIndex[term][fieldName][docRef][metadataKey] = [] - } - - this.invertedIndex[term][fieldName][docRef][metadataKey].push(metadata) - } - } - - } -} - -/** - * Calculates the average document length for this index - * - * @private - */ -lunr.Builder.prototype.calculateAverageFieldLengths = function () { - - var fieldRefs = Object.keys(this.fieldLengths), - numberOfFields = fieldRefs.length, - accumulator = {}, - documentsWithField = {} - - for (var i = 0; i < numberOfFields; i++) { - var fieldRef = lunr.FieldRef.fromString(fieldRefs[i]), - field = fieldRef.fieldName - - documentsWithField[field] || (documentsWithField[field] = 0) - documentsWithField[field] += 1 - - accumulator[field] || (accumulator[field] = 0) - accumulator[field] += this.fieldLengths[fieldRef] - } - - var fields = Object.keys(this._fields) - - for (var i = 0; i < fields.length; i++) { - var fieldName = fields[i] - accumulator[fieldName] = accumulator[fieldName] / documentsWithField[fieldName] - } - - this.averageFieldLength = accumulator -} - -/** - * Builds a vector space model of every document using lunr.Vector - * - * @private - */ -lunr.Builder.prototype.createFieldVectors = function () { - var fieldVectors = {}, - fieldRefs = Object.keys(this.fieldTermFrequencies), - fieldRefsLength = fieldRefs.length, - termIdfCache = Object.create(null) - - for (var i = 0; i < fieldRefsLength; i++) { - var fieldRef = lunr.FieldRef.fromString(fieldRefs[i]), - fieldName = fieldRef.fieldName, - fieldLength = this.fieldLengths[fieldRef], - fieldVector = new lunr.Vector, - termFrequencies = this.fieldTermFrequencies[fieldRef], - terms = Object.keys(termFrequencies), - termsLength = terms.length - - - var fieldBoost = this._fields[fieldName].boost || 1, - docBoost = this._documents[fieldRef.docRef].boost || 1 - - for (var j = 0; j < termsLength; j++) { - var term = terms[j], - tf = termFrequencies[term], - termIndex = this.invertedIndex[term]._index, - idf, score, scoreWithPrecision - - if (termIdfCache[term] === undefined) { - idf = lunr.idf(this.invertedIndex[term], this.documentCount) - termIdfCache[term] = idf - } else { - idf = termIdfCache[term] - } - - score = idf * ((this._k1 + 1) * tf) / (this._k1 * (1 - this._b + this._b * (fieldLength / this.averageFieldLength[fieldName])) + tf) - score *= fieldBoost - score *= docBoost - scoreWithPrecision = Math.round(score * 1000) / 1000 - // Converts 1.23456789 to 1.234. - // Reducing the precision so that the vectors take up less - // space when serialised. Doing it now so that they behave - // the same before and after serialisation. Also, this is - // the fastest approach to reducing a number's precision in - // JavaScript. - - fieldVector.insert(termIndex, scoreWithPrecision) - } - - fieldVectors[fieldRef] = fieldVector - } - - this.fieldVectors = fieldVectors -} - -/** - * Creates a token set of all tokens in the index using lunr.TokenSet - * - * @private - */ -lunr.Builder.prototype.createTokenSet = function () { - this.tokenSet = lunr.TokenSet.fromArray( - Object.keys(this.invertedIndex).sort() - ) -} - -/** - * Builds the index, creating an instance of lunr.Index. - * - * This completes the indexing process and should only be called - * once all documents have been added to the index. - * - * @returns {lunr.Index} - */ -lunr.Builder.prototype.build = function () { - this.calculateAverageFieldLengths() - this.createFieldVectors() - this.createTokenSet() - - return new lunr.Index({ - invertedIndex: this.invertedIndex, - fieldVectors: this.fieldVectors, - tokenSet: this.tokenSet, - fields: Object.keys(this._fields), - pipeline: this.searchPipeline - }) -} - -/** - * Applies a plugin to the index builder. - * - * A plugin is a function that is called with the index builder as its context. - * Plugins can be used to customise or extend the behaviour of the index - * in some way. A plugin is just a function, that encapsulated the custom - * behaviour that should be applied when building the index. - * - * The plugin function will be called with the index builder as its argument, additional - * arguments can also be passed when calling use. The function will be called - * with the index builder as its context. - * - * @param {Function} plugin The plugin to apply. - */ -lunr.Builder.prototype.use = function (fn) { - var args = Array.prototype.slice.call(arguments, 1) - args.unshift(this) - fn.apply(this, args) -} -/** - * Contains and collects metadata about a matching document. - * A single instance of lunr.MatchData is returned as part of every - * lunr.Index~Result. - * - * @constructor - * @param {string} term - The term this match data is associated with - * @param {string} field - The field in which the term was found - * @param {object} metadata - The metadata recorded about this term in this field - * @property {object} metadata - A cloned collection of metadata associated with this document. - * @see {@link lunr.Index~Result} - */ -lunr.MatchData = function (term, field, metadata) { - var clonedMetadata = Object.create(null), - metadataKeys = Object.keys(metadata || {}) - - // Cloning the metadata to prevent the original - // being mutated during match data combination. - // Metadata is kept in an array within the inverted - // index so cloning the data can be done with - // Array#slice - for (var i = 0; i < metadataKeys.length; i++) { - var key = metadataKeys[i] - clonedMetadata[key] = metadata[key].slice() - } - - this.metadata = Object.create(null) - - if (term !== undefined) { - this.metadata[term] = Object.create(null) - this.metadata[term][field] = clonedMetadata - } -} - -/** - * An instance of lunr.MatchData will be created for every term that matches a - * document. However only one instance is required in a lunr.Index~Result. This - * method combines metadata from another instance of lunr.MatchData with this - * objects metadata. - * - * @param {lunr.MatchData} otherMatchData - Another instance of match data to merge with this one. - * @see {@link lunr.Index~Result} - */ -lunr.MatchData.prototype.combine = function (otherMatchData) { - var terms = Object.keys(otherMatchData.metadata) - - for (var i = 0; i < terms.length; i++) { - var term = terms[i], - fields = Object.keys(otherMatchData.metadata[term]) - - if (this.metadata[term] == undefined) { - this.metadata[term] = Object.create(null) - } - - for (var j = 0; j < fields.length; j++) { - var field = fields[j], - keys = Object.keys(otherMatchData.metadata[term][field]) - - if (this.metadata[term][field] == undefined) { - this.metadata[term][field] = Object.create(null) - } - - for (var k = 0; k < keys.length; k++) { - var key = keys[k] - - if (this.metadata[term][field][key] == undefined) { - this.metadata[term][field][key] = otherMatchData.metadata[term][field][key] - } else { - this.metadata[term][field][key] = this.metadata[term][field][key].concat(otherMatchData.metadata[term][field][key]) - } - - } - } - } -} - -/** - * Add metadata for a term/field pair to this instance of match data. - * - * @param {string} term - The term this match data is associated with - * @param {string} field - The field in which the term was found - * @param {object} metadata - The metadata recorded about this term in this field - */ -lunr.MatchData.prototype.add = function (term, field, metadata) { - if (!(term in this.metadata)) { - this.metadata[term] = Object.create(null) - this.metadata[term][field] = metadata - return - } - - if (!(field in this.metadata[term])) { - this.metadata[term][field] = metadata - return - } - - var metadataKeys = Object.keys(metadata) - - for (var i = 0; i < metadataKeys.length; i++) { - var key = metadataKeys[i] - - if (key in this.metadata[term][field]) { - this.metadata[term][field][key] = this.metadata[term][field][key].concat(metadata[key]) - } else { - this.metadata[term][field][key] = metadata[key] - } - } -} -/** - * A lunr.Query provides a programmatic way of defining queries to be performed - * against a {@link lunr.Index}. - * - * Prefer constructing a lunr.Query using the {@link lunr.Index#query} method - * so the query object is pre-initialized with the right index fields. - * - * @constructor - * @property {lunr.Query~Clause[]} clauses - An array of query clauses. - * @property {string[]} allFields - An array of all available fields in a lunr.Index. - */ -lunr.Query = function (allFields) { - this.clauses = [] - this.allFields = allFields -} - -/** - * Constants for indicating what kind of automatic wildcard insertion will be used when constructing a query clause. - * - * This allows wildcards to be added to the beginning and end of a term without having to manually do any string - * concatenation. - * - * The wildcard constants can be bitwise combined to select both leading and trailing wildcards. - * - * @constant - * @default - * @property {number} wildcard.NONE - The term will have no wildcards inserted, this is the default behaviour - * @property {number} wildcard.LEADING - Prepend the term with a wildcard, unless a leading wildcard already exists - * @property {number} wildcard.TRAILING - Append a wildcard to the term, unless a trailing wildcard already exists - * @see lunr.Query~Clause - * @see lunr.Query#clause - * @see lunr.Query#term - * @example query term with trailing wildcard - * query.term('foo', { wildcard: lunr.Query.wildcard.TRAILING }) - * @example query term with leading and trailing wildcard - * query.term('foo', { - * wildcard: lunr.Query.wildcard.LEADING | lunr.Query.wildcard.TRAILING - * }) - */ - -lunr.Query.wildcard = new String ("*") -lunr.Query.wildcard.NONE = 0 -lunr.Query.wildcard.LEADING = 1 -lunr.Query.wildcard.TRAILING = 2 - -/** - * Constants for indicating what kind of presence a term must have in matching documents. - * - * @constant - * @enum {number} - * @see lunr.Query~Clause - * @see lunr.Query#clause - * @see lunr.Query#term - * @example query term with required presence - * query.term('foo', { presence: lunr.Query.presence.REQUIRED }) - */ -lunr.Query.presence = { - /** - * Term's presence in a document is optional, this is the default value. - */ - OPTIONAL: 1, - - /** - * Term's presence in a document is required, documents that do not contain - * this term will not be returned. - */ - REQUIRED: 2, - - /** - * Term's presence in a document is prohibited, documents that do contain - * this term will not be returned. - */ - PROHIBITED: 3 -} - -/** - * A single clause in a {@link lunr.Query} contains a term and details on how to - * match that term against a {@link lunr.Index}. - * - * @typedef {Object} lunr.Query~Clause - * @property {string[]} fields - The fields in an index this clause should be matched against. - * @property {number} [boost=1] - Any boost that should be applied when matching this clause. - * @property {number} [editDistance] - Whether the term should have fuzzy matching applied, and how fuzzy the match should be. - * @property {boolean} [usePipeline] - Whether the term should be passed through the search pipeline. - * @property {number} [wildcard=lunr.Query.wildcard.NONE] - Whether the term should have wildcards appended or prepended. - * @property {number} [presence=lunr.Query.presence.OPTIONAL] - The terms presence in any matching documents. - */ - -/** - * Adds a {@link lunr.Query~Clause} to this query. - * - * Unless the clause contains the fields to be matched all fields will be matched. In addition - * a default boost of 1 is applied to the clause. - * - * @param {lunr.Query~Clause} clause - The clause to add to this query. - * @see lunr.Query~Clause - * @returns {lunr.Query} - */ -lunr.Query.prototype.clause = function (clause) { - if (!('fields' in clause)) { - clause.fields = this.allFields - } - - if (!('boost' in clause)) { - clause.boost = 1 - } - - if (!('usePipeline' in clause)) { - clause.usePipeline = true - } - - if (!('wildcard' in clause)) { - clause.wildcard = lunr.Query.wildcard.NONE - } - - if ((clause.wildcard & lunr.Query.wildcard.LEADING) && (clause.term.charAt(0) != lunr.Query.wildcard)) { - clause.term = "*" + clause.term - } - - if ((clause.wildcard & lunr.Query.wildcard.TRAILING) && (clause.term.slice(-1) != lunr.Query.wildcard)) { - clause.term = "" + clause.term + "*" - } - - if (!('presence' in clause)) { - clause.presence = lunr.Query.presence.OPTIONAL - } - - this.clauses.push(clause) - - return this -} - -/** - * A negated query is one in which every clause has a presence of - * prohibited. These queries require some special processing to return - * the expected results. - * - * @returns boolean - */ -lunr.Query.prototype.isNegated = function () { - for (var i = 0; i < this.clauses.length; i++) { - if (this.clauses[i].presence != lunr.Query.presence.PROHIBITED) { - return false - } - } - - return true -} - -/** - * Adds a term to the current query, under the covers this will create a {@link lunr.Query~Clause} - * to the list of clauses that make up this query. - * - * The term is used as is, i.e. no tokenization will be performed by this method. Instead conversion - * to a token or token-like string should be done before calling this method. - * - * The term will be converted to a string by calling `toString`. Multiple terms can be passed as an - * array, each term in the array will share the same options. - * - * @param {object|object[]} term - The term(s) to add to the query. - * @param {object} [options] - Any additional properties to add to the query clause. - * @returns {lunr.Query} - * @see lunr.Query#clause - * @see lunr.Query~Clause - * @example adding a single term to a query - * query.term("foo") - * @example adding a single term to a query and specifying search fields, term boost and automatic trailing wildcard - * query.term("foo", { - * fields: ["title"], - * boost: 10, - * wildcard: lunr.Query.wildcard.TRAILING - * }) - * @example using lunr.tokenizer to convert a string to tokens before using them as terms - * query.term(lunr.tokenizer("foo bar")) - */ -lunr.Query.prototype.term = function (term, options) { - if (Array.isArray(term)) { - term.forEach(function (t) { this.term(t, lunr.utils.clone(options)) }, this) - return this - } - - var clause = options || {} - clause.term = term.toString() - - this.clause(clause) - - return this -} -lunr.QueryParseError = function (message, start, end) { - this.name = "QueryParseError" - this.message = message - this.start = start - this.end = end -} - -lunr.QueryParseError.prototype = new Error -lunr.QueryLexer = function (str) { - this.lexemes = [] - this.str = str - this.length = str.length - this.pos = 0 - this.start = 0 - this.escapeCharPositions = [] -} - -lunr.QueryLexer.prototype.run = function () { - var state = lunr.QueryLexer.lexText - - while (state) { - state = state(this) - } -} - -lunr.QueryLexer.prototype.sliceString = function () { - var subSlices = [], - sliceStart = this.start, - sliceEnd = this.pos - - for (var i = 0; i < this.escapeCharPositions.length; i++) { - sliceEnd = this.escapeCharPositions[i] - subSlices.push(this.str.slice(sliceStart, sliceEnd)) - sliceStart = sliceEnd + 1 - } - - subSlices.push(this.str.slice(sliceStart, this.pos)) - this.escapeCharPositions.length = 0 - - return subSlices.join('') -} - -lunr.QueryLexer.prototype.emit = function (type) { - this.lexemes.push({ - type: type, - str: this.sliceString(), - start: this.start, - end: this.pos - }) - - this.start = this.pos -} - -lunr.QueryLexer.prototype.escapeCharacter = function () { - this.escapeCharPositions.push(this.pos - 1) - this.pos += 1 -} - -lunr.QueryLexer.prototype.next = function () { - if (this.pos >= this.length) { - return lunr.QueryLexer.EOS - } - - var char = this.str.charAt(this.pos) - this.pos += 1 - return char -} - -lunr.QueryLexer.prototype.width = function () { - return this.pos - this.start -} - -lunr.QueryLexer.prototype.ignore = function () { - if (this.start == this.pos) { - this.pos += 1 - } - - this.start = this.pos -} - -lunr.QueryLexer.prototype.backup = function () { - this.pos -= 1 -} - -lunr.QueryLexer.prototype.acceptDigitRun = function () { - var char, charCode - - do { - char = this.next() - charCode = char.charCodeAt(0) - } while (charCode > 47 && charCode < 58) - - if (char != lunr.QueryLexer.EOS) { - this.backup() - } -} - -lunr.QueryLexer.prototype.more = function () { - return this.pos < this.length -} - -lunr.QueryLexer.EOS = 'EOS' -lunr.QueryLexer.FIELD = 'FIELD' -lunr.QueryLexer.TERM = 'TERM' -lunr.QueryLexer.EDIT_DISTANCE = 'EDIT_DISTANCE' -lunr.QueryLexer.BOOST = 'BOOST' -lunr.QueryLexer.PRESENCE = 'PRESENCE' - -lunr.QueryLexer.lexField = function (lexer) { - lexer.backup() - lexer.emit(lunr.QueryLexer.FIELD) - lexer.ignore() - return lunr.QueryLexer.lexText -} - -lunr.QueryLexer.lexTerm = function (lexer) { - if (lexer.width() > 1) { - lexer.backup() - lexer.emit(lunr.QueryLexer.TERM) - } - - lexer.ignore() - - if (lexer.more()) { - return lunr.QueryLexer.lexText - } -} - -lunr.QueryLexer.lexEditDistance = function (lexer) { - lexer.ignore() - lexer.acceptDigitRun() - lexer.emit(lunr.QueryLexer.EDIT_DISTANCE) - return lunr.QueryLexer.lexText -} - -lunr.QueryLexer.lexBoost = function (lexer) { - lexer.ignore() - lexer.acceptDigitRun() - lexer.emit(lunr.QueryLexer.BOOST) - return lunr.QueryLexer.lexText -} - -lunr.QueryLexer.lexEOS = function (lexer) { - if (lexer.width() > 0) { - lexer.emit(lunr.QueryLexer.TERM) - } -} - -// This matches the separator used when tokenising fields -// within a document. These should match otherwise it is -// not possible to search for some tokens within a document. -// -// It is possible for the user to change the separator on the -// tokenizer so it _might_ clash with any other of the special -// characters already used within the search string, e.g. :. -// -// This means that it is possible to change the separator in -// such a way that makes some words unsearchable using a search -// string. -lunr.QueryLexer.termSeparator = lunr.tokenizer.separator - -lunr.QueryLexer.lexText = function (lexer) { - while (true) { - var char = lexer.next() - - if (char == lunr.QueryLexer.EOS) { - return lunr.QueryLexer.lexEOS - } - - // Escape character is '\' - if (char.charCodeAt(0) == 92) { - lexer.escapeCharacter() - continue - } - - if (char == ":") { - return lunr.QueryLexer.lexField - } - - if (char == "~") { - lexer.backup() - if (lexer.width() > 0) { - lexer.emit(lunr.QueryLexer.TERM) - } - return lunr.QueryLexer.lexEditDistance - } - - if (char == "^") { - lexer.backup() - if (lexer.width() > 0) { - lexer.emit(lunr.QueryLexer.TERM) - } - return lunr.QueryLexer.lexBoost - } - - // "+" indicates term presence is required - // checking for length to ensure that only - // leading "+" are considered - if (char == "+" && lexer.width() === 1) { - lexer.emit(lunr.QueryLexer.PRESENCE) - return lunr.QueryLexer.lexText - } - - // "-" indicates term presence is prohibited - // checking for length to ensure that only - // leading "-" are considered - if (char == "-" && lexer.width() === 1) { - lexer.emit(lunr.QueryLexer.PRESENCE) - return lunr.QueryLexer.lexText - } - - if (char.match(lunr.QueryLexer.termSeparator)) { - return lunr.QueryLexer.lexTerm - } - } -} - -lunr.QueryParser = function (str, query) { - this.lexer = new lunr.QueryLexer (str) - this.query = query - this.currentClause = {} - this.lexemeIdx = 0 -} - -lunr.QueryParser.prototype.parse = function () { - this.lexer.run() - this.lexemes = this.lexer.lexemes - - var state = lunr.QueryParser.parseClause - - while (state) { - state = state(this) - } - - return this.query -} - -lunr.QueryParser.prototype.peekLexeme = function () { - return this.lexemes[this.lexemeIdx] -} - -lunr.QueryParser.prototype.consumeLexeme = function () { - var lexeme = this.peekLexeme() - this.lexemeIdx += 1 - return lexeme -} - -lunr.QueryParser.prototype.nextClause = function () { - var completedClause = this.currentClause - this.query.clause(completedClause) - this.currentClause = {} -} - -lunr.QueryParser.parseClause = function (parser) { - var lexeme = parser.peekLexeme() - - if (lexeme == undefined) { - return - } - - switch (lexeme.type) { - case lunr.QueryLexer.PRESENCE: - return lunr.QueryParser.parsePresence - case lunr.QueryLexer.FIELD: - return lunr.QueryParser.parseField - case lunr.QueryLexer.TERM: - return lunr.QueryParser.parseTerm - default: - var errorMessage = "expected either a field or a term, found " + lexeme.type - - if (lexeme.str.length >= 1) { - errorMessage += " with value '" + lexeme.str + "'" - } - - throw new lunr.QueryParseError (errorMessage, lexeme.start, lexeme.end) - } -} - -lunr.QueryParser.parsePresence = function (parser) { - var lexeme = parser.consumeLexeme() - - if (lexeme == undefined) { - return - } - - switch (lexeme.str) { - case "-": - parser.currentClause.presence = lunr.Query.presence.PROHIBITED - break - case "+": - parser.currentClause.presence = lunr.Query.presence.REQUIRED - break - default: - var errorMessage = "unrecognised presence operator'" + lexeme.str + "'" - throw new lunr.QueryParseError (errorMessage, lexeme.start, lexeme.end) - } - - var nextLexeme = parser.peekLexeme() - - if (nextLexeme == undefined) { - var errorMessage = "expecting term or field, found nothing" - throw new lunr.QueryParseError (errorMessage, lexeme.start, lexeme.end) - } - - switch (nextLexeme.type) { - case lunr.QueryLexer.FIELD: - return lunr.QueryParser.parseField - case lunr.QueryLexer.TERM: - return lunr.QueryParser.parseTerm - default: - var errorMessage = "expecting term or field, found '" + nextLexeme.type + "'" - throw new lunr.QueryParseError (errorMessage, nextLexeme.start, nextLexeme.end) - } -} - -lunr.QueryParser.parseField = function (parser) { - var lexeme = parser.consumeLexeme() - - if (lexeme == undefined) { - return - } - - if (parser.query.allFields.indexOf(lexeme.str) == -1) { - var possibleFields = parser.query.allFields.map(function (f) { return "'" + f + "'" }).join(', '), - errorMessage = "unrecognised field '" + lexeme.str + "', possible fields: " + possibleFields - - throw new lunr.QueryParseError (errorMessage, lexeme.start, lexeme.end) - } - - parser.currentClause.fields = [lexeme.str] - - var nextLexeme = parser.peekLexeme() - - if (nextLexeme == undefined) { - var errorMessage = "expecting term, found nothing" - throw new lunr.QueryParseError (errorMessage, lexeme.start, lexeme.end) - } - - switch (nextLexeme.type) { - case lunr.QueryLexer.TERM: - return lunr.QueryParser.parseTerm - default: - var errorMessage = "expecting term, found '" + nextLexeme.type + "'" - throw new lunr.QueryParseError (errorMessage, nextLexeme.start, nextLexeme.end) - } -} - -lunr.QueryParser.parseTerm = function (parser) { - var lexeme = parser.consumeLexeme() - - if (lexeme == undefined) { - return - } - - parser.currentClause.term = lexeme.str.toLowerCase() - - if (lexeme.str.indexOf("*") != -1) { - parser.currentClause.usePipeline = false - } - - var nextLexeme = parser.peekLexeme() - - if (nextLexeme == undefined) { - parser.nextClause() - return - } - - switch (nextLexeme.type) { - case lunr.QueryLexer.TERM: - parser.nextClause() - return lunr.QueryParser.parseTerm - case lunr.QueryLexer.FIELD: - parser.nextClause() - return lunr.QueryParser.parseField - case lunr.QueryLexer.EDIT_DISTANCE: - return lunr.QueryParser.parseEditDistance - case lunr.QueryLexer.BOOST: - return lunr.QueryParser.parseBoost - case lunr.QueryLexer.PRESENCE: - parser.nextClause() - return lunr.QueryParser.parsePresence - default: - var errorMessage = "Unexpected lexeme type '" + nextLexeme.type + "'" - throw new lunr.QueryParseError (errorMessage, nextLexeme.start, nextLexeme.end) - } -} - -lunr.QueryParser.parseEditDistance = function (parser) { - var lexeme = parser.consumeLexeme() - - if (lexeme == undefined) { - return - } - - var editDistance = parseInt(lexeme.str, 10) - - if (isNaN(editDistance)) { - var errorMessage = "edit distance must be numeric" - throw new lunr.QueryParseError (errorMessage, lexeme.start, lexeme.end) - } - - parser.currentClause.editDistance = editDistance - - var nextLexeme = parser.peekLexeme() - - if (nextLexeme == undefined) { - parser.nextClause() - return - } - - switch (nextLexeme.type) { - case lunr.QueryLexer.TERM: - parser.nextClause() - return lunr.QueryParser.parseTerm - case lunr.QueryLexer.FIELD: - parser.nextClause() - return lunr.QueryParser.parseField - case lunr.QueryLexer.EDIT_DISTANCE: - return lunr.QueryParser.parseEditDistance - case lunr.QueryLexer.BOOST: - return lunr.QueryParser.parseBoost - case lunr.QueryLexer.PRESENCE: - parser.nextClause() - return lunr.QueryParser.parsePresence - default: - var errorMessage = "Unexpected lexeme type '" + nextLexeme.type + "'" - throw new lunr.QueryParseError (errorMessage, nextLexeme.start, nextLexeme.end) - } -} - -lunr.QueryParser.parseBoost = function (parser) { - var lexeme = parser.consumeLexeme() - - if (lexeme == undefined) { - return - } - - var boost = parseInt(lexeme.str, 10) - - if (isNaN(boost)) { - var errorMessage = "boost must be numeric" - throw new lunr.QueryParseError (errorMessage, lexeme.start, lexeme.end) - } - - parser.currentClause.boost = boost - - var nextLexeme = parser.peekLexeme() - - if (nextLexeme == undefined) { - parser.nextClause() - return - } - - switch (nextLexeme.type) { - case lunr.QueryLexer.TERM: - parser.nextClause() - return lunr.QueryParser.parseTerm - case lunr.QueryLexer.FIELD: - parser.nextClause() - return lunr.QueryParser.parseField - case lunr.QueryLexer.EDIT_DISTANCE: - return lunr.QueryParser.parseEditDistance - case lunr.QueryLexer.BOOST: - return lunr.QueryParser.parseBoost - case lunr.QueryLexer.PRESENCE: - parser.nextClause() - return lunr.QueryParser.parsePresence - default: - var errorMessage = "Unexpected lexeme type '" + nextLexeme.type + "'" - throw new lunr.QueryParseError (errorMessage, nextLexeme.start, nextLexeme.end) - } -} - - /** - * export the module via AMD, CommonJS or as a browser global - * Export code from https://github.com/umdjs/umd/blob/master/returnExports.js - */ - ;(function (root, factory) { - if (typeof define === 'function' && define.amd) { - // AMD. Register as an anonymous module. - define(factory) - } else if (typeof exports === 'object') { - /** - * Node. Does not work with strict CommonJS, but - * only CommonJS-like environments that support module.exports, - * like Node. - */ - module.exports = factory() - } else { - // Browser globals (root is window) - root.lunr = factory() - } - }(this, function () { - /** - * Just return a value to define the module export. - * This example returns an object, but the module - * can return a function as the exported value. - */ - return lunr - })) -})(); diff --git a/site/search/main.js b/site/search/main.js deleted file mode 100644 index a5e469d7c..000000000 --- a/site/search/main.js +++ /dev/null @@ -1,109 +0,0 @@ -function getSearchTermFromLocation() { - var sPageURL = window.location.search.substring(1); - var sURLVariables = sPageURL.split('&'); - for (var i = 0; i < sURLVariables.length; i++) { - var sParameterName = sURLVariables[i].split('='); - if (sParameterName[0] == 'q') { - return decodeURIComponent(sParameterName[1].replace(/\+/g, '%20')); - } - } -} - -function joinUrl (base, path) { - if (path.substring(0, 1) === "/") { - // path starts with `/`. Thus it is absolute. - return path; - } - if (base.substring(base.length-1) === "/") { - // base ends with `/` - return base + path; - } - return base + "/" + path; -} - -function escapeHtml (value) { - return value.replace(/&/g, '&') - .replace(/"/g, '"') - .replace(//g, '>'); -} - -function formatResult (location, title, summary) { - return ''; -} - -function displayResults (results) { - var search_results = document.getElementById("mkdocs-search-results"); - while (search_results.firstChild) { - search_results.removeChild(search_results.firstChild); - } - if (results.length > 0){ - for (var i=0; i < results.length; i++){ - var result = results[i]; - var html = formatResult(result.location, result.title, result.summary); - search_results.insertAdjacentHTML('beforeend', html); - } - } else { - var noResultsText = search_results.getAttribute('data-no-results-text'); - if (!noResultsText) { - noResultsText = "No results found"; - } - search_results.insertAdjacentHTML('beforeend', '

' + noResultsText + '

'); - } -} - -function doSearch () { - var query = document.getElementById('mkdocs-search-query').value; - if (query.length > min_search_length) { - if (!window.Worker) { - displayResults(search(query)); - } else { - searchWorker.postMessage({query: query}); - } - } else { - // Clear results for short queries - displayResults([]); - } -} - -function initSearch () { - var search_input = document.getElementById('mkdocs-search-query'); - if (search_input) { - search_input.addEventListener("keyup", doSearch); - } - var term = getSearchTermFromLocation(); - if (term) { - search_input.value = term; - doSearch(); - } -} - -function onWorkerMessage (e) { - if (e.data.allowSearch) { - initSearch(); - } else if (e.data.results) { - var results = e.data.results; - displayResults(results); - } else if (e.data.config) { - min_search_length = e.data.config.min_search_length-1; - } -} - -if (!window.Worker) { - console.log('Web Worker API not supported'); - // load index in main thread - $.getScript(joinUrl(base_url, "search/worker.js")).done(function () { - console.log('Loaded worker'); - init(); - window.postMessage = function (msg) { - onWorkerMessage({data: msg}); - }; - }).fail(function (jqxhr, settings, exception) { - console.error('Could not load worker.js'); - }); -} else { - // Wrap search in a web worker - var searchWorker = new Worker(joinUrl(base_url, "search/worker.js")); - searchWorker.postMessage({init: true}); - searchWorker.onmessage = onWorkerMessage; -} diff --git a/site/search/search_index.json b/site/search/search_index.json deleted file mode 100644 index bbddadd9d..000000000 --- a/site/search/search_index.json +++ /dev/null @@ -1 +0,0 @@ -{"config":{"indexing":"full","lang":["en"],"min_search_length":3,"prebuild_index":false,"separator":"[\\s\\-]+"},"docs":[{"location":"","text":"The JEO WordPress geojournalism platform allows news organizations, bloggers and NGOs to publish news stories as layers of information on digital maps. With JEO, creating the interaction between data layers and contextual information is intuitive and interactive. JEO WordPress Plugin Features: MapBox maps react-map-gl library Custom tile layers Layer filtering options, allowing you to mix tile layer. Geocoding WordPress posts using OpenStreetMaps (Nominatim), suporting the post type Post . Customizable marker icons that can be associated to categories, custom taxonomies or posts directly. Map markers query integrated to posts query. Support WPML and Polylang multilanguages plugins Projects using JEO: Plenamata InfoAmazonia Pasifika EnviroNews Ekuatorial MekongEye Caci Read documentation or See in Wordpress store JEO WordPress Theme Read the documentation","title":"Home"},{"location":"#jeo-wordpress-plugin","text":"Features: MapBox maps react-map-gl library Custom tile layers Layer filtering options, allowing you to mix tile layer. Geocoding WordPress posts using OpenStreetMaps (Nominatim), suporting the post type Post . Customizable marker icons that can be associated to categories, custom taxonomies or posts directly. Map markers query integrated to posts query. Support WPML and Polylang multilanguages plugins Projects using JEO: Plenamata InfoAmazonia Pasifika EnviroNews Ekuatorial MekongEye Caci Read documentation or See in Wordpress store","title":"JEO WordPress Plugin"},{"location":"#jeo-wordpress-theme","text":"Read the documentation","title":"JEO WordPress Theme"},{"location":"concepts/","text":"Concepts Layer A layer is the basic building block for map interactions. Each layer can have its legends, attributions, etc. JEO supports four layer types out-of-the-box: Mapbox styles Mapbox tilesets Mapbox vector tiles (MVTs) TileLayers Map A map is composed of one or more layers and can be associated with related geolocated posts, that'll be rendered as markers on the map. A map post is used for reusable map interactions, that can be embedded in any post as a shortcode or Gutenberg block . One-time Map Instead of creating a reusable map any time you want to add a map interaction, you can create and configure an inline single-use map via the One-Time Map block for Gutenberg.","title":"Concepts"},{"location":"concepts/#concepts","text":"","title":"Concepts"},{"location":"concepts/#layer","text":"A layer is the basic building block for map interactions. Each layer can have its legends, attributions, etc. JEO supports four layer types out-of-the-box: Mapbox styles Mapbox tilesets Mapbox vector tiles (MVTs) TileLayers","title":"Layer"},{"location":"concepts/#map","text":"A map is composed of one or more layers and can be associated with related geolocated posts, that'll be rendered as markers on the map. A map post is used for reusable map interactions, that can be embedded in any post as a shortcode or Gutenberg block .","title":"Map"},{"location":"concepts/#one-time-map","text":"Instead of creating a reusable map any time you want to add a map interaction, you can create and configure an inline single-use map via the One-Time Map block for Gutenberg.","title":"One-time Map"},{"location":"discovery/","text":"Discovery Discovery page is an interactive map where the user can add layers, see stories based on published geolocated posts and share it's own customized map with others. Discovery page has a left sidebar with two tabs: STORIES and MAP LAYERS . You can hide this sidebar by clicking on the two left arrow icon, at the right of the sidebar. How to create a Discovery page To create a discovery page, you only need to create a normal Wordpress page and select Discovery in the Model settings. Any content that is added to that page will be ignored and the page will only display the Discovery feature. Stories Stories are the geolocated posts that are shown on the map. Clicking on SHOW FILTERS allows you to filter these stories by date and tags. Below these filters, a list with the result will be shown. Each result is also a link to the story itself. Also, hovering the mouse on a map marker will highlight the related story int the left sidebar. In the same way, hovering the mouse on View in map text will highlight the map marker realted to it. Map Layers In Map Layers tab you can search for maps and apply its layers to your interactive map. You can filter maps by name and select different layers from different maps. After selecting at least one layer from a map, you can click on Clear to remove all the selected layers from this map before applying the changes. It is also possible to add all layers from a map with one single click when selecting Apply next to a map result. At the top of the sidebar, you will be able to see all the selected layers, allowing you to remove some of them. These layers always have a drag and drop function, so that you can re-order them. After reviewing these layers, you need to click on Apply changes to show them on discovery map. Sharing At the right-top side of the map, there's a share button where you can share the content on Twitter , Facebook , E-mail or even generate a custom link for your map. Also, clicking on embed makes a popup appears with a HTML code to have your custom map embed into another website.","title":"Discovery"},{"location":"discovery/#discovery","text":"Discovery page is an interactive map where the user can add layers, see stories based on published geolocated posts and share it's own customized map with others. Discovery page has a left sidebar with two tabs: STORIES and MAP LAYERS . You can hide this sidebar by clicking on the two left arrow icon, at the right of the sidebar.","title":"Discovery"},{"location":"discovery/#how-to-create-a-discovery-page","text":"To create a discovery page, you only need to create a normal Wordpress page and select Discovery in the Model settings. Any content that is added to that page will be ignored and the page will only display the Discovery feature.","title":"How to create a Discovery page"},{"location":"discovery/#stories","text":"Stories are the geolocated posts that are shown on the map. Clicking on SHOW FILTERS allows you to filter these stories by date and tags. Below these filters, a list with the result will be shown. Each result is also a link to the story itself. Also, hovering the mouse on a map marker will highlight the related story int the left sidebar. In the same way, hovering the mouse on View in map text will highlight the map marker realted to it.","title":"Stories"},{"location":"discovery/#map-layers","text":"In Map Layers tab you can search for maps and apply its layers to your interactive map. You can filter maps by name and select different layers from different maps. After selecting at least one layer from a map, you can click on Clear to remove all the selected layers from this map before applying the changes. It is also possible to add all layers from a map with one single click when selecting Apply next to a map result. At the top of the sidebar, you will be able to see all the selected layers, allowing you to remove some of them. These layers always have a drag and drop function, so that you can re-order them. After reviewing these layers, you need to click on Apply changes to show them on discovery map.","title":"Map Layers"},{"location":"discovery/#sharing","text":"At the right-top side of the map, there's a share button where you can share the content on Twitter , Facebook , E-mail or even generate a custom link for your map. Also, clicking on embed makes a popup appears with a HTML code to have your custom map embed into another website.","title":"Sharing"},{"location":"geolocating-posts/","text":"Geolocating posts When editing a post, JEO will add an extra panel to the sidebar: Geolocation . Clicking on Geolocate this post , a popup will then be displayed. New points can be added to the post by clicking on Add new point button. You can search for a specific location in the search bar. It's also possible to choose the relevance of each point, which is useful when you have more than one point. Visualizing a map with geolocated posts Map with geolocated posts contain markers on the localization of each post. Clicking on them will open a popup with its title and publication date. Clicking on the title will take you to the post itself.","title":"Geolocating posts"},{"location":"geolocating-posts/#geolocating-posts","text":"When editing a post, JEO will add an extra panel to the sidebar: Geolocation . Clicking on Geolocate this post , a popup will then be displayed. New points can be added to the post by clicking on Add new point button. You can search for a specific location in the search bar. It's also possible to choose the relevance of each point, which is useful when you have more than one point.","title":"Geolocating posts"},{"location":"geolocating-posts/#visualizing-a-map-with-geolocated-posts","text":"Map with geolocated posts contain markers on the localization of each post. Clicking on them will open a popup with its title and publication date. Clicking on the title will take you to the post itself.","title":"Visualizing a map with geolocated posts"},{"location":"getting-started/","text":"Getting started Installing After activating the plugin, a new item will appear on the WordPress dashboard: a menu containing the Maps , Layers and Story Maps post types, and the JEO settings menus. Configuring the plugin Under JEO main menu, at Jeo Settings page, you can configure: the default latitude, longitude, and zoom for your maps; the Mapbox API settings that'll be used by the plugin; the geocoder that'll be used by the plugin -- currently only Nominatim is available. the style of some functionalities of the plugin, such as fonts ans colors.","title":"Getting started"},{"location":"getting-started/#getting-started","text":"","title":"Getting started"},{"location":"getting-started/#installing","text":"After activating the plugin, a new item will appear on the WordPress dashboard: a menu containing the Maps , Layers and Story Maps post types, and the JEO settings menus.","title":"Installing"},{"location":"getting-started/#configuring-the-plugin","text":"Under JEO main menu, at Jeo Settings page, you can configure: the default latitude, longitude, and zoom for your maps; the Mapbox API settings that'll be used by the plugin; the geocoder that'll be used by the plugin -- currently only Nominatim is available. the style of some functionalities of the plugin, such as fonts ans colors.","title":"Configuring the plugin"},{"location":"index-plugin/","text":"JEO Plugin The JEO plugin acts as a geojournalism platform that allows news organizations, bloggers and NGOs to publish news stories as layers of information on digital maps. With JEO, creating the interaction between data layers and contextual information is intuitive and interactive. You can post geotagged stories and create richly designed pages for each one of the featured stories. At the same time, by simply imputing the ids of layers hosted on MapBox, you can manage sophisticated maps without losing performance, add legends directly with HTML and set the map parameters. All direct at the WordPress dashboard. Features MapBox maps react-map-gl library Custom tile layers Layer filtering options, allowing you to mix tile layer. Geocoding WordPress posts using OpenStreetMaps (Nominatim), suporting the post type Post . Customizable marker icons that can be associated to categories, custom taxonomies or posts directly. Map markers query integrated to posts query. Support WPML and Polylang multilanguages plugins User tutorials Getting started Concepts Installing and configuring the plugin Layers Creating layers Maps Creating maps Discovery Using discovery Story Map Creating and using Story Map Posts Geolocating posts Using a map shortcode Using a map block Using an one-time map block Using a map embed Developer documentation Tutorials Adding new Layer Types Geographical Information of a post Writing a Geocoder Differences between the old and the present JEO Migration","title":"Overview"},{"location":"index-plugin/#jeo-plugin","text":"The JEO plugin acts as a geojournalism platform that allows news organizations, bloggers and NGOs to publish news stories as layers of information on digital maps. With JEO, creating the interaction between data layers and contextual information is intuitive and interactive. You can post geotagged stories and create richly designed pages for each one of the featured stories. At the same time, by simply imputing the ids of layers hosted on MapBox, you can manage sophisticated maps without losing performance, add legends directly with HTML and set the map parameters. All direct at the WordPress dashboard.","title":"JEO Plugin"},{"location":"index-plugin/#features","text":"MapBox maps react-map-gl library Custom tile layers Layer filtering options, allowing you to mix tile layer. Geocoding WordPress posts using OpenStreetMaps (Nominatim), suporting the post type Post . Customizable marker icons that can be associated to categories, custom taxonomies or posts directly. Map markers query integrated to posts query. Support WPML and Polylang multilanguages plugins","title":"Features"},{"location":"index-plugin/#user-tutorials","text":"","title":"User tutorials"},{"location":"index-plugin/#getting-started","text":"Concepts Installing and configuring the plugin","title":"Getting started"},{"location":"index-plugin/#layers","text":"Creating layers","title":"Layers"},{"location":"index-plugin/#maps","text":"Creating maps","title":"Maps"},{"location":"index-plugin/#discovery","text":"Using discovery","title":"Discovery"},{"location":"index-plugin/#story-map","text":"Creating and using Story Map","title":"Story Map"},{"location":"index-plugin/#posts","text":"Geolocating posts Using a map shortcode Using a map block Using an one-time map block Using a map embed","title":"Posts"},{"location":"index-plugin/#developer-documentation","text":"","title":"Developer documentation"},{"location":"index-plugin/#tutorials","text":"Adding new Layer Types Geographical Information of a post Writing a Geocoder","title":"Tutorials"},{"location":"index-plugin/#differences-between-the-old-and-the-present-jeo","text":"Migration","title":"Differences between the old and the present JEO"},{"location":"layer-post/","text":"Creating a layer One of the custom post types that JEO plugin provides is Layer . Is in the layer where you will be able to add legend and color to your map. A map may contain one or more layers. Entering the Layer post editor, you'll see a preview of the current layer (or a default layer if the current layer haven't been edited yet) and three sidebar panels: Settings , Attributions and Legend . Layer settings On the Layer settings panel, you can change the layer type. JEO supports four layer types out-of-the-box: Mapbox styles Mapbox tilesets Mapbox vector tiles (MVTs) TileLayers You can also inform an address, following the standard username/id , to compose your map style. If an access token is needed for this layer, you can put it into the Acess token input. There's also an Edit interactions button. Here, you can add popups to your layer when specific actions (clicking or hovering the mouse) are made (e.g.: Clicking on a building and displaying its height) Layer legend On the Layer legend panel, you can add legends to your layer (barscale, simple-color, icons or circles and colorize them. Attributions Here is where you can give layer credits, setting a link to download it or access more information about it. These attributions will be shown on the bottom of the map and inside the popup that shows up when the INFO button is clicked in a map.","title":"Layers"},{"location":"layer-post/#creating-a-layer","text":"One of the custom post types that JEO plugin provides is Layer . Is in the layer where you will be able to add legend and color to your map. A map may contain one or more layers. Entering the Layer post editor, you'll see a preview of the current layer (or a default layer if the current layer haven't been edited yet) and three sidebar panels: Settings , Attributions and Legend .","title":"Creating a layer"},{"location":"layer-post/#layer-settings","text":"On the Layer settings panel, you can change the layer type. JEO supports four layer types out-of-the-box: Mapbox styles Mapbox tilesets Mapbox vector tiles (MVTs) TileLayers You can also inform an address, following the standard username/id , to compose your map style. If an access token is needed for this layer, you can put it into the Acess token input. There's also an Edit interactions button. Here, you can add popups to your layer when specific actions (clicking or hovering the mouse) are made (e.g.: Clicking on a building and displaying its height)","title":"Layer settings"},{"location":"layer-post/#layer-legend","text":"On the Layer legend panel, you can add legends to your layer (barscale, simple-color, icons or circles and colorize them.","title":"Layer legend"},{"location":"layer-post/#attributions","text":"Here is where you can give layer credits, setting a link to download it or access more information about it. These attributions will be shown on the bottom of the map and inside the popup that shows up when the INFO button is clicked in a map.","title":"Attributions"},{"location":"map-block/","text":"Map block After creating maps , it is possible to display them apart or inside a block. This functionality makes possible to group maps, increasing your post organization. Displaying a map apart When creating a new post, note that is available a new block category: JEO . Selecting JEO Map block, you can search for any map you've created. With a map selected, you are able to choose an optional alignment ( Left , Right , Centre , Wide Width or Full Width ). Centre is the standard alignment. Left Right Wide Width Full Width Displaying grouped maps Besides the alignment option, there's also a group functionality available to arrange maps. Visualizing a map into a post If your map has more than one layer, you can swap them and select which one do you want to see, depending on the map layer settings. Check out more about map layers here","title":"Map block"},{"location":"map-block/#map-block","text":"After creating maps , it is possible to display them apart or inside a block. This functionality makes possible to group maps, increasing your post organization.","title":"Map block"},{"location":"map-block/#displaying-a-map-apart","text":"When creating a new post, note that is available a new block category: JEO . Selecting JEO Map block, you can search for any map you've created. With a map selected, you are able to choose an optional alignment ( Left , Right , Centre , Wide Width or Full Width ). Centre is the standard alignment.","title":"Displaying a map apart"},{"location":"map-block/#left","text":"","title":"Left"},{"location":"map-block/#right","text":"","title":"Right"},{"location":"map-block/#wide-width","text":"","title":"Wide Width"},{"location":"map-block/#full-width","text":"","title":"Full Width"},{"location":"map-block/#displaying-grouped-maps","text":"Besides the alignment option, there's also a group functionality available to arrange maps.","title":"Displaying grouped maps"},{"location":"map-block/#visualizing-a-map-into-a-post","text":"If your map has more than one layer, you can swap them and select which one do you want to see, depending on the map layer settings. Check out more about map layers here","title":"Visualizing a map into a post"},{"location":"map-embed/","text":"Embedding a map JEO plugin allows a map to be inserted into a post by pasting a link on the editor. This is what is called Embed map and it's very easy to be done. When editing a Map , one of the setting panels is Status & Visibility . There you can find the embed URL of that specific map. Copying this link and pasting on the post editor will result on an embed map. Warning : If your post displays a Not Found error, do the following steps: Go to Wordpress Dashboard > Settings > Permalinks Change the Common Settings (you may change to a different setting and then change back to the original) and click on Save Changes Now you should be able to see the embed maps with no problems.","title":"Map embed"},{"location":"map-embed/#embedding-a-map","text":"JEO plugin allows a map to be inserted into a post by pasting a link on the editor. This is what is called Embed map and it's very easy to be done. When editing a Map , one of the setting panels is Status & Visibility . There you can find the embed URL of that specific map. Copying this link and pasting on the post editor will result on an embed map. Warning : If your post displays a Not Found error, do the following steps: Go to Wordpress Dashboard > Settings > Permalinks Change the Common Settings (you may change to a different setting and then change back to the original) and click on Save Changes Now you should be able to see the embed maps with no problems.","title":"Embedding a map"},{"location":"map-post/","text":"Creating maps One of the custom post types that JEO plugin provides is Map . Entering the Map post editor, you'll see a preview of the current map (or a default map, if no layers are selected) and three sidebar panels: Map settings , Map layers , and Related posts . Map settings On the Map settings panel, you can change the initial center of the map setting a latitude and longitude. You can also change the default zoom settings or even disable it. When visualizing a map in a post, clicking with right button of the mouse and dragging it makes the map rotate. Also, scrolling the mouse wheel changes the map zoom. Both of these options can be enabled or disabled with the checkboxes. Map layers On the Map layers panel, you can visualize all the layers which are part of the map. Clicking on the Edit layers settings , a popup will open. There, you can add, remove and rearrange layers, define their types and whether its legends will be shown on the map. Also, you can selec which specific interactions you want to work with this especific map by clicking on Load interactions . A legend can be of one of these types: Fixed : the layer will always be shown in the map; Switchable : the layer visibility can be toggled on and off; Swappable : only one swappable layer can be visible at a time. Checkout how to create new layers here Related posts On the Related posts panel, you can set which geolocated posts will be used as markers on the post. You can filter the posts by categories, tags, dates interval or, for advanced uses, meta queries using WordPress post_meta .","title":"Maps"},{"location":"map-post/#creating-maps","text":"One of the custom post types that JEO plugin provides is Map . Entering the Map post editor, you'll see a preview of the current map (or a default map, if no layers are selected) and three sidebar panels: Map settings , Map layers , and Related posts .","title":"Creating maps"},{"location":"map-post/#map-settings","text":"On the Map settings panel, you can change the initial center of the map setting a latitude and longitude. You can also change the default zoom settings or even disable it. When visualizing a map in a post, clicking with right button of the mouse and dragging it makes the map rotate. Also, scrolling the mouse wheel changes the map zoom. Both of these options can be enabled or disabled with the checkboxes.","title":"Map settings"},{"location":"map-post/#map-layers","text":"On the Map layers panel, you can visualize all the layers which are part of the map. Clicking on the Edit layers settings , a popup will open. There, you can add, remove and rearrange layers, define their types and whether its legends will be shown on the map. Also, you can selec which specific interactions you want to work with this especific map by clicking on Load interactions . A legend can be of one of these types: Fixed : the layer will always be shown in the map; Switchable : the layer visibility can be toggled on and off; Swappable : only one swappable layer can be visible at a time. Checkout how to create new layers here","title":"Map layers"},{"location":"map-post/#related-posts","text":"On the Related posts panel, you can set which geolocated posts will be used as markers on the post. You can filter the posts by categories, tags, dates interval or, for advanced uses, meta queries using WordPress post_meta .","title":"Related posts"},{"location":"map-shortcode/","text":"Map shortcode A map can be inserted on any page or post using the jeo-map shortcode. The shortcode accepts three attributes: map_id (required): The ID of the map you want to insert; width (optional): The width of the map. It'll default to 600px or whatever the active theme defines for the 'div.jeomap' CSS class; height (optional): The height of the map. Will default to 600px or whatever the active theme defines for the 'div.jeomap' CSS class. Examples: [jeo-map map_id=99] You have to inform at least the ID of the Map you want to insert. By default, it will be inserted with a size of 600\u00d7600px (or whatever the active theme defines), but you can also change it: [jeo-map map_id=99 width=\"800px\" height=\"800px\"]","title":"Map shortcode"},{"location":"map-shortcode/#map-shortcode","text":"A map can be inserted on any page or post using the jeo-map shortcode. The shortcode accepts three attributes: map_id (required): The ID of the map you want to insert; width (optional): The width of the map. It'll default to 600px or whatever the active theme defines for the 'div.jeomap' CSS class; height (optional): The height of the map. Will default to 600px or whatever the active theme defines for the 'div.jeomap' CSS class. Examples: [jeo-map map_id=99] You have to inform at least the ID of the Map you want to insert. By default, it will be inserted with a size of 600\u00d7600px (or whatever the active theme defines), but you can also change it: [jeo-map map_id=99 width=\"800px\" height=\"800px\"]","title":"Map shortcode"},{"location":"one-time-map-block/","text":"One-time map block It is possible to use maps into posts without having to create a new map or using an existing one. For this, JEO plugin makes availabe another type of block: One-time map . When creating a new post, note that is available a new block category: JEO . Selecting JEO One-time Map block, a standard map preview will appear. This type of map allow all the same customizations of a normal JEO Map , such as: modifying coordinates, zoom settings, related posts, alignment options and layers settings . To know more precisely how to customize all these options, check out JEO Map page.","title":"One-time map block"},{"location":"one-time-map-block/#one-time-map-block","text":"It is possible to use maps into posts without having to create a new map or using an existing one. For this, JEO plugin makes availabe another type of block: One-time map . When creating a new post, note that is available a new block category: JEO . Selecting JEO One-time Map block, a standard map preview will appear. This type of map allow all the same customizations of a normal JEO Map , such as: modifying coordinates, zoom settings, related posts, alignment options and layers settings . To know more precisely how to customize all these options, check out JEO Map page.","title":"One-time map block"},{"location":"story-map/","text":"Story Map Story Map is a way of making an interactive storytelling with customized maps and layers. You are able to select specific locations and related layers and texts to these locations. This will generate a map that, when the page is scrolled, will change from one location/layer/text to another. This group of location, layer and text are called Slide . Creating a Story Map Story Map is a custom post type in JEO Plugin. When creating one, you'll be asked for a map. All of the layers that you are going to use in your Story Map come from this previous selected map. Also, Story Map has a Navigate function , which we will talk about later. This Navigate function also uses this selected map to work. Story settings Here you can set a description for your story map which will be shown at the beginning of the post, with the title and publish date. You can also define whether or not your story map will have an introduction and a Navigate map button . All the text fields has a buit-in Rich Text Editor. This means that you can put bold text, links, lists, etc inside your text field. Navigate map button appears at the end of a Story Map and allows the user to navigate through the map. This map will follow all the settings of the previous selected. Slides settings A Story Map slide contains a title, content, location and selected layers (The order of the layers will always follow the map layers order that were defined when creating this map). You can remove a slide by clicking on Remove button, preview the map in a slide clicking on Preview button, and set the location to the current slide clicking on Lock current spot button. You can see what's the current slide at the top-right side of the block. You can pick a location to a slide by dragging the map with left mouse button and changing its peaching and bearing by dragging the map with right mouse button. Slides can be rearranged by clicking on = icon and dragging it to put the slide in another position. Looking at a Story Map This is how a Story Map look in a post: Clicking on START will take the user to the first slide. Clicking on skip intro will take the user to the map itself so they can navigate through it (this button is only available if the navigate setting is enabled in story map settings). These buttons, the title, description and publish date only appears if Shows story map introduction is enabled. If it isn't, the story map starts on the first slide. While the page is scrolled, the current slide changes to the next one and the map do a transition to the next location and change its layers. At the end of the storytelling, if the correct setting is enabled, a NAVIGATE THE MAP button will shows up. Clicking on it will take the user to the map itself and allowing they to interact with it.","title":"Story map"},{"location":"story-map/#story-map","text":"Story Map is a way of making an interactive storytelling with customized maps and layers. You are able to select specific locations and related layers and texts to these locations. This will generate a map that, when the page is scrolled, will change from one location/layer/text to another. This group of location, layer and text are called Slide .","title":"Story Map"},{"location":"story-map/#creating-a-story-map","text":"Story Map is a custom post type in JEO Plugin. When creating one, you'll be asked for a map. All of the layers that you are going to use in your Story Map come from this previous selected map. Also, Story Map has a Navigate function , which we will talk about later. This Navigate function also uses this selected map to work.","title":"Creating a Story Map"},{"location":"story-map/#story-settings","text":"Here you can set a description for your story map which will be shown at the beginning of the post, with the title and publish date. You can also define whether or not your story map will have an introduction and a Navigate map button . All the text fields has a buit-in Rich Text Editor. This means that you can put bold text, links, lists, etc inside your text field. Navigate map button appears at the end of a Story Map and allows the user to navigate through the map. This map will follow all the settings of the previous selected.","title":"Story settings"},{"location":"story-map/#slides-settings","text":"A Story Map slide contains a title, content, location and selected layers (The order of the layers will always follow the map layers order that were defined when creating this map). You can remove a slide by clicking on Remove button, preview the map in a slide clicking on Preview button, and set the location to the current slide clicking on Lock current spot button. You can see what's the current slide at the top-right side of the block. You can pick a location to a slide by dragging the map with left mouse button and changing its peaching and bearing by dragging the map with right mouse button. Slides can be rearranged by clicking on = icon and dragging it to put the slide in another position.","title":"Slides settings"},{"location":"story-map/#looking-at-a-story-map","text":"This is how a Story Map look in a post: Clicking on START will take the user to the first slide. Clicking on skip intro will take the user to the map itself so they can navigate through it (this button is only available if the navigate setting is enabled in story map settings). These buttons, the title, description and publish date only appears if Shows story map introduction is enabled. If it isn't, the story map starts on the first slide. While the page is scrolled, the current slide changes to the next one and the map do a transition to the next location and change its layers. At the end of the storytelling, if the correct setting is enabled, a NAVIGATE THE MAP button will shows up. Clicking on it will take the user to the map itself and allowing they to interact with it.","title":"Looking at a Story Map"},{"location":"dev/dependency-upgrade-report/","text":"Dependency Upgrade Review Generated on 2026-03-18. Baseline Platform baseline: WordPress 6.6+, PHP 8.0+, Node 24. Source files: .nvmrc , README.md , src/readme.txt , .github/workflows/deploy-wordpress-org.yml , .github/workflows/node-frontend.yml , .github/workflows/php-compat.yml , .github/workflows/wordpress-smoke.yml , scripts/check-node-version.mjs . Current scope: 22 direct npm runtime packages, 8 direct npm development packages, 2 direct Composer packages and 3 transitive Composer packages. Tooling notes Automated approval gates are the Node Frontend Checks, PHP Compatibility and WordPress Smoke Tests GitHub Actions workflows. A minimum frontend unit-test layer is available via npm run test:unit and enforced by the Node workflow from Phase 3 onward. The report now captures a frontend runtime snapshot with the resolved React, ReactDOM and Gutenberg editor package versions plus their published peer ranges. The installed tree now resolves a local react-autosize-textarea override so the stale upstream peer metadata no longer raises npm peer warnings against React 18.3.1. Previous stabilization track: the dependency-major batches are complete apart from the external monitoring paths for react-leaflet 5 and PHPCS 4, both still waiting on upstream support. Current plugin sanity track: Node 24 is now the supported frontend runtime for local development, CI and release validation. Current plugin sanity track: the implemented work now covers audit cleanup, incremental frontend loading, bundle budgets, runtime hardening, pipeline alignment and the editor, Discovery and geocoding follow-ups validated in this branch. Current plugin sanity track: the override-governance batch is complete, so every local npm override now has a documented reason, review trigger and removal criterion. Current plugin sanity track: the remaining open items are external monitoring only: react-leaflet 5 against the current React line, PHPCS 4 against upstream WPCS support, and third-party wp-cli PHP 8.5 deprecation noise. Local validation PHP: PHP 8.5.4 (cli) (built: Mar 10 2026 23:15:23) (NTS). Composer: Composer version 2.9.5 2026-01-29 11:40:53. Environment caveats Local Composer/PHP validation is available in this shell environment (Composer version 2.9.5 2026-01-29 11:40:53; PHP 8.5.4 (cli) (built: Mar 10 2026 23:15:23) (NTS)). The npm and Packagist lookups require network access when the report is regenerated. Frontend runtime snapshot React: 18.3.1 . ReactDOM: 18.3.1 . Local npm overrides: react-autosize-textarea -> file:npm-overrides/react-autosize-textarea , js-yaml -> ^4.1.1 , webpack-dev-server -> ^5.2.3 , @typescript-eslint/typescript-estree > minimatch -> ^9.0.9 , markdownlint-cli > minimatch -> ^3.1.5 , copy-webpack-plugin > serialize-javascript -> ^7.0.4 . Package Declared Resolved React peer ReactDOM peer @wordpress/edit-post ^8.41.0 8.41.0 ^18.0.0 ^18.0.0 @wordpress/editor ^14.41.0 14.41.0 ^18.0.0 ^18.0.0 @wordpress/plugins ^7.41.0 7.41.0 ^18.0.0 ^18.0.0 @wordpress/block-editor transitive 15.14.0 ^18.0.0 ^18.0.0 Local npm override governance Selector Spec Resolved Type Reason Review when Removal criteria Introduced react-autosize-textarea file:npm-overrides/react-autosize-textarea Unavailable Local package override The Gutenberg editor stack still resolves react-autosize-textarea with stale peer metadata that does not accept the React 18 line used by this project. Any refresh of @wordpress/scripts or the @wordpress/* editor packages. / Any React or ReactDOM line change. Remove when the installed Gutenberg tree resolves React-compatible peer metadata without the local file override and npm ls stays clean. Batch 4 js-yaml ^4.1.1 4.1.1 Security pin Pins js-yaml to a patched line so the npm audit baseline stays clean even when upstream tooling lags behind. Any lockfile refresh. / Any @wordpress/scripts or webpack toolchain refresh. Remove when package-lock naturally resolves js-yaml >= 4.1.1 without the override and npm audit remains green. Sanity Batch 1 webpack-dev-server ^5.2.3 5.2.3 Security pin Keeps the dev-server subtree on a patched line required by the current audit baseline. Any @wordpress/scripts or webpack toolchain refresh. / Any lockfile refresh. Remove when the resolved toolchain already pulls webpack-dev-server >= 5.2.3 without the override and audits stay green. Sanity Batch 1 @typescript-eslint/typescript-estree > minimatch ^9.0.9 9.0.9, 3.1.5 Nested security pin Pins minimatch inside the TypeScript ESLint parser subtree to a patched line without replacing the parent package. Any eslint or @typescript-eslint refresh. / Any lockfile refresh. Remove when the subtree resolves a patched minimatch line on its own and npm audit remains green. Sanity Batch 1 markdownlint-cli > minimatch ^3.1.5 9.0.9, 3.1.5 Nested security pin Pins minimatch in the markdownlint-cli subtree so markdown tooling does not reintroduce the old advisory. Any markdownlint-cli refresh. / Any lockfile refresh. Remove when markdownlint-cli resolves a patched minimatch line without the nested override and npm audit remains green. Sanity Batch 1 copy-webpack-plugin > serialize-javascript ^7.0.4 7.0.4 Nested security pin Pins serialize-javascript in the copy-webpack-plugin subtree to the secure line used by the current build baseline. Any webpack or copy-webpack-plugin refresh. / Any lockfile refresh. Remove when copy-webpack-plugin resolves serialize-javascript >= 7.0.4 without help and npm audit remains green. Sanity Batch 1 react-autosize-textarea : The local package mirrors upstream runtime files and only broadens peer metadata. Execution plan Phase 0 - Baseline and tracking scaffolding (Completed): Report generator, dependency reports and phased smoke-test guidance. Packages: None. Automated checks: node --check scripts/dependency-review.mjs , npm run deps:report . Manual review: None. Approval notes: No dependency versions change in this phase. Phase 1 - Frontend gates for approval (Completed): Create the Node workflow and treat PHP compatibility plus WordPress smoke checks as mandatory future gates. Packages: None. Automated checks: GitHub Actions Node frontend workflow , GitHub Actions PHP Compatibility workflow , GitHub Actions WordPress Smoke Tests workflow . Manual review: Confirm the Node frontend, PHP Compatibility and WordPress Smoke Tests workflows trigger on push and pull_request events. Approval notes: No dependency versions or lockfiles change in this phase. Phase 2 - Build tooling major (Completed): Upgrade the WordPress build toolchain major and keep the custom webpack config working. Packages: @wordpress/scripts . Automated checks: npm run build , GitHub Actions Node frontend workflow , GitHub Actions PHP Compatibility workflow , GitHub Actions WordPress Smoke Tests workflow . Manual review: No dedicated manual review unless asset or wp-admin regressions become visible. Approval notes: The webpack.config.js override continued to work without code changes on @wordpress/scripts 31.x., The local WordPress smoke attempt is environment-dependent and should still be confirmed by the GitHub workflow before merge. Phase 3 - Minimum JS test layer (Completed): Add unit tests for the contracts that protect the remaining majors. Packages: None. Automated checks: npm run build , npm run test:unit , GitHub Actions Node frontend workflow , GitHub Actions PHP Compatibility workflow , GitHub Actions WordPress Smoke Tests workflow . Manual review: Confirm the new test coverage protects date selector propagation, sidebar form normalization and Eta template compilation. Approval notes: No dependency versions or lockfiles change in this phase., The Node workflow now treats npm run test:unit as a mandatory step., The local WordPress smoke test passed after applying the separate PR #501 CLI-hook fix locally for validation only; src/includes/class-jeo.php remains outside this phase commit., scripts/wordpress-smoke.sh now honors WP_CLI_PHP so Homebrew wp-cli setups can run the local smoke test on PHP 8.4 without the PHP 8.5 deprecation noise. Phase 4 - Date picker major (Completed): Upgrade the date picker used in related-post filters. Packages: react-datepicker . Automated checks: npm run build , npm run test:unit , GitHub Actions Node frontend workflow , GitHub Actions PHP Compatibility workflow , GitHub Actions WordPress Smoke Tests workflow . Manual review: Select start and end dates in the related-posts sidebar., Save the post, reload the editor and verify persisted values., Clear dates and confirm empty values stay cleared after saving. Approval notes: react-datepicker moved from 7.6.0 to 9.1.0 without requiring source changes in the related-posts selector., The automated suite stayed green, but npm run build now reports one non-blocking webpack warning from react-datepicker's optional date-fns-tz require path. Phase 5 - Template runtime major (Completed): Upgrade Eta without changing the popup template contract. Packages: eta . Automated checks: npm run build , npm run test:unit , GitHub Actions Node frontend workflow , GitHub Actions PHP Compatibility workflow , GitHub Actions WordPress Smoke Tests workflow . Manual review: Open a map popup and verify the title, date and featured image rendering., Open the map Info panel and verify template-driven content still renders., Verify any locally customized popup template still compiles. Approval notes: eta moved from 3.5.0 to 4.5.1 and now uses the browser-safe eta/core entry inside the frontend helper., A local jest.config.js now tells the unit runner to transform eta/dist/core.js so the existing template-compiler tests stay representative., Manual review confirmed the Info panel and popup template rendering stayed functional, and the later same-point plus null-geocode popup fixes are now closed in Batch 1. Phase 6 - Leaflet sidebar major (Monitoring): Upgrade the post geocoding sidebar map and keep editing interactions intact once the upstream React line is compatible. Packages: react-leaflet . Automated checks: npm view react-leaflet version peerDependencies --json , npm install react-leaflet@^5.0.0 --dry-run . Manual review: No dedicated product review while the upstream React 19 path stays unavailable; keep covering add, edit, drag, cancel, save and reopen flows under the supported react-leaflet 4 line in the core smoke checklist. Approval notes: react-leaflet 5.0.0 is the current published major, but it now declares peerDependencies on react and react-dom ^19.0.0., This repository currently resolves react 18.3.1 and react-dom 18.3.1 through the existing Gutenberg stack, so npm rejects the major upgrade before runtime checks can begin., Treat this path as external upstream monitoring, not as an immediate repository-owned bugfix batch. Phase 7 - CKEditor React major evaluation (Completed): Evaluate the React wrapper major only if it remains compatible with the installed CKEditor core line. Packages: @ckeditor/ckeditor5-react . Automated checks: npm run build , npm run test:unit , GitHub Actions Node frontend workflow , GitHub Actions PHP Compatibility workflow , GitHub Actions WordPress Smoke Tests workflow . Manual review: Load the storymap editor and confirm the toolbar initializes., Verify text selection formatting still applies and the selection is retained., Upload an image, save the storymap and reopen it. Approval notes: @ckeditor/ckeditor5-react moved from 9.5.0 to 11.0.1 without requiring source changes in the storymap editor., The published wrapper's peer requirements remain compatible with the installed ckeditor5 47.6.1 line and with React 18.3.1., Manual review confirmed editor loading, formatting and save/reopen flows stayed functional, and the later slide-order fixes are now closed in Batch 1 and Batch 3. Phase 8 - PHPCS major evaluation (Monitoring): Evaluate PHPCS 4 only after upstream WPCS support makes the major viable. Packages: squizlabs/php_codesniffer . Automated checks: composer validate --no-check-publish , composer show squizlabs/php_codesniffer --locked , composer show wp-coding-standards/wpcs --locked , vendor/bin/phpcs --standard=phpcs.xml.dist , php scripts/check-php-compat.php . Manual review: None. Approval notes: Packagist confirms that squizlabs/php_codesniffer 4.0.1 exists, but the installed WPCS 3.3.0 line still requires squizlabs/php_codesniffer ^3.13.4., The PHPCS 3.x baseline is now clean in this repository, so the remaining PHPCS 4 path is external upstream monitoring rather than a pending product fix. Batch 0 - Local prerequisites and report reset (Completed): Absorb the validated local prerequisites, remove the invalid cli_init hook dependency from local smoke, and reopen the dependency report for the stabilization track. Packages: None. Automated checks: composer validate --no-check-publish , php scripts/check-php-compat.php , WP_CLI_PHP=/opt/homebrew/opt/php@8.4/bin/php WP_DIR=/tmp/jeo-plugin-wordpress-smoke bash scripts/wordpress-smoke.sh , node --check scripts/dependency-review.mjs , node scripts/dependency-review.mjs --write . Manual review: None. Approval notes: No dependency versions or lockfiles change in this batch., Local WordPress smoke no longer depends on a validation-only patch outside the working tree., A clean worktree still needs the ignored src/js/build assets materialized locally before the smoke can run without asset-include warnings. Batch 1 - Baseline fixes and warning cleanup (Completed): Fix the published-map baseline bugs, repair reorder flows, remove the react-datepicker webpack warning and add focused regression tests. Packages: None. Automated checks: npm ci , npm run build , npm run test:unit , php scripts/check-php-compat.php , WP_CLI_PHP=/opt/homebrew/opt/php@8.4/bin/php WP_DIR=/tmp/jeo-plugin-wordpress-smoke bash scripts/wordpress-smoke.sh . Manual review: Verify published-map popups, same-point post navigation and the absence of the null-geocode crash., Verify storymap slide ordering, save and reopen in the editor. Approval notes: No dependency versions should change in this batch., The react-datepicker version stays on 9.1.0; only its optional date-fns-tz webpack warning is removed., Published-map review confirmed the null-geocode crash is gone and same-point posts now use a navigable popup instead of depending on spiderfy timing., Storymap review confirmed the explicit slide ordering controls, accordion behavior and save or reopen flow stay consistent., npm ci still reports the tracked react-autosize-textarea peer warning plus the deprecated packages scheduled for Batches 2 through 4, but Batch 1 no longer adds any new npm or webpack warnings of its own. Batch 2 - Autosuggest, date-range and form replacements (Completed): Replace the deprecated input and form helpers while preserving the current geocoding, Discovery date filtering and formData contracts. Packages: react-autosuggest , react-bootstrap-daterangepicker , react-jsonschema-form , bootstrap-daterangepicker . Automated checks: npm ci , npm run build , npm run test:unit , php scripts/check-php-compat.php , WP_CLI_PHP=/opt/homebrew/opt/php@8.4/bin/php WP_DIR=/tmp/jeo-plugin-wordpress-smoke bash scripts/wordpress-smoke.sh . Manual review: Verify geocoding autosuggest and post or map autosuggest flows., Verify Discovery date apply and clear flows., Verify layer and attribution forms render, normalize formData and save correctly. Approval notes: The saved query and formData shapes stayed unchanged via legacy-compatible helpers for async combobox selection, Discovery date ranges and merged schema formData updates., npm ci no longer emits the deprecation warnings that previously came from react-autosuggest, react-bootstrap-daterangepicker or react-jsonschema-form., The remaining npm install noise is now limited to the tracked react-beautiful-dnd deprecation, the react-autosize-textarea peer warning and broader transitive ecosystem deprecations that Batch 2 did not introduce. Batch 3 - Drag-and-drop consolidation (Completed): Remove react-beautiful-dnd in favor of the existing react-movable stack and close the remaining reorder flows that were actually part of the product. Packages: react-beautiful-dnd . Automated checks: npm ci , npm run build , npm run test:unit , php scripts/check-php-compat.php , WP_CLI_PHP=/opt/homebrew/opt/php@8.4/bin/php WP_DIR=/tmp/jeo-plugin-wordpress-smoke bash scripts/wordpress-smoke.sh . Manual review: Verify map-layer ordering, storymap slide ordering and any nested layer ordering still exposed in the editor. Approval notes: The saved layer and slide structures stayed unchanged while the supported reorder flows moved onto react-movable., npm ci no longer emits the react-beautiful-dnd deprecation warning because the package has been removed from package.json and package-lock.json. Batch 4 - Gutenberg and React alignment (Completed): Revalidate the latest published Gutenberg editor packages, clear the remaining React peer warning with a minimum local override, and document the still-blocked react-leaflet 5 path. Packages: @wordpress/edit-post , @wordpress/editor , @wordpress/plugins , react-autosize-textarea . Automated checks: npm ci , npm run build , npm run test:unit , npm ls react-autosize-textarea react react-dom , php scripts/check-php-compat.php , WP_CLI_PHP=/opt/homebrew/opt/php@8.4/bin/php WP_DIR=/tmp/jeo-plugin-wordpress-smoke bash scripts/wordpress-smoke.sh . Manual review: Verify the map editor, layer editor, storymap editor and regular post editor load, save and stay console-clean. Approval notes: No newer published @wordpress/edit-post, @wordpress/editor or @wordpress/plugins versions were available beyond the already installed line, and their published peerDependencies still target React 18., Batch 4 therefore uses a local react-autosize-textarea override to modernize peer metadata without changing the runtime implementation or adding react or react-dom directly. Batch 5 - react-leaflet major (Monitoring): Upgrade react-leaflet to 5.x after a React-19-compatible Gutenberg stack exists and validate the entire geocoding sidebar map flow. Packages: react-leaflet . Automated checks: npm ci , npm run build , npm run test:unit , php scripts/check-php-compat.php , WP_CLI_PHP=/opt/homebrew/opt/php@8.4/bin/php WP_DIR=/tmp/jeo-plugin-wordpress-smoke bash scripts/wordpress-smoke.sh . Manual review: No dedicated product review while the upstream React 19 path stays unavailable; keep covering add, edit, drag, cancel, save and reopen flows under the supported react-leaflet 4 line in the core smoke checklist. Approval notes: The latest published Gutenberg editor packages still peer on react and react-dom ^18.0.0, while react-leaflet 5.0.0 requires react and react-dom ^19.0.0., Do not start this batch until upstream publishes a supported React-19-compatible editor stack or the repository adopts another supported route., Treat this path as external upstream monitoring, not as an immediate repository-owned bugfix batch. Batch 6 - PHP 8.5 compatibility (Completed): Extend the local and CI PHP compatibility coverage to PHP 8.5 and remove repository-owned warnings or deprecations under that runtime. Packages: None. Automated checks: composer validate --no-check-publish , php scripts/check-php-compat.php with PHP 8.0 through 8.5 , WP_CLI_PHP=/opt/homebrew/opt/php@8.4/bin/php WP_DIR=/tmp/jeo-plugin-wordpress-smoke bash scripts/wordpress-smoke.sh , WP_CLI_PHP=/opt/homebrew/bin/php WP_DIR=/tmp/jeo-plugin-wordpress-smoke-85 bash scripts/wordpress-smoke.sh . Manual review: Repeat the runtime smoke while watching for repository-owned PHP 8.5 warnings or deprecations. Approval notes: Static compatibility checks are green on PHP 8.0 through 8.5, and local WordPress smoke passes on both PHP 8.4 and PHP 8.5., WordPress core 6.9 treats PHP 8.5 as beta support, while stable wp-cli support for PHP 8.5 is still planned upstream., Keep PHP 8.5 experimental in CI for now and track the remaining wp-cli deprecation noise as third-party monitoring only because repository-owned checks are clean. Batch 7 - PHP lint stack cleanup (Completed): Clean the PHPCS 3.x baseline across WPCS, PHPCSExtra and PHPCSUtils before re-evaluating PHPCS 4. Packages: squizlabs/php_codesniffer , wp-coding-standards/wpcs . Automated checks: composer validate --no-check-publish , php scripts/check-php-compat.php , vendor/bin/phpcs --standard=phpcs.xml.dist . Manual review: None. Approval notes: The PHPCS 3.x baseline is now green after autofixes plus manual cleanup across runtime PHP, templates and tooling scripts., The runtime-touching PHP cleanup also passed local WordPress smoke, so the lint cleanup did not introduce regressions in embeds, maps or story maps., Official WPCS metadata and the 3.3.0 release notes still require PHP_CodeSniffer 3.13.4, while older WPCS 2.3.0 notes only mention preliminary CI testing against the PHPCS 4 development branch., PHPCS 4 now remains an external upstream monitoring path until the installed WPCS line officially supports that major. Sanity Batch 0 - Runtime policy and reproducibility (Completed): Promote Node 24 to the project runtime, enforce it through an environment preflight, and align the deploy pipeline with deterministic installs. Packages: None. Automated checks: npm run check:env , npm ci , npm run build , npm run build:report . Manual review: None. Approval notes: This batch is mostly process and tooling work; the main validation is that the documented Node 24 path and the preflight behave correctly., Node 24 is now the single supported frontend runtime in local development and CI., The final full test rerun reconfirmed that the Node 24 preflight, deterministic install path and bundle-report flow are still the active project baseline. Sanity Batch 1 - npm health and security (Completed): Refresh the JavaScript dependency tree under Node 24, eliminate the known js-yaml vulnerability, and make npm and Composer audits part of the maintained process. Packages: js-yaml . Automated checks: npm ci , npm audit --package-lock-only , npm audit --omit=dev , composer audit --locked . Manual review: None. Approval notes: Audit coverage is now part of the expected maintenance flow., Remaining npm install notices are transitive deprecation warnings from the upstream Gutenberg or wp-scripts toolchain, not audit failures in this repository., The final full rerun kept npm audit and composer audit clean, so this batch is now fully closed. Sanity Batch 2 - Incremental loading for layers, maps and taxonomies (Completed): Replace unbounded frontend collection loads with chunked hydration by selected IDs, paginated layer search, async taxonomy queries and lazy Discovery map hydration. Packages: None. Automated checks: npm ci , npm run test:unit , npm run build , php scripts/check-php-compat.php , WP_CLI_PHP=/opt/homebrew/opt/php@8.4/bin/php WP_DIR=/tmp/jeo-plugin-wordpress-smoke bash scripts/wordpress-smoke.sh . Manual review: Verify that maps, onetime maps, storymaps and sidebars still show every already-selected layer., Verify paginated layer search in the map editor modal, including Load more behavior., Verify Discovery map loading, Load more maps and selected-layer hydration from shared or URL-driven state., Verify async category and tag selection in the related-posts editor flow. Approval notes: Published maps and embeds keep completeness by exact selected IDs; pagination is used only for large libraries and list views., This batch targets the memory-risk patterns that previously depended on per_page -1, nopaging true or oversized taxonomy fetches., Manual validation closed the batch after fixing the map-editor reload regression and the Discovery lazy-load crashes; the separate Discovery marker and View in map follow-up tracked at the time has now been closed in the current branch. Sanity Batch 3 - Bundle budgets and build stability (Completed): Add explicit bundle-size reporting and budgets, and keep the frontend build on a predictable asset graph that preserves editor stability. Packages: None. Automated checks: npm run build , npm run build:report , npm run test:unit . Manual review: Verify the map editor, storymap editor, Discovery and published maps still load after the chunking change. Approval notes: This batch changes build structure and measurement, not the public asset contract exposed by the plugin., The budget report is now part of the approval evidence for future frontend-heavy changes., Manual validation closed the batch after keeping the explicit budget tooling but settling on the more predictable asset graph that avoided the intermittent reload and template issues seen in existing map editors. Sanity Batch 4 - Logs, warnings and editor runtime cleanup (Completed): Remove repository-owned console noise, clean dead debug traces, document the remaining experimental Gutenberg render hook, and fix editor runtime issues uncovered during console-clean validation. Packages: None. Automated checks: npm run test:unit , npm run build , php scripts/check-php-compat.php . Manual review: Verify the editor flows touched in this track stay console-clean during normal use. Approval notes: The remaining install-time npm deprecation notices come from upstream tooling packages, not from repository-owned source., The current @wordpress/components line still requires __experimentalRenderItem for the customized async combobox rendering used by this project., Manual validation closed the batch after fixing the Onetime Map block's saved-attribute round-trip and guarding preview rendering when a selected layer record is temporarily unresolved. Sanity Batch 5 - Runtime hardening and request hygiene (Completed): Tighten geocoding and related request flows with nonce checks, capability checks, sanitized inputs, safer remote requests and cleaner error handling. Packages: None. Automated checks: php scripts/check-php-compat.php , vendor/bin/phpcs --standard=phpcs.xml.dist , WP_CLI_PHP=/opt/homebrew/opt/php@8.4/bin/php WP_DIR=/tmp/jeo-plugin-wordpress-smoke bash scripts/wordpress-smoke.sh . Manual review: Verify geocoding autosuggest, reverse geocoding and save or reopen flows still work normally., Verify public maps, embeds and storymaps touched by the hardening changes still bootstrap correctly. Approval notes: This batch is about safety and failure handling, not feature changes., The old REST tag collection broadening was removed so large term collections no longer bypass normal limits., The draggable-marker reliability issue that was noted after this batch has now been fixed in the current branch. Sanity Batch 6 - Pipeline and quality gate alignment (Completed): Align the release and CI pipelines with the same deterministic install, audit, build, bundle-budget, PHP compatibility, PHPCS and smoke-test gates used locally. Packages: None. Automated checks: npm ci , npm run test:unit , npm run build , npm run build:report , npm audit --package-lock-only , composer validate --no-check-publish , composer audit --locked , php scripts/check-php-compat.php , vendor/bin/phpcs --standard=phpcs.xml.dist , WP_CLI_PHP=/opt/homebrew/opt/php@8.4/bin/php WP_DIR=/tmp/jeo-plugin-wordpress-smoke bash scripts/wordpress-smoke.sh . Manual review: None. Approval notes: This batch closes the process loop for the new sanity track., PHP 8.5 stays monitored in the process documentation but is not promoted to a blocking smoke gate while wp-cli still lacks stable upstream support for that runtime., A full rerun of the documented gates confirmed that the sanity-track changes did not reintroduce repository-owned warnings, vulnerabilities or failing quality checks. Sanity Batch 7 - Override governance and maintenance (Completed): Inventory every npm override, record its reason and removal criteria, and re-review the set whenever @wordpress/scripts, the Gutenberg dependency tree or another major frontend refresh changes the upstream graph. Packages: None. Automated checks: node scripts/dependency-review.mjs --write , npm audit --package-lock-only , npm ls react-autosize-textarea react react-dom . Manual review: Confirm that each override still solves a current problem, that its rationale is documented, and that anything already absorbed upstream is removed. Approval notes: This batch is governance-oriented and should not change runtime behavior unless an obsolete override is removed or replaced., Use it after significant @wordpress/scripts or Gutenberg refreshes so override drift does not become silent technical debt., The dependency-review script now validates that every active package.json override has governance metadata and that no stale governance entries survive after an override is removed. Execution log 2026-03-16 - Phase 0 - Baseline and tracking scaffolding (Completed): Added phase planning, package-level execution tracking and the phased approval checklist without changing dependency versions. Automated checks: node --check scripts/dependency-review.mjs: Passed, npm run deps:report: Passed (Regenerated the JSON and Markdown reports against current npm and Packagist metadata.). Manual review: None. Documents updated: docs/dev/dependency-upgrade-report.json , docs/dev/dependency-upgrade-report.md , docs/dev/dependency-upgrade-smoke-tests.md . 2026-03-16 - Phase 1 - Frontend gates for approval (Completed): Added a Node frontend GitHub Actions workflow and promoted the PHP compatibility plus WordPress smoke workflows to the mandatory gate set for future dependency phases. Automated checks: npm ci: Passed (Installed the existing lockfile successfully. The run repeated the already tracked react-autosize-textarea peer warnings and the known deprecation notices for react-beautiful-dnd and react-jsonschema-form.), npm run build: Passed (wp-scripts build completed successfully with the existing bundle-size warnings only.). Manual review: Confirm the Node frontend, PHP Compatibility and WordPress Smoke Tests workflows trigger on push and pull_request events.. Documents updated: .github/workflows/node-frontend.yml , README.md , docs/dev/dependency-upgrade-report.json , docs/dev/dependency-upgrade-report.md , docs/dev/dependency-upgrade-smoke-tests.md . 2026-03-16 - Phase 2 - Build tooling major (Completed): Upgraded @wordpress/scripts from 30.27.0 to 31.6.0. The existing webpack.config.js override remained compatible and no script changes were required. Automated checks: npm install @wordpress/scripts@^31.6.0 --save-dev: Passed (Updated the direct dependency and lockfile to @wordpress/scripts 31.6.0 while repeating only the already known react-autosize-textarea peer warnings.), npm run build: Passed (wp-scripts build completed successfully with the same bundle-size warnings as before the upgrade.), php scripts/check-php-compat.php: Passed (Static PHP 8.0-8.4 compatibility review found no blocking issues after the tooling upgrade.), WP_DIR=/tmp/jeo-plugin-wordpress-smoke bash scripts/wordpress-smoke.sh: Failed (The local runtime smoke attempt reached wp-cli and WordPress download, but failed against the local MariaDB login for wordpress@localhost. The GitHub WordPress Smoke Tests workflow remains the authoritative gate for this check.). Manual review: No dedicated manual review unless asset or wp-admin regressions become visible.. Documents updated: package.json , package-lock.json , docs/dev/dependency-upgrade-report.json , docs/dev/dependency-upgrade-report.md . 2026-03-16 - Phase 3 - Minimum JS test layer (Completed): Added the minimum JS test layer with focused coverage for date-selector propagation, sidebar attribution normalization and Eta template compilation, and made the Node workflow run npm run test:unit as a required step. Automated checks: npm run test:unit: Passed (The new wp-scripts unit suite passed with 3 test files and 9 assertions.), npm run build: Passed (wp-scripts build completed successfully with the existing bundle-size warnings only.), php scripts/check-php-compat.php: Passed (Static PHP 8.0-8.4 compatibility review remained green after the frontend test-layer changes.), WP_DIR=/tmp/jeo-plugin-wordpress-smoke bash scripts/wordpress-smoke.sh: Passed (The local runtime smoke passed after configuring the local MariaDB wordpress database/user and temporarily applying the separate PR #501 fix that removes the invalid cli_init callback registration from src/includes/class-jeo.php. The script now also honors WP_CLI_PHP so Homebrew wp-cli setups can run the same smoke on PHP 8.4 without the PHP 8.5 deprecation noise. class-jeo.php stays out of the Phase 3 commit because it already has its own PR.). Manual review: Confirm the new test coverage protects date selector propagation, sidebar form normalization and Eta template compilation.. Documents updated: .github/workflows/node-frontend.yml , README.md , docs/dev/php-compatibility.md , package.json , scripts/wordpress-smoke.sh , docs/dev/dependency-upgrade-report.json , docs/dev/dependency-upgrade-report.md . 2026-03-16 - Phase 4 - Date picker major (Completed): Upgraded react-datepicker from 7.6.0 to 9.1.0. The existing date persistence helpers continued to satisfy the component contract without source changes. Automated checks: npm install react-datepicker@^9.1.0: Passed (Updated the direct dependency and lockfile to react-datepicker 9.1.0 while repeating only the already tracked react-autosize-textarea peer warnings.), npm run test:unit: Passed (The focused wp-scripts unit suite remained green with 3 test files and 9 assertions after the date picker major.), npm run build: Passed (wp-scripts build completed successfully with the existing bundle-size warnings plus one new webpack critical-dependency warning from react-datepicker's optional date-fns-tz require path.), php scripts/check-php-compat.php: Passed (Static PHP 8.0-8.4 compatibility review remained green after the frontend dependency update.), WP_CLI_PHP=/opt/homebrew/opt/php@8.4/bin/php WP_DIR=/tmp/jeo-plugin-wordpress-smoke bash scripts/wordpress-smoke.sh: Passed (The local runtime smoke passed with the PHP 8.4 wp-cli override and the same temporary PR #501 class-jeo.php patch used for local validation only.). Manual review: Select start and end dates in the related-posts sidebar., Save the post, reload the editor and verify persisted values., Clear dates and confirm empty values stay cleared after saving.. Documents updated: package.json , package-lock.json , docs/dev/dependency-upgrade-report.json , docs/dev/dependency-upgrade-report.md . 2026-03-16 - Phase 5 - Template runtime major (Completed): Upgraded eta from 3.5.0 to 4.5.1. The frontend helper now imports the browser-safe eta/core entry and the Jest config transforms that subpath for the focused template tests. Automated checks: npm install eta@^4.4.1: Passed (Updated the direct dependency and lockfile to eta 4.5.1 while repeating only the already tracked react-autosize-textarea peer warnings.), npm run test:unit: Passed (The focused wp-scripts unit suite remained green with 3 test files and 9 assertions after pointing the helper at eta/core and teaching Jest to transform eta/dist/core.js.), npm run build: Passed (wp-scripts build completed successfully after switching the frontend helper to eta/core. The existing bundle-size warnings remain, as does the known react-datepicker optional date-fns-tz warning from Phase 4.), php scripts/check-php-compat.php: Passed (Static PHP 8.0-8.4 compatibility review remained green after the Eta major update.), WP_CLI_PHP=/opt/homebrew/opt/php@8.4/bin/php WP_DIR=/tmp/jeo-plugin-wordpress-smoke bash scripts/wordpress-smoke.sh: Passed (The local runtime smoke passed with the PHP 8.4 wp-cli override and the same temporary PR #501 class-jeo.php patch used for local validation only.). Manual review: Open a map popup and verify the title, date and featured image rendering., Open the map Info panel and verify template-driven content still renders., Verify any locally customized popup template still compiles.. Documents updated: package.json , package-lock.json , jest.config.js , src/js/src/jeo-map/template-compiler.js , docs/dev/dependency-upgrade-report.json , docs/dev/dependency-upgrade-report.md . 2026-03-16 - Phase 6 - Leaflet sidebar major (Blocked): Evaluated the react-leaflet major and found it blocked by the repository's current React line before any code changes could be applied. Automated checks: npm view react-leaflet version peerDependencies --json: Passed (Confirmed that the current published major is react-leaflet 5.0.0 and that it declares peerDependencies on leaflet ^1.9.0 plus react and react-dom ^19.0.0.), node -e \"console.log(require('react/package.json').version)\": Passed (Confirmed that the installed runtime still resolves react 18.3.1.), node -e \"console.log(require('react-dom/package.json').version)\": Passed (Confirmed that the installed runtime still resolves react-dom 18.3.1.), npm install react-leaflet@^5.0.0 --dry-run: Failed (npm returned ERESOLVE because react-leaflet 5.0.0 requires react and react-dom ^19.0.0, which conflicts with the repository's current React 18.3.1 line.). Manual review: None. Documents updated: docs/dev/dependency-upgrade-report.json , docs/dev/dependency-upgrade-report.md . 2026-03-16 - Phase 7 - CKEditor React major evaluation (Completed): Upgraded @ckeditor/ckeditor5-react from 9.5.0 to 11.0.1. The wrapper stayed compatible with ckeditor5 47.6.1 and React 18.3.1 without requiring storymap editor source changes. Automated checks: npm view @ckeditor/ckeditor5-react version peerDependencies --json: Passed (Confirmed that the current published wrapper line is 11.0.1 and that its peer requirements accept ckeditor5 >=46.0.0 plus React 18.), npm install @ckeditor/ckeditor5-react@^11.0.1: Passed (Updated the direct dependency and lockfile to @ckeditor/ckeditor5-react 11.0.1 while repeating only the already tracked react-autosize-textarea peer warnings.), npm run test:unit: Passed (The focused wp-scripts unit suite remained green with 3 test files and 9 assertions after the CKEditor React wrapper major.), npm run build: Passed (wp-scripts build completed successfully with the existing bundle-size warnings and the known react-datepicker optional date-fns-tz warning from Phase 4.), php scripts/check-php-compat.php: Passed (Static PHP 8.0-8.4 compatibility review remained green after the CKEditor React wrapper update.), WP_CLI_PHP=/opt/homebrew/opt/php@8.4/bin/php WP_DIR=/tmp/jeo-plugin-wordpress-smoke bash scripts/wordpress-smoke.sh: Passed (The local runtime smoke passed with the PHP 8.4 wp-cli override and the same temporary PR #501 class-jeo.php patch used for local validation only.). Manual review: Load the storymap editor and confirm the toolbar initializes., Verify text selection formatting still applies and the selection is retained., Upload an image, save the storymap and reopen it.. Documents updated: package.json , package-lock.json , docs/dev/dependency-upgrade-report.json , docs/dev/dependency-upgrade-report.md . 2026-03-16 - Phase 8 - PHPCS major evaluation (Blocked): Evaluated the PHPCS major and found the upgrade blocked by WPCS compatibility and by the repository's current PHPCS baseline. Automated checks: composer validate --no-check-publish: Passed (The current composer.json is structurally valid.), composer show squizlabs/php_codesniffer --locked: Passed (Confirmed that the installed PHPCS line is 3.13.5.), composer show wp-coding-standards/wpcs --locked: Passed (Confirmed that the installed WPCS line is 3.3.0 and that it requires squizlabs/php_codesniffer ^3.13.4.), vendor/bin/phpcs --standard=phpcs.xml.dist: Failed (The current PHPCS 3.x run reports a large existing baseline of coding-standard violations across the repository, including scripts and template files, so the lint gate is not yet green even before attempting PHPCS 4.), php scripts/check-php-compat.php: Passed (Static PHP 8.0-8.4 compatibility review remained green during the PHPCS evaluation.). Manual review: None. Documents updated: docs/dev/dependency-upgrade-report.json , docs/dev/dependency-upgrade-report.md . 2026-03-16 - Batch 0 - Local prerequisites and report reset (Completed): Absorbed the validated local prerequisites for the stabilization track: the invalid cli_init hook no longer needs a local-only patch, the Composer metadata cleanup is part of the repository, and the dependency report now carries the new batch plan. Automated checks: composer validate --no-check-publish: Passed (The updated composer.json stays structurally valid after the SPDX and tooling-script cleanup.), php scripts/check-php-compat.php: Passed (Static PHP 8.0-8.4 compatibility review remained green after absorbing the local prerequisites.), WP_CLI_PHP=/opt/homebrew/opt/php@8.4/bin/php WP_DIR=/tmp/jeo-plugin-wordpress-smoke bash scripts/wordpress-smoke.sh: Passed (The local WordPress smoke now passes without any local-only patch to src/includes/class-jeo.php. In a clean worktree it still emits PHP warnings until the ignored src/js/build assets are generated locally.), node --check scripts/dependency-review.mjs: Passed, node scripts/dependency-review.mjs --write: Passed (Regenerated the JSON and Markdown dependency reports with the reopened stabilization track and Batch 0 ready for approval.). Manual review: None. Documents updated: composer.json , src/includes/class-jeo.php , scripts/dependency-review.mjs , scripts/dependency-review.overrides.json , docs/dev/dependency-upgrade-report.json , docs/dev/dependency-upgrade-report.md , docs/dev/dependency-upgrade-smoke-tests.md . 2026-03-16 - Batch 1 - Baseline fixes and warning cleanup (Completed): Fixed the null-related-post geocode crash in frontend and Discovery GeoJSON builders, replaced brittle same-point spiderfy timing with a navigable popup for posts that share a location, repaired storymap slide ordering with explicit move controls and removed the targeted react-datepicker webpack warning with focused regression tests. Automated checks: npm ci: Passed (A clean install completed successfully after rebuilding node_modules. The run still reports the tracked react-autosize-textarea peer warning plus the deprecated packages scheduled for Batches 2 through 4, but no new install warnings were introduced by Batch 1.), npm run test:unit: Passed (The focused wp-scripts unit suite now passes with 5 test files and 17 assertions, including new coverage for GeoJSON sanitization and storymap reorder helpers.), npm run build: Passed (wp-scripts build completed successfully from a clean install with only the existing bundle-size warnings. The previous react-datepicker optional date-fns-tz critical-dependency warning no longer appears.), php scripts/check-php-compat.php: Passed (Static PHP 8.0-8.4 compatibility review remained green after the frontend baseline fixes.), WP_CLI_PHP=/opt/homebrew/opt/php@8.4/bin/php WP_DIR=/tmp/jeo-plugin-wordpress-smoke bash scripts/wordpress-smoke.sh: Passed (The local runtime smoke passed after rebuilding the ignored src/js/build assets, so the previous asset-include warnings no longer appear in the smoke log.). Manual review: Published-map review confirmed the null-geocode crash disappeared and same-point posts can be navigated inside the popup, including dense clusters., Storymap review confirmed slide ordering, accordion behavior and save or reopen stayed stable after the UI changes.. Documents updated: scripts/dependency-review.overrides.json , docs/dev/dependency-upgrade-report.json , docs/dev/dependency-upgrade-report.md , docs/dev/dependency-upgrade-smoke-tests.md , src/js/src/discovery/blocks/stories.js , src/js/src/jeo-map/class-jeo-map.js , src/js/src/map-blocks/storymap-editor.js , src/js/src/map-blocks/storymap-editor.scss , src/js/src/map-blocks/storymap-ordering.js , src/js/src/map-blocks/storymap-ordering.test.js , src/js/src/shared/story-geojson.js , src/js/src/shared/story-geojson.test.js , webpack.config.js , src/css/jeo-map.scss . 2026-03-16 - Batch 2 - Autosuggest, date-range and form replacements (Completed): Replaced react-autosuggest with an internal async combobox wrapper, moved Discovery date filtering off react-bootstrap-daterangepicker onto a react-datepicker-based internal component, and replaced react-jsonschema-form with an internal @rjsf/core adapter while preserving the existing query and formData contracts. Automated checks: npm ci: Passed (A clean reinstall completed successfully after removing the locally corrupted node_modules tree left by an earlier ENOTEMPTY failure. The run no longer emits deprecation warnings for react-autosuggest, react-bootstrap-daterangepicker or react-jsonschema-form; the remaining install noise is the tracked react-beautiful-dnd deprecation, the react-autosize-textarea peer warning and broader transitive npm ecosystem deprecations.), npm run test:unit: Passed (The wp-scripts unit suite passed with 8 test files and 25 assertions, including new coverage for the async combobox helper, the schema-form adapter and the Discovery date-range wrapper.), npm run build: Passed (wp-scripts build completed successfully after the replacements with only the existing bundle-size warnings.), php scripts/check-php-compat.php: Passed (Static PHP 8.0-8.4 compatibility review remained green after the frontend replacement batch.), WP_CLI_PHP=/opt/homebrew/opt/php@8.4/bin/php WP_DIR=/tmp/jeo-plugin-wordpress-smoke bash scripts/wordpress-smoke.sh: Passed (The local runtime smoke stayed green after replacing the autosuggest, date-range and schema-form helpers.). Manual review: Verify geocoding autosuggest suggestions load, typing stays responsive and selecting a result updates the field value correctly., Verify the post or map autosuggest search still surfaces suggestions and inserts the selected entity correctly., Verify Discovery date apply and clear flows still update the after and before query values as expected., Verify layer and attribution forms render, normalize formData and save correctly with the new schema-form adapter.. Documents updated: package.json , package-lock.json , jest.config.js , scripts/dependency-review.overrides.json , docs/dev/dependency-upgrade-report.json , docs/dev/dependency-upgrade-report.md , docs/dev/dependency-upgrade-smoke-tests.md , src/js/src/discovery/blocks/date-range-filter.js , src/js/src/discovery/blocks/date-range-filter.test.js , src/js/src/discovery/blocks/stories.js , src/js/src/discovery/style/discovery.scss , src/js/src/layers-sidebar/attribution-settings.js , src/js/src/layers-sidebar/layer-settings.js , src/js/src/map-blocks/jeo-autosuggest.js , src/js/src/map-blocks/map-editor.css , src/js/src/posts-sidebar/geo-auto-complete.css , src/js/src/posts-sidebar/geo-auto-complete.js , src/js/src/shared/async-combobox-control.js , src/js/src/shared/async-combobox-control.test.js , src/js/src/shared/schema-form.js , src/js/src/shared/schema-form.test.js , src/js/src/shared/wp-form-controls.js . 2026-03-17 - Batch 3 - Drag-and-drop consolidation (Completed): Removed react-beautiful-dnd, migrated the remaining map-layer ordering UI to react-movable and removed the accidental nested storymap layer drag affordance while keeping the supported slide ordering and layer toggle flows intact. Automated checks: npm ci: Passed (A clean reinstall completed successfully after clearing the locally stuck node_modules tree. The prior react-beautiful-dnd deprecation warning disappeared; the remaining install noise is the tracked react-autosize-textarea peer warning plus broader transitive npm ecosystem deprecations.), npm run test:unit: Passed (The wp-scripts unit suite remained green with 8 test files and 25 assertions after the drag-and-drop migration.), npm run build: Passed (wp-scripts build completed successfully after the migration with only the existing bundle-size warnings. The emitted mapsSidebar bundle is smaller now that react-beautiful-dnd is gone.), php scripts/check-php-compat.php: Passed (Static PHP 8.0-8.4 compatibility review remained green after the frontend drag-and-drop changes.), WP_CLI_PHP=/opt/homebrew/opt/php@8.4/bin/php WP_DIR=/tmp/jeo-plugin-wordpress-smoke bash scripts/wordpress-smoke.sh: Passed (The local runtime smoke stayed green after moving the remaining reorder flows to react-movable.). Manual review: Verify selected map-layer ordering in the map editor modal, including saving and reopening after reordering., Verify storymap slide ordering still works alongside the existing move up/down controls., Verify storymap slide layer toggling still works without exposing a reorder affordance that the product never supported.. Documents updated: package.json , package-lock.json , scripts/dependency-review.overrides.json , docs/dev/dependency-upgrade-report.json , docs/dev/dependency-upgrade-report.md , docs/dev/dependency-upgrade-smoke-tests.md , src/js/src/map-blocks/layer-settings.js , src/js/src/map-blocks/layers-settings.js , src/js/src/map-blocks/storymap-editor.js , src/js/src/map-blocks/storymap-editor.scss . 2026-03-17 - Batch 4 - Gutenberg and React alignment (Completed): Confirmed that the currently installed Gutenberg editor packages are already on the latest published line, applied a local react-autosize-textarea override to remove the stale React peer warning, and documented that react-leaflet 5 remains blocked by upstream React 18 peer ranges. Automated checks: npm ci: Passed (A clean reinstall completed successfully after rebuilding node_modules from scratch. The prior react-autosize-textarea peer warning disappeared; the remaining install noise is broader transitive npm ecosystem deprecations such as inflight, rimraf, glob and eslint support notices.), npm ls react-autosize-textarea react react-dom: Passed (The installed tree now resolves react-autosize-textarea@7.1.0-jeo.0 from npm-overrides/react-autosize-textarea under the Gutenberg editor packages, with React 18.3.1 and ReactDOM 18.3.1 accepted cleanly.), npm run test:unit: Passed (The wp-scripts unit suite remained green with 8 test files and 25 assertions after the local override and lockfile refresh.), npm run build: Passed (wp-scripts build completed successfully after the override with only the existing bundle-size warnings.), php scripts/check-php-compat.php: Passed (Static PHP 8.0-8.4 compatibility review remained green; Batch 4 did not change any PHP source.), WP_CLI_PHP=/opt/homebrew/opt/php@8.4/bin/php WP_DIR=/tmp/jeo-plugin-wordpress-smoke bash scripts/wordpress-smoke.sh: Passed (The local runtime smoke stayed green after the lockfile refresh and override application.). Manual review: Verify the map editor, layer editor, storymap editor and regular post editor load, save and stay console-clean.. Documents updated: package.json , package-lock.json , npm-overrides/react-autosize-textarea/package.json , npm-overrides/react-autosize-textarea/lib/index.js , npm-overrides/react-autosize-textarea/lib/TextareaAutosize.js , npm-overrides/react-autosize-textarea/lib/index.d.ts , npm-overrides/react-autosize-textarea/lib/TextareaAutosize.d.ts , scripts/dependency-review.overrides.json , docs/dev/dependency-upgrade-report.json , docs/dev/dependency-upgrade-report.md , docs/dev/dependency-upgrade-smoke-tests.md . 2026-03-17 - Batch 6 - PHP 8.5 compatibility (Completed): Extended the repository-owned PHP compatibility checks and CI matrix to PHP 8.5, documented that WordPress core already treats PHP 8.5 as beta support while stable wp-cli support is still planned upstream, and verified that local WordPress smoke stays green on PHP 8.4 and PHP 8.5 with only third-party wp-cli deprecation noise under PHP 8.5. Automated checks: composer validate --no-check-publish: Passed (The current composer.json remains structurally valid.), php scripts/check-php-compat.php on PHP 8.0 through 8.5: Passed (The static compatibility review stayed green on Homebrew PHP 8.0, 8.1, 8.2, 8.3, 8.4 and 8.5 after extending the script and report wording to PHP 8.5.), WP_CLI_PHP=/opt/homebrew/opt/php@8.4/bin/php WP_DIR=/tmp/jeo-plugin-wordpress-smoke bash scripts/wordpress-smoke.sh: Passed (The local runtime smoke remains green on PHP 8.4, which is still the cleanest local signal because wp-cli itself does not emit PHP 8.5 deprecation noise on this line.), WP_CLI_PHP=/opt/homebrew/bin/php WP_DIR=/tmp/jeo-plugin-wordpress-smoke-85 bash scripts/wordpress-smoke.sh: Passed (The local runtime smoke also passes on PHP 8.5. The log still contains third-party deprecations from wp-cli bundled libraries, but no repository-owned PHP 8.5 warnings or deprecations surfaced.). Manual review: Repeat the runtime smoke while watching specifically for repository-owned PHP 8.5 warnings or deprecations, not third-party wp-cli noise.. Documents updated: .github/workflows/php-compat.yml , .github/workflows/wordpress-smoke.yml , README.md , docs/dev/php-compatibility.md , src/readme.txt , trunk/readme.txt , scripts/check-php-compat.php , scripts/dependency-review.overrides.json , docs/dev/dependency-upgrade-report.json , docs/dev/dependency-upgrade-report.md , docs/dev/dependency-upgrade-smoke-tests.md . 2026-03-17 - Batch 7 - PHP lint stack cleanup (Completed): Cleaned the PHPCS 3.x baseline across scripts, templates and runtime PHP so the repository is now green on the current WPCS or PHPCS stack, while keeping the PHPCS 4 evaluation blocked until upstream WPCS support lands. Automated checks: composer validate --no-check-publish: Passed (The current composer.json remains structurally valid during the PHPCS cleanup batch.), php scripts/check-php-compat.php: Passed (The repository-owned PHP compatibility scanner stayed green after the lint-driven cleanup.), vendor/bin/phpcs --standard=phpcs.xml.dist: Passed (The PHPCS 3.x baseline is now clean across the repository after autofixes plus manual cleanup of docblocks, escaping, naming and query-input handling.), WP_CLI_PHP=/opt/homebrew/opt/php@8.4/bin/php WP_DIR=/tmp/jeo-plugin-wordpress-smoke bash scripts/wordpress-smoke.sh: Passed (Local WordPress smoke stayed green after the runtime-touching PHP cleanup, confirming that embeds, maps and story maps still bootstrap correctly.), node scripts/dependency-review.mjs --write: Passed (Regenerated the JSON and Markdown dependency reports with Batch 7 marked as completed and the PHPCS 4 path still blocked by WPCS support.). Manual review: None. Documents updated: scripts/dependency-review.overrides.json , docs/dev/dependency-upgrade-report.json , docs/dev/dependency-upgrade-report.md , docs/dev/dependency-upgrade-smoke-tests.md , scripts/check-php-compat.php , scripts/wordpress-smoke-check.php , src/includes/class-jeo.php , src/includes/geocode/class-geocode-handler.php , src/includes/geocode/class-geocoder.php , src/includes/geocode/geocoders/class-nominatim.php , src/includes/index.php , src/includes/layer-types/class-layer-types.php , src/includes/layers/class-layers.php , src/includes/legend-types/class-legend-types.php , src/includes/loaders.php , src/includes/maps/class-maps.php , src/includes/menu/class-menu.php , src/includes/settings/class-settings.php , src/includes/settings/settings-page.php , src/includes/sidebars/class-sidebars.php , src/includes/storymap/class-storymap.php , src/includes/traits/class-rest-validate-meta.php , src/includes/traits/class-singleton.php , src/index.php , src/jeo.php , src/templates/discovery.php , src/templates/embed-discovery.php , src/templates/embed-storymap.php , src/templates/embed.php , src/templates/map-content-layers-list.php , src/templates/single-map.php , src/templates/single-storymap.php . 2026-03-17 - Sanity Batch 0 - Runtime policy and reproducibility (Completed): Promoted Node 24 to the project runtime, added an explicit environment preflight, and aligned the release workflow with deterministic installs and the same baseline used by the frontend CI gates. Automated checks: npm run check:env: Passed (The preflight now enforces Node 24 as the supported frontend runtime for this repository.), npm ci: Passed (A clean reinstall completed successfully under Node 24 with the refreshed lockfile.), npm run build:report: Passed (The bundle report now runs as part of the documented frontend approval path.). Manual review: None. Documents updated: .nvmrc , package.json , .github/workflows/node-frontend.yml , .github/workflows/deploy-wordpress-org.yml , README.md , scripts/check-node-version.mjs , scripts/dependency-review.overrides.json , docs/dev/dependency-upgrade-report.json , docs/dev/dependency-upgrade-report.md , docs/dev/dependency-upgrade-smoke-tests.md . 2026-03-17 - Sanity Batch 1 - npm health and security (Completed): Refreshed the Node 24 dependency tree, removed the known js-yaml vulnerability, documented audit coverage as a first-class maintenance gate, and kept npm and Composer audit outputs clean. Automated checks: npm audit --package-lock-only: Passed (The production and development dependency graph is now free of known advisories at the lockfile level.), npm audit --omit=dev: Passed (The installable runtime dependency tree is clean.), composer audit --locked: Passed (Composer dependencies remain free of known advisories.). Manual review: None. Documents updated: package.json , package-lock.json , scripts/dependency-review.overrides.json , docs/dev/dependency-upgrade-report.json , docs/dev/dependency-upgrade-report.md , docs/dev/dependency-upgrade-smoke-tests.md . 2026-03-17 - Sanity Batch 2 - Incremental loading for layers, maps and taxonomies (Completed): Replaced unbounded collection loading with chunked hydration by selected IDs, paginated layer libraries, async taxonomy lookup and lazy Discovery map hydration without dropping already-selected data. Manual validation then fixed the map-editor reload regression, the Discovery lazy-load crashes, and the later Discovery marker plus View in map follow-up that had been tracked separately at the time. Automated checks: npm run test:unit: Passed (The new shared rest-record helpers and the existing JS coverage stayed green after the loading-strategy rewrite.), npm run build: Passed (The frontend bundles compile successfully after replacing per_page -1 style loading with chunked and paginated flows.), WP_CLI_PHP=/opt/homebrew/opt/php@8.4/bin/php WP_DIR=/tmp/jeo-plugin-wordpress-smoke bash scripts/wordpress-smoke.sh: Passed (Local runtime smoke stayed green after the Discovery and editor data-loading changes.). Manual review: Verify exact selected layers still appear in map, onetime map, storymap and sidebar flows., Verify paginated layer search and Load more behavior in the map editor modal., Verify Discovery map loading, shared-link hydration and async taxonomy selectors.. Documents updated: src/js/src/shared/rest-records.js , src/js/src/shared/rest-records.test.js , src/js/src/map-blocks/map-editor.js , src/js/src/map-blocks/onetime-map-editor.js , src/js/src/map-blocks/map-editor-preview.js , src/js/src/map-blocks/layers-settings.js , src/js/src/map-blocks/storymap-editor.js , src/js/src/maps-sidebar/maps-sidebar.js , src/js/src/discovery/blocks/map-layers.js , src/js/src/discovery/blocks/stories.js , src/js/src/discovery/blocks/tag-filter-control.js , src/js/src/discovery/style/discovery.scss , src/js/src/posts-selector/index.js , src/js/src/posts-selector/async-tokens-selector.js , src/includes/class-jeo.php , scripts/dependency-review.overrides.json , docs/dev/dependency-upgrade-report.json , docs/dev/dependency-upgrade-report.md , docs/dev/dependency-upgrade-smoke-tests.md . 2026-03-17 - Sanity Batch 3 - Bundle budgets and build stability (Completed): Added an explicit bundle-size report and budgets, then finalized a more predictable asset graph after validation showed intermittent reload issues with the more aggressive chunking setup. Automated checks: npm run build: Passed (The webpack build remains green after the budget and asset-graph adjustments.), npm run build:report: Passed (Current budget highlights: mapBlocks 1092 of 1150 KB, discovery 241 of 280 KB and jeoMap 79 of 120 KB.). Manual review: Verify the main editors, Discovery and published maps still load correctly after the asset graph changes.. Documents updated: webpack.config.js , scripts/report-bundle-sizes.mjs , src/js/src/map-blocks/index.js , src/js/src/map-blocks/onetime-map-editor.js , src/js/src/map-blocks/onetime-map-config.js , src/js/src/map-blocks/onetime-map-config.test.js , scripts/dependency-review.overrides.json , docs/dev/dependency-upgrade-report.json , docs/dev/dependency-upgrade-report.md , docs/dev/dependency-upgrade-smoke-tests.md . 2026-03-17 - Sanity Batch 4 - Logs, warnings and editor runtime cleanup (Completed): Removed repository-owned console noise and dead debug traces, documented the remaining async combobox dependency on the current experimental Gutenberg render hook, and fixed the editor-side runtime errors surfaced during manual validation. Automated checks: npm run test:unit: Passed (The editor-side cleanup did not change the passing JS test suite.), npm run build: Passed (The build stayed green with no new repository-owned warnings.), php scripts/check-php-compat.php: Passed (The JS cleanup did not affect PHP compatibility checks.). Manual review: Verify normal editor use stays console-clean after the cleanup.. Documents updated: src/js/src/shared/async-combobox-control.js , src/js/src/layers-sidebar/interactions-settings.js , src/js/src/posts-sidebar/legends-editor/editors/circle.js , src/js/src/posts-sidebar/legends-editor/legend-editor.js , src/includes/legend-types/JeoLegend.js , src/js/src/discovery/blocks/stories.js , src/js/src/discovery/index.js , src/js/src/map-blocks/index.js , src/js/src/map-blocks/onetime-map-editor.js , src/js/src/map-blocks/onetime-map-config.js , src/js/src/map-blocks/onetime-map-config.test.js , scripts/dependency-review.overrides.json , docs/dev/dependency-upgrade-report.json , docs/dev/dependency-upgrade-report.md , docs/dev/dependency-upgrade-smoke-tests.md . 2026-03-17 - Sanity Batch 5 - Runtime hardening and request hygiene (Completed): Hardened geocoding and adjacent runtime request paths with nonce and capability checks, safer remote-request settings, cleaner JSON responses and more defensive client-side handling; the later draggable-marker reliability and accented-search fixes are now closed in the current branch. Automated checks: php scripts/check-php-compat.php: Passed (Repository-owned PHP checks stayed green after the request hardening changes.), vendor/bin/phpcs --standard=phpcs.xml.dist: Passed (The hardened request and sanitization paths remain clean under the current PHPCS stack.), WP_CLI_PHP=/opt/homebrew/opt/php@8.4/bin/php WP_DIR=/tmp/jeo-plugin-wordpress-smoke bash scripts/wordpress-smoke.sh: Passed (The local runtime smoke stayed green after the geocoding and bootstrap hardening work.). Manual review: Verify geocoding autosuggest, reverse geocoding and save or reopen flows., Verify embeds and public maps touched by bootstrap changes still load.. Documents updated: src/includes/class-jeo.php , src/includes/geocode/class-geocode-handler.php , src/includes/geocode/geocoders/class-nominatim.php , src/js/src/posts-sidebar/geo-auto-complete.js , src/js/src/posts-sidebar/geo-posts.js , scripts/dependency-review.overrides.json , docs/dev/dependency-upgrade-report.json , docs/dev/dependency-upgrade-report.md , docs/dev/dependency-upgrade-smoke-tests.md . 2026-03-17 - Sanity Batch 6 - Pipeline and quality gate alignment (Completed): Closed the sanity-track loop by aligning release and CI expectations with the same install, audit, bundle-budget, PHP compatibility, PHPCS and smoke-test gates now documented for local maintenance, and confirmed them again in a full rerun after the late editor fixes. Automated checks: composer validate --no-check-publish: Passed (The current Composer metadata remains structurally valid.), npm ci: Passed (A full reinstall under Node 24 completed successfully; the remaining install-time notices are the same upstream tooling deprecations already tracked for future review.), npm run test:unit: Passed (All current frontend unit tests passed again after the final editor and geocoding-related fixes.), npm run build: Passed (The frontend build remains green after the final sanity-track fixes, with no repository-owned warnings.), npm run build:report: Passed (Bundle budgets remain within limits, including mapBlocks 1092 of 1150 KB, discovery 241 of 280 KB and jeoMap 79 of 120 KB.), composer audit --locked: Passed (Composer dependencies remain free of known advisories.), npm audit --package-lock-only: Passed (The npm lockfile remains free of known advisories after the full sanity-track refresh.), npm audit --omit=dev: Passed (The installed runtime dependency tree remains free of known npm advisories.), npm ls react-autosize-textarea react react-dom: Passed (The frontend runtime tree remains aligned on React 18.3.1 with the local react-autosize-textarea override applied cleanly.), php scripts/check-php-compat.php: Passed (Static compatibility checks remain green for PHP 8.0 through 8.5.), vendor/bin/phpcs --standard=phpcs.xml.dist: Passed (The repository stays clean on the current PHPCS and WPCS line.), WP_CLI_PHP=/opt/homebrew/opt/php@8.4/bin/php WP_DIR=/tmp/jeo-plugin-wordpress-smoke bash scripts/wordpress-smoke.sh: Passed (The runtime smoke remains green at the end of the full sanity track.). Manual review: None. Documents updated: .github/workflows/deploy-wordpress-org.yml , .github/workflows/node-frontend.yml , README.md , scripts/dependency-review.overrides.json , docs/dev/dependency-upgrade-report.json , docs/dev/dependency-upgrade-report.md , docs/dev/dependency-upgrade-smoke-tests.md . 2026-03-17 - Sanity Batch 7 - Override governance and maintenance (Completed): Added an explicit npm override inventory with reason, review triggers and removal criteria for every active override, and made the dependency-review script fail when the governance map falls out of sync with package.json. Automated checks: node scripts/dependency-review.mjs --write: Passed (The report generator now renders a dedicated override-governance section and verifies that every active override is documented.), npm audit --package-lock-only: Passed (The current override set still keeps the lockfile free of known advisories.), npm ls react-autosize-textarea react react-dom: Passed (The local react-autosize-textarea override still resolves cleanly against React 18.3.1 and ReactDOM 18.3.1.). Manual review: Confirm that each active override still has a clear reason, a review trigger and an actionable removal criterion.. Documents updated: scripts/dependency-review.mjs , scripts/dependency-review.overrides.json , docs/dev/dependency-upgrade-report.json , docs/dev/dependency-upgrade-report.md . Non-maintained or high-risk package lines None flagged in the current snapshot. npm direct dependencies Package Type Declared / Resolved Latest compatible Latest major Maintenance Impact Decision Batch Execution Evidence Usage @ckeditor/ckeditor5-react Storymap editor ^11.0.1 -> 11.0.1 Already on the latest compatible line No newer major detected Maintained (latest published 2025-12-18) CKEditor runtime inside storymap editing flows Keep current version Phase 7 Completed npm Repository package.json src/js/src/map-blocks/storymap-editor.js @nazka/map-gl-js-spiderfy Map rendering ^1.2.10 -> 1.2.10 Already on the latest compatible line No newer major detected Maintained (latest published 2025-09-03) Frontend maps, editor previews, Mapbox/MapLibre runtime compatibility Keep current version Monitor Tracking only npm Repository package.json src/js/src/jeo-map/class-jeo-map.js @rjsf/core UI / helper ^6.4.1 -> 6.4.1 Already on the latest compatible line No newer major detected Maintained (latest published 2026-03-07) Browser runtime and React-driven editor UI behavior Keep current version Monitor Tracking only npm Repository package.json src/js/src/shared/schema-form.js @rjsf/validator-ajv8 UI / helper ^6.4.1 -> 6.4.1 Already on the latest compatible line No newer major detected Maintained (latest published 2026-03-07) Browser runtime and React-driven editor UI behavior Keep current version Monitor Tracking only npm Repository package.json src/js/src/shared/schema-form.js @wordpress/edit-post Gutenberg / editor ^8.41.0 -> 8.41.0 Already on the latest compatible line No newer major detected Maintained (latest published 2026-03-04) Node 24 build pipeline, Gutenberg editor runtime and React peer alignment Keep current version Batch 4 Completed npm Repository package.json @wordpress/editor Gutenberg / editor ^14.41.0 -> 14.41.0 Already on the latest compatible line No newer major detected Maintained (latest published 2026-03-04) Node 24 build pipeline, Gutenberg editor runtime and React peer alignment Keep current version Batch 4 Completed npm Repository package.json src/js/src/posts-sidebar/index.js src/js/src/layers-sidebar/layers-sidebar.js src/js/src/maps-sidebar/map-embed-url.js @wordpress/plugins Gutenberg / editor ^7.41.0 -> 7.41.0 Already on the latest compatible line No newer major detected Maintained (latest published 2026-03-04) Node 24 build pipeline, Gutenberg editor runtime and React peer alignment Keep current version Batch 4 Completed npm Repository package.json src/js/src/posts-sidebar/index.js src/js/src/maps-sidebar/index.js src/js/src/layers-sidebar/index.js @wordpress/scripts Build tooling ^31.6.0 -> 31.6.0 Already on the latest compatible line No newer major detected Maintained (latest published 2026-03-04) Asset compilation, webpack config and CI reproducibility on Node 24 Keep current version Phase 2 Completed npm Repository package.json webpack.config.js ajv Build tooling ^8.18.0 -> 8.18.0 Already on the latest compatible line No newer major detected Maintained (latest published 2026-02-14) Asset compilation, webpack config and CI reproducibility on Node 24 Keep current version Monitor Tracking only npm Repository package.json src/js/src/shared/schema-form.js ckeditor5 Storymap editor ^47.6.1 -> 47.6.1 Already on the latest compatible line No newer major detected Maintained (latest published 2026-03-11) CKEditor runtime inside storymap editing flows Keep current version Monitor Tracking only npm Repository package.json src/js/src/map-blocks/storymap-editor.js classnames UI / helper ^2.5.1 -> 2.5.1 Already on the latest compatible line No newer major detected Maintained (latest published 2023-12-29) Browser runtime and React-driven editor UI behavior Keep current version Monitor Tracking only npm Repository package.json src/js/src/posts-sidebar/geo-posts.js src/js/src/posts-selector/interval-selector.js src/js/src/jeo-storymap/storymap-display.js css-loader Build tooling ^7.1.4 -> 7.1.4 Already on the latest compatible line No newer major detected Maintained (latest published 2026-02-16) Asset compilation, webpack config and CI reproducibility on Node 24 Keep current version Monitor Tracking only npm Repository package.json eta UI / helper ^4.5.1 -> 4.5.1 Already on the latest compatible line No newer major detected Maintained (latest published 2026-02-05) Browser runtime and React-driven editor UI behavior Keep current version Phase 5 Completed npm Repository package.json src/js/src/jeo-map/class-jeo-map.js src/templates/post-popup.ejs src/js/src/jeo-map/template-compiler.js leaflet Map rendering ^1.9.4 -> 1.9.4 Already on the latest compatible line No newer major detected Maintained (latest published 2023-05-18) Frontend maps, editor previews, Mapbox/MapLibre runtime compatibility Keep current version Monitor Tracking only npm Repository package.json src/js/src/posts-sidebar/geo-posts.js lodash-es UI / helper ^4.17.23 -> 4.17.23 Already on the latest compatible line No newer major detected Maintained (latest published 2026-01-21) Browser runtime and React-driven editor UI behavior Keep current version Monitor Tracking only npm Repository package.json src/js/src/posts-sidebar/geo-auto-complete.js src/js/src/posts-sidebar/geo-posts.js src/js/src/map-blocks/map-preview-layer.js map-gl-utils Map rendering ^0.50.4 -> 0.50.4 Already on the latest compatible line No newer major detected Maintained (latest published 2025-01-21) Frontend maps, editor previews, Mapbox/MapLibre runtime compatibility Keep current version Monitor Tracking only npm Repository package.json src/js/src/lib/mapboxgl/mapboxgl-loader.js src/js/src/lib/maplibregl/maplibregl-loader.js mapbox-gl Map rendering ^3.20.0 -> 3.20.0 Already on the latest compatible line No newer major detected Maintained (latest published 2026-03-12) Frontend maps, editor previews, Mapbox/MapLibre runtime compatibility Keep current version Monitor Tracking only npm Repository src/readme.txt package.json src/js/src/lib/mapboxgl/mapboxgl-loader.js maplibre-gl Map rendering ^5.20.1 -> 5.20.1 Update to 5.20.2 No newer major detected Maintained (latest published 2026-03-17) Frontend maps, editor previews, Mapbox/MapLibre runtime compatibility Candidate for the minor batch Monitor Tracking only npm Repository src/readme.txt package.json src/js/src/lib/maplibregl/maplibregl-loader.js maplibregl-mapbox-request-transformer Map rendering ^0.0.3 -> 0.0.3 Already on the latest compatible line No newer major detected Maintained (latest published 2025-08-08) Frontend maps, editor previews, Mapbox/MapLibre runtime compatibility Keep current version Monitor Tracking only npm Repository package.json src/js/src/lib/maplibregl/maplibregl-loader.js postcss Build tooling ^8.5.8 -> 8.5.8 Already on the latest compatible line No newer major detected Maintained (latest published 2026-03-02) Asset compilation, webpack config and CI reproducibility on Node 24 Keep current version Monitor Tracking only npm Repository package.json react-datepicker UI / helper ^9.1.0 -> 9.1.0 Already on the latest compatible line No newer major detected Maintained (latest published 2025-12-19) Browser runtime and React-driven editor UI behavior Keep current version Phase 4 Completed npm Repository package.json webpack.config.js src/js/src/posts-selector/interval-selector.js src/js/src/discovery/style/discovery.scss react-leaflet Map rendering ^4.2.1 -> 4.2.1 Already on the latest compatible line Review upgrade path to 5.0.0 Maintained (latest published 2024-12-14) Frontend maps, editor previews, Mapbox/MapLibre runtime compatibility Monitor upstream blocker Monitor Tracking only npm Repository package.json src/js/src/posts-sidebar/geo-posts.js react-map-gl Map rendering ^8.1.0 -> 8.1.0 Already on the latest compatible line No newer major detected Maintained (latest published 2025-10-03) Frontend maps, editor previews, Mapbox/MapLibre runtime compatibility Keep current version Monitor Tracking only npm Repository src/readme.txt package.json src/js/src/lib/mapboxgl/mapboxgl-react.js src/js/src/lib/maplibregl/maplibregl-react.js react-movable UI / helper ^3.4.0 -> 3.4.1 Already on the latest compatible line No newer major detected Maintained (latest published 2025-02-26) Browser runtime and React-driven editor UI behavior Keep current version Monitor Tracking only npm Repository package.json src/js/src/map-blocks/layers-settings.js src/js/src/discovery/blocks/map-layers.js sass Build tooling ^1.98.0 -> 1.98.0 Already on the latest compatible line No newer major detected Maintained (latest published 2026-03-10) Asset compilation, webpack config and CI reproducibility on Node 24 Keep current version Monitor Tracking only npm Repository package.json sass-loader Build tooling ^16.0.7 -> 16.0.7 Already on the latest compatible line No newer major detected Maintained (latest published 2026-02-05) Asset compilation, webpack config and CI reproducibility on Node 24 Keep current version Monitor Tracking only npm Repository package.json scrollama UI / helper ^3.2.0 -> 3.2.0 Already on the latest compatible line No newer major detected Maintained (latest published 2022-06-17) Browser runtime and React-driven editor UI behavior Keep current version Monitor Tracking only npm Repository package.json src/js/src/jeo-storymap/storymap-display.js style-loader Build tooling ^4.0.0 -> 4.0.0 Already on the latest compatible line No newer major detected Maintained (latest published 2024-04-08) Asset compilation, webpack config and CI reproducibility on Node 24 Keep current version Monitor Tracking only npm Repository package.json use-debounce UI / helper ^10.1.0 -> 10.1.0 Already on the latest compatible line No newer major detected Maintained (latest published 2026-01-08) Browser runtime and React-driven editor UI behavior Keep current version Monitor Tracking only npm Repository package.json src/js/src/map-blocks/layer-editor-preview.js src/js/src/layers-sidebar/layer-settings.js src/js/src/layers-sidebar/layers-sidebar.js webpack Build tooling ^5.105.4 -> 5.105.4 Already on the latest compatible line No newer major detected Maintained (latest published 2026-03-03) Asset compilation, webpack config and CI reproducibility on Node 24 Keep current version Monitor Tracking only npm Repository package.json webpack.config.js src/js/src/lib/mapgl-loader.js src/js/src/lib/mapboxgl/mapboxgl-react.js Composer direct and transitive dependencies Package Type Declared / Resolved Latest compatible Latest major Maintenance Impact Decision Batch Execution Evidence Usage dealerdirect/phpcodesniffer-composer-installer PHP tooling transitive -> 1.2.0 Already on the latest compatible line No newer major detected Maintained (latest published 2025-11-11) Developer-only PHPCS/WPCS checks; validate PHP 8.0 and WPCS compatibility before majors Keep current version Monitor Tracking only Packagist Repository composer.lock composer.json phpcsstandards/phpcsextra PHP tooling transitive -> 1.5.0 Already on the latest compatible line No newer major detected Maintained (latest published 2025-11-12) Developer-only PHPCS/WPCS checks; validate PHP 8.0 and WPCS compatibility before majors Keep current version Monitor Tracking only Packagist Repository composer.lock phpcsstandards/phpcsutils PHP tooling transitive -> 1.2.2 Already on the latest compatible line No newer major detected Maintained (latest published 2025-12-08) Developer-only PHPCS/WPCS checks; validate PHP 8.0 and WPCS compatibility before majors Keep current version Monitor Tracking only Packagist Repository composer.lock squizlabs/php_codesniffer PHP tooling ^3.13.5 -> 3.13.5 Already on the latest compatible line Review upgrade path to 4.0.1 Maintained (latest published 2025-11-10) Developer-only PHPCS/WPCS checks; validate PHP 8.0 and WPCS compatibility before majors Monitor upstream blocker Monitor Tracking only Packagist Repository composer.lock composer.json wp-coding-standards/wpcs PHP tooling ^3.3.0 -> 3.3.0 Already on the latest compatible line No newer major detected Maintained (latest published 2025-11-25) Developer-only PHPCS/WPCS checks; validate PHP 8.0 and WPCS compatibility before majors Keep current version Batch 7 Completed Packagist Repository composer.json composer.lock Transitive watchlist Package Type Declared / Resolved Latest compatible Latest major Maintenance Impact Decision Batch Execution Evidence Usage react-autosize-textarea UI / helper transitive -> Unavailable Keep the local override in place while the published Gutenberg editor packages still carry this stale peer metadata. Remove the override only after upstream ships React-18 or React-19-compatible peer metadata or the parent editor stack is replaced through a supported upgrade path. Maintained upstream, local override applied for stale peer metadata (latest published 2020-07-08) Browser runtime and React-driven editor UI behavior Watch via the parent package batch Batch 4 Completed npm package.json Smoke tests for each batch Detailed checklist: docs/dev/dependency-upgrade-smoke-tests.md . Build plugin assets : npm run build completes without new warnings or bundle errors. Create and edit a map : The Maps editor loads, map settings persist, and the preview updates without console errors. Edit a storymap with CKEditor : The storymap editor loads, inline rich text editing works, and image upload still attaches media. Reorder layers and storymap slides : Drag-and-drop or movable ordering works in all UIs that currently rely on reordering. Edit layer and legend forms : Schema-driven forms, legend settings and interaction settings still render and save. Filter stories by date in Discovery : The date range picker applies and clears filters correctly. Use geocoding autosuggest : Address suggestions load, selection writes values back, and no stale request errors appear. Render maps in both Mapbox and MapLibre modes : Frontend and editor previews render with both runtimes, including layers, legends and interactions. Package notes @ckeditor/ckeditor5-react : Execution: Upgraded to 11.0.1 after confirming compatibility with ckeditor5 47.6.1 and React 18.3.1. @ckeditor/ckeditor5-react : Execution: No storymap editor source changes were required for the wrapper major itself. @wordpress/edit-post : Keep this aligned with the rest of the Gutenberg package batch to avoid React peer drift. @wordpress/edit-post : Execution: Revalidated in Batch 4: 8.41.0 is already the latest published line available to this repository. @wordpress/edit-post : Execution: Its published peerDependencies still target react and react-dom ^18.0.0, so no upstream React-line unlock happened in this batch. @wordpress/editor : Keep this aligned with the rest of the Gutenberg package batch to avoid React peer drift. @wordpress/editor : Execution: Revalidated in Batch 4: 14.41.0 is already the latest published line available to this repository. @wordpress/editor : Execution: Its published peerDependencies still target react and react-dom ^18.0.0, so no upstream React-line unlock happened in this batch. @wordpress/plugins : Execution: Revalidated in Batch 4: 7.41.0 is already the latest published line available to this repository. @wordpress/plugins : Execution: Its published peerDependencies still target react and react-dom ^18.0.0, so this package does not yet help unlock react-leaflet 5. @wordpress/scripts : Treat this package as the lead dependency for the build batch because it constrains Babel, webpack, PostCSS and much of the Gutenberg toolchain. @wordpress/scripts : Execution: Lead package for the tooling major batch. @wordpress/scripts : Execution: Upgraded to 31.x without requiring webpack.config.js changes in this repository. eta : Execution: Upgraded to 4.5.1 after the template compilation contract was covered by the minimum JS test layer. eta : Execution: The frontend helper now imports eta/core and Jest transforms eta/dist/core.js to keep the browser build and unit runner aligned. react-datepicker : Execution: Upgraded to 9.1.0; keep the related-posts date persistence flow under manual review after the major. react-datepicker : Execution: Phase 4 introduced a non-blocking webpack warning from react-datepicker's optional date-fns-tz require path. react-datepicker : Execution: Batch 1 removes that warning without changing the declared react-datepicker version. react-leaflet : Execution: Keep this on external upstream monitoring until Gutenberg exposes a supported React 19 path or another supported route replaces the current editor stack. react-leaflet : Execution: The repository remains on the supported react-leaflet 4 line; the geocoding fixes documented in this branch are product work, not part of the blocked major-upgrade path. squizlabs/php_codesniffer : Direct dev dependency. Evaluate majors only after confirming WPCS compatibility. squizlabs/php_codesniffer : Execution: Packagist publishes PHPCS 4.0.1, but the installed WPCS 3.3.0 line still requires squizlabs/php_codesniffer ^3.13.4. squizlabs/php_codesniffer : Execution: The repository PHPCS 3.x baseline is now clean, so PHPCS 4 stays on external upstream monitoring until the installed WPCS line officially supports that major. wp-coding-standards/wpcs : Lead Composer dependency because it constrains PHPCS, PHPCSExtra and PHPCSUtils. wp-coding-standards/wpcs : Execution: Batch 7 cleaned the repository baseline while keeping WPCS 3.3.0 on its current supported PHPCS 3.x line. wp-coding-standards/wpcs : Execution: This package remains the upstream compatibility gate that must move before PHPCS 4 can be re-evaluated. react-autosize-textarea : Observed locally through npm ls react react-dom. react-autosize-textarea : Execution: Batch 4 confirmed that the latest published Gutenberg editor packages still resolve this stale peer metadata and therefore applied a minimum local override at npm-overrides/react-autosize-textarea. react-autosize-textarea : Execution: npm ci and npm ls no longer report invalid React peer ranges after the override.","title":"Dependency upgrade report"},{"location":"dev/dependency-upgrade-report/#dependency-upgrade-review","text":"Generated on 2026-03-18.","title":"Dependency Upgrade Review"},{"location":"dev/dependency-upgrade-report/#baseline","text":"Platform baseline: WordPress 6.6+, PHP 8.0+, Node 24. Source files: .nvmrc , README.md , src/readme.txt , .github/workflows/deploy-wordpress-org.yml , .github/workflows/node-frontend.yml , .github/workflows/php-compat.yml , .github/workflows/wordpress-smoke.yml , scripts/check-node-version.mjs . Current scope: 22 direct npm runtime packages, 8 direct npm development packages, 2 direct Composer packages and 3 transitive Composer packages.","title":"Baseline"},{"location":"dev/dependency-upgrade-report/#tooling-notes","text":"Automated approval gates are the Node Frontend Checks, PHP Compatibility and WordPress Smoke Tests GitHub Actions workflows. A minimum frontend unit-test layer is available via npm run test:unit and enforced by the Node workflow from Phase 3 onward. The report now captures a frontend runtime snapshot with the resolved React, ReactDOM and Gutenberg editor package versions plus their published peer ranges. The installed tree now resolves a local react-autosize-textarea override so the stale upstream peer metadata no longer raises npm peer warnings against React 18.3.1. Previous stabilization track: the dependency-major batches are complete apart from the external monitoring paths for react-leaflet 5 and PHPCS 4, both still waiting on upstream support. Current plugin sanity track: Node 24 is now the supported frontend runtime for local development, CI and release validation. Current plugin sanity track: the implemented work now covers audit cleanup, incremental frontend loading, bundle budgets, runtime hardening, pipeline alignment and the editor, Discovery and geocoding follow-ups validated in this branch. Current plugin sanity track: the override-governance batch is complete, so every local npm override now has a documented reason, review trigger and removal criterion. Current plugin sanity track: the remaining open items are external monitoring only: react-leaflet 5 against the current React line, PHPCS 4 against upstream WPCS support, and third-party wp-cli PHP 8.5 deprecation noise.","title":"Tooling notes"},{"location":"dev/dependency-upgrade-report/#local-validation","text":"PHP: PHP 8.5.4 (cli) (built: Mar 10 2026 23:15:23) (NTS). Composer: Composer version 2.9.5 2026-01-29 11:40:53.","title":"Local validation"},{"location":"dev/dependency-upgrade-report/#environment-caveats","text":"Local Composer/PHP validation is available in this shell environment (Composer version 2.9.5 2026-01-29 11:40:53; PHP 8.5.4 (cli) (built: Mar 10 2026 23:15:23) (NTS)). The npm and Packagist lookups require network access when the report is regenerated.","title":"Environment caveats"},{"location":"dev/dependency-upgrade-report/#frontend-runtime-snapshot","text":"React: 18.3.1 . ReactDOM: 18.3.1 . Local npm overrides: react-autosize-textarea -> file:npm-overrides/react-autosize-textarea , js-yaml -> ^4.1.1 , webpack-dev-server -> ^5.2.3 , @typescript-eslint/typescript-estree > minimatch -> ^9.0.9 , markdownlint-cli > minimatch -> ^3.1.5 , copy-webpack-plugin > serialize-javascript -> ^7.0.4 . Package Declared Resolved React peer ReactDOM peer @wordpress/edit-post ^8.41.0 8.41.0 ^18.0.0 ^18.0.0 @wordpress/editor ^14.41.0 14.41.0 ^18.0.0 ^18.0.0 @wordpress/plugins ^7.41.0 7.41.0 ^18.0.0 ^18.0.0 @wordpress/block-editor transitive 15.14.0 ^18.0.0 ^18.0.0","title":"Frontend runtime snapshot"},{"location":"dev/dependency-upgrade-report/#local-npm-override-governance","text":"Selector Spec Resolved Type Reason Review when Removal criteria Introduced react-autosize-textarea file:npm-overrides/react-autosize-textarea Unavailable Local package override The Gutenberg editor stack still resolves react-autosize-textarea with stale peer metadata that does not accept the React 18 line used by this project. Any refresh of @wordpress/scripts or the @wordpress/* editor packages. / Any React or ReactDOM line change. Remove when the installed Gutenberg tree resolves React-compatible peer metadata without the local file override and npm ls stays clean. Batch 4 js-yaml ^4.1.1 4.1.1 Security pin Pins js-yaml to a patched line so the npm audit baseline stays clean even when upstream tooling lags behind. Any lockfile refresh. / Any @wordpress/scripts or webpack toolchain refresh. Remove when package-lock naturally resolves js-yaml >= 4.1.1 without the override and npm audit remains green. Sanity Batch 1 webpack-dev-server ^5.2.3 5.2.3 Security pin Keeps the dev-server subtree on a patched line required by the current audit baseline. Any @wordpress/scripts or webpack toolchain refresh. / Any lockfile refresh. Remove when the resolved toolchain already pulls webpack-dev-server >= 5.2.3 without the override and audits stay green. Sanity Batch 1 @typescript-eslint/typescript-estree > minimatch ^9.0.9 9.0.9, 3.1.5 Nested security pin Pins minimatch inside the TypeScript ESLint parser subtree to a patched line without replacing the parent package. Any eslint or @typescript-eslint refresh. / Any lockfile refresh. Remove when the subtree resolves a patched minimatch line on its own and npm audit remains green. Sanity Batch 1 markdownlint-cli > minimatch ^3.1.5 9.0.9, 3.1.5 Nested security pin Pins minimatch in the markdownlint-cli subtree so markdown tooling does not reintroduce the old advisory. Any markdownlint-cli refresh. / Any lockfile refresh. Remove when markdownlint-cli resolves a patched minimatch line without the nested override and npm audit remains green. Sanity Batch 1 copy-webpack-plugin > serialize-javascript ^7.0.4 7.0.4 Nested security pin Pins serialize-javascript in the copy-webpack-plugin subtree to the secure line used by the current build baseline. Any webpack or copy-webpack-plugin refresh. / Any lockfile refresh. Remove when copy-webpack-plugin resolves serialize-javascript >= 7.0.4 without help and npm audit remains green. Sanity Batch 1 react-autosize-textarea : The local package mirrors upstream runtime files and only broadens peer metadata.","title":"Local npm override governance"},{"location":"dev/dependency-upgrade-report/#execution-plan","text":"Phase 0 - Baseline and tracking scaffolding (Completed): Report generator, dependency reports and phased smoke-test guidance. Packages: None. Automated checks: node --check scripts/dependency-review.mjs , npm run deps:report . Manual review: None. Approval notes: No dependency versions change in this phase. Phase 1 - Frontend gates for approval (Completed): Create the Node workflow and treat PHP compatibility plus WordPress smoke checks as mandatory future gates. Packages: None. Automated checks: GitHub Actions Node frontend workflow , GitHub Actions PHP Compatibility workflow , GitHub Actions WordPress Smoke Tests workflow . Manual review: Confirm the Node frontend, PHP Compatibility and WordPress Smoke Tests workflows trigger on push and pull_request events. Approval notes: No dependency versions or lockfiles change in this phase. Phase 2 - Build tooling major (Completed): Upgrade the WordPress build toolchain major and keep the custom webpack config working. Packages: @wordpress/scripts . Automated checks: npm run build , GitHub Actions Node frontend workflow , GitHub Actions PHP Compatibility workflow , GitHub Actions WordPress Smoke Tests workflow . Manual review: No dedicated manual review unless asset or wp-admin regressions become visible. Approval notes: The webpack.config.js override continued to work without code changes on @wordpress/scripts 31.x., The local WordPress smoke attempt is environment-dependent and should still be confirmed by the GitHub workflow before merge. Phase 3 - Minimum JS test layer (Completed): Add unit tests for the contracts that protect the remaining majors. Packages: None. Automated checks: npm run build , npm run test:unit , GitHub Actions Node frontend workflow , GitHub Actions PHP Compatibility workflow , GitHub Actions WordPress Smoke Tests workflow . Manual review: Confirm the new test coverage protects date selector propagation, sidebar form normalization and Eta template compilation. Approval notes: No dependency versions or lockfiles change in this phase., The Node workflow now treats npm run test:unit as a mandatory step., The local WordPress smoke test passed after applying the separate PR #501 CLI-hook fix locally for validation only; src/includes/class-jeo.php remains outside this phase commit., scripts/wordpress-smoke.sh now honors WP_CLI_PHP so Homebrew wp-cli setups can run the local smoke test on PHP 8.4 without the PHP 8.5 deprecation noise. Phase 4 - Date picker major (Completed): Upgrade the date picker used in related-post filters. Packages: react-datepicker . Automated checks: npm run build , npm run test:unit , GitHub Actions Node frontend workflow , GitHub Actions PHP Compatibility workflow , GitHub Actions WordPress Smoke Tests workflow . Manual review: Select start and end dates in the related-posts sidebar., Save the post, reload the editor and verify persisted values., Clear dates and confirm empty values stay cleared after saving. Approval notes: react-datepicker moved from 7.6.0 to 9.1.0 without requiring source changes in the related-posts selector., The automated suite stayed green, but npm run build now reports one non-blocking webpack warning from react-datepicker's optional date-fns-tz require path. Phase 5 - Template runtime major (Completed): Upgrade Eta without changing the popup template contract. Packages: eta . Automated checks: npm run build , npm run test:unit , GitHub Actions Node frontend workflow , GitHub Actions PHP Compatibility workflow , GitHub Actions WordPress Smoke Tests workflow . Manual review: Open a map popup and verify the title, date and featured image rendering., Open the map Info panel and verify template-driven content still renders., Verify any locally customized popup template still compiles. Approval notes: eta moved from 3.5.0 to 4.5.1 and now uses the browser-safe eta/core entry inside the frontend helper., A local jest.config.js now tells the unit runner to transform eta/dist/core.js so the existing template-compiler tests stay representative., Manual review confirmed the Info panel and popup template rendering stayed functional, and the later same-point plus null-geocode popup fixes are now closed in Batch 1. Phase 6 - Leaflet sidebar major (Monitoring): Upgrade the post geocoding sidebar map and keep editing interactions intact once the upstream React line is compatible. Packages: react-leaflet . Automated checks: npm view react-leaflet version peerDependencies --json , npm install react-leaflet@^5.0.0 --dry-run . Manual review: No dedicated product review while the upstream React 19 path stays unavailable; keep covering add, edit, drag, cancel, save and reopen flows under the supported react-leaflet 4 line in the core smoke checklist. Approval notes: react-leaflet 5.0.0 is the current published major, but it now declares peerDependencies on react and react-dom ^19.0.0., This repository currently resolves react 18.3.1 and react-dom 18.3.1 through the existing Gutenberg stack, so npm rejects the major upgrade before runtime checks can begin., Treat this path as external upstream monitoring, not as an immediate repository-owned bugfix batch. Phase 7 - CKEditor React major evaluation (Completed): Evaluate the React wrapper major only if it remains compatible with the installed CKEditor core line. Packages: @ckeditor/ckeditor5-react . Automated checks: npm run build , npm run test:unit , GitHub Actions Node frontend workflow , GitHub Actions PHP Compatibility workflow , GitHub Actions WordPress Smoke Tests workflow . Manual review: Load the storymap editor and confirm the toolbar initializes., Verify text selection formatting still applies and the selection is retained., Upload an image, save the storymap and reopen it. Approval notes: @ckeditor/ckeditor5-react moved from 9.5.0 to 11.0.1 without requiring source changes in the storymap editor., The published wrapper's peer requirements remain compatible with the installed ckeditor5 47.6.1 line and with React 18.3.1., Manual review confirmed editor loading, formatting and save/reopen flows stayed functional, and the later slide-order fixes are now closed in Batch 1 and Batch 3. Phase 8 - PHPCS major evaluation (Monitoring): Evaluate PHPCS 4 only after upstream WPCS support makes the major viable. Packages: squizlabs/php_codesniffer . Automated checks: composer validate --no-check-publish , composer show squizlabs/php_codesniffer --locked , composer show wp-coding-standards/wpcs --locked , vendor/bin/phpcs --standard=phpcs.xml.dist , php scripts/check-php-compat.php . Manual review: None. Approval notes: Packagist confirms that squizlabs/php_codesniffer 4.0.1 exists, but the installed WPCS 3.3.0 line still requires squizlabs/php_codesniffer ^3.13.4., The PHPCS 3.x baseline is now clean in this repository, so the remaining PHPCS 4 path is external upstream monitoring rather than a pending product fix. Batch 0 - Local prerequisites and report reset (Completed): Absorb the validated local prerequisites, remove the invalid cli_init hook dependency from local smoke, and reopen the dependency report for the stabilization track. Packages: None. Automated checks: composer validate --no-check-publish , php scripts/check-php-compat.php , WP_CLI_PHP=/opt/homebrew/opt/php@8.4/bin/php WP_DIR=/tmp/jeo-plugin-wordpress-smoke bash scripts/wordpress-smoke.sh , node --check scripts/dependency-review.mjs , node scripts/dependency-review.mjs --write . Manual review: None. Approval notes: No dependency versions or lockfiles change in this batch., Local WordPress smoke no longer depends on a validation-only patch outside the working tree., A clean worktree still needs the ignored src/js/build assets materialized locally before the smoke can run without asset-include warnings. Batch 1 - Baseline fixes and warning cleanup (Completed): Fix the published-map baseline bugs, repair reorder flows, remove the react-datepicker webpack warning and add focused regression tests. Packages: None. Automated checks: npm ci , npm run build , npm run test:unit , php scripts/check-php-compat.php , WP_CLI_PHP=/opt/homebrew/opt/php@8.4/bin/php WP_DIR=/tmp/jeo-plugin-wordpress-smoke bash scripts/wordpress-smoke.sh . Manual review: Verify published-map popups, same-point post navigation and the absence of the null-geocode crash., Verify storymap slide ordering, save and reopen in the editor. Approval notes: No dependency versions should change in this batch., The react-datepicker version stays on 9.1.0; only its optional date-fns-tz webpack warning is removed., Published-map review confirmed the null-geocode crash is gone and same-point posts now use a navigable popup instead of depending on spiderfy timing., Storymap review confirmed the explicit slide ordering controls, accordion behavior and save or reopen flow stay consistent., npm ci still reports the tracked react-autosize-textarea peer warning plus the deprecated packages scheduled for Batches 2 through 4, but Batch 1 no longer adds any new npm or webpack warnings of its own. Batch 2 - Autosuggest, date-range and form replacements (Completed): Replace the deprecated input and form helpers while preserving the current geocoding, Discovery date filtering and formData contracts. Packages: react-autosuggest , react-bootstrap-daterangepicker , react-jsonschema-form , bootstrap-daterangepicker . Automated checks: npm ci , npm run build , npm run test:unit , php scripts/check-php-compat.php , WP_CLI_PHP=/opt/homebrew/opt/php@8.4/bin/php WP_DIR=/tmp/jeo-plugin-wordpress-smoke bash scripts/wordpress-smoke.sh . Manual review: Verify geocoding autosuggest and post or map autosuggest flows., Verify Discovery date apply and clear flows., Verify layer and attribution forms render, normalize formData and save correctly. Approval notes: The saved query and formData shapes stayed unchanged via legacy-compatible helpers for async combobox selection, Discovery date ranges and merged schema formData updates., npm ci no longer emits the deprecation warnings that previously came from react-autosuggest, react-bootstrap-daterangepicker or react-jsonschema-form., The remaining npm install noise is now limited to the tracked react-beautiful-dnd deprecation, the react-autosize-textarea peer warning and broader transitive ecosystem deprecations that Batch 2 did not introduce. Batch 3 - Drag-and-drop consolidation (Completed): Remove react-beautiful-dnd in favor of the existing react-movable stack and close the remaining reorder flows that were actually part of the product. Packages: react-beautiful-dnd . Automated checks: npm ci , npm run build , npm run test:unit , php scripts/check-php-compat.php , WP_CLI_PHP=/opt/homebrew/opt/php@8.4/bin/php WP_DIR=/tmp/jeo-plugin-wordpress-smoke bash scripts/wordpress-smoke.sh . Manual review: Verify map-layer ordering, storymap slide ordering and any nested layer ordering still exposed in the editor. Approval notes: The saved layer and slide structures stayed unchanged while the supported reorder flows moved onto react-movable., npm ci no longer emits the react-beautiful-dnd deprecation warning because the package has been removed from package.json and package-lock.json. Batch 4 - Gutenberg and React alignment (Completed): Revalidate the latest published Gutenberg editor packages, clear the remaining React peer warning with a minimum local override, and document the still-blocked react-leaflet 5 path. Packages: @wordpress/edit-post , @wordpress/editor , @wordpress/plugins , react-autosize-textarea . Automated checks: npm ci , npm run build , npm run test:unit , npm ls react-autosize-textarea react react-dom , php scripts/check-php-compat.php , WP_CLI_PHP=/opt/homebrew/opt/php@8.4/bin/php WP_DIR=/tmp/jeo-plugin-wordpress-smoke bash scripts/wordpress-smoke.sh . Manual review: Verify the map editor, layer editor, storymap editor and regular post editor load, save and stay console-clean. Approval notes: No newer published @wordpress/edit-post, @wordpress/editor or @wordpress/plugins versions were available beyond the already installed line, and their published peerDependencies still target React 18., Batch 4 therefore uses a local react-autosize-textarea override to modernize peer metadata without changing the runtime implementation or adding react or react-dom directly. Batch 5 - react-leaflet major (Monitoring): Upgrade react-leaflet to 5.x after a React-19-compatible Gutenberg stack exists and validate the entire geocoding sidebar map flow. Packages: react-leaflet . Automated checks: npm ci , npm run build , npm run test:unit , php scripts/check-php-compat.php , WP_CLI_PHP=/opt/homebrew/opt/php@8.4/bin/php WP_DIR=/tmp/jeo-plugin-wordpress-smoke bash scripts/wordpress-smoke.sh . Manual review: No dedicated product review while the upstream React 19 path stays unavailable; keep covering add, edit, drag, cancel, save and reopen flows under the supported react-leaflet 4 line in the core smoke checklist. Approval notes: The latest published Gutenberg editor packages still peer on react and react-dom ^18.0.0, while react-leaflet 5.0.0 requires react and react-dom ^19.0.0., Do not start this batch until upstream publishes a supported React-19-compatible editor stack or the repository adopts another supported route., Treat this path as external upstream monitoring, not as an immediate repository-owned bugfix batch. Batch 6 - PHP 8.5 compatibility (Completed): Extend the local and CI PHP compatibility coverage to PHP 8.5 and remove repository-owned warnings or deprecations under that runtime. Packages: None. Automated checks: composer validate --no-check-publish , php scripts/check-php-compat.php with PHP 8.0 through 8.5 , WP_CLI_PHP=/opt/homebrew/opt/php@8.4/bin/php WP_DIR=/tmp/jeo-plugin-wordpress-smoke bash scripts/wordpress-smoke.sh , WP_CLI_PHP=/opt/homebrew/bin/php WP_DIR=/tmp/jeo-plugin-wordpress-smoke-85 bash scripts/wordpress-smoke.sh . Manual review: Repeat the runtime smoke while watching for repository-owned PHP 8.5 warnings or deprecations. Approval notes: Static compatibility checks are green on PHP 8.0 through 8.5, and local WordPress smoke passes on both PHP 8.4 and PHP 8.5., WordPress core 6.9 treats PHP 8.5 as beta support, while stable wp-cli support for PHP 8.5 is still planned upstream., Keep PHP 8.5 experimental in CI for now and track the remaining wp-cli deprecation noise as third-party monitoring only because repository-owned checks are clean. Batch 7 - PHP lint stack cleanup (Completed): Clean the PHPCS 3.x baseline across WPCS, PHPCSExtra and PHPCSUtils before re-evaluating PHPCS 4. Packages: squizlabs/php_codesniffer , wp-coding-standards/wpcs . Automated checks: composer validate --no-check-publish , php scripts/check-php-compat.php , vendor/bin/phpcs --standard=phpcs.xml.dist . Manual review: None. Approval notes: The PHPCS 3.x baseline is now green after autofixes plus manual cleanup across runtime PHP, templates and tooling scripts., The runtime-touching PHP cleanup also passed local WordPress smoke, so the lint cleanup did not introduce regressions in embeds, maps or story maps., Official WPCS metadata and the 3.3.0 release notes still require PHP_CodeSniffer 3.13.4, while older WPCS 2.3.0 notes only mention preliminary CI testing against the PHPCS 4 development branch., PHPCS 4 now remains an external upstream monitoring path until the installed WPCS line officially supports that major. Sanity Batch 0 - Runtime policy and reproducibility (Completed): Promote Node 24 to the project runtime, enforce it through an environment preflight, and align the deploy pipeline with deterministic installs. Packages: None. Automated checks: npm run check:env , npm ci , npm run build , npm run build:report . Manual review: None. Approval notes: This batch is mostly process and tooling work; the main validation is that the documented Node 24 path and the preflight behave correctly., Node 24 is now the single supported frontend runtime in local development and CI., The final full test rerun reconfirmed that the Node 24 preflight, deterministic install path and bundle-report flow are still the active project baseline. Sanity Batch 1 - npm health and security (Completed): Refresh the JavaScript dependency tree under Node 24, eliminate the known js-yaml vulnerability, and make npm and Composer audits part of the maintained process. Packages: js-yaml . Automated checks: npm ci , npm audit --package-lock-only , npm audit --omit=dev , composer audit --locked . Manual review: None. Approval notes: Audit coverage is now part of the expected maintenance flow., Remaining npm install notices are transitive deprecation warnings from the upstream Gutenberg or wp-scripts toolchain, not audit failures in this repository., The final full rerun kept npm audit and composer audit clean, so this batch is now fully closed. Sanity Batch 2 - Incremental loading for layers, maps and taxonomies (Completed): Replace unbounded frontend collection loads with chunked hydration by selected IDs, paginated layer search, async taxonomy queries and lazy Discovery map hydration. Packages: None. Automated checks: npm ci , npm run test:unit , npm run build , php scripts/check-php-compat.php , WP_CLI_PHP=/opt/homebrew/opt/php@8.4/bin/php WP_DIR=/tmp/jeo-plugin-wordpress-smoke bash scripts/wordpress-smoke.sh . Manual review: Verify that maps, onetime maps, storymaps and sidebars still show every already-selected layer., Verify paginated layer search in the map editor modal, including Load more behavior., Verify Discovery map loading, Load more maps and selected-layer hydration from shared or URL-driven state., Verify async category and tag selection in the related-posts editor flow. Approval notes: Published maps and embeds keep completeness by exact selected IDs; pagination is used only for large libraries and list views., This batch targets the memory-risk patterns that previously depended on per_page -1, nopaging true or oversized taxonomy fetches., Manual validation closed the batch after fixing the map-editor reload regression and the Discovery lazy-load crashes; the separate Discovery marker and View in map follow-up tracked at the time has now been closed in the current branch. Sanity Batch 3 - Bundle budgets and build stability (Completed): Add explicit bundle-size reporting and budgets, and keep the frontend build on a predictable asset graph that preserves editor stability. Packages: None. Automated checks: npm run build , npm run build:report , npm run test:unit . Manual review: Verify the map editor, storymap editor, Discovery and published maps still load after the chunking change. Approval notes: This batch changes build structure and measurement, not the public asset contract exposed by the plugin., The budget report is now part of the approval evidence for future frontend-heavy changes., Manual validation closed the batch after keeping the explicit budget tooling but settling on the more predictable asset graph that avoided the intermittent reload and template issues seen in existing map editors. Sanity Batch 4 - Logs, warnings and editor runtime cleanup (Completed): Remove repository-owned console noise, clean dead debug traces, document the remaining experimental Gutenberg render hook, and fix editor runtime issues uncovered during console-clean validation. Packages: None. Automated checks: npm run test:unit , npm run build , php scripts/check-php-compat.php . Manual review: Verify the editor flows touched in this track stay console-clean during normal use. Approval notes: The remaining install-time npm deprecation notices come from upstream tooling packages, not from repository-owned source., The current @wordpress/components line still requires __experimentalRenderItem for the customized async combobox rendering used by this project., Manual validation closed the batch after fixing the Onetime Map block's saved-attribute round-trip and guarding preview rendering when a selected layer record is temporarily unresolved. Sanity Batch 5 - Runtime hardening and request hygiene (Completed): Tighten geocoding and related request flows with nonce checks, capability checks, sanitized inputs, safer remote requests and cleaner error handling. Packages: None. Automated checks: php scripts/check-php-compat.php , vendor/bin/phpcs --standard=phpcs.xml.dist , WP_CLI_PHP=/opt/homebrew/opt/php@8.4/bin/php WP_DIR=/tmp/jeo-plugin-wordpress-smoke bash scripts/wordpress-smoke.sh . Manual review: Verify geocoding autosuggest, reverse geocoding and save or reopen flows still work normally., Verify public maps, embeds and storymaps touched by the hardening changes still bootstrap correctly. Approval notes: This batch is about safety and failure handling, not feature changes., The old REST tag collection broadening was removed so large term collections no longer bypass normal limits., The draggable-marker reliability issue that was noted after this batch has now been fixed in the current branch. Sanity Batch 6 - Pipeline and quality gate alignment (Completed): Align the release and CI pipelines with the same deterministic install, audit, build, bundle-budget, PHP compatibility, PHPCS and smoke-test gates used locally. Packages: None. Automated checks: npm ci , npm run test:unit , npm run build , npm run build:report , npm audit --package-lock-only , composer validate --no-check-publish , composer audit --locked , php scripts/check-php-compat.php , vendor/bin/phpcs --standard=phpcs.xml.dist , WP_CLI_PHP=/opt/homebrew/opt/php@8.4/bin/php WP_DIR=/tmp/jeo-plugin-wordpress-smoke bash scripts/wordpress-smoke.sh . Manual review: None. Approval notes: This batch closes the process loop for the new sanity track., PHP 8.5 stays monitored in the process documentation but is not promoted to a blocking smoke gate while wp-cli still lacks stable upstream support for that runtime., A full rerun of the documented gates confirmed that the sanity-track changes did not reintroduce repository-owned warnings, vulnerabilities or failing quality checks. Sanity Batch 7 - Override governance and maintenance (Completed): Inventory every npm override, record its reason and removal criteria, and re-review the set whenever @wordpress/scripts, the Gutenberg dependency tree or another major frontend refresh changes the upstream graph. Packages: None. Automated checks: node scripts/dependency-review.mjs --write , npm audit --package-lock-only , npm ls react-autosize-textarea react react-dom . Manual review: Confirm that each override still solves a current problem, that its rationale is documented, and that anything already absorbed upstream is removed. Approval notes: This batch is governance-oriented and should not change runtime behavior unless an obsolete override is removed or replaced., Use it after significant @wordpress/scripts or Gutenberg refreshes so override drift does not become silent technical debt., The dependency-review script now validates that every active package.json override has governance metadata and that no stale governance entries survive after an override is removed.","title":"Execution plan"},{"location":"dev/dependency-upgrade-report/#execution-log","text":"2026-03-16 - Phase 0 - Baseline and tracking scaffolding (Completed): Added phase planning, package-level execution tracking and the phased approval checklist without changing dependency versions. Automated checks: node --check scripts/dependency-review.mjs: Passed, npm run deps:report: Passed (Regenerated the JSON and Markdown reports against current npm and Packagist metadata.). Manual review: None. Documents updated: docs/dev/dependency-upgrade-report.json , docs/dev/dependency-upgrade-report.md , docs/dev/dependency-upgrade-smoke-tests.md . 2026-03-16 - Phase 1 - Frontend gates for approval (Completed): Added a Node frontend GitHub Actions workflow and promoted the PHP compatibility plus WordPress smoke workflows to the mandatory gate set for future dependency phases. Automated checks: npm ci: Passed (Installed the existing lockfile successfully. The run repeated the already tracked react-autosize-textarea peer warnings and the known deprecation notices for react-beautiful-dnd and react-jsonschema-form.), npm run build: Passed (wp-scripts build completed successfully with the existing bundle-size warnings only.). Manual review: Confirm the Node frontend, PHP Compatibility and WordPress Smoke Tests workflows trigger on push and pull_request events.. Documents updated: .github/workflows/node-frontend.yml , README.md , docs/dev/dependency-upgrade-report.json , docs/dev/dependency-upgrade-report.md , docs/dev/dependency-upgrade-smoke-tests.md . 2026-03-16 - Phase 2 - Build tooling major (Completed): Upgraded @wordpress/scripts from 30.27.0 to 31.6.0. The existing webpack.config.js override remained compatible and no script changes were required. Automated checks: npm install @wordpress/scripts@^31.6.0 --save-dev: Passed (Updated the direct dependency and lockfile to @wordpress/scripts 31.6.0 while repeating only the already known react-autosize-textarea peer warnings.), npm run build: Passed (wp-scripts build completed successfully with the same bundle-size warnings as before the upgrade.), php scripts/check-php-compat.php: Passed (Static PHP 8.0-8.4 compatibility review found no blocking issues after the tooling upgrade.), WP_DIR=/tmp/jeo-plugin-wordpress-smoke bash scripts/wordpress-smoke.sh: Failed (The local runtime smoke attempt reached wp-cli and WordPress download, but failed against the local MariaDB login for wordpress@localhost. The GitHub WordPress Smoke Tests workflow remains the authoritative gate for this check.). Manual review: No dedicated manual review unless asset or wp-admin regressions become visible.. Documents updated: package.json , package-lock.json , docs/dev/dependency-upgrade-report.json , docs/dev/dependency-upgrade-report.md . 2026-03-16 - Phase 3 - Minimum JS test layer (Completed): Added the minimum JS test layer with focused coverage for date-selector propagation, sidebar attribution normalization and Eta template compilation, and made the Node workflow run npm run test:unit as a required step. Automated checks: npm run test:unit: Passed (The new wp-scripts unit suite passed with 3 test files and 9 assertions.), npm run build: Passed (wp-scripts build completed successfully with the existing bundle-size warnings only.), php scripts/check-php-compat.php: Passed (Static PHP 8.0-8.4 compatibility review remained green after the frontend test-layer changes.), WP_DIR=/tmp/jeo-plugin-wordpress-smoke bash scripts/wordpress-smoke.sh: Passed (The local runtime smoke passed after configuring the local MariaDB wordpress database/user and temporarily applying the separate PR #501 fix that removes the invalid cli_init callback registration from src/includes/class-jeo.php. The script now also honors WP_CLI_PHP so Homebrew wp-cli setups can run the same smoke on PHP 8.4 without the PHP 8.5 deprecation noise. class-jeo.php stays out of the Phase 3 commit because it already has its own PR.). Manual review: Confirm the new test coverage protects date selector propagation, sidebar form normalization and Eta template compilation.. Documents updated: .github/workflows/node-frontend.yml , README.md , docs/dev/php-compatibility.md , package.json , scripts/wordpress-smoke.sh , docs/dev/dependency-upgrade-report.json , docs/dev/dependency-upgrade-report.md . 2026-03-16 - Phase 4 - Date picker major (Completed): Upgraded react-datepicker from 7.6.0 to 9.1.0. The existing date persistence helpers continued to satisfy the component contract without source changes. Automated checks: npm install react-datepicker@^9.1.0: Passed (Updated the direct dependency and lockfile to react-datepicker 9.1.0 while repeating only the already tracked react-autosize-textarea peer warnings.), npm run test:unit: Passed (The focused wp-scripts unit suite remained green with 3 test files and 9 assertions after the date picker major.), npm run build: Passed (wp-scripts build completed successfully with the existing bundle-size warnings plus one new webpack critical-dependency warning from react-datepicker's optional date-fns-tz require path.), php scripts/check-php-compat.php: Passed (Static PHP 8.0-8.4 compatibility review remained green after the frontend dependency update.), WP_CLI_PHP=/opt/homebrew/opt/php@8.4/bin/php WP_DIR=/tmp/jeo-plugin-wordpress-smoke bash scripts/wordpress-smoke.sh: Passed (The local runtime smoke passed with the PHP 8.4 wp-cli override and the same temporary PR #501 class-jeo.php patch used for local validation only.). Manual review: Select start and end dates in the related-posts sidebar., Save the post, reload the editor and verify persisted values., Clear dates and confirm empty values stay cleared after saving.. Documents updated: package.json , package-lock.json , docs/dev/dependency-upgrade-report.json , docs/dev/dependency-upgrade-report.md . 2026-03-16 - Phase 5 - Template runtime major (Completed): Upgraded eta from 3.5.0 to 4.5.1. The frontend helper now imports the browser-safe eta/core entry and the Jest config transforms that subpath for the focused template tests. Automated checks: npm install eta@^4.4.1: Passed (Updated the direct dependency and lockfile to eta 4.5.1 while repeating only the already tracked react-autosize-textarea peer warnings.), npm run test:unit: Passed (The focused wp-scripts unit suite remained green with 3 test files and 9 assertions after pointing the helper at eta/core and teaching Jest to transform eta/dist/core.js.), npm run build: Passed (wp-scripts build completed successfully after switching the frontend helper to eta/core. The existing bundle-size warnings remain, as does the known react-datepicker optional date-fns-tz warning from Phase 4.), php scripts/check-php-compat.php: Passed (Static PHP 8.0-8.4 compatibility review remained green after the Eta major update.), WP_CLI_PHP=/opt/homebrew/opt/php@8.4/bin/php WP_DIR=/tmp/jeo-plugin-wordpress-smoke bash scripts/wordpress-smoke.sh: Passed (The local runtime smoke passed with the PHP 8.4 wp-cli override and the same temporary PR #501 class-jeo.php patch used for local validation only.). Manual review: Open a map popup and verify the title, date and featured image rendering., Open the map Info panel and verify template-driven content still renders., Verify any locally customized popup template still compiles.. Documents updated: package.json , package-lock.json , jest.config.js , src/js/src/jeo-map/template-compiler.js , docs/dev/dependency-upgrade-report.json , docs/dev/dependency-upgrade-report.md . 2026-03-16 - Phase 6 - Leaflet sidebar major (Blocked): Evaluated the react-leaflet major and found it blocked by the repository's current React line before any code changes could be applied. Automated checks: npm view react-leaflet version peerDependencies --json: Passed (Confirmed that the current published major is react-leaflet 5.0.0 and that it declares peerDependencies on leaflet ^1.9.0 plus react and react-dom ^19.0.0.), node -e \"console.log(require('react/package.json').version)\": Passed (Confirmed that the installed runtime still resolves react 18.3.1.), node -e \"console.log(require('react-dom/package.json').version)\": Passed (Confirmed that the installed runtime still resolves react-dom 18.3.1.), npm install react-leaflet@^5.0.0 --dry-run: Failed (npm returned ERESOLVE because react-leaflet 5.0.0 requires react and react-dom ^19.0.0, which conflicts with the repository's current React 18.3.1 line.). Manual review: None. Documents updated: docs/dev/dependency-upgrade-report.json , docs/dev/dependency-upgrade-report.md . 2026-03-16 - Phase 7 - CKEditor React major evaluation (Completed): Upgraded @ckeditor/ckeditor5-react from 9.5.0 to 11.0.1. The wrapper stayed compatible with ckeditor5 47.6.1 and React 18.3.1 without requiring storymap editor source changes. Automated checks: npm view @ckeditor/ckeditor5-react version peerDependencies --json: Passed (Confirmed that the current published wrapper line is 11.0.1 and that its peer requirements accept ckeditor5 >=46.0.0 plus React 18.), npm install @ckeditor/ckeditor5-react@^11.0.1: Passed (Updated the direct dependency and lockfile to @ckeditor/ckeditor5-react 11.0.1 while repeating only the already tracked react-autosize-textarea peer warnings.), npm run test:unit: Passed (The focused wp-scripts unit suite remained green with 3 test files and 9 assertions after the CKEditor React wrapper major.), npm run build: Passed (wp-scripts build completed successfully with the existing bundle-size warnings and the known react-datepicker optional date-fns-tz warning from Phase 4.), php scripts/check-php-compat.php: Passed (Static PHP 8.0-8.4 compatibility review remained green after the CKEditor React wrapper update.), WP_CLI_PHP=/opt/homebrew/opt/php@8.4/bin/php WP_DIR=/tmp/jeo-plugin-wordpress-smoke bash scripts/wordpress-smoke.sh: Passed (The local runtime smoke passed with the PHP 8.4 wp-cli override and the same temporary PR #501 class-jeo.php patch used for local validation only.). Manual review: Load the storymap editor and confirm the toolbar initializes., Verify text selection formatting still applies and the selection is retained., Upload an image, save the storymap and reopen it.. Documents updated: package.json , package-lock.json , docs/dev/dependency-upgrade-report.json , docs/dev/dependency-upgrade-report.md . 2026-03-16 - Phase 8 - PHPCS major evaluation (Blocked): Evaluated the PHPCS major and found the upgrade blocked by WPCS compatibility and by the repository's current PHPCS baseline. Automated checks: composer validate --no-check-publish: Passed (The current composer.json is structurally valid.), composer show squizlabs/php_codesniffer --locked: Passed (Confirmed that the installed PHPCS line is 3.13.5.), composer show wp-coding-standards/wpcs --locked: Passed (Confirmed that the installed WPCS line is 3.3.0 and that it requires squizlabs/php_codesniffer ^3.13.4.), vendor/bin/phpcs --standard=phpcs.xml.dist: Failed (The current PHPCS 3.x run reports a large existing baseline of coding-standard violations across the repository, including scripts and template files, so the lint gate is not yet green even before attempting PHPCS 4.), php scripts/check-php-compat.php: Passed (Static PHP 8.0-8.4 compatibility review remained green during the PHPCS evaluation.). Manual review: None. Documents updated: docs/dev/dependency-upgrade-report.json , docs/dev/dependency-upgrade-report.md . 2026-03-16 - Batch 0 - Local prerequisites and report reset (Completed): Absorbed the validated local prerequisites for the stabilization track: the invalid cli_init hook no longer needs a local-only patch, the Composer metadata cleanup is part of the repository, and the dependency report now carries the new batch plan. Automated checks: composer validate --no-check-publish: Passed (The updated composer.json stays structurally valid after the SPDX and tooling-script cleanup.), php scripts/check-php-compat.php: Passed (Static PHP 8.0-8.4 compatibility review remained green after absorbing the local prerequisites.), WP_CLI_PHP=/opt/homebrew/opt/php@8.4/bin/php WP_DIR=/tmp/jeo-plugin-wordpress-smoke bash scripts/wordpress-smoke.sh: Passed (The local WordPress smoke now passes without any local-only patch to src/includes/class-jeo.php. In a clean worktree it still emits PHP warnings until the ignored src/js/build assets are generated locally.), node --check scripts/dependency-review.mjs: Passed, node scripts/dependency-review.mjs --write: Passed (Regenerated the JSON and Markdown dependency reports with the reopened stabilization track and Batch 0 ready for approval.). Manual review: None. Documents updated: composer.json , src/includes/class-jeo.php , scripts/dependency-review.mjs , scripts/dependency-review.overrides.json , docs/dev/dependency-upgrade-report.json , docs/dev/dependency-upgrade-report.md , docs/dev/dependency-upgrade-smoke-tests.md . 2026-03-16 - Batch 1 - Baseline fixes and warning cleanup (Completed): Fixed the null-related-post geocode crash in frontend and Discovery GeoJSON builders, replaced brittle same-point spiderfy timing with a navigable popup for posts that share a location, repaired storymap slide ordering with explicit move controls and removed the targeted react-datepicker webpack warning with focused regression tests. Automated checks: npm ci: Passed (A clean install completed successfully after rebuilding node_modules. The run still reports the tracked react-autosize-textarea peer warning plus the deprecated packages scheduled for Batches 2 through 4, but no new install warnings were introduced by Batch 1.), npm run test:unit: Passed (The focused wp-scripts unit suite now passes with 5 test files and 17 assertions, including new coverage for GeoJSON sanitization and storymap reorder helpers.), npm run build: Passed (wp-scripts build completed successfully from a clean install with only the existing bundle-size warnings. The previous react-datepicker optional date-fns-tz critical-dependency warning no longer appears.), php scripts/check-php-compat.php: Passed (Static PHP 8.0-8.4 compatibility review remained green after the frontend baseline fixes.), WP_CLI_PHP=/opt/homebrew/opt/php@8.4/bin/php WP_DIR=/tmp/jeo-plugin-wordpress-smoke bash scripts/wordpress-smoke.sh: Passed (The local runtime smoke passed after rebuilding the ignored src/js/build assets, so the previous asset-include warnings no longer appear in the smoke log.). Manual review: Published-map review confirmed the null-geocode crash disappeared and same-point posts can be navigated inside the popup, including dense clusters., Storymap review confirmed slide ordering, accordion behavior and save or reopen stayed stable after the UI changes.. Documents updated: scripts/dependency-review.overrides.json , docs/dev/dependency-upgrade-report.json , docs/dev/dependency-upgrade-report.md , docs/dev/dependency-upgrade-smoke-tests.md , src/js/src/discovery/blocks/stories.js , src/js/src/jeo-map/class-jeo-map.js , src/js/src/map-blocks/storymap-editor.js , src/js/src/map-blocks/storymap-editor.scss , src/js/src/map-blocks/storymap-ordering.js , src/js/src/map-blocks/storymap-ordering.test.js , src/js/src/shared/story-geojson.js , src/js/src/shared/story-geojson.test.js , webpack.config.js , src/css/jeo-map.scss . 2026-03-16 - Batch 2 - Autosuggest, date-range and form replacements (Completed): Replaced react-autosuggest with an internal async combobox wrapper, moved Discovery date filtering off react-bootstrap-daterangepicker onto a react-datepicker-based internal component, and replaced react-jsonschema-form with an internal @rjsf/core adapter while preserving the existing query and formData contracts. Automated checks: npm ci: Passed (A clean reinstall completed successfully after removing the locally corrupted node_modules tree left by an earlier ENOTEMPTY failure. The run no longer emits deprecation warnings for react-autosuggest, react-bootstrap-daterangepicker or react-jsonschema-form; the remaining install noise is the tracked react-beautiful-dnd deprecation, the react-autosize-textarea peer warning and broader transitive npm ecosystem deprecations.), npm run test:unit: Passed (The wp-scripts unit suite passed with 8 test files and 25 assertions, including new coverage for the async combobox helper, the schema-form adapter and the Discovery date-range wrapper.), npm run build: Passed (wp-scripts build completed successfully after the replacements with only the existing bundle-size warnings.), php scripts/check-php-compat.php: Passed (Static PHP 8.0-8.4 compatibility review remained green after the frontend replacement batch.), WP_CLI_PHP=/opt/homebrew/opt/php@8.4/bin/php WP_DIR=/tmp/jeo-plugin-wordpress-smoke bash scripts/wordpress-smoke.sh: Passed (The local runtime smoke stayed green after replacing the autosuggest, date-range and schema-form helpers.). Manual review: Verify geocoding autosuggest suggestions load, typing stays responsive and selecting a result updates the field value correctly., Verify the post or map autosuggest search still surfaces suggestions and inserts the selected entity correctly., Verify Discovery date apply and clear flows still update the after and before query values as expected., Verify layer and attribution forms render, normalize formData and save correctly with the new schema-form adapter.. Documents updated: package.json , package-lock.json , jest.config.js , scripts/dependency-review.overrides.json , docs/dev/dependency-upgrade-report.json , docs/dev/dependency-upgrade-report.md , docs/dev/dependency-upgrade-smoke-tests.md , src/js/src/discovery/blocks/date-range-filter.js , src/js/src/discovery/blocks/date-range-filter.test.js , src/js/src/discovery/blocks/stories.js , src/js/src/discovery/style/discovery.scss , src/js/src/layers-sidebar/attribution-settings.js , src/js/src/layers-sidebar/layer-settings.js , src/js/src/map-blocks/jeo-autosuggest.js , src/js/src/map-blocks/map-editor.css , src/js/src/posts-sidebar/geo-auto-complete.css , src/js/src/posts-sidebar/geo-auto-complete.js , src/js/src/shared/async-combobox-control.js , src/js/src/shared/async-combobox-control.test.js , src/js/src/shared/schema-form.js , src/js/src/shared/schema-form.test.js , src/js/src/shared/wp-form-controls.js . 2026-03-17 - Batch 3 - Drag-and-drop consolidation (Completed): Removed react-beautiful-dnd, migrated the remaining map-layer ordering UI to react-movable and removed the accidental nested storymap layer drag affordance while keeping the supported slide ordering and layer toggle flows intact. Automated checks: npm ci: Passed (A clean reinstall completed successfully after clearing the locally stuck node_modules tree. The prior react-beautiful-dnd deprecation warning disappeared; the remaining install noise is the tracked react-autosize-textarea peer warning plus broader transitive npm ecosystem deprecations.), npm run test:unit: Passed (The wp-scripts unit suite remained green with 8 test files and 25 assertions after the drag-and-drop migration.), npm run build: Passed (wp-scripts build completed successfully after the migration with only the existing bundle-size warnings. The emitted mapsSidebar bundle is smaller now that react-beautiful-dnd is gone.), php scripts/check-php-compat.php: Passed (Static PHP 8.0-8.4 compatibility review remained green after the frontend drag-and-drop changes.), WP_CLI_PHP=/opt/homebrew/opt/php@8.4/bin/php WP_DIR=/tmp/jeo-plugin-wordpress-smoke bash scripts/wordpress-smoke.sh: Passed (The local runtime smoke stayed green after moving the remaining reorder flows to react-movable.). Manual review: Verify selected map-layer ordering in the map editor modal, including saving and reopening after reordering., Verify storymap slide ordering still works alongside the existing move up/down controls., Verify storymap slide layer toggling still works without exposing a reorder affordance that the product never supported.. Documents updated: package.json , package-lock.json , scripts/dependency-review.overrides.json , docs/dev/dependency-upgrade-report.json , docs/dev/dependency-upgrade-report.md , docs/dev/dependency-upgrade-smoke-tests.md , src/js/src/map-blocks/layer-settings.js , src/js/src/map-blocks/layers-settings.js , src/js/src/map-blocks/storymap-editor.js , src/js/src/map-blocks/storymap-editor.scss . 2026-03-17 - Batch 4 - Gutenberg and React alignment (Completed): Confirmed that the currently installed Gutenberg editor packages are already on the latest published line, applied a local react-autosize-textarea override to remove the stale React peer warning, and documented that react-leaflet 5 remains blocked by upstream React 18 peer ranges. Automated checks: npm ci: Passed (A clean reinstall completed successfully after rebuilding node_modules from scratch. The prior react-autosize-textarea peer warning disappeared; the remaining install noise is broader transitive npm ecosystem deprecations such as inflight, rimraf, glob and eslint support notices.), npm ls react-autosize-textarea react react-dom: Passed (The installed tree now resolves react-autosize-textarea@7.1.0-jeo.0 from npm-overrides/react-autosize-textarea under the Gutenberg editor packages, with React 18.3.1 and ReactDOM 18.3.1 accepted cleanly.), npm run test:unit: Passed (The wp-scripts unit suite remained green with 8 test files and 25 assertions after the local override and lockfile refresh.), npm run build: Passed (wp-scripts build completed successfully after the override with only the existing bundle-size warnings.), php scripts/check-php-compat.php: Passed (Static PHP 8.0-8.4 compatibility review remained green; Batch 4 did not change any PHP source.), WP_CLI_PHP=/opt/homebrew/opt/php@8.4/bin/php WP_DIR=/tmp/jeo-plugin-wordpress-smoke bash scripts/wordpress-smoke.sh: Passed (The local runtime smoke stayed green after the lockfile refresh and override application.). Manual review: Verify the map editor, layer editor, storymap editor and regular post editor load, save and stay console-clean.. Documents updated: package.json , package-lock.json , npm-overrides/react-autosize-textarea/package.json , npm-overrides/react-autosize-textarea/lib/index.js , npm-overrides/react-autosize-textarea/lib/TextareaAutosize.js , npm-overrides/react-autosize-textarea/lib/index.d.ts , npm-overrides/react-autosize-textarea/lib/TextareaAutosize.d.ts , scripts/dependency-review.overrides.json , docs/dev/dependency-upgrade-report.json , docs/dev/dependency-upgrade-report.md , docs/dev/dependency-upgrade-smoke-tests.md . 2026-03-17 - Batch 6 - PHP 8.5 compatibility (Completed): Extended the repository-owned PHP compatibility checks and CI matrix to PHP 8.5, documented that WordPress core already treats PHP 8.5 as beta support while stable wp-cli support is still planned upstream, and verified that local WordPress smoke stays green on PHP 8.4 and PHP 8.5 with only third-party wp-cli deprecation noise under PHP 8.5. Automated checks: composer validate --no-check-publish: Passed (The current composer.json remains structurally valid.), php scripts/check-php-compat.php on PHP 8.0 through 8.5: Passed (The static compatibility review stayed green on Homebrew PHP 8.0, 8.1, 8.2, 8.3, 8.4 and 8.5 after extending the script and report wording to PHP 8.5.), WP_CLI_PHP=/opt/homebrew/opt/php@8.4/bin/php WP_DIR=/tmp/jeo-plugin-wordpress-smoke bash scripts/wordpress-smoke.sh: Passed (The local runtime smoke remains green on PHP 8.4, which is still the cleanest local signal because wp-cli itself does not emit PHP 8.5 deprecation noise on this line.), WP_CLI_PHP=/opt/homebrew/bin/php WP_DIR=/tmp/jeo-plugin-wordpress-smoke-85 bash scripts/wordpress-smoke.sh: Passed (The local runtime smoke also passes on PHP 8.5. The log still contains third-party deprecations from wp-cli bundled libraries, but no repository-owned PHP 8.5 warnings or deprecations surfaced.). Manual review: Repeat the runtime smoke while watching specifically for repository-owned PHP 8.5 warnings or deprecations, not third-party wp-cli noise.. Documents updated: .github/workflows/php-compat.yml , .github/workflows/wordpress-smoke.yml , README.md , docs/dev/php-compatibility.md , src/readme.txt , trunk/readme.txt , scripts/check-php-compat.php , scripts/dependency-review.overrides.json , docs/dev/dependency-upgrade-report.json , docs/dev/dependency-upgrade-report.md , docs/dev/dependency-upgrade-smoke-tests.md . 2026-03-17 - Batch 7 - PHP lint stack cleanup (Completed): Cleaned the PHPCS 3.x baseline across scripts, templates and runtime PHP so the repository is now green on the current WPCS or PHPCS stack, while keeping the PHPCS 4 evaluation blocked until upstream WPCS support lands. Automated checks: composer validate --no-check-publish: Passed (The current composer.json remains structurally valid during the PHPCS cleanup batch.), php scripts/check-php-compat.php: Passed (The repository-owned PHP compatibility scanner stayed green after the lint-driven cleanup.), vendor/bin/phpcs --standard=phpcs.xml.dist: Passed (The PHPCS 3.x baseline is now clean across the repository after autofixes plus manual cleanup of docblocks, escaping, naming and query-input handling.), WP_CLI_PHP=/opt/homebrew/opt/php@8.4/bin/php WP_DIR=/tmp/jeo-plugin-wordpress-smoke bash scripts/wordpress-smoke.sh: Passed (Local WordPress smoke stayed green after the runtime-touching PHP cleanup, confirming that embeds, maps and story maps still bootstrap correctly.), node scripts/dependency-review.mjs --write: Passed (Regenerated the JSON and Markdown dependency reports with Batch 7 marked as completed and the PHPCS 4 path still blocked by WPCS support.). Manual review: None. Documents updated: scripts/dependency-review.overrides.json , docs/dev/dependency-upgrade-report.json , docs/dev/dependency-upgrade-report.md , docs/dev/dependency-upgrade-smoke-tests.md , scripts/check-php-compat.php , scripts/wordpress-smoke-check.php , src/includes/class-jeo.php , src/includes/geocode/class-geocode-handler.php , src/includes/geocode/class-geocoder.php , src/includes/geocode/geocoders/class-nominatim.php , src/includes/index.php , src/includes/layer-types/class-layer-types.php , src/includes/layers/class-layers.php , src/includes/legend-types/class-legend-types.php , src/includes/loaders.php , src/includes/maps/class-maps.php , src/includes/menu/class-menu.php , src/includes/settings/class-settings.php , src/includes/settings/settings-page.php , src/includes/sidebars/class-sidebars.php , src/includes/storymap/class-storymap.php , src/includes/traits/class-rest-validate-meta.php , src/includes/traits/class-singleton.php , src/index.php , src/jeo.php , src/templates/discovery.php , src/templates/embed-discovery.php , src/templates/embed-storymap.php , src/templates/embed.php , src/templates/map-content-layers-list.php , src/templates/single-map.php , src/templates/single-storymap.php . 2026-03-17 - Sanity Batch 0 - Runtime policy and reproducibility (Completed): Promoted Node 24 to the project runtime, added an explicit environment preflight, and aligned the release workflow with deterministic installs and the same baseline used by the frontend CI gates. Automated checks: npm run check:env: Passed (The preflight now enforces Node 24 as the supported frontend runtime for this repository.), npm ci: Passed (A clean reinstall completed successfully under Node 24 with the refreshed lockfile.), npm run build:report: Passed (The bundle report now runs as part of the documented frontend approval path.). Manual review: None. Documents updated: .nvmrc , package.json , .github/workflows/node-frontend.yml , .github/workflows/deploy-wordpress-org.yml , README.md , scripts/check-node-version.mjs , scripts/dependency-review.overrides.json , docs/dev/dependency-upgrade-report.json , docs/dev/dependency-upgrade-report.md , docs/dev/dependency-upgrade-smoke-tests.md . 2026-03-17 - Sanity Batch 1 - npm health and security (Completed): Refreshed the Node 24 dependency tree, removed the known js-yaml vulnerability, documented audit coverage as a first-class maintenance gate, and kept npm and Composer audit outputs clean. Automated checks: npm audit --package-lock-only: Passed (The production and development dependency graph is now free of known advisories at the lockfile level.), npm audit --omit=dev: Passed (The installable runtime dependency tree is clean.), composer audit --locked: Passed (Composer dependencies remain free of known advisories.). Manual review: None. Documents updated: package.json , package-lock.json , scripts/dependency-review.overrides.json , docs/dev/dependency-upgrade-report.json , docs/dev/dependency-upgrade-report.md , docs/dev/dependency-upgrade-smoke-tests.md . 2026-03-17 - Sanity Batch 2 - Incremental loading for layers, maps and taxonomies (Completed): Replaced unbounded collection loading with chunked hydration by selected IDs, paginated layer libraries, async taxonomy lookup and lazy Discovery map hydration without dropping already-selected data. Manual validation then fixed the map-editor reload regression, the Discovery lazy-load crashes, and the later Discovery marker plus View in map follow-up that had been tracked separately at the time. Automated checks: npm run test:unit: Passed (The new shared rest-record helpers and the existing JS coverage stayed green after the loading-strategy rewrite.), npm run build: Passed (The frontend bundles compile successfully after replacing per_page -1 style loading with chunked and paginated flows.), WP_CLI_PHP=/opt/homebrew/opt/php@8.4/bin/php WP_DIR=/tmp/jeo-plugin-wordpress-smoke bash scripts/wordpress-smoke.sh: Passed (Local runtime smoke stayed green after the Discovery and editor data-loading changes.). Manual review: Verify exact selected layers still appear in map, onetime map, storymap and sidebar flows., Verify paginated layer search and Load more behavior in the map editor modal., Verify Discovery map loading, shared-link hydration and async taxonomy selectors.. Documents updated: src/js/src/shared/rest-records.js , src/js/src/shared/rest-records.test.js , src/js/src/map-blocks/map-editor.js , src/js/src/map-blocks/onetime-map-editor.js , src/js/src/map-blocks/map-editor-preview.js , src/js/src/map-blocks/layers-settings.js , src/js/src/map-blocks/storymap-editor.js , src/js/src/maps-sidebar/maps-sidebar.js , src/js/src/discovery/blocks/map-layers.js , src/js/src/discovery/blocks/stories.js , src/js/src/discovery/blocks/tag-filter-control.js , src/js/src/discovery/style/discovery.scss , src/js/src/posts-selector/index.js , src/js/src/posts-selector/async-tokens-selector.js , src/includes/class-jeo.php , scripts/dependency-review.overrides.json , docs/dev/dependency-upgrade-report.json , docs/dev/dependency-upgrade-report.md , docs/dev/dependency-upgrade-smoke-tests.md . 2026-03-17 - Sanity Batch 3 - Bundle budgets and build stability (Completed): Added an explicit bundle-size report and budgets, then finalized a more predictable asset graph after validation showed intermittent reload issues with the more aggressive chunking setup. Automated checks: npm run build: Passed (The webpack build remains green after the budget and asset-graph adjustments.), npm run build:report: Passed (Current budget highlights: mapBlocks 1092 of 1150 KB, discovery 241 of 280 KB and jeoMap 79 of 120 KB.). Manual review: Verify the main editors, Discovery and published maps still load correctly after the asset graph changes.. Documents updated: webpack.config.js , scripts/report-bundle-sizes.mjs , src/js/src/map-blocks/index.js , src/js/src/map-blocks/onetime-map-editor.js , src/js/src/map-blocks/onetime-map-config.js , src/js/src/map-blocks/onetime-map-config.test.js , scripts/dependency-review.overrides.json , docs/dev/dependency-upgrade-report.json , docs/dev/dependency-upgrade-report.md , docs/dev/dependency-upgrade-smoke-tests.md . 2026-03-17 - Sanity Batch 4 - Logs, warnings and editor runtime cleanup (Completed): Removed repository-owned console noise and dead debug traces, documented the remaining async combobox dependency on the current experimental Gutenberg render hook, and fixed the editor-side runtime errors surfaced during manual validation. Automated checks: npm run test:unit: Passed (The editor-side cleanup did not change the passing JS test suite.), npm run build: Passed (The build stayed green with no new repository-owned warnings.), php scripts/check-php-compat.php: Passed (The JS cleanup did not affect PHP compatibility checks.). Manual review: Verify normal editor use stays console-clean after the cleanup.. Documents updated: src/js/src/shared/async-combobox-control.js , src/js/src/layers-sidebar/interactions-settings.js , src/js/src/posts-sidebar/legends-editor/editors/circle.js , src/js/src/posts-sidebar/legends-editor/legend-editor.js , src/includes/legend-types/JeoLegend.js , src/js/src/discovery/blocks/stories.js , src/js/src/discovery/index.js , src/js/src/map-blocks/index.js , src/js/src/map-blocks/onetime-map-editor.js , src/js/src/map-blocks/onetime-map-config.js , src/js/src/map-blocks/onetime-map-config.test.js , scripts/dependency-review.overrides.json , docs/dev/dependency-upgrade-report.json , docs/dev/dependency-upgrade-report.md , docs/dev/dependency-upgrade-smoke-tests.md . 2026-03-17 - Sanity Batch 5 - Runtime hardening and request hygiene (Completed): Hardened geocoding and adjacent runtime request paths with nonce and capability checks, safer remote-request settings, cleaner JSON responses and more defensive client-side handling; the later draggable-marker reliability and accented-search fixes are now closed in the current branch. Automated checks: php scripts/check-php-compat.php: Passed (Repository-owned PHP checks stayed green after the request hardening changes.), vendor/bin/phpcs --standard=phpcs.xml.dist: Passed (The hardened request and sanitization paths remain clean under the current PHPCS stack.), WP_CLI_PHP=/opt/homebrew/opt/php@8.4/bin/php WP_DIR=/tmp/jeo-plugin-wordpress-smoke bash scripts/wordpress-smoke.sh: Passed (The local runtime smoke stayed green after the geocoding and bootstrap hardening work.). Manual review: Verify geocoding autosuggest, reverse geocoding and save or reopen flows., Verify embeds and public maps touched by bootstrap changes still load.. Documents updated: src/includes/class-jeo.php , src/includes/geocode/class-geocode-handler.php , src/includes/geocode/geocoders/class-nominatim.php , src/js/src/posts-sidebar/geo-auto-complete.js , src/js/src/posts-sidebar/geo-posts.js , scripts/dependency-review.overrides.json , docs/dev/dependency-upgrade-report.json , docs/dev/dependency-upgrade-report.md , docs/dev/dependency-upgrade-smoke-tests.md . 2026-03-17 - Sanity Batch 6 - Pipeline and quality gate alignment (Completed): Closed the sanity-track loop by aligning release and CI expectations with the same install, audit, bundle-budget, PHP compatibility, PHPCS and smoke-test gates now documented for local maintenance, and confirmed them again in a full rerun after the late editor fixes. Automated checks: composer validate --no-check-publish: Passed (The current Composer metadata remains structurally valid.), npm ci: Passed (A full reinstall under Node 24 completed successfully; the remaining install-time notices are the same upstream tooling deprecations already tracked for future review.), npm run test:unit: Passed (All current frontend unit tests passed again after the final editor and geocoding-related fixes.), npm run build: Passed (The frontend build remains green after the final sanity-track fixes, with no repository-owned warnings.), npm run build:report: Passed (Bundle budgets remain within limits, including mapBlocks 1092 of 1150 KB, discovery 241 of 280 KB and jeoMap 79 of 120 KB.), composer audit --locked: Passed (Composer dependencies remain free of known advisories.), npm audit --package-lock-only: Passed (The npm lockfile remains free of known advisories after the full sanity-track refresh.), npm audit --omit=dev: Passed (The installed runtime dependency tree remains free of known npm advisories.), npm ls react-autosize-textarea react react-dom: Passed (The frontend runtime tree remains aligned on React 18.3.1 with the local react-autosize-textarea override applied cleanly.), php scripts/check-php-compat.php: Passed (Static compatibility checks remain green for PHP 8.0 through 8.5.), vendor/bin/phpcs --standard=phpcs.xml.dist: Passed (The repository stays clean on the current PHPCS and WPCS line.), WP_CLI_PHP=/opt/homebrew/opt/php@8.4/bin/php WP_DIR=/tmp/jeo-plugin-wordpress-smoke bash scripts/wordpress-smoke.sh: Passed (The runtime smoke remains green at the end of the full sanity track.). Manual review: None. Documents updated: .github/workflows/deploy-wordpress-org.yml , .github/workflows/node-frontend.yml , README.md , scripts/dependency-review.overrides.json , docs/dev/dependency-upgrade-report.json , docs/dev/dependency-upgrade-report.md , docs/dev/dependency-upgrade-smoke-tests.md . 2026-03-17 - Sanity Batch 7 - Override governance and maintenance (Completed): Added an explicit npm override inventory with reason, review triggers and removal criteria for every active override, and made the dependency-review script fail when the governance map falls out of sync with package.json. Automated checks: node scripts/dependency-review.mjs --write: Passed (The report generator now renders a dedicated override-governance section and verifies that every active override is documented.), npm audit --package-lock-only: Passed (The current override set still keeps the lockfile free of known advisories.), npm ls react-autosize-textarea react react-dom: Passed (The local react-autosize-textarea override still resolves cleanly against React 18.3.1 and ReactDOM 18.3.1.). Manual review: Confirm that each active override still has a clear reason, a review trigger and an actionable removal criterion.. Documents updated: scripts/dependency-review.mjs , scripts/dependency-review.overrides.json , docs/dev/dependency-upgrade-report.json , docs/dev/dependency-upgrade-report.md .","title":"Execution log"},{"location":"dev/dependency-upgrade-report/#non-maintained-or-high-risk-package-lines","text":"None flagged in the current snapshot.","title":"Non-maintained or high-risk package lines"},{"location":"dev/dependency-upgrade-report/#npm-direct-dependencies","text":"Package Type Declared / Resolved Latest compatible Latest major Maintenance Impact Decision Batch Execution Evidence Usage @ckeditor/ckeditor5-react Storymap editor ^11.0.1 -> 11.0.1 Already on the latest compatible line No newer major detected Maintained (latest published 2025-12-18) CKEditor runtime inside storymap editing flows Keep current version Phase 7 Completed npm Repository package.json src/js/src/map-blocks/storymap-editor.js @nazka/map-gl-js-spiderfy Map rendering ^1.2.10 -> 1.2.10 Already on the latest compatible line No newer major detected Maintained (latest published 2025-09-03) Frontend maps, editor previews, Mapbox/MapLibre runtime compatibility Keep current version Monitor Tracking only npm Repository package.json src/js/src/jeo-map/class-jeo-map.js @rjsf/core UI / helper ^6.4.1 -> 6.4.1 Already on the latest compatible line No newer major detected Maintained (latest published 2026-03-07) Browser runtime and React-driven editor UI behavior Keep current version Monitor Tracking only npm Repository package.json src/js/src/shared/schema-form.js @rjsf/validator-ajv8 UI / helper ^6.4.1 -> 6.4.1 Already on the latest compatible line No newer major detected Maintained (latest published 2026-03-07) Browser runtime and React-driven editor UI behavior Keep current version Monitor Tracking only npm Repository package.json src/js/src/shared/schema-form.js @wordpress/edit-post Gutenberg / editor ^8.41.0 -> 8.41.0 Already on the latest compatible line No newer major detected Maintained (latest published 2026-03-04) Node 24 build pipeline, Gutenberg editor runtime and React peer alignment Keep current version Batch 4 Completed npm Repository package.json @wordpress/editor Gutenberg / editor ^14.41.0 -> 14.41.0 Already on the latest compatible line No newer major detected Maintained (latest published 2026-03-04) Node 24 build pipeline, Gutenberg editor runtime and React peer alignment Keep current version Batch 4 Completed npm Repository package.json src/js/src/posts-sidebar/index.js src/js/src/layers-sidebar/layers-sidebar.js src/js/src/maps-sidebar/map-embed-url.js @wordpress/plugins Gutenberg / editor ^7.41.0 -> 7.41.0 Already on the latest compatible line No newer major detected Maintained (latest published 2026-03-04) Node 24 build pipeline, Gutenberg editor runtime and React peer alignment Keep current version Batch 4 Completed npm Repository package.json src/js/src/posts-sidebar/index.js src/js/src/maps-sidebar/index.js src/js/src/layers-sidebar/index.js @wordpress/scripts Build tooling ^31.6.0 -> 31.6.0 Already on the latest compatible line No newer major detected Maintained (latest published 2026-03-04) Asset compilation, webpack config and CI reproducibility on Node 24 Keep current version Phase 2 Completed npm Repository package.json webpack.config.js ajv Build tooling ^8.18.0 -> 8.18.0 Already on the latest compatible line No newer major detected Maintained (latest published 2026-02-14) Asset compilation, webpack config and CI reproducibility on Node 24 Keep current version Monitor Tracking only npm Repository package.json src/js/src/shared/schema-form.js ckeditor5 Storymap editor ^47.6.1 -> 47.6.1 Already on the latest compatible line No newer major detected Maintained (latest published 2026-03-11) CKEditor runtime inside storymap editing flows Keep current version Monitor Tracking only npm Repository package.json src/js/src/map-blocks/storymap-editor.js classnames UI / helper ^2.5.1 -> 2.5.1 Already on the latest compatible line No newer major detected Maintained (latest published 2023-12-29) Browser runtime and React-driven editor UI behavior Keep current version Monitor Tracking only npm Repository package.json src/js/src/posts-sidebar/geo-posts.js src/js/src/posts-selector/interval-selector.js src/js/src/jeo-storymap/storymap-display.js css-loader Build tooling ^7.1.4 -> 7.1.4 Already on the latest compatible line No newer major detected Maintained (latest published 2026-02-16) Asset compilation, webpack config and CI reproducibility on Node 24 Keep current version Monitor Tracking only npm Repository package.json eta UI / helper ^4.5.1 -> 4.5.1 Already on the latest compatible line No newer major detected Maintained (latest published 2026-02-05) Browser runtime and React-driven editor UI behavior Keep current version Phase 5 Completed npm Repository package.json src/js/src/jeo-map/class-jeo-map.js src/templates/post-popup.ejs src/js/src/jeo-map/template-compiler.js leaflet Map rendering ^1.9.4 -> 1.9.4 Already on the latest compatible line No newer major detected Maintained (latest published 2023-05-18) Frontend maps, editor previews, Mapbox/MapLibre runtime compatibility Keep current version Monitor Tracking only npm Repository package.json src/js/src/posts-sidebar/geo-posts.js lodash-es UI / helper ^4.17.23 -> 4.17.23 Already on the latest compatible line No newer major detected Maintained (latest published 2026-01-21) Browser runtime and React-driven editor UI behavior Keep current version Monitor Tracking only npm Repository package.json src/js/src/posts-sidebar/geo-auto-complete.js src/js/src/posts-sidebar/geo-posts.js src/js/src/map-blocks/map-preview-layer.js map-gl-utils Map rendering ^0.50.4 -> 0.50.4 Already on the latest compatible line No newer major detected Maintained (latest published 2025-01-21) Frontend maps, editor previews, Mapbox/MapLibre runtime compatibility Keep current version Monitor Tracking only npm Repository package.json src/js/src/lib/mapboxgl/mapboxgl-loader.js src/js/src/lib/maplibregl/maplibregl-loader.js mapbox-gl Map rendering ^3.20.0 -> 3.20.0 Already on the latest compatible line No newer major detected Maintained (latest published 2026-03-12) Frontend maps, editor previews, Mapbox/MapLibre runtime compatibility Keep current version Monitor Tracking only npm Repository src/readme.txt package.json src/js/src/lib/mapboxgl/mapboxgl-loader.js maplibre-gl Map rendering ^5.20.1 -> 5.20.1 Update to 5.20.2 No newer major detected Maintained (latest published 2026-03-17) Frontend maps, editor previews, Mapbox/MapLibre runtime compatibility Candidate for the minor batch Monitor Tracking only npm Repository src/readme.txt package.json src/js/src/lib/maplibregl/maplibregl-loader.js maplibregl-mapbox-request-transformer Map rendering ^0.0.3 -> 0.0.3 Already on the latest compatible line No newer major detected Maintained (latest published 2025-08-08) Frontend maps, editor previews, Mapbox/MapLibre runtime compatibility Keep current version Monitor Tracking only npm Repository package.json src/js/src/lib/maplibregl/maplibregl-loader.js postcss Build tooling ^8.5.8 -> 8.5.8 Already on the latest compatible line No newer major detected Maintained (latest published 2026-03-02) Asset compilation, webpack config and CI reproducibility on Node 24 Keep current version Monitor Tracking only npm Repository package.json react-datepicker UI / helper ^9.1.0 -> 9.1.0 Already on the latest compatible line No newer major detected Maintained (latest published 2025-12-19) Browser runtime and React-driven editor UI behavior Keep current version Phase 4 Completed npm Repository package.json webpack.config.js src/js/src/posts-selector/interval-selector.js src/js/src/discovery/style/discovery.scss react-leaflet Map rendering ^4.2.1 -> 4.2.1 Already on the latest compatible line Review upgrade path to 5.0.0 Maintained (latest published 2024-12-14) Frontend maps, editor previews, Mapbox/MapLibre runtime compatibility Monitor upstream blocker Monitor Tracking only npm Repository package.json src/js/src/posts-sidebar/geo-posts.js react-map-gl Map rendering ^8.1.0 -> 8.1.0 Already on the latest compatible line No newer major detected Maintained (latest published 2025-10-03) Frontend maps, editor previews, Mapbox/MapLibre runtime compatibility Keep current version Monitor Tracking only npm Repository src/readme.txt package.json src/js/src/lib/mapboxgl/mapboxgl-react.js src/js/src/lib/maplibregl/maplibregl-react.js react-movable UI / helper ^3.4.0 -> 3.4.1 Already on the latest compatible line No newer major detected Maintained (latest published 2025-02-26) Browser runtime and React-driven editor UI behavior Keep current version Monitor Tracking only npm Repository package.json src/js/src/map-blocks/layers-settings.js src/js/src/discovery/blocks/map-layers.js sass Build tooling ^1.98.0 -> 1.98.0 Already on the latest compatible line No newer major detected Maintained (latest published 2026-03-10) Asset compilation, webpack config and CI reproducibility on Node 24 Keep current version Monitor Tracking only npm Repository package.json sass-loader Build tooling ^16.0.7 -> 16.0.7 Already on the latest compatible line No newer major detected Maintained (latest published 2026-02-05) Asset compilation, webpack config and CI reproducibility on Node 24 Keep current version Monitor Tracking only npm Repository package.json scrollama UI / helper ^3.2.0 -> 3.2.0 Already on the latest compatible line No newer major detected Maintained (latest published 2022-06-17) Browser runtime and React-driven editor UI behavior Keep current version Monitor Tracking only npm Repository package.json src/js/src/jeo-storymap/storymap-display.js style-loader Build tooling ^4.0.0 -> 4.0.0 Already on the latest compatible line No newer major detected Maintained (latest published 2024-04-08) Asset compilation, webpack config and CI reproducibility on Node 24 Keep current version Monitor Tracking only npm Repository package.json use-debounce UI / helper ^10.1.0 -> 10.1.0 Already on the latest compatible line No newer major detected Maintained (latest published 2026-01-08) Browser runtime and React-driven editor UI behavior Keep current version Monitor Tracking only npm Repository package.json src/js/src/map-blocks/layer-editor-preview.js src/js/src/layers-sidebar/layer-settings.js src/js/src/layers-sidebar/layers-sidebar.js webpack Build tooling ^5.105.4 -> 5.105.4 Already on the latest compatible line No newer major detected Maintained (latest published 2026-03-03) Asset compilation, webpack config and CI reproducibility on Node 24 Keep current version Monitor Tracking only npm Repository package.json webpack.config.js src/js/src/lib/mapgl-loader.js src/js/src/lib/mapboxgl/mapboxgl-react.js","title":"npm direct dependencies"},{"location":"dev/dependency-upgrade-report/#composer-direct-and-transitive-dependencies","text":"Package Type Declared / Resolved Latest compatible Latest major Maintenance Impact Decision Batch Execution Evidence Usage dealerdirect/phpcodesniffer-composer-installer PHP tooling transitive -> 1.2.0 Already on the latest compatible line No newer major detected Maintained (latest published 2025-11-11) Developer-only PHPCS/WPCS checks; validate PHP 8.0 and WPCS compatibility before majors Keep current version Monitor Tracking only Packagist Repository composer.lock composer.json phpcsstandards/phpcsextra PHP tooling transitive -> 1.5.0 Already on the latest compatible line No newer major detected Maintained (latest published 2025-11-12) Developer-only PHPCS/WPCS checks; validate PHP 8.0 and WPCS compatibility before majors Keep current version Monitor Tracking only Packagist Repository composer.lock phpcsstandards/phpcsutils PHP tooling transitive -> 1.2.2 Already on the latest compatible line No newer major detected Maintained (latest published 2025-12-08) Developer-only PHPCS/WPCS checks; validate PHP 8.0 and WPCS compatibility before majors Keep current version Monitor Tracking only Packagist Repository composer.lock squizlabs/php_codesniffer PHP tooling ^3.13.5 -> 3.13.5 Already on the latest compatible line Review upgrade path to 4.0.1 Maintained (latest published 2025-11-10) Developer-only PHPCS/WPCS checks; validate PHP 8.0 and WPCS compatibility before majors Monitor upstream blocker Monitor Tracking only Packagist Repository composer.lock composer.json wp-coding-standards/wpcs PHP tooling ^3.3.0 -> 3.3.0 Already on the latest compatible line No newer major detected Maintained (latest published 2025-11-25) Developer-only PHPCS/WPCS checks; validate PHP 8.0 and WPCS compatibility before majors Keep current version Batch 7 Completed Packagist Repository composer.json composer.lock","title":"Composer direct and transitive dependencies"},{"location":"dev/dependency-upgrade-report/#transitive-watchlist","text":"Package Type Declared / Resolved Latest compatible Latest major Maintenance Impact Decision Batch Execution Evidence Usage react-autosize-textarea UI / helper transitive -> Unavailable Keep the local override in place while the published Gutenberg editor packages still carry this stale peer metadata. Remove the override only after upstream ships React-18 or React-19-compatible peer metadata or the parent editor stack is replaced through a supported upgrade path. Maintained upstream, local override applied for stale peer metadata (latest published 2020-07-08) Browser runtime and React-driven editor UI behavior Watch via the parent package batch Batch 4 Completed npm package.json","title":"Transitive watchlist"},{"location":"dev/dependency-upgrade-report/#smoke-tests-for-each-batch","text":"Detailed checklist: docs/dev/dependency-upgrade-smoke-tests.md . Build plugin assets : npm run build completes without new warnings or bundle errors. Create and edit a map : The Maps editor loads, map settings persist, and the preview updates without console errors. Edit a storymap with CKEditor : The storymap editor loads, inline rich text editing works, and image upload still attaches media. Reorder layers and storymap slides : Drag-and-drop or movable ordering works in all UIs that currently rely on reordering. Edit layer and legend forms : Schema-driven forms, legend settings and interaction settings still render and save. Filter stories by date in Discovery : The date range picker applies and clears filters correctly. Use geocoding autosuggest : Address suggestions load, selection writes values back, and no stale request errors appear. Render maps in both Mapbox and MapLibre modes : Frontend and editor previews render with both runtimes, including layers, legends and interactions.","title":"Smoke tests for each batch"},{"location":"dev/dependency-upgrade-report/#package-notes","text":"@ckeditor/ckeditor5-react : Execution: Upgraded to 11.0.1 after confirming compatibility with ckeditor5 47.6.1 and React 18.3.1. @ckeditor/ckeditor5-react : Execution: No storymap editor source changes were required for the wrapper major itself. @wordpress/edit-post : Keep this aligned with the rest of the Gutenberg package batch to avoid React peer drift. @wordpress/edit-post : Execution: Revalidated in Batch 4: 8.41.0 is already the latest published line available to this repository. @wordpress/edit-post : Execution: Its published peerDependencies still target react and react-dom ^18.0.0, so no upstream React-line unlock happened in this batch. @wordpress/editor : Keep this aligned with the rest of the Gutenberg package batch to avoid React peer drift. @wordpress/editor : Execution: Revalidated in Batch 4: 14.41.0 is already the latest published line available to this repository. @wordpress/editor : Execution: Its published peerDependencies still target react and react-dom ^18.0.0, so no upstream React-line unlock happened in this batch. @wordpress/plugins : Execution: Revalidated in Batch 4: 7.41.0 is already the latest published line available to this repository. @wordpress/plugins : Execution: Its published peerDependencies still target react and react-dom ^18.0.0, so this package does not yet help unlock react-leaflet 5. @wordpress/scripts : Treat this package as the lead dependency for the build batch because it constrains Babel, webpack, PostCSS and much of the Gutenberg toolchain. @wordpress/scripts : Execution: Lead package for the tooling major batch. @wordpress/scripts : Execution: Upgraded to 31.x without requiring webpack.config.js changes in this repository. eta : Execution: Upgraded to 4.5.1 after the template compilation contract was covered by the minimum JS test layer. eta : Execution: The frontend helper now imports eta/core and Jest transforms eta/dist/core.js to keep the browser build and unit runner aligned. react-datepicker : Execution: Upgraded to 9.1.0; keep the related-posts date persistence flow under manual review after the major. react-datepicker : Execution: Phase 4 introduced a non-blocking webpack warning from react-datepicker's optional date-fns-tz require path. react-datepicker : Execution: Batch 1 removes that warning without changing the declared react-datepicker version. react-leaflet : Execution: Keep this on external upstream monitoring until Gutenberg exposes a supported React 19 path or another supported route replaces the current editor stack. react-leaflet : Execution: The repository remains on the supported react-leaflet 4 line; the geocoding fixes documented in this branch are product work, not part of the blocked major-upgrade path. squizlabs/php_codesniffer : Direct dev dependency. Evaluate majors only after confirming WPCS compatibility. squizlabs/php_codesniffer : Execution: Packagist publishes PHPCS 4.0.1, but the installed WPCS 3.3.0 line still requires squizlabs/php_codesniffer ^3.13.4. squizlabs/php_codesniffer : Execution: The repository PHPCS 3.x baseline is now clean, so PHPCS 4 stays on external upstream monitoring until the installed WPCS line officially supports that major. wp-coding-standards/wpcs : Lead Composer dependency because it constrains PHPCS, PHPCSExtra and PHPCSUtils. wp-coding-standards/wpcs : Execution: Batch 7 cleaned the repository baseline while keeping WPCS 3.3.0 on its current supported PHPCS 3.x line. wp-coding-standards/wpcs : Execution: This package remains the upstream compatibility gate that must move before PHPCS 4 can be re-evaluated. react-autosize-textarea : Observed locally through npm ls react react-dom. react-autosize-textarea : Execution: Batch 4 confirmed that the latest published Gutenberg editor packages still resolve this stale peer metadata and therefore applied a minimum local override at npm-overrides/react-autosize-textarea. react-autosize-textarea : Execution: npm ci and npm ls no longer report invalid React peer ranges after the override.","title":"Package notes"},{"location":"dev/dependency-upgrade-smoke-tests/","text":"Dependency Upgrade Smoke Tests Use this checklist for both the completed stabilization track and the current plugin sanity track. The dependency report keeps both rounds documented; approvals should follow the batch sequence that matches the active round. Approval package Summarize the dependencies, lockfiles and source files touched by the batch. Update docs/dev/dependency-upgrade-report.json , docs/dev/dependency-upgrade-report.md and this checklist. Record automated check results in the report executionLog . List only the manual review items that apply to the current batch. Ask for approval before creating a commit. Preconditions Use a WordPress instance that matches the current baseline: WordPress 6.6+, PHP 8.0+, Node 24 as the supported frontend runtime. At the start of every dependency review or planning batch that can touch editor or runtime behavior, regenerate the dependency report and record the resolved React, ReactDOM and Gutenberg editor package versions plus peer ranges from the frontend runtime snapshot. Test both editor and frontend behavior whenever a batch touches runtime code. Treat .github/workflows/node-frontend.yml , .github/workflows/php-compat.yml and .github/workflows/wordpress-smoke.yml as the mandatory CI gates. Run npm run check:env before local installs or frontend checks so unsupported Node versions fail early. When the local WordPress smoke runs from a clean worktree, make sure the ignored src/js/build assets have been generated first, or record the resulting asset-include warnings explicitly in the batch report. If the local WordPress smoke script cannot complete because the machine is missing the expected database or wp-cli runtime setup, record the failure details in the batch report and use the GitHub workflow as the authoritative runtime gate. Treat a blocked evaluation as a valid batch outcome only after recording the blocker, the evidence and the next step in the report. Automated checks by batch Batch 0: run composer validate --no-check-publish , php scripts/check-php-compat.php , WP_CLI_PHP=/opt/homebrew/opt/php@8.4/bin/php WP_DIR=/tmp/jeo-plugin-wordpress-smoke bash scripts/wordpress-smoke.sh , node --check scripts/dependency-review.mjs and node scripts/dependency-review.mjs --write . Batch 1: run npm ci , npm run build , npm run test:unit , php scripts/check-php-compat.php and WP_CLI_PHP=/opt/homebrew/opt/php@8.4/bin/php WP_DIR=/tmp/jeo-plugin-wordpress-smoke bash scripts/wordpress-smoke.sh . Batch 1: after npm run build , also run npm run build:report and npm run audit:npm . Batch 2: run npm ci , npm run build , npm run test:unit , php scripts/check-php-compat.php and WP_CLI_PHP=/opt/homebrew/opt/php@8.4/bin/php WP_DIR=/tmp/jeo-plugin-wordpress-smoke bash scripts/wordpress-smoke.sh . Batch 3: run npm ci , npm run build , npm run test:unit , php scripts/check-php-compat.php and WP_CLI_PHP=/opt/homebrew/opt/php@8.4/bin/php WP_DIR=/tmp/jeo-plugin-wordpress-smoke bash scripts/wordpress-smoke.sh . Batch 4: run npm ci , npm run build , npm run test:unit , npm ls react-autosize-textarea react react-dom , php scripts/check-php-compat.php and WP_CLI_PHP=/opt/homebrew/opt/php@8.4/bin/php WP_DIR=/tmp/jeo-plugin-wordpress-smoke bash scripts/wordpress-smoke.sh . Batch 5: do not start local execution until upstream Gutenberg and react-leaflet support a compatible React line; once that external blocker moves, run npm ci , npm run build , npm run test:unit , php scripts/check-php-compat.php and WP_CLI_PHP=/opt/homebrew/opt/php@8.4/bin/php WP_DIR=/tmp/jeo-plugin-wordpress-smoke bash scripts/wordpress-smoke.sh . Batch 6: run composer validate --no-check-publish , php scripts/check-php-compat.php with PHP 8.0 through 8.5 , the local WordPress smoke on PHP 8.4 , and the local WordPress smoke on PHP 8.5 , recording any third-party wp-cli deprecation noise separately from repository-owned warnings. Batch 7: run composer validate --no-check-publish , php scripts/check-php-compat.php , vendor/bin/phpcs --standard=phpcs.xml.dist , and WP_CLI_PHP=/opt/homebrew/opt/php@8.4/bin/php WP_DIR=/tmp/jeo-plugin-wordpress-smoke bash scripts/wordpress-smoke.sh if runtime-touching PHP files changed. Sanity Batch 0: run npm run check:env , npm ci , npm run build , and npm run build:report . Sanity Batch 1: run npm ci , npm audit --package-lock-only , npm audit --omit=dev , and composer audit --locked . Sanity Batch 2: run npm ci , npm run test:unit , npm run build , php scripts/check-php-compat.php , and WP_CLI_PHP=/opt/homebrew/opt/php@8.4/bin/php WP_DIR=/tmp/jeo-plugin-wordpress-smoke bash scripts/wordpress-smoke.sh . Sanity Batch 3: run npm run test:unit , npm run build , and npm run build:report . Sanity Batch 4: run npm run test:unit , npm run build , and php scripts/check-php-compat.php . Sanity Batch 5: run php scripts/check-php-compat.php , vendor/bin/phpcs --standard=phpcs.xml.dist , and WP_CLI_PHP=/opt/homebrew/opt/php@8.4/bin/php WP_DIR=/tmp/jeo-plugin-wordpress-smoke bash scripts/wordpress-smoke.sh . Sanity Batch 6: run npm ci , npm run test:unit , npm run build , npm run build:report , npm audit --package-lock-only , composer validate --no-check-publish , composer audit --locked , php scripts/check-php-compat.php , vendor/bin/phpcs --standard=phpcs.xml.dist , and WP_CLI_PHP=/opt/homebrew/opt/php@8.4/bin/php WP_DIR=/tmp/jeo-plugin-wordpress-smoke bash scripts/wordpress-smoke.sh . Sanity Batch 7: run node scripts/dependency-review.mjs --write , npm audit --package-lock-only , and npm ls react-autosize-textarea react react-dom while reviewing the active overrides inventory. Manual review by batch Batch 0: no product review is required. Batch 1: verify published-map popups, same-point post individualization, the absence of the null-geocode crash, and storymap slide plus nested layer reorder, including save and reopen after reordering. Batch 2: verify geocoding autosuggest suggestions and selection, post/map autosuggest insertion, Discovery date apply/clear behavior, and layer or attribution forms backed by schema data. Batch 3: verify selected map-layer ordering, storymap slide ordering and storymap layer toggling after removing the unsupported nested drag affordance. Batch 4: verify editor loading, save flows and console cleanliness in the map editor, layer editor, storymap editor and regular post editor after the local react-autosize-textarea override and lockfile refresh. Batch 5: no dedicated product review is required while the React 19 path stays externally blocked; keep add, edit, drag, cancel, save and reopen coverage for the supported react-leaflet 4 flow in the core functional checklist. Batch 6: verify the same product smoke already covered by the runtime batches, but execute it while watching specifically for repository-owned PHP 8.5 warnings or deprecations and track third-party wp-cli noise separately as external monitoring. Batch 7: no dedicated product review is required unless the PHPCS cleanup changes runtime PHP logic; otherwise review the lint delta, escaping or nonce-sensitive paths, and embed or template handlers in code review. Sanity Batch 0: no product review is required. Sanity Batch 1: no product review is required. Sanity Batch 2: verify exact selected-layer hydration, paginated layer search, Discovery map lazy loading and async taxonomy selection. Sanity Batch 3: verify the main editors, Discovery and published maps still load after the shared-chunk and bundle-budget changes. Sanity Batch 4: verify normal editor use stays console-clean after the cleanup. Sanity Batch 5: verify geocoding and public bootstrap flows that were hardened in PHP and JS. Sanity Batch 6: no dedicated product review is required beyond confirming the technical gates and documentation are aligned. Sanity Batch 7: no product review is required; confirm that each active override still has a documented reason, clear removal criteria, and no upstream replacement that would let us drop it. Core functional checklist Build assets Expected result: the build finishes successfully and no new webpack or peer-dependency warnings are introduced. Create and edit a map Expected result: the Maps editor opens with the Post sidebar active, map settings persist, the preview updates, and no Mapbox runtime CSS warning appears in the editor console. View published maps with related posts Expected result: popups render, multiple posts at one point can be individually reached and null related points do not crash the map. Edit a storymap with CKEditor Expected result: storymap rich text editing works, images upload, the saved content reopens correctly, and the hidden/open Slides settings panels keep the same visual width. Reorder layers and storymap slides Expected result: every reorder UI works, with no dropped state, duplicated items or stale order. Edit layer and legend forms Expected result: schema-driven forms, legend controls and interaction settings render, validate and save correctly. Filter and share Discovery stories Expected result: applying and clearing date and tag filters updates the result set as expected, removing a selected tag restores the default result set, shared Discovery URLs restore active filters and selected layers, and View in map plus marker hover keep the correct Discovery card and point(s) in sync. Use geocoding autosuggest Expected result: suggestions load, selecting a result updates the field, accented place names such as Sao Paulo or S\u00e3o Paulo resolve normally, dragging the active marker redefines the location reliably, and no stale request errors appear. Render maps in Mapbox mode Expected result: frontend maps and editor previews render with layers, legends and interactions intact. Render maps in MapLibre mode Expected result: frontend maps and editor previews render with layers, legends and interactions intact. Notes by batch type Baseline-fix batch: prioritize reproducing the known bug first, then landing a focused fix plus a regression test. Replacement batch: preserve existing saved data contracts, validate that the removed package disappears from npm ci warnings, and spot-check the replacement wrapper at the exact editor/frontend entry points it now owns. Gutenberg or React batch: prioritize editor loading, save flows and peer-warning cleanup before attempting dependent runtime majors. Map or rendering batch: prioritize both runtime modes, previews, popups and layer interactions. PHP 8.5 batch: distinguish repository-owned deprecations from external wp-cli or toolchain noise. PHP lint stack batch: evaluate WPCS, PHPCS, PHPCSExtra and PHPCSUtils together instead of treating PHPCS as an isolated major. Override governance batch: treat package.json overrides as temporary maintenance decisions that must be revisited after major @wordpress/scripts or Gutenberg dependency refreshes. Plugin sanity track: use Sanity Batch 0 through Sanity Batch 7 for the post-upgrade stabilization work, and keep the earlier numbered batches as historical record of the prior dependency-major round.","title":"Dependency upgrade smoke tests"},{"location":"dev/dependency-upgrade-smoke-tests/#dependency-upgrade-smoke-tests","text":"Use this checklist for both the completed stabilization track and the current plugin sanity track. The dependency report keeps both rounds documented; approvals should follow the batch sequence that matches the active round.","title":"Dependency Upgrade Smoke Tests"},{"location":"dev/dependency-upgrade-smoke-tests/#approval-package","text":"Summarize the dependencies, lockfiles and source files touched by the batch. Update docs/dev/dependency-upgrade-report.json , docs/dev/dependency-upgrade-report.md and this checklist. Record automated check results in the report executionLog . List only the manual review items that apply to the current batch. Ask for approval before creating a commit.","title":"Approval package"},{"location":"dev/dependency-upgrade-smoke-tests/#preconditions","text":"Use a WordPress instance that matches the current baseline: WordPress 6.6+, PHP 8.0+, Node 24 as the supported frontend runtime. At the start of every dependency review or planning batch that can touch editor or runtime behavior, regenerate the dependency report and record the resolved React, ReactDOM and Gutenberg editor package versions plus peer ranges from the frontend runtime snapshot. Test both editor and frontend behavior whenever a batch touches runtime code. Treat .github/workflows/node-frontend.yml , .github/workflows/php-compat.yml and .github/workflows/wordpress-smoke.yml as the mandatory CI gates. Run npm run check:env before local installs or frontend checks so unsupported Node versions fail early. When the local WordPress smoke runs from a clean worktree, make sure the ignored src/js/build assets have been generated first, or record the resulting asset-include warnings explicitly in the batch report. If the local WordPress smoke script cannot complete because the machine is missing the expected database or wp-cli runtime setup, record the failure details in the batch report and use the GitHub workflow as the authoritative runtime gate. Treat a blocked evaluation as a valid batch outcome only after recording the blocker, the evidence and the next step in the report.","title":"Preconditions"},{"location":"dev/dependency-upgrade-smoke-tests/#automated-checks-by-batch","text":"Batch 0: run composer validate --no-check-publish , php scripts/check-php-compat.php , WP_CLI_PHP=/opt/homebrew/opt/php@8.4/bin/php WP_DIR=/tmp/jeo-plugin-wordpress-smoke bash scripts/wordpress-smoke.sh , node --check scripts/dependency-review.mjs and node scripts/dependency-review.mjs --write . Batch 1: run npm ci , npm run build , npm run test:unit , php scripts/check-php-compat.php and WP_CLI_PHP=/opt/homebrew/opt/php@8.4/bin/php WP_DIR=/tmp/jeo-plugin-wordpress-smoke bash scripts/wordpress-smoke.sh . Batch 1: after npm run build , also run npm run build:report and npm run audit:npm . Batch 2: run npm ci , npm run build , npm run test:unit , php scripts/check-php-compat.php and WP_CLI_PHP=/opt/homebrew/opt/php@8.4/bin/php WP_DIR=/tmp/jeo-plugin-wordpress-smoke bash scripts/wordpress-smoke.sh . Batch 3: run npm ci , npm run build , npm run test:unit , php scripts/check-php-compat.php and WP_CLI_PHP=/opt/homebrew/opt/php@8.4/bin/php WP_DIR=/tmp/jeo-plugin-wordpress-smoke bash scripts/wordpress-smoke.sh . Batch 4: run npm ci , npm run build , npm run test:unit , npm ls react-autosize-textarea react react-dom , php scripts/check-php-compat.php and WP_CLI_PHP=/opt/homebrew/opt/php@8.4/bin/php WP_DIR=/tmp/jeo-plugin-wordpress-smoke bash scripts/wordpress-smoke.sh . Batch 5: do not start local execution until upstream Gutenberg and react-leaflet support a compatible React line; once that external blocker moves, run npm ci , npm run build , npm run test:unit , php scripts/check-php-compat.php and WP_CLI_PHP=/opt/homebrew/opt/php@8.4/bin/php WP_DIR=/tmp/jeo-plugin-wordpress-smoke bash scripts/wordpress-smoke.sh . Batch 6: run composer validate --no-check-publish , php scripts/check-php-compat.php with PHP 8.0 through 8.5 , the local WordPress smoke on PHP 8.4 , and the local WordPress smoke on PHP 8.5 , recording any third-party wp-cli deprecation noise separately from repository-owned warnings. Batch 7: run composer validate --no-check-publish , php scripts/check-php-compat.php , vendor/bin/phpcs --standard=phpcs.xml.dist , and WP_CLI_PHP=/opt/homebrew/opt/php@8.4/bin/php WP_DIR=/tmp/jeo-plugin-wordpress-smoke bash scripts/wordpress-smoke.sh if runtime-touching PHP files changed. Sanity Batch 0: run npm run check:env , npm ci , npm run build , and npm run build:report . Sanity Batch 1: run npm ci , npm audit --package-lock-only , npm audit --omit=dev , and composer audit --locked . Sanity Batch 2: run npm ci , npm run test:unit , npm run build , php scripts/check-php-compat.php , and WP_CLI_PHP=/opt/homebrew/opt/php@8.4/bin/php WP_DIR=/tmp/jeo-plugin-wordpress-smoke bash scripts/wordpress-smoke.sh . Sanity Batch 3: run npm run test:unit , npm run build , and npm run build:report . Sanity Batch 4: run npm run test:unit , npm run build , and php scripts/check-php-compat.php . Sanity Batch 5: run php scripts/check-php-compat.php , vendor/bin/phpcs --standard=phpcs.xml.dist , and WP_CLI_PHP=/opt/homebrew/opt/php@8.4/bin/php WP_DIR=/tmp/jeo-plugin-wordpress-smoke bash scripts/wordpress-smoke.sh . Sanity Batch 6: run npm ci , npm run test:unit , npm run build , npm run build:report , npm audit --package-lock-only , composer validate --no-check-publish , composer audit --locked , php scripts/check-php-compat.php , vendor/bin/phpcs --standard=phpcs.xml.dist , and WP_CLI_PHP=/opt/homebrew/opt/php@8.4/bin/php WP_DIR=/tmp/jeo-plugin-wordpress-smoke bash scripts/wordpress-smoke.sh . Sanity Batch 7: run node scripts/dependency-review.mjs --write , npm audit --package-lock-only , and npm ls react-autosize-textarea react react-dom while reviewing the active overrides inventory.","title":"Automated checks by batch"},{"location":"dev/dependency-upgrade-smoke-tests/#manual-review-by-batch","text":"Batch 0: no product review is required. Batch 1: verify published-map popups, same-point post individualization, the absence of the null-geocode crash, and storymap slide plus nested layer reorder, including save and reopen after reordering. Batch 2: verify geocoding autosuggest suggestions and selection, post/map autosuggest insertion, Discovery date apply/clear behavior, and layer or attribution forms backed by schema data. Batch 3: verify selected map-layer ordering, storymap slide ordering and storymap layer toggling after removing the unsupported nested drag affordance. Batch 4: verify editor loading, save flows and console cleanliness in the map editor, layer editor, storymap editor and regular post editor after the local react-autosize-textarea override and lockfile refresh. Batch 5: no dedicated product review is required while the React 19 path stays externally blocked; keep add, edit, drag, cancel, save and reopen coverage for the supported react-leaflet 4 flow in the core functional checklist. Batch 6: verify the same product smoke already covered by the runtime batches, but execute it while watching specifically for repository-owned PHP 8.5 warnings or deprecations and track third-party wp-cli noise separately as external monitoring. Batch 7: no dedicated product review is required unless the PHPCS cleanup changes runtime PHP logic; otherwise review the lint delta, escaping or nonce-sensitive paths, and embed or template handlers in code review. Sanity Batch 0: no product review is required. Sanity Batch 1: no product review is required. Sanity Batch 2: verify exact selected-layer hydration, paginated layer search, Discovery map lazy loading and async taxonomy selection. Sanity Batch 3: verify the main editors, Discovery and published maps still load after the shared-chunk and bundle-budget changes. Sanity Batch 4: verify normal editor use stays console-clean after the cleanup. Sanity Batch 5: verify geocoding and public bootstrap flows that were hardened in PHP and JS. Sanity Batch 6: no dedicated product review is required beyond confirming the technical gates and documentation are aligned. Sanity Batch 7: no product review is required; confirm that each active override still has a documented reason, clear removal criteria, and no upstream replacement that would let us drop it.","title":"Manual review by batch"},{"location":"dev/dependency-upgrade-smoke-tests/#core-functional-checklist","text":"Build assets Expected result: the build finishes successfully and no new webpack or peer-dependency warnings are introduced. Create and edit a map Expected result: the Maps editor opens with the Post sidebar active, map settings persist, the preview updates, and no Mapbox runtime CSS warning appears in the editor console. View published maps with related posts Expected result: popups render, multiple posts at one point can be individually reached and null related points do not crash the map. Edit a storymap with CKEditor Expected result: storymap rich text editing works, images upload, the saved content reopens correctly, and the hidden/open Slides settings panels keep the same visual width. Reorder layers and storymap slides Expected result: every reorder UI works, with no dropped state, duplicated items or stale order. Edit layer and legend forms Expected result: schema-driven forms, legend controls and interaction settings render, validate and save correctly. Filter and share Discovery stories Expected result: applying and clearing date and tag filters updates the result set as expected, removing a selected tag restores the default result set, shared Discovery URLs restore active filters and selected layers, and View in map plus marker hover keep the correct Discovery card and point(s) in sync. Use geocoding autosuggest Expected result: suggestions load, selecting a result updates the field, accented place names such as Sao Paulo or S\u00e3o Paulo resolve normally, dragging the active marker redefines the location reliably, and no stale request errors appear. Render maps in Mapbox mode Expected result: frontend maps and editor previews render with layers, legends and interactions intact. Render maps in MapLibre mode Expected result: frontend maps and editor previews render with layers, legends and interactions intact.","title":"Core functional checklist"},{"location":"dev/dependency-upgrade-smoke-tests/#notes-by-batch-type","text":"Baseline-fix batch: prioritize reproducing the known bug first, then landing a focused fix plus a regression test. Replacement batch: preserve existing saved data contracts, validate that the removed package disappears from npm ci warnings, and spot-check the replacement wrapper at the exact editor/frontend entry points it now owns. Gutenberg or React batch: prioritize editor loading, save flows and peer-warning cleanup before attempting dependent runtime majors. Map or rendering batch: prioritize both runtime modes, previews, popups and layer interactions. PHP 8.5 batch: distinguish repository-owned deprecations from external wp-cli or toolchain noise. PHP lint stack batch: evaluate WPCS, PHPCS, PHPCSExtra and PHPCSUtils together instead of treating PHPCS as an isolated major. Override governance batch: treat package.json overrides as temporary maintenance decisions that must be revisited after major @wordpress/scripts or Gutenberg dependency refreshes. Plugin sanity track: use Sanity Batch 0 through Sanity Batch 7 for the post-upgrade stabilization work, and keep the earlier numbered batches as historical record of the prior dependency-major round.","title":"Notes by batch type"},{"location":"dev/geo-information/","text":"Geographical Information of a post Each post can be related to one or more points on the map. For each point, JEO collects geographical information such as city and country names. For complete information on this, see Geocoders . How geographical information is stored Each related point is stored as one entry of the _related_point metadata key. Each entry is an object with all the information retrieved by the geocoder. Here is an example of two entries related to the same post, that could be get using: get_post_meta( $post_id, '_related_point' ); '_related_point' => [ 'relevance' => 'primary', '_geocode_lat' => '-23,54659435', '_geocode_lon' => '-46,644533061712', '_geocode_full_address' => 'Edif\u00edcio Copan, Rua Ara\u00fajo, Vila Buarque, Rep\u00fablica, S\u00e3o Paulo, Regi\u00e3o Imediata de S\u00e3o Paulo, Regi\u00e3o Metropolitana de S\u00e3o Paulo, Regi\u00e3o Intermedi\u00e1ria de S\u00e3o Paulo, S\u00e3o Paulo, Regi\u00e3o Sudeste, 01046-010, Brasil', '_geocode_country' => 'Brasil', '_geocode_country_code' => '', '_geocode_city' => 'S\u00e3o Paulo', '_geocode_region_level_2' => 'S\u00e3o Paulo', '_geocode_region_level_3' => 'Regi\u00e3o Intermedi\u00e1ria de S\u00e3o Paulo', '_geocode_city_level_1' => 'Vila Buarque', ], '_related_point' => [ 'relevance' => 'secondary', '_geocode_lat' => '-23,183525102463', '_geocode_lon' => '-46,898231506348', '_geocode_full_address' => 'Rua Jorge Gebran, Parque do Col\u00e9gio, Ch\u00e1cara Urbana, Jundia\u00ed, Regi\u00e3o Imediata de Jundia\u00ed, Regi\u00e3o Intermedi\u00e1ria de Campinas, S\u00e3o Paulo, Regi\u00e3o Sudeste, 13209-090, Brasil', '_geocode_country' => 'Brasil', '_geocode_country_code' => '', '_geocode_city' => 'Jundia\u00ed', '_geocode_region_level_2' => 'S\u00e3o Paulo', '_geocode_region_level_3' => 'Regi\u00e3o Intermedi\u00e1ria de Campinas', '_geocode_city_level_1' => 'Parque do Col\u00e9gio', ] How to search for posts by geoinformation? (indexes) When you save geographical information of the points, JEO also creates other metadata that will allow developers to query posts by specific geographical information. Since each point is stored as a serialized data in the database, this would not allow us to filter posts by country_code for example. That's why we create indexes. For the example above, this post would also have one individual metadata entry for each information, like this: [ '_geocode_lat_p' => '-23,54659435', '_geocode_lon_p' => '-46,644533061712', '_geocode_country_p' => 'Brasil', '_geocode_country_code_p' => '', '_geocode_city_p' => 'S\u00e3o Paulo', '_geocode_region_level_2_p' => 'S\u00e3o Paulo', '_geocode_region_level_3_p' => 'Regi\u00e3o Intermedi\u00e1ria de S\u00e3o Paulo', '_geocode_city_level_1_p' => 'Vila Buarque', '_geocode_lat_s' => '-23,183525102463', '_geocode_lon_s' => '-46,898231506348', '_geocode_country_s' => 'Brasil', '_geocode_country_code_s' => '', '_geocode_city_s' => 'Jundia\u00ed', '_geocode_region_level_2_s' => 'S\u00e3o Paulo', '_geocode_region_level_3_s' => 'Regi\u00e3o Intermedi\u00e1ria de Campinas', '_geocode_city_level_1_s' => 'Parque do Col\u00e9gio', ] Note: _s and _p suffixes indicate if the relevance of that information is primary or secondary. Note 2: Full addresses are not indexed Now we have all the information as individual metadata and this allows me to query by them, however, the pairs are disconnected (if I had more than one primary point, it would be impossible to know what are the latitude-longitude pairs. That's why the information we actually use is the serialized object). Fetching posts by geoinformation Give me all the posts that have primary points with the country code 'BR' : $posts = new WP_Query([ 'meta_query' => [ [ 'key' => '_geocode_country_code_p', 'value' => 'BR' ] ] ]); Give me all the posts whose city is 'Manaus' : $posts = new WP_Query([ 'meta_query' => [ [ 'key' => '_geocode_city_s', 'value' => 'Manaus' ], [ 'key' => '_geocode_city_p', 'value' => 'Manaus' ], 'relation' => 'OR' ] ]);","title":"Geo information"},{"location":"dev/geo-information/#geographical-information-of-a-post","text":"Each post can be related to one or more points on the map. For each point, JEO collects geographical information such as city and country names. For complete information on this, see Geocoders .","title":"Geographical Information of a post"},{"location":"dev/geo-information/#how-geographical-information-is-stored","text":"Each related point is stored as one entry of the _related_point metadata key. Each entry is an object with all the information retrieved by the geocoder. Here is an example of two entries related to the same post, that could be get using: get_post_meta( $post_id, '_related_point' ); '_related_point' => [ 'relevance' => 'primary', '_geocode_lat' => '-23,54659435', '_geocode_lon' => '-46,644533061712', '_geocode_full_address' => 'Edif\u00edcio Copan, Rua Ara\u00fajo, Vila Buarque, Rep\u00fablica, S\u00e3o Paulo, Regi\u00e3o Imediata de S\u00e3o Paulo, Regi\u00e3o Metropolitana de S\u00e3o Paulo, Regi\u00e3o Intermedi\u00e1ria de S\u00e3o Paulo, S\u00e3o Paulo, Regi\u00e3o Sudeste, 01046-010, Brasil', '_geocode_country' => 'Brasil', '_geocode_country_code' => '', '_geocode_city' => 'S\u00e3o Paulo', '_geocode_region_level_2' => 'S\u00e3o Paulo', '_geocode_region_level_3' => 'Regi\u00e3o Intermedi\u00e1ria de S\u00e3o Paulo', '_geocode_city_level_1' => 'Vila Buarque', ], '_related_point' => [ 'relevance' => 'secondary', '_geocode_lat' => '-23,183525102463', '_geocode_lon' => '-46,898231506348', '_geocode_full_address' => 'Rua Jorge Gebran, Parque do Col\u00e9gio, Ch\u00e1cara Urbana, Jundia\u00ed, Regi\u00e3o Imediata de Jundia\u00ed, Regi\u00e3o Intermedi\u00e1ria de Campinas, S\u00e3o Paulo, Regi\u00e3o Sudeste, 13209-090, Brasil', '_geocode_country' => 'Brasil', '_geocode_country_code' => '', '_geocode_city' => 'Jundia\u00ed', '_geocode_region_level_2' => 'S\u00e3o Paulo', '_geocode_region_level_3' => 'Regi\u00e3o Intermedi\u00e1ria de Campinas', '_geocode_city_level_1' => 'Parque do Col\u00e9gio', ]","title":"How geographical information is stored"},{"location":"dev/geo-information/#how-to-search-for-posts-by-geoinformation-indexes","text":"When you save geographical information of the points, JEO also creates other metadata that will allow developers to query posts by specific geographical information. Since each point is stored as a serialized data in the database, this would not allow us to filter posts by country_code for example. That's why we create indexes. For the example above, this post would also have one individual metadata entry for each information, like this: [ '_geocode_lat_p' => '-23,54659435', '_geocode_lon_p' => '-46,644533061712', '_geocode_country_p' => 'Brasil', '_geocode_country_code_p' => '', '_geocode_city_p' => 'S\u00e3o Paulo', '_geocode_region_level_2_p' => 'S\u00e3o Paulo', '_geocode_region_level_3_p' => 'Regi\u00e3o Intermedi\u00e1ria de S\u00e3o Paulo', '_geocode_city_level_1_p' => 'Vila Buarque', '_geocode_lat_s' => '-23,183525102463', '_geocode_lon_s' => '-46,898231506348', '_geocode_country_s' => 'Brasil', '_geocode_country_code_s' => '', '_geocode_city_s' => 'Jundia\u00ed', '_geocode_region_level_2_s' => 'S\u00e3o Paulo', '_geocode_region_level_3_s' => 'Regi\u00e3o Intermedi\u00e1ria de Campinas', '_geocode_city_level_1_s' => 'Parque do Col\u00e9gio', ] Note: _s and _p suffixes indicate if the relevance of that information is primary or secondary. Note 2: Full addresses are not indexed Now we have all the information as individual metadata and this allows me to query by them, however, the pairs are disconnected (if I had more than one primary point, it would be impossible to know what are the latitude-longitude pairs. That's why the information we actually use is the serialized object).","title":"How to search for posts by geoinformation? (indexes)"},{"location":"dev/geo-information/#fetching-posts-by-geoinformation","text":"Give me all the posts that have primary points with the country code 'BR' : $posts = new WP_Query([ 'meta_query' => [ [ 'key' => '_geocode_country_code_p', 'value' => 'BR' ] ] ]); Give me all the posts whose city is 'Manaus' : $posts = new WP_Query([ 'meta_query' => [ [ 'key' => '_geocode_city_s', 'value' => 'Manaus' ], [ 'key' => '_geocode_city_p', 'value' => 'Manaus' ], 'relation' => 'OR' ] ]);","title":"Fetching posts by geoinformation"},{"location":"dev/geocoders/","text":"Writing a Geocoder A Geocoder is a service that finds geographical coordinates from a search by address information. It's also able to get address details based on the geographical coordinates, which is called Reverse Geocoding. JEO needs a geocoder service in a few situations, such as when users indicate to where on a map a story (posts) is related. JEO comes with two native geocoder services users can choose from: Nominatim and Google. But new services can easily be added by plugins. This page documents how to do this. Registering a Geocoder Hook a function to the jeo_register_geocoders action and call the register with the following code: add_action('jeo_register_geocoders', function($geocoders) { $geocoders->register_geocoder([ 'slug' => 'my-geocoder', 'name' => 'My Geocoder', 'description' => __('My Geocoder description', 'my-textdomain'), 'class_name' => 'MyGeocoderClass' ]); }); This will tell JEO that there is a new Geocoder service available and give some information about it. Name and description will be used in the Administration panel so the admin can recognize and choose from the available Geocoders which one is to be used. slug needs to be a unique identifier for the geocoder class_name is the name of the Geocoder class Creating the Geocoder class Now we need to create the geocoder class. This will be a class that extends \\Jeo\\Geocoder and implement some methods that do the actual geocoding. Inside the same hook, declare the class and two required methods: geocode($search_string) The method that receives the search string, does the request to the geocoder servers and returns the coordinates and address details; reverse_geocode($lat, $lon) The method that receives latitude and longitude, requests the geocoder server, and returns the full location details in the same format as the geocode method does. While geocode() returns an array of search results, reverse_geocode() returns only one result. Each result is an array that must have only the keys expected by the JEO plugin, so each Geocoder must find the best correspondence between each field and the fields expected by JEO. Note: Only lat and lon are required. Sample response with all accepted fields: [ [ 'lat' => '', 'lon' => '', 'full_address' => '', 'country' => '', 'country_code' => '', 'region_level_1' => '', 'region_level_2' => '', // State goes here 'region_level_3' => '', 'city' => '', 'city_level_1' => '', ] ] Here is a simple example: add_action('jeo_register_geocoders', function($geocoders) { $geocoders->register_geocoder([ 'slug' => 'my-geocoder', 'name' => 'My Geocoder', 'description' => __('My Geocoder description', 'my-textdomain'), 'class_name' => 'MyGeocoderClass' ]); class MyGeocoderClass extends \\Jeo\\Geocoder { public function geocode($search_string) { $params = [ 'q' => $search_string, 'format' => 'json', 'addressdetails' => 1 ]; $r = wp_remote_get( add_query_arg($params, 'https://my-geocoder-server.org/search') ); $data = wp_remote_retrieve_body( $r ); $data = \\json_decode($data); $response = []; if (\\is_array($data)) { foreach ($data as $match) { $r = $this->format_response_item( (array) $match ); if ($r) $response[] = $r; } } return $response; } public function reverse_geocode($lat, $lon) { $params = [ 'lat' => $lat, 'lon' => $lon, 'format' => 'json', 'addressdetails' => 1 ]; $r = wp_remote_get( add_query_arg($params, 'https://my-geocoder-server.org/reverse') ); $data = wp_remote_retrieve_body( $r ); $data = \\json_decode($data); return $this->format_response_item( (array) $data ); } private function format_response_item($match) { $response = [ 'lat' => $match['lat'], 'lon' => $match['lon'], 'full_address' => $match['display_name'], 'country' => $match['country'], 'country_code' => $match['country_code'], 'region_level_1' => $match['region_level_1'], 'region_level_2' => $match['region_level_2'], // State goes here 'region_level_3' => $match['region_level_3'], 'city' => $match['city'], 'city_level_1' => $match['city_level_1'], ]; return $response; } } }); And that's it! Your new Geocoder is ready! Adding additional Settings to the Geocoder Some geocoder services might need or offer additional settings. Some might require the user to enter its API key, others might let the users restrict the search to a specific country to get better results when searching. You can also easily add new settings to your Geocoder that will automatically be presented to the user on the Settings page. Declare a method get_settings() in your class that will return an array of all the settings your Geocoder accepts. Each setting is described by an array with the following keys: slug : a slug for your option. You don't have to worry about naming conflicts, it will be stored inside your geocoders options; name : a human-readable name; description : an explanation to the user of what this setting is. Let's see an example only with the relevant code: add_action('jeo_register_geocoders', function($geocoders) { // ... class MyGeocoderClass extends \\Jeo\\Geocoder { // ... public function get_settings() { // Note it is an array of arrays return [ [ 'slug' => 'api_key', 'name' => __('API Key', 'my-text-domain'), 'description' => __('Enter the API key you can get visiting your panel at my-gecoder.org/panel', 'my-text-domain') ] ]; } } }); And this is what you will see in the admin panel: Accessing Settings values Now that you have registered a setting and the user can change its value in the admin panel, you can use it in your geocoder. To get its value, simply call $this->get_option($option_name) . Example: // ... // ... public function geocode($search_string) { $params = [ 'q' => $search_string, 'format' => 'json', 'addressdetails' => 1, 'api_key' => $this->get_option('api_key') ]; // ... return $response; } // ... Declaring default values You can also add the get_default_options() method to your class to set default values for each setting. This is optional and is done like this: add_action('jeo_register_geocoders', function($geocoders) { // ... class MyGeocoderClass extends \\Jeo\\Geocoder { // ... public function get_default_options() { return [ 'api_key' => 'sand-box-api-key' // the key must match the slug of the setting registered in get_settings() ]; } } }); Advanced: Even further settings customization If your geocoder needs some special settings that a simple text input won't handle, there is yet another method you can declare to add arbitrary HTML code to the Settings page. settings_footer($settings) must echo HTML code that will be rendered at the end of your Geocoder settings page. It received the $settings object, which is an instance of \\Jeo\\Settings and have some helpers you can use. You only need to print form fields with the right names and JEO will take care of saving them for you. To get the right field name use $settings->get_geocoder_option_field_name($name) . Example: // ... // ... public function settings_footer($settings) { ?>

My Select option

) } ); }; -export default LayerSettings; +export default memo( LayerSettings, ( prevProps, nextProps ) => { + const prevStyle = prevProps.itemProps?.style || {}; + const nextStyle = nextProps.itemProps?.style || {}; + return ( + prevProps.index === nextProps.index && + prevProps.isDragged === nextProps.isDragged && + prevProps.isSelected === nextProps.isSelected && + prevProps.isOutOfBounds === nextProps.isOutOfBounds && + isEqual( prevProps.settings, nextProps.settings ) && + isEqual( prevProps.widths, nextProps.widths ) && + prevStyle.visibility === nextStyle.visibility && + prevStyle.zIndex === nextStyle.zIndex + ); +} ); diff --git a/src/js/src/map-blocks/layers-panel.css b/src/js/src/map-blocks/layers-panel.css index 329bd6d3d..2a6d4f7be 100644 --- a/src/js/src/map-blocks/layers-panel.css +++ b/src/js/src/map-blocks/layers-panel.css @@ -1,3 +1,33 @@ .jeo-layers-panel .components-button.is-button { margin: 10px 10px 0; } + +.jeo-layer-description, .jeo-layer-reason { + font-size: 0.85em; + margin: 4px 0 8px; +} + +.jeo-layer-reason { + color: #666; + font-style: italic; +} + +.jeo-layer-meta { + font-size: 0.85em; + color: #666; + margin: 2px 0 6px; +} + +.jeo-layer-meta strong { + color: #333; +} + +.jeo-layer-autostyle { + margin: 2px 0 6px; + padding: 4px 8px; + font-size: 0.8em; + color: #8a5300; + background: #fff4e0; + border-left: 3px solid #e15a2d; + border-radius: 2px; +} diff --git a/src/js/src/map-blocks/layers-panel.js b/src/js/src/map-blocks/layers-panel.js index 0d5c88eff..310c2954b 100644 --- a/src/js/src/map-blocks/layers-panel.js +++ b/src/js/src/map-blocks/layers-panel.js @@ -1,15 +1,10 @@ import { Button } from '@wordpress/components'; -import { __ } from '@wordpress/i18n'; +import { __, _x } from '@wordpress/i18n'; import { layerUseLabels, loadLayer } from './utils'; +import { decodeHtmlEntity, decodeHtmlEntities, sanitizeHtml } from '../shared/html'; import './layers-panel.css'; -const decodeHtmlEntity = function ( str ) { - return str.replace( /&#(\d+);/g, function ( match, dec ) { - return String.fromCharCode( dec ); - } ); -}; - export default function ( { attributes, loadedLayers, @@ -31,17 +26,47 @@ export default function ( {
    { layers.map( ( layerSettings ) => { const settings = loadLayer( loadedLayers, layerSettings ); + const attribution = settings.layer?.meta?.attribution || ''; + const themes = layerSettings.themes || settings.layer?.meta?.themes || ''; + const description = settings.layer?.excerpt?.rendered || settings.layer?.content?.rendered || ''; return ( settings.layer && (
  1. - { decodeHtmlEntity( settings.layer.title.rendered ) } -{ ' ' } + { decodeHtmlEntity( settings.layer.title.rendered ) } — { ' ' } { settings.layer.meta.type }

    + { description && ( +

    + { decodeHtmlEntities( description.replace( /<[^>]+>/g, '' ) ) } +

    + ) } + { layerSettings.reason && ( +

    { layerSettings.reason }

    + ) } + { layerSettings.auto_style && ( +

    + { __( + 'Automatic style applied (this layer had no saved style). Edit layer settings to customize it.', + 'jeowp' + ) } +

    + ) } + { themes && ( +

    + { __( 'Themes:', 'jeowp' ) } { themes } +

    + ) } + { attribution && ( +

    + { __( 'Source:', 'jeowp' ) }{ ' ' } + +

    + ) } { layerUseLabels[ settings.use ] } { settings.use !== 'fixed' && settings.default && - ' - ' + __( 'Default', 'jeowp' ) } + ' - ' + _x( 'Default', 'layer usage default label', 'jeowp' ) }
  2. ) ); diff --git a/src/js/src/map-blocks/layers-settings.js b/src/js/src/map-blocks/layers-settings.js index bd6678c26..ebeec1490 100644 --- a/src/js/src/map-blocks/layers-settings.js +++ b/src/js/src/map-blocks/layers-settings.js @@ -1,5 +1,5 @@ import { Button, Card, CardBody, Spinner } from '@wordpress/components'; -import { Fragment, useEffect, useState } from '@wordpress/element'; +import { Fragment, useCallback, useEffect, useMemo, useRef, useState } from '@wordpress/element'; import { __ } from '@wordpress/i18n'; import { SelectControl, TextControl } from '../shared/wp-form-controls'; @@ -8,18 +8,41 @@ import { List, arrayMove } from 'react-movable'; import LayerSettings from './layer-settings'; import { mergeLayerTypeOptions } from './layer-type-options'; import { loadLayer } from './utils'; +import { decodeHtmlEntity, decodeHtmlEntities, sanitizeHtml } from '../shared/html'; import { usePaginatedRecords } from '../shared/rest-records'; import './layers-settings.css'; -const setLayer = ( id ) => ( { id, use: 'fixed', default: true } ); +const setLayer = ( id ) => ( { id, use: 'fixed', default: true, provenance: 'manual' } ); -const anySwapDefault = ( settings ) => - settings.some( ( s ) => s.use === 'swappable' && s.default ); +const anySwapDefault = ( settings ) => { + return settings.some( ( s ) => s.use === 'swappable' && s.default ); +} export default function LayersSettings ( { attributes, setAttributes, loadedLayers, loadingLayers, closeModal } ) { - const setLayers = ( layers ) => setAttributes( { layers } ); - let widths = []; + const attributesRef = useRef( attributes ); + useEffect( () => { + attributesRef.current = attributes; + }, [ attributes ] ); + + const setLayers = useCallback( ( layers ) => { + setAttributes( { layers } ); + }, [ setAttributes ] ); + + // Keep the last non-empty loadedLayers snapshot so the list never unmounts + // (or drops items) while the entity query re-resolves after layers change. + const loadedLayersRef = useRef( loadedLayers ); + useEffect( () => { + if ( ( loadedLayers || [] ).length > 0 ) { + loadedLayersRef.current = loadedLayers; + } + }, [ loadedLayers ] ); + const stableLoadedLayers = + ( loadedLayers || [] ).length > 0 + ? loadedLayers + : loadedLayersRef.current || []; + + const widths = useMemo( () => [], [] ); const [ layerTypeFilter, setLayerTypeFilter ] = useState( '' ); const [ layerNameFilter, setLayerNameFilter ] = useState( '' ); @@ -67,39 +90,210 @@ export default function LayersSettings ( { attributes, setAttributes, loadedLaye const [ firstLayer, ...otherLayers ] = attributes.layers; if ( firstLayer && firstLayer.use !== 'fixed' ) { setLayers( [ - { ...firstLayer, use: 'fixed', default: false }, + { ...firstLayer, use: 'fixed', default: true }, ...otherLayers, ] ); } }, [ attributes, setAttributes ] ); - const decodeHtmlEntity = function ( str ) { - return str.replace( /&#(\d+);/g, function ( match, dec ) { - return String.fromCharCode( dec ); - } ); - }; + const onLayerOrderChange = useCallback( + ( { oldIndex, newIndex } ) => { + if ( oldIndex === newIndex ) { + return; + } - const onLayerOrderChange = ( { oldIndex, newIndex } ) => { - if ( oldIndex === newIndex ) { - return; - } + const resultLayers = arrayMove( + attributesRef.current.layers, + oldIndex, + newIndex + ); - const resultLayers = arrayMove( attributes.layers, oldIndex, newIndex ); + // Set base layer always as fixed + if ( resultLayers.length ) { + resultLayers[ 0 ].use = 'fixed'; + } - // Set base layer always as fixed - if ( resultLayers.length ) { - resultLayers[0].use = 'fixed'; - } + // Reset fixed default param + resultLayers.forEach( ( setting ) => { + if ( setting.use === 'fixed' ) { + setting.default = true; + } + } ); + + setLayers( resultLayers ); + }, + [ setLayers ] + ); + + const handleSwitchDefault = useCallback( + ( id, def ) => + setLayers( + attributesRef.current.layers.map( ( settings ) => + settings.id === id ? { ...settings, default: def } : settings + ) + ), + [ setLayers ] + ); + + const handleSwitchShowLegend = useCallback( + ( id, def ) => { + setLayers( + attributesRef.current.layers.map( ( settings ) => + settings.id === id ? { ...settings, show_legend: def } : settings + ) + ); + }, + [ setLayers ] + ); + + const handleUpdateStyleLayers = useCallback( + ( id, def ) => { + setLayers( + attributesRef.current.layers.map( ( settings ) => + settings.id === id ? { ...settings, style_layers: def } : settings + ) + ); + }, + [ setLayers ] + ); + + const handleSwitchUseStyle = useCallback( + ( id, def ) => { + const currentJeoLayerProps = stableLoadedLayers.find( + ( layerPost ) => layerPost.id === id + ); + const layerType = window.JeoLayerTypes.getLayerType( + currentJeoLayerProps.meta.type + ); + + if ( def ) { + layerType + ._getStyleDefinition( { + ...currentJeoLayerProps.meta, + layer_id: currentJeoLayerProps.id, + } ) + .then( ( response ) => { + if ( ! response ) { + return; + } + + let styleLayers = response.layers; + + styleLayers = styleLayers.map( ( layer ) => { + if ( + layer.layout && + typeof layer.layout.visibility !== 'undefined' && + layer.layout.visibility === 'none' + ) { + return { + id: layer.id, + show: false, + }; + } + + return { + id: layer.id, + show: true, + }; + } ); + + setLayers( + attributesRef.current.layers.map( ( settings ) => { + return settings.id === id + ? { + ...settings, + load_as_style: true, + style_layers: styleLayers, + } + : { + ...settings, + load_as_style: false, + style_layers: [], + }; + } ) + ); + } ); + + setLayers( + attributesRef.current.layers.map( ( settings ) => { + return settings.id === id + ? { ...settings, load_as_style: true, style_layers: [] } + : { + ...settings, + load_as_style: false, + style_layers: [], + }; + } ) + ); + } + }, + [ setLayers, stableLoadedLayers ] + ); - // Reset fixed default param - resultLayers.forEach( ( setting ) => { - if ( setting.use === 'fixed' ) { - setting.default = true; + const handleSwapDefault = useCallback( + ( id, def ) => + def && // radio-like behavior: can only be turned on. + setLayers( + attributesRef.current.layers.map( ( settings ) => ( { + ...settings, + default: + settings.use === 'swappable' // update only the swappable layers + ? settings.id === id // radio-like behavior: turn off all other swappable layers + : settings.default, + } ) ) + ), + [ setLayers ] + ); + + const handleUpdateUse = useCallback( + ( id, use ) => + setLayers( + attributesRef.current.layers.map( ( settings ) => { + if ( settings.id !== id ) { + return settings; + } + return { + ...settings, + use, + default: + use === 'swappable' + ? ! anySwapDefault( attributesRef.current.layers ) + : use === 'fixed' + ? true + : settings.default, + }; + } ) + ), + [ setLayers ] + ); + + const handleRemoveLayer = useCallback( + ( id ) => { + const confirmation = confirm( + __( 'Do you really want to delete this layer?', 'jeowp' ) + ); + + if ( confirmation ) { + return setLayers( + attributesRef.current.layers.filter( + ( settings ) => settings.id !== id + ) + ); } - } ); + }, + [ setLayers ] + ); - setLayers( resultLayers ); - }; + const handleUpdateStyle = useCallback( + ( id, style ) => { + setLayers( + attributesRef.current.layers.map( ( settings ) => + settings.id === id ? { ...settings, style } : settings + ) + ); + }, + [ setLayers ] + ); return ( @@ -191,12 +385,37 @@ export default function LayersSettings ( { attributes, setAttributes, loadedLaye { decodeHtmlEntity( layer.title.rendered ) } | { layer.meta.type }

    + { ( layer.excerpt?.rendered || layer.content?.rendered ) && ( +

    + { decodeHtmlEntities( ( layer.excerpt?.rendered || layer.content?.rendered ).replace( /<[^>]+>/g, '' ) ) } +

    + ) } + { ( layer.meta.attribution || layer['layer-theme']?.length > 0 ) && ( +

    + { layer.meta.attribution && ( + + { __( 'Source:', 'jeowp' ) }{ ' ' } + + + ) } + { layer['layer-theme']?.length > 0 && ( + + { __( 'Themes:', 'jeowp' ) } { layer['layer-theme'].map( ( t ) => t.name ).join( ', ' ) } + + ) } +

    + ) }
    { ! inUse && (

    { + const new_layer = setLayer( layer.id ); + const existing_ids = new Set( attributes.layers.map( ( l ) => l.id ) ); + if ( existing_ids.has( new_layer.id ) ) { + return; + } setAttributes( { - layers: [ ...attributes.layers, setLayer( layer.id ) ], + layers: [ ...attributes.layers, new_layer ], } ); } } className="add-button" @@ -247,8 +466,8 @@ export default function LayersSettings ( { attributes, setAttributes, loadedLaye

    ) } - { ! loadingLayers && attributes.layers.length > 0 && ( - 0 ) && attributes.layers.length > 0 && ( + ( @@ -305,7 +524,7 @@ export default function LayersSettings ( { attributes, setAttributes, loadedLaye return; } - const currentJeoLayerProps = loadedLayers.find(layerPost => layerPost.id === layer.id); + const currentJeoLayerProps = stableLoadedLayers.find(layerPost => layerPost.id === layer.id); if ( ! currentJeoLayerProps ) { return; } @@ -370,7 +589,16 @@ export default function LayersSettings ( { attributes, setAttributes, loadedLaye } ) ) ); - const updateUse = ( use ) => + const updateStyle = ( style ) => + setLayers( + attributesRef.current.layers.map( ( settings ) => + settings.id === layer.id + ? { ...settings, style } + : settings + ) + ); + + const updateUse = ( use ) => setLayers( attributes.layers.map( ( settings ) => { if ( settings.id !== layer.id ) { @@ -401,7 +629,7 @@ export default function LayersSettings ( { attributes, setAttributes, loadedLaye } }; - const loadedLayer = loadLayer( loadedLayers, layer ); + const loadedLayer = loadLayer( stableLoadedLayers, layer ); if(!loadedLayer.layer) { // TODO: Remove deleted layers @@ -421,9 +649,10 @@ export default function LayersSettings ( { attributes, setAttributes, loadedLaye switchShowLegend={ switchShowLegend } swapDefault={ swapDefault } updateUse={ updateUse } + updateStyle={ updateStyle } widths={ widths } updateStyleLayers={ updateStyleLayers } - key={ index } + key={ layer.id } />; } } /> diff --git a/src/js/src/map-blocks/map-editor-preview.js b/src/js/src/map-blocks/map-editor-preview.js index d6b787759..4bcefd76c 100644 --- a/src/js/src/map-blocks/map-editor-preview.js +++ b/src/js/src/map-blocks/map-editor-preview.js @@ -43,17 +43,11 @@ export default function MapEditorPreview() { ); const [ zoomState, setZoomState ] = useState( 'initial_zoom' ); - const [ key, setKey ] = useState( 0 ); const mapRef = useRef( undefined ); - // Expose setPanLimitsFromMap for the sidebar MapSettings button. - // Since the block runs inside the iframe and the sidebar in the parent, - // we use the editor data store to bridge them: MapSettings dispatches - // a custom action, and this block listens via a window message (future). - // For now, store the function on the iframe window so MapPanel can - // call it via cross-frame messaging if needed. + // Bridge pan-limits to the sidebar (parent document) via parent window. useEffect( () => { - window.__jeoSetPanLimitsFromMap = () => { + const fn = () => { const { current: map } = mapRef; if ( map ) { const bounds = map.getBounds(); @@ -69,9 +63,17 @@ export default function MapEditorPreview() { } ); } }; - return () => { - delete window.__jeoSetPanLimitsFromMap; - }; + try { + window.parent.__jeoSetPanLimitsFromMap = fn; + return () => { + delete window.parent.__jeoSetPanLimitsFromMap; + }; + } catch ( e ) { + window.__jeoSetPanLimitsFromMap = fn; + return () => { + delete window.__jeoSetPanLimitsFromMap; + }; + } }, [ setPostMeta ] ); const { @@ -151,8 +153,6 @@ export default function MapEditorPreview() { border: 0, } ); - // Stop mouse events from propagating to the block editor's selection - // handler so the map remains draggable inside the block. const stopPropagation = useCallback( ( e ) => e.stopPropagation(), [] ); return ( @@ -175,10 +175,7 @@ export default function MapEditorPreview() { className="zoom-button" variant="primary" isLarge - onClick={ () => { - setZoomState( 'initial_zoom' ); - setKey( ( currentKey ) => currentKey + 1 ); - } } + onClick={ () => setZoomState( 'initial_zoom' ) } > { __( 'Initial Zoom', 'jeowp' ) } @@ -192,7 +189,6 @@ export default function MapEditorPreview() { setPostMeta( { min_zoom: 0.1 } ); } setZoomState( 'min_zoom' ); - setKey( ( currentKey ) => currentKey + 1 ); } } > { __( 'Min Zoom', 'jeowp' ) } @@ -207,7 +203,6 @@ export default function MapEditorPreview() { setPostMeta( { max_zoom: 0.1 } ); } setZoomState( 'max_zoom' ); - setKey( ( currentKey ) => currentKey + 1 ); } } > { __( 'Max Zoom', 'jeowp' ) } @@ -217,7 +212,7 @@ export default function MapEditorPreview() { { ! shouldRenderMap && } { shouldRenderMap && ( currentKey + 1 ); }, [ attributes.align, window.screen.width ] ); - const decodeHtmlEntity = function ( str ) { - return str.replace( /&#(\d+);/g, function ( match, dec ) { - return String.fromCharCode( dec ); - } ); - }; - const mapRef = useRef( undefined ); const { record: loadedMap, isResolving: loadingMap } = useEntityRecord( 'postType', 'map', attributes.map_id, { @@ -95,6 +95,11 @@ export default function MapEditor ( {attributes, setAttributes } ) { applyComposedVisibility(); }, [ useComposedPreview, composedPreview.manifest, layerSettingsKey ] ); + const styleBase = useMemo( + () => loadedMap?.meta?.layers ? findStyleLayer( loadedLayers, loadedMap.meta.layers ) : null, + [ loadedLayers, loadedMap?.meta?.layers ] + ); + return (
    { attributes.map_id && ( loadingMap || !loadedMap ) && } diff --git a/src/js/src/map-blocks/map-panel.js b/src/js/src/map-blocks/map-panel.js index b56c94229..b201e23d2 100644 --- a/src/js/src/map-blocks/map-panel.js +++ b/src/js/src/map-blocks/map-panel.js @@ -15,7 +15,8 @@ export default function MapPanel( { attributes, setAttributes, renderPanel: Panel, - setPanLimitsFromMap + setPanLimitsFromMap, + isMapBlock = false, } ) { useEffect( () => { const isNewMap = [ @@ -67,6 +68,7 @@ export default function MapPanel( { attributes={ attributes } setAttributes={ setAttributes } setPanLimitsFromMap={ handleSetPanLimitsFromMap } + isMapBlock={ isMapBlock } /> ); diff --git a/src/js/src/map-blocks/map-preview-layer.js b/src/js/src/map-blocks/map-preview-layer.js index b40df32fe..7d7c73bde 100644 --- a/src/js/src/map-blocks/map-preview-layer.js +++ b/src/js/src/map-blocks/map-preview-layer.js @@ -2,9 +2,40 @@ import { memo } from '@wordpress/element'; import { isEqual } from 'lodash-es'; import { Layer, Source } from '../lib/mapgl-react'; +import { isStyleLayerType } from '../shared/style-layer-types'; +import { resolveTileUrl } from '../shared/styles'; + +export function getStyleProps( style = {} ) { + const props = {}; + + if ( style.filter !== undefined ) { + props.filter = style.filter; + } + if ( style.paint !== undefined ) { + props.paint = style.paint; + } + if ( style.layout !== undefined ) { + props.layout = style.layout; + } + + return props; +} export function renderLayer( { layer, instance } ) { - if ( [ 'swappable', 'switchable' ].includes( instance.use ) && ! instance.default ) { + if ( instance.load_as_style ) { + return null; + } + + // Style types (mapbox, style-json, …) render as the map's base style, + // hoisted via findStyleLayer — never as individual GL layers. + if ( isStyleLayerType( layer.type ) ) { + return null; + } + + if ( + [ 'swappable', 'switchable' ].includes( instance.use ) && + ! instance.default + ) { return null; } @@ -12,45 +43,230 @@ export function renderLayer( { layer, instance } ) { const layerId = `layer_${ instance.id }`; const sourceId = `source_${ instance.id }`; - switch ( layer.type ) { - case 'mapbox': { - return null; + const resolveStyle = ( inst, ly ) => { + if ( inst.style?.use_default && ly.default_style ) { + return ly.default_style; } + return inst.style || {}; + }; + + const applyOpacity = ( style, opacity ) => { + if ( typeof opacity !== 'number' || opacity >= 1 ) { + return style; + } + + const paint = style.paint ? { ...style.paint } : {}; + const opacity_props = [ + 'fill-opacity', + 'line-opacity', + 'circle-opacity', + 'symbol-opacity', + 'raster-opacity', + 'heatmap-opacity', + 'fill-extrusion-opacity', + ]; + + opacity_props.forEach( ( prop ) => { + if ( typeof paint[ prop ] === 'number' ) { + paint[ prop ] = paint[ prop ] * opacity; + } + } ); + return { ...style, paint }; + }; + + switch ( layer.type ) { case 'mapbox-tileset-raster': { const tilesetId = options.tileset_id ?? ''; - const tilesetUrl = tilesetId.includes( 'mapbox://' ) ? tilesetId : `mapbox://${ tilesetId }`; + const tilesetUrl = tilesetId.includes( 'mapbox://' ) + ? tilesetId + : `mapbox://${ tilesetId }`; + const opacity = + typeof instance.opacity === 'number' ? instance.opacity : 1; return ( - - + + ); } case 'mapbox-tileset-vector': { const tilesetId = options.tileset_id ?? ''; - const tilesetUrl = tilesetId.includes( 'mapbox://' ) ? tilesetId : `mapbox://${ tilesetId }`; + const tilesetUrl = tilesetId.includes( 'mapbox://' ) + ? tilesetId + : `mapbox://${ tilesetId }`; + const effectiveStyle = applyOpacity( + resolveStyle( instance, layer ), + instance.opacity + ); return ( - - + + ); } case 'mvt': { + const effectiveStyle = applyOpacity( + resolveStyle( instance, layer ), + instance.opacity + ); + return ( - - + + ); } case 'tilelayer': { + const opacity = + typeof instance.opacity === 'number' ? instance.opacity : 1; + return ( + + + + ); + } + + case 'geojson': { + const opacity = + typeof instance.opacity === 'number' ? instance.opacity : 1; + + // Inline GeoJSON takes precedence over the URL. + const inlineRaw = ( options.inline_geojson || '' ).trim(); + let data = options.data; + if ( inlineRaw.startsWith( '{' ) || inlineRaw.startsWith( '[' ) ) { + try { + data = JSON.parse( inlineRaw ); + } catch { + // Invalid inline JSON: fall back to the URL. + } + } + + if ( ! data ) { + return null; + } + + const sourceKey = + typeof data === 'string' ? data : inlineRaw || 'inline'; + + // Instance style (or the layer's default_style when the instance + // defers to it) merged over the shared fill defaults. + const effectiveStyle = resolveStyle( instance, layer ); + const instancePaint = + effectiveStyle.paint && typeof effectiveStyle.paint === 'object' + ? effectiveStyle.paint + : {}; + const instanceLayout = + effectiveStyle.layout && typeof effectiveStyle.layout === 'object' + ? effectiveStyle.layout + : {}; + + // Merge over the shared defaults from JeoLayerTypes.getFallbackPaint(). + const fallbackPaint = + window.JeoLayerTypes?.getFallbackPaint?.( 'fill' ) || {}; + const paintDefaults = { + 'fill-color': fallbackPaint[ 'fill-color' ] || '#8e44ad', + 'fill-opacity': + typeof fallbackPaint[ 'fill-opacity' ] === 'number' + ? fallbackPaint[ 'fill-opacity' ] + : 0.15, + 'fill-outline-color': + fallbackPaint[ 'fill-outline-color' ] || + fallbackPaint[ 'fill-color' ] || + '#8e44ad', + }; + + // Only "fill" is exposed in the schema for now; the switch is the + // extension point for future render types (line, circle, ...). + const renderType = options.type || 'fill'; + + if ( 'fill' !== renderType ) { + const fallback = + window.JeoLayerTypes?.getFallbackPaint?.( renderType ) || {}; + const paint = applyOpacity( + { paint: { ...fallback, ...instancePaint } }, + opacity + ).paint; + return ( + + + + ); + } + + const paint = applyOpacity( + { paint: { ...paintDefaults, ...instancePaint } }, + opacity + ).paint; + return ( - - + + ); } @@ -61,8 +277,12 @@ export function renderLayer( { layer, instance } ) { } export const MemoizedRenderLayer = memo( renderLayer, ( props, prevProps ) => { - return isEqual( - props.layer.layer_type_options, - prevProps.layer.layer_type_options + return ( + isEqual( + props.layer.layer_type_options, + prevProps.layer.layer_type_options + ) && + isEqual( props.layer.default_style, prevProps.layer.default_style ) && + isEqual( props.instance.style, prevProps.instance.style ) ); } ); diff --git a/src/js/src/map-blocks/map-preview-layer.test.js b/src/js/src/map-blocks/map-preview-layer.test.js new file mode 100644 index 000000000..4ce7237d9 --- /dev/null +++ b/src/js/src/map-blocks/map-preview-layer.test.js @@ -0,0 +1,385 @@ +import { Layer } from '../lib/mapgl-react'; +import { getStyleProps, renderLayer } from './map-preview-layer'; + +jest.mock( '../lib/mapgl-react', () => ( { + Layer: () => null, + Source: ( { children } ) => children, + Map: () => null, +} ) ); + +function collectLayerElements( node, found = [] ) { + if ( ! node || typeof node !== 'object' ) { + return found; + } + + if ( node.type === Layer ) { + found.push( node ); + } + + if ( Array.isArray( node.props?.children ) ) { + node.props.children.forEach( ( child ) => + collectLayerElements( child, found ) + ); + } else { + collectLayerElements( node.props?.children, found ); + } + + return found; +} + +describe( 'getStyleProps', () => { + it( 'returns no keys for an empty style', () => { + const props = getStyleProps( {} ); + + expect( props ).toEqual( {} ); + expect( 'filter' in props ).toBe( false ); + expect( 'paint' in props ).toBe( false ); + expect( 'layout' in props ).toBe( false ); + } ); + + it( 'keeps only the defined style keys', () => { + expect( + getStyleProps( { filter: [ '==', 'class', 'river' ] } ) + ).toEqual( { + filter: [ '==', 'class', 'river' ], + } ); + + expect( + getStyleProps( { paint: { 'line-color': '#0000ff' } } ) + ).toEqual( { paint: { 'line-color': '#0000ff' } } ); + + expect( getStyleProps( { layout: { 'line-cap': 'round' } } ) ).toEqual( + { + layout: { 'line-cap': 'round' }, + } + ); + } ); + + it( 'returns all keys for a complete style', () => { + const style = { + filter: [ '==', 'class', 'river' ], + paint: { 'line-color': '#0000ff' }, + layout: { 'line-cap': 'round' }, + }; + + expect( getStyleProps( style ) ).toEqual( style ); + } ); +} ); + +describe( 'renderLayer vector layer specs', () => { + const vectorCases = [ + [ + 'mapbox-tileset-vector', + { + tileset_id: 'mapbox.mapbox-streets-v8', + source_layer: 'waterway', + type: 'line', + style_source_type: 'vector', + }, + ], + [ + 'mvt', + { + url: 'https://example.com/tiles/{z}/{x}/{y}.pbf', + source_layer: 'waterway', + type: 'line', + style_source_type: 'vector', + }, + ], + ]; + + it.each( vectorCases )( + 'omits undefined style keys for %s', + ( type, options ) => { + const element = renderLayer( { + layer: { type, layer_type_options: options }, + instance: { id: 1, use: 'fixed' }, + } ); + + const layers = collectLayerElements( element ); + expect( layers ).toHaveLength( 1 ); + + const { props } = layers[ 0 ]; + + // MapLibre rejects present-but-undefined keys in addLayer(). + expect( 'filter' in props ).toBe( false ); + expect( 'paint' in props ).toBe( false ); + expect( 'layout' in props ).toBe( false ); + } + ); + + it.each( vectorCases )( + 'forwards defined style keys for %s', + ( type, options ) => { + const style = { + filter: [ '==', 'class', 'river' ], + paint: { 'line-color': '#0000ff', 'line-opacity': 1 }, + layout: { 'line-cap': 'round' }, + }; + + const element = renderLayer( { + layer: { type, layer_type_options: options }, + instance: { id: 1, use: 'fixed', style }, + } ); + + const [ layer ] = collectLayerElements( element ); + + expect( layer.props.filter ).toEqual( style.filter ); + expect( layer.props.paint ).toEqual( style.paint ); + expect( layer.props.layout ).toEqual( style.layout ); + } + ); +} ); + +describe( 'renderLayer geojson layer', () => { + const geojsonLayer = { + type: 'geojson', + layer_type_options: { + data: 'https://example.com/boundary.geojson', + }, + }; + const geojsonInstanceStyle = { + id: 1, + use: 'fixed', + style: { + paint: { + 'fill-color': '#8e44ad', + 'fill-opacity': 0.15, + 'fill-outline-color': '#8e44ad', + }, + }, + }; + + it( 'renders a single fill layer with a fill-outline-color', () => { + const element = renderLayer( { + layer: geojsonLayer, + instance: geojsonInstanceStyle, + } ); + + const layers = collectLayerElements( element ); + expect( layers ).toHaveLength( 1 ); + expect( layers[ 0 ].props.type ).toBe( 'fill' ); + expect( layers[ 0 ].props.paint[ 'fill-outline-color' ] ).toBe( + '#8e44ad' + ); + } ); + + it( 'applies instance opacity to the fill opacity', () => { + const element = renderLayer( { + layer: geojsonLayer, + instance: { ...geojsonInstanceStyle, opacity: 0.5 }, + } ); + + const [ fill ] = collectLayerElements( element ); + expect( fill.props.paint[ 'fill-opacity' ] ).toBeCloseTo( 0.075 ); + } ); + + it( 'falls back to default paint values', () => { + const element = renderLayer( { + layer: geojsonLayer, + instance: { id: 1, use: 'fixed' }, + } ); + + const [ fill ] = collectLayerElements( element ); + expect( fill.props.paint ).toEqual( { + 'fill-color': '#8e44ad', + 'fill-opacity': 0.15, + 'fill-outline-color': '#8e44ad', + } ); + } ); + + it( 'uses inline GeoJSON when provided', () => { + const inline = { + type: 'FeatureCollection', + features: [], + }; + const element = renderLayer( { + layer: { + type: 'geojson', + layer_type_options: { + data: 'https://example.com/boundary.geojson', + inline_geojson: JSON.stringify( inline ), + }, + }, + instance: { id: 1, use: 'fixed' }, + } ); + + expect( element.props.type ).toBe( 'geojson' ); + expect( element.props.data ).toEqual( inline ); + } ); + + it( 'prefers inline GeoJSON over the URL', () => { + const element = renderLayer( { + layer: { + type: 'geojson', + layer_type_options: { + data: 'https://example.com/boundary.geojson', + inline_geojson: '{"type":"FeatureCollection","features":[]}', + }, + }, + instance: { id: 1, use: 'fixed' }, + } ); + + expect( typeof element.props.data ).toBe( 'object' ); + } ); + + it( 'falls back to the URL when inline GeoJSON is invalid', () => { + const element = renderLayer( { + layer: { + type: 'geojson', + layer_type_options: { + data: 'https://example.com/boundary.geojson', + inline_geojson: '{invalid json', + }, + }, + instance: { id: 1, use: 'fixed' }, + } ); + + expect( element.props.data ).toBe( + 'https://example.com/boundary.geojson' + ); + } ); + + it( 'renders nothing when neither inline JSON nor URL is set', () => { + const element = renderLayer( { + layer: { + type: 'geojson', + layer_type_options: {}, + }, + instance: { id: 1, use: 'fixed' }, + } ); + + expect( element ).toBeNull(); + } ); + + it( 'renders a single layer for non-fill render types (extension point)', () => { + const element = renderLayer( { + layer: { + type: 'geojson', + layer_type_options: { + data: 'https://example.com/roads.geojson', + type: 'line', + }, + }, + instance: { id: 1, use: 'fixed' }, + } ); + + const layers = collectLayerElements( element ); + expect( layers ).toHaveLength( 1 ); + expect( layers[ 0 ].props.type ).toBe( 'line' ); + expect( layers[ 0 ].props.id ).toBe( 'layer_1' ); + } ); + + it( 'lets the instance style paint override the defaults', () => { + const element = renderLayer( { + layer: geojsonLayer, + instance: { + id: 1, + use: 'fixed', + style: { + paint: { + 'fill-color': '#222222', + }, + }, + }, + } ); + + const [ fill ] = collectLayerElements( element ); + expect( fill.props.paint[ 'fill-color' ] ).toBe( '#222222' ); + expect( fill.props.paint[ 'fill-outline-color' ] ).toBe( '#8e44ad' ); + } ); + + it( 'applies instance opacity to the instance paint', () => { + const element = renderLayer( { + layer: geojsonLayer, + instance: { + id: 1, + use: 'fixed', + opacity: 0.5, + style: { + paint: { + 'fill-color': '#222222', + 'fill-opacity': 0.4, + }, + }, + }, + } ); + + const [ fill ] = collectLayerElements( element ); + expect( fill.props.paint[ 'fill-opacity' ] ).toBeCloseTo( 0.2 ); + } ); + + it( 'resolves use_default to the layer default_style', () => { + const element = renderLayer( { + layer: { + ...geojsonLayer, + default_style: { + paint: { + 'fill-color': '#107a48', + 'fill-opacity': 0.3, + 'fill-outline-color': '#0b5c36', + }, + }, + }, + instance: { + id: 1, + use: 'fixed', + style: { use_default: true }, + }, + } ); + + const [ fill ] = collectLayerElements( element ); + expect( fill.props.paint[ 'fill-color' ] ).toBe( '#107a48' ); + expect( fill.props.paint[ 'fill-opacity' ] ).toBeCloseTo( 0.3 ); + } ); +} ); + +describe( 'renderLayer style layer types', () => { + beforeEach( () => { + delete window.JeoLayerTypes; + } ); + + afterEach( () => { + delete window.JeoLayerTypes; + } ); + + it( 'renders nothing for style types (mapbox, style-json)', () => { + expect( + renderLayer( { + layer: { + type: 'mapbox', + layer_type_options: { style_id: 'mapbox/dark-v11' }, + }, + instance: { id: 1, use: 'fixed', default: true }, + } ) + ).toBeNull(); + + expect( + renderLayer( { + layer: { + type: 'style-json', + layer_type_options: { + style_url: 'https://tiles.openfreemap.org/styles/dark', + }, + }, + instance: { id: 2, use: 'fixed', default: true }, + } ) + ).toBeNull(); + } ); + + it( 'recognizes style types registered with isStyle on the registry', () => { + window.JeoLayerTypes = { + isStyle: ( slug ) => 'my-future-style' === slug, + }; + + expect( + renderLayer( { + layer: { + type: 'my-future-style', + layer_type_options: { style_url: 'https://example.com/s.json' }, + }, + instance: { id: 3, use: 'fixed', default: true }, + } ) + ).toBeNull(); + } ); +} ); diff --git a/src/js/src/map-blocks/map-settings.js b/src/js/src/map-blocks/map-settings.js index c657385e6..cd4730384 100644 --- a/src/js/src/map-blocks/map-settings.js +++ b/src/js/src/map-blocks/map-settings.js @@ -30,7 +30,7 @@ function parseNumber( value ) { return isNaN( numValue ) ? value : numValue; } -export default ( { attributes, setAttributes, setPanLimitsFromMap } ) => { +export default ( { attributes, setAttributes, setPanLimitsFromMap, isMapBlock = false } ) => { const { center_lat: centerLat, center_lon: centerLon, @@ -174,7 +174,7 @@ export default ( { attributes, setAttributes, setPanLimitsFromMap } ) => { { return setAttributes({ pan_limits: { @@ -188,7 +188,7 @@ export default ( { attributes, setAttributes, setPanLimitsFromMap } ) => { { return setAttributes({ pan_limits: { @@ -203,7 +203,7 @@ export default ( { attributes, setAttributes, setPanLimitsFromMap } ) => { { return setAttributes({ pan_limits: { @@ -217,7 +217,7 @@ export default ( { attributes, setAttributes, setPanLimitsFromMap } ) => { { return setAttributes({ pan_limits: { @@ -244,6 +244,7 @@ export default ( { attributes, setAttributes, setPanLimitsFromMap } ) => {

    + { ! isMapBlock && (

    { __( 'For public maps only', 'jeowp' ) }

    @@ -267,6 +268,7 @@ export default ( { attributes, setAttributes, setPanLimitsFromMap } ) => { } } />
    + ) } ); diff --git a/src/js/src/map-blocks/minimap-config.js b/src/js/src/map-blocks/minimap-config.js new file mode 100644 index 000000000..261c2f1ce --- /dev/null +++ b/src/js/src/map-blocks/minimap-config.js @@ -0,0 +1,116 @@ +import { __ } from '@wordpress/i18n'; + +function sourcedHtmlAttribute( attribute ) { + return { + type: 'string', + source: 'attribute', + selector: 'div', + attribute, + }; +} + +function coerceNumber( value, fallback ) { + if ( value === '' || value === undefined || value === null ) { + return fallback; + } + + const normalizedValue = Number( value ); + return Number.isFinite( normalizedValue ) ? normalizedValue : fallback; +} + +export function coerceMinimapAttributes( attributes, defaults = {} ) { + return { + ...attributes, + center_lat: coerceNumber( attributes.center_lat, defaults.center_lat ), + center_lon: coerceNumber( attributes.center_lon, defaults.center_lon ), + initial_zoom: coerceNumber( attributes.initial_zoom, defaults.initial_zoom ), + min_zoom: coerceNumber( attributes.min_zoom, defaults.min_zoom ), + max_zoom: coerceNumber( attributes.max_zoom, defaults.max_zoom ), + }; +} + +export const minimapAttributes = { + layers: { + type: 'array', + default: [], + items: { + type: 'object', + properties: { + id: { type: 'number' }, + use: { type: 'string' }, + default: { type: 'boolean' }, + show_legend: { type: 'boolean' }, + load_as_style: { type: 'boolean' }, + opacity: { type: 'number' }, + provenance: { type: 'string' }, + }, + }, + }, + center_lat: sourcedHtmlAttribute( 'data-center_lat' ), + center_lon: sourcedHtmlAttribute( 'data-center_lon' ), + initial_zoom: sourcedHtmlAttribute( 'data-initial_zoom' ), + min_zoom: sourcedHtmlAttribute( 'data-min_zoom' ), + max_zoom: sourcedHtmlAttribute( 'data-max_zoom' ), + disable_scroll_zoom: { + type: 'boolean', + }, + disable_drag_pan: { + type: 'boolean', + }, + disable_drag_rotate: { + type: 'boolean', + }, + enable_fullscreen: { + type: 'boolean', + }, + pan_limits: { + type: 'object', + properties: { + east: { + description: __( 'East pan limit', 'jeowp' ), + type: 'number', + }, + north: { + description: __( 'North pan limit', 'jeowp' ), + type: 'number', + }, + south: { + description: __( 'South pan limit', 'jeowp' ), + type: 'number', + }, + west: { + description: __( 'West pan limit', 'jeowp' ), + type: 'number', + }, + }, + }, + pins: { + type: 'array', + default: [], + items: { + type: 'object', + properties: { + lat: { type: 'number' }, + lon: { type: 'number' }, + relevance: { type: 'string' }, + address: { type: 'string' }, + }, + }, + }, + show_pins: { + type: 'boolean', + default: true, + }, + status: { + type: 'string', + default: 'idle', + }, + message: { + type: 'string', + default: '', + }, + prompt: { + type: 'string', + default: '', + }, +}; diff --git a/src/js/src/map-blocks/minimap-display.js b/src/js/src/map-blocks/minimap-display.js new file mode 100644 index 000000000..0d7448f91 --- /dev/null +++ b/src/js/src/map-blocks/minimap-display.js @@ -0,0 +1,40 @@ +import { useBlockProps } from '@wordpress/block-editor'; + +export default function MinimapDisplay( { attributes } ) { + if ( attributes.status === 'error' ) { + return null; + } + + const allLayers = []; + + if ( attributes.base_layer ) { + allLayers.push( attributes.base_layer ); + } + + if ( attributes.layers && attributes.layers.length > 0 ) { + allLayers.push( ...attributes.layers ); + } + + const hasPins = attributes.pins && attributes.pins.length > 0; + + const blockProps = useBlockProps.save( { + className: 'jeomap', + 'data-center_lat': attributes.center_lat, + 'data-center_lon': attributes.center_lon, + 'data-initial_zoom': attributes.initial_zoom, + 'data-min_zoom': attributes.min_zoom, + 'data-max_zoom': attributes.max_zoom, + 'data-disable_scroll_zoom': attributes.disable_scroll_zoom || undefined, + 'data-disable_drag_pan': attributes.disable_drag_pan || undefined, + 'data-disable_drag_rotate': attributes.disable_drag_rotate || undefined, + 'data-enable_fullscreen': attributes.enable_fullscreen || undefined, + 'data-layers': allLayers.length > 0 ? JSON.stringify( allLayers ) : undefined, + 'data-pan_limits': attributes.pan_limits ? JSON.stringify( attributes.pan_limits ) : undefined, + 'data-pins': hasPins ? JSON.stringify( attributes.pins ) : undefined, + 'data-show_pins': hasPins ? String( !! attributes.show_pins ) : undefined, + } ); + + return ( +
    + ); +} diff --git a/src/js/src/map-blocks/minimap-editor.js b/src/js/src/map-blocks/minimap-editor.js new file mode 100644 index 000000000..7f1008590 --- /dev/null +++ b/src/js/src/map-blocks/minimap-editor.js @@ -0,0 +1,1180 @@ +import apiFetch from '@wordpress/api-fetch'; +import { InspectorControls, useBlockProps } from '@wordpress/block-editor'; +import { Button, Notice, PanelBody, Placeholder, Spinner } from '@wordpress/components'; +import { useSelect } from '@wordpress/data'; +import { useCallback, useEffect, useMemo, useRef, useState } from '@wordpress/element'; +import { debounce } from 'lodash'; +import { __, _x } from '@wordpress/i18n'; + +import { Map, Marker } from '../lib/mapgl-react'; +import LayersSettingsModal from './layers-settings-modal'; +import { MemoizedRenderLayer } from './map-preview-layer'; +import { + applyStyleLayerFiltering, + findStyleLayer, + styleLayerMapProps, +} from './use-style-layer'; +import { + applyComposedVisibilityFromSettings, + handleEditorMapPreviewError, + hasMapboxStyleLayers, + useComposedPayloadPreviewStyle, + useEditorMapboxTransformRequest, +} from './mapbox-style-preview'; +import { coerceMinimapAttributes } from './minimap-config'; +import MapPanel from './map-panel'; +import LayersPanel from './layers-panel'; +import { useRecordsByIds } from '../shared/rest-records'; +import { handleChatKeyDown } from '../shared/chat-input-keys'; +import { RadioControl, SelectControl, TextareaControl } from '../shared/wp-form-controls'; +import './onetime-map-editor.css'; + +const { map_defaults: mapDefaults } = globalThis.jeo_settings; + +const PIN_ICON_PRIMARY = globalThis.jeoMapVars?.images?.[ '/js/src/icons/news-marker' ]?.url; +const PIN_ICON_SECONDARY = globalThis.jeoMapVars?.images?.[ '/js/src/icons/news-marker-hover' ]?.url; + +const AI_CONFIGURED = Boolean( globalThis.jeoMapVars?.ai_configured ); + +const LOADING_MESSAGES = [ + __( 'Analyzing content…', 'jeowp' ), + __( 'Searching for map layers…', 'jeowp' ), + __( 'Generating map configuration…', 'jeowp' ), +]; + +function generateUUID() { + if ( typeof crypto !== 'undefined' && typeof crypto.randomUUID === 'function' ) { + try { + return crypto.randomUUID(); + } catch ( e ) { + // Fall through to manual UUID. + } + } + + const bytes = new Uint8Array( 16 ); + if ( typeof crypto !== 'undefined' && typeof crypto.getRandomValues === 'function' ) { + crypto.getRandomValues( bytes ); + } else { + for ( let i = 0; i < 16; i++ ) { + bytes[ i ] = Math.floor( Math.random() * 256 ); + } + } + + // Set version (4) and variant (RFC4122) bits. + bytes[ 6 ] = ( bytes[ 6 ] & 0x0f ) | 0x40; + bytes[ 8 ] = ( bytes[ 8 ] & 0x3f ) | 0x80; + + const hex = Array.from( bytes, ( b ) => b.toString( 16 ).padStart( 2, '0' ) ).join( '' ); + return `${ hex.slice( 0, 8 ) }-${ hex.slice( 8, 12 ) }-${ hex.slice( 12, 16 ) }-${ hex.slice( 16, 20 ) }-${ hex.slice( 20 ) }`; +} + +/** + * Renders a geolocation pin as a non-interactive map marker. + * + * Mirrors the frontend rendering in `class-jeo-map.js::addOwnPinsAsMarkers()`: + * primary pins use the `news-marker` icon, secondary ones (`relevance === 'secondary'`) + * use `news-marker-hover`. Falls back to a plain CSS dot when the localized + * icon URLs are unavailable. + * + * The `Marker` component resolves the active runtime (MapLibre GL or Mapbox GL) + * from the parent `` context, so pins render under both runtimes. + * + * @param {Object} props + * @param {Object} props.pin Pin object (`lat`, `lon`, `relevance`, `address`). + */ +function MinimapPinMarker( { pin } ) { + const lat = parseFloat( pin.lat ); + const lon = parseFloat( pin.lon ); + + if ( Number.isNaN( lat ) || Number.isNaN( lon ) ) { + return null; + } + + const isSecondary = pin.relevance === 'secondary'; + const iconUrl = isSecondary ? PIN_ICON_SECONDARY : PIN_ICON_PRIMARY; + + const style = iconUrl + ? { + backgroundImage: `url(${ iconUrl })`, + width: '27px', + height: '36px', + backgroundSize: 'cover', + pointerEvents: 'none', + } + : { + // Fallback dot when localized icons are unavailable. + width: '14px', + height: '14px', + borderRadius: '50%', + backgroundColor: isSecondary ? '#9e9e9e' : '#e03e3e', + border: '2px solid #ffffff', + boxShadow: '0 1px 4px rgba(0,0,0,0.5)', + pointerEvents: 'none', + }; + + return ( + +
    + + ); +} + +export default function MinimapEditor( { attributes, setAttributes, clientId } ) { + const blockProps = useBlockProps(); + const postId = useSelect( ( select ) => + select( 'core/editor' ).getCurrentPostId(), [] ); + const [ modal, setModal ] = useState( false ); + const [ baseVariant, setBaseVariant ] = useState( + attributes.base_layer?.variant || 'dark' + ); + const [ chatLoading, setChatLoading ] = useState( false ); + const [ chatInput, setChatInput ] = useState( '' ); + const [ chatPromptVisible, setChatPromptVisible ] = useState( false ); + const [ chatPrompt, setChatPrompt ] = useState( '' ); + const [ loadingMsgIndex, setLoadingMsgIndex ] = useState( 0 ); + const messagesEndRef = useRef( null ); + const attrsRef = useRef( attributes ); + attrsRef.current = attributes; + + useEffect( () => { + messagesEndRef.current?.scrollIntoView( { behavior: 'smooth' } ); + }, [ attributes.conversation ] ); + + useEffect( () => { + if ( 'ready' === attributes.status && ! attributes.conversation_id ) { + setAttributes( { conversation_id: generateUUID() } ); + } + }, [ attributes.status, attributes.conversation_id, setAttributes ] ); + + useEffect( () => { + if ( attributes.status !== 'loading' ) { + setLoadingMsgIndex( 0 ); + return; + } + const timer = setInterval( () => { + setLoadingMsgIndex( ( i ) => ( i + 1 ) % LOADING_MESSAGES.length ); + }, 3000 ); + return () => clearInterval( timer ); + }, [ attributes.status ] ); + + const normalizedAttributes = useMemo( () => { + return coerceMinimapAttributes( attributes, { + center_lat: mapDefaults.lat, + center_lon: mapDefaults.lng, + initial_zoom: mapDefaults.zoom, + min_zoom: 0, + max_zoom: 20, + } ); + }, [ attributes ] ); + + const allLayers = useMemo( () => { + const list = []; + if ( attributes.base_layer ) { + list.push( attributes.base_layer ); + } + if ( attributes.layers ) { + list.push( ...attributes.layers ); + } + return list; + }, [ attributes.base_layer, attributes.layers ] ); + + const layerIds = useMemo( () => { + return allLayers.map( ( l ) => l.id ).filter( ( id ) => id > 0 ); + }, [ allLayers ] ); + + const { records: loadedLayers = [], isLoading: loadingLayers } = useRecordsByIds( { + path: '/jeo/v1/map-layer', + ids: layerIds, + enabled: layerIds.length > 0, + query: { context: 'edit' }, + } ); + + const styleBase = useMemo( + () => findStyleLayer( loadedLayers, allLayers ), + [ loadedLayers, allLayers ] + ); + + const layerSettingsKey = useMemo( + () => JSON.stringify( allLayers || [] ), + [ allLayers ] + ); + const hasMapboxLayers = hasMapboxStyleLayers( loadedLayers ); + const composedPayload = useMemo( () => ( { + scope: 'preview', + kind: 'minimap', + postId, + blockId: clientId, + layers: allLayers || [], + } ), [ postId, clientId, layerSettingsKey ] ); + const composedPreview = useComposedPayloadPreviewStyle( { + enabled: hasMapboxLayers, + payload: composedPayload, + refreshKey: layerSettingsKey, + } ); + const useComposedPreview = Boolean( + hasMapboxLayers && composedPreview.style + ); + const isPreparingComposedPreview = Boolean( + ! loadingLayers && + hasMapboxLayers && + ! composedPreview.style && + ! composedPreview.error + ); + const transformRequest = useEditorMapboxTransformRequest( loadedLayers ); + const applyComposedVisibility = useCallback( () => { + if ( useComposedPreview ) { + applyComposedVisibilityFromSettings( + mapRef.current, + composedPreview.manifest, + allLayers || [] + ); + } + }, [ + composedPreview.manifest, + layerSettingsKey, + useComposedPreview, + ] ); + + useEffect( () => { + applyComposedVisibility(); + }, [ applyComposedVisibility ] ); + + const generate = useCallback( () => { + const postId = wp.data.select( 'core/editor' ).getCurrentPostId(); + if ( ! postId ) { + // eslint-disable-next-line no-console + console.error( '[JEO Minimap] Cannot generate: post ID is not available yet. Save the post first.' ); + setAttributes( { + status: 'error', + message: __( 'Save the post before generating the map.', 'jeowp' ), + } ); + return; + } + + setAttributes( { status: 'loading' } ); + + const convId = attrsRef.current.conversation_id || generateUUID(); + + try { + apiFetch( { + path: '/jeo/v1/minimap/setup', + method: 'POST', + data: { post_id: postId, conversation_id: convId }, + } ) + .then( ( response ) => { + if ( response.success ) { + const updates = { + status: 'ready', + layers: response.layers || [], + base_layer: response.base_layer || null, + center_lat: response.center_lat, + center_lon: response.center_lon, + initial_zoom: response.initial_zoom, + pins: response.pins || [], + message: response.message || '', + conversation_id: convId, + conversation: [ + ...( attrsRef.current.conversation || [] ), + { + role: 'assistant', + text: response.message || __( 'Map generated from post content.', 'jeowp' ), + ts: new Date().toISOString(), + }, + ], + }; + + if ( response.base_layer?.variant ) { + setBaseVariant( response.base_layer.variant ); + } + + setAttributes( updates ); + } else { + setAttributes( { + status: 'error', + message: response.message || __( 'Failed to suggest layers.', 'jeowp' ), + } ); + } + } ) + .catch( ( error ) => { + // eslint-disable-next-line no-console + console.error( '[JEO Minimap] Setup request failed:', error ); + setAttributes( { + status: 'error', + message: error.message || __( 'Request failed.', 'jeowp' ), + } ); + } ); + } catch ( syncError ) { + // eslint-disable-next-line no-console + console.error( '[JEO Minimap] Synchronous error during setup:', syncError ); + setAttributes( { + status: 'error', + message: syncError.message || __( 'Could not start map generation.', 'jeowp' ), + } ); + } + }, [ setAttributes ] ); + + const sendChat = useCallback( ( message, type = 'text', payload = {} ) => { + if ( chatLoading ) { + return; + } + const text = message || ''; + if ( ! text.trim() && 'regenerate' !== type ) { + return; + } + + const attrs = attrsRef.current; + const convId = attrs.conversation_id || generateUUID(); + if ( ! attrs.conversation_id ) { + setAttributes( { conversation_id: convId } ); + } + + setChatLoading( true ); + + try { + apiFetch( { + path: '/jeo/v1/minimap/chat', + method: 'POST', + data: { + conversation_id: convId, + post_id: wp.data.select( 'core/editor' ).getCurrentPostId(), + message: text, + type, + payload, + current_map_state: { + layers: attrs.layers || [], + base_layer: attrs.base_layer || null, + center_lat: attrs.center_lat, + center_lon: attrs.center_lon, + initial_zoom: attrs.initial_zoom, + pins: attrs.pins || [], + }, + }, + } ) + .then( ( response ) => { + const convUpdates = []; + if ( text.trim() && 'regenerate' !== type ) { + convUpdates.push( { role: 'user', text, ts: new Date().toISOString() } ); + } + + if ( response.success ) { + convUpdates.push( { + role: 'assistant', + text: response.assistant_message || response.message || __( 'Map updated.', 'jeowp' ), + ts: new Date().toISOString(), + } ); + + setAttributes( { + layers: response.layers || attrs.layers, + base_layer: response.base_layer || attrs.base_layer, + center_lat: response.center_lat ?? attrs.center_lat, + center_lon: response.center_lon ?? attrs.center_lon, + initial_zoom: response.initial_zoom ?? attrs.initial_zoom, + pins: response.pins || attrs.pins, + message: response.message || '', + conversation: [ ...( attrs.conversation || [] ), ...convUpdates ], + } ); + + if ( response.base_layer?.variant ) { + setBaseVariant( response.base_layer.variant ); + } + } else { + convUpdates.push( { + role: 'assistant', + text: response.message || __( 'Failed to update map.', 'jeowp' ), + ts: new Date().toISOString(), + } ); + setAttributes( { + conversation: [ ...( attrs.conversation || [] ), ...convUpdates ], + } ); + } + } ) + .catch( ( error ) => { + const attrs = attrsRef.current; + const convUpdates = []; + if ( text.trim() && 'regenerate' !== type ) { + convUpdates.push( { role: 'user', text, ts: new Date().toISOString() } ); + } + convUpdates.push( { + role: 'assistant', + text: error.message || __( 'Request failed.', 'jeowp' ), + ts: new Date().toISOString(), + } ); + setAttributes( { + conversation: [ ...( attrs.conversation || [] ), ...convUpdates ], + } ); + } ) + .finally( () => { + setChatLoading( false ); + setChatInput( '' ); + } ); + } catch ( syncError ) { + // eslint-disable-next-line no-console + console.error( '[JEO Minimap] Synchronous error during chat:', syncError ); + const currentAttrs = attrsRef.current; + const convUpdates = []; + if ( text.trim() && 'regenerate' !== type ) { + convUpdates.push( { role: 'user', text, ts: new Date().toISOString() } ); + } + convUpdates.push( { + role: 'assistant', + text: syncError.message || __( 'Could not send message.', 'jeowp' ), + ts: new Date().toISOString(), + } ); + setAttributes( { + conversation: [ ...( currentAttrs.conversation || [] ), ...convUpdates ], + } ); + setChatLoading( false ); + setChatInput( '' ); + } + }, [ chatLoading, setAttributes ] ); + + const generateFromChatPrompt = useCallback( () => { + if ( ! chatPrompt?.trim() || chatLoading ) { + return; + } + + const attrs = attrsRef.current; + const convId = attrs.conversation_id || generateUUID(); + const userMsg = chatPrompt.trim(); + + const postId = wp.data.select( 'core/editor' ).getCurrentPostId(); + if ( ! postId ) { + setAttributes( { + status: 'error', + message: __( 'Save the post before generating the map.', 'jeowp' ), + } ); + return; + } + + setChatLoading( true ); + setChatPromptVisible( false ); + + try { + apiFetch( { + path: '/jeo/v1/minimap/setup-prompt', + method: 'POST', + data: { + prompt: userMsg, + post_id: postId, + conversation_id: convId, + }, + } ) + .then( ( response ) => { + const currentAttrs = attrsRef.current; + const convUpdates = [ + { role: 'user', text: userMsg, ts: new Date().toISOString() }, + ]; + + if ( response.success ) { + convUpdates.push( { + role: 'assistant', + text: response.assistant_message || response.message || __( 'Map generated from prompt.', 'jeowp' ), + ts: new Date().toISOString(), + } ); + + setAttributes( { + layers: response.layers || [], + base_layer: response.base_layer || null, + center_lat: response.center_lat, + center_lon: response.center_lon, + initial_zoom: response.initial_zoom, + pins: response.pins || currentAttrs.pins, + message: response.message || '', + conversation_id: convId, + conversation: [ ...( currentAttrs.conversation || [] ), ...convUpdates ], + } ); + + if ( response.base_layer?.variant ) { + setBaseVariant( response.base_layer.variant ); + } + } else { + convUpdates.push( { + role: 'assistant', + text: response.message || __( 'Failed to generate from prompt.', 'jeowp' ), + ts: new Date().toISOString(), + } ); + setAttributes( { + conversation: [ ...( currentAttrs.conversation || [] ), ...convUpdates ], + } ); + } + } ) + .catch( ( error ) => { + const currentAttrs = attrsRef.current; + setAttributes( { + conversation: [ ...( currentAttrs.conversation || [] ), + { role: 'user', text: userMsg, ts: new Date().toISOString() }, + { + role: 'assistant', + text: error.message || __( 'Request failed.', 'jeowp' ), + ts: new Date().toISOString(), + }, + ], + } ); + } ) + .finally( () => { + setChatLoading( false ); + setChatPrompt( '' ); + } ); + } catch ( syncError ) { + // eslint-disable-next-line no-console + console.error( '[JEO Minimap] Synchronous error during chat prompt:', syncError ); + const currentAttrs = attrsRef.current; + setAttributes( { + conversation: [ ...( currentAttrs.conversation || [] ), + { role: 'user', text: userMsg, ts: new Date().toISOString() }, + { + role: 'assistant', + text: syncError.message || __( 'Could not generate from prompt.', 'jeowp' ), + ts: new Date().toISOString(), + }, + ], + } ); + setChatLoading( false ); + setChatPrompt( '' ); + } + }, [ chatPrompt, chatLoading, setAttributes ] ); + + const resuggest = useCallback( () => { + if ( attrsRef.current.conversation_id ) { + sendChat( '', 'regenerate' ); + return; + } + + const postId = wp.data.select( 'core/editor' ).getCurrentPostId(); + if ( ! postId ) { + setAttributes( { + status: 'error', + message: __( 'Save the post before generating the map.', 'jeowp' ), + } ); + return; + } + + const convId = generateUUID(); + setAttributes( { status: 'loading', message: '' } ); + + try { + apiFetch( { + path: '/jeo/v1/minimap/setup', + method: 'POST', + data: { post_id: postId, conversation_id: convId }, + } ) + .then( ( response ) => { + if ( response.success ) { + const updates = { + status: 'ready', + layers: response.layers || [], + base_layer: response.base_layer || null, + center_lat: response.center_lat, + center_lon: response.center_lon, + initial_zoom: response.initial_zoom, + pins: response.pins || [], + message: response.message || '', + conversation_id: convId, + conversation: [ + { + role: 'assistant', + text: response.message || __( 'Map regenerated from post content.', 'jeowp' ), + ts: new Date().toISOString(), + }, + ], + }; + + if ( response.base_layer?.variant ) { + setBaseVariant( response.base_layer.variant ); + } + + setAttributes( updates ); + } else { + setAttributes( { + status: 'error', + message: response.message || __( 'Failed to suggest layers.', 'jeowp' ), + } ); + } + } ) + .catch( ( error ) => { + // eslint-disable-next-line no-console + console.error( '[JEO Minimap] Resuggest request failed:', error ); + setAttributes( { + status: 'error', + message: error.message || __( 'Request failed.', 'jeowp' ), + } ); + } ); + } catch ( syncError ) { + // eslint-disable-next-line no-console + console.error( '[JEO Minimap] Synchronous error during resuggest:', syncError ); + setAttributes( { + status: 'error', + message: syncError.message || __( 'Could not start map generation.', 'jeowp' ), + } ); + } + }, [ sendChat, setAttributes ] ); + + const generateFromPrompt = useCallback( () => { + if ( ! attributes.prompt?.trim() ) { + return; + } + + const postId = wp.data.select( 'core/editor' ).getCurrentPostId(); + if ( ! postId ) { + setAttributes( { + status: 'error', + message: __( 'Save the post before generating the map.', 'jeowp' ), + } ); + return; + } + + setAttributes( { status: 'loading' } ); + + const convId = attrsRef.current.conversation_id || generateUUID(); + + try { + apiFetch( { + path: '/jeo/v1/minimap/setup-prompt', + method: 'POST', + data: { + prompt: attributes.prompt, + post_id: postId, + conversation_id: convId, + }, + } ) + .then( ( response ) => { + if ( response.success ) { + const convUpdates = [ + { role: 'user', text: attributes.prompt, ts: new Date().toISOString() }, + { + role: 'assistant', + text: response.assistant_message || response.message || __( 'Map generated from prompt.', 'jeowp' ), + ts: new Date().toISOString(), + }, + ]; + + const updates = { + status: 'ready', + layers: response.layers || [], + base_layer: response.base_layer || null, + center_lat: response.center_lat, + center_lon: response.center_lon, + initial_zoom: response.initial_zoom, + pins: response.pins || [], + message: response.message || '', + conversation_id: convId, + conversation: [ ...( attrsRef.current.conversation || [] ), ...convUpdates ], + }; + + if ( response.base_layer?.variant ) { + setBaseVariant( response.base_layer.variant ); + } + + setAttributes( updates ); + } else { + setAttributes( { + status: 'error', + message: response.message || __( 'Failed to generate from prompt.', 'jeowp' ), + } ); + } + } ) + .catch( ( error ) => { + // eslint-disable-next-line no-console + console.error( '[JEO Minimap] Prompt request failed:', error ); + setAttributes( { + status: 'error', + message: error.message || __( 'Request failed.', 'jeowp' ), + conversation: [ ...( attrsRef.current.conversation || [] ), + { role: 'user', text: attributes.prompt, ts: new Date().toISOString() }, + { + role: 'assistant', + text: error.message || __( 'Request failed.', 'jeowp' ), + ts: new Date().toISOString(), + }, + ], + } ); + } ); + } catch ( syncError ) { + // eslint-disable-next-line no-console + console.error( '[JEO Minimap] Synchronous error during prompt setup:', syncError ); + setAttributes( { + status: 'error', + message: syncError.message || __( 'Could not start map generation.', 'jeowp' ), + conversation: [ ...( attrsRef.current.conversation || [] ), + { role: 'user', text: attributes.prompt, ts: new Date().toISOString() }, + { + role: 'assistant', + text: syncError.message || __( 'Could not start map generation.', 'jeowp' ), + ts: new Date().toISOString(), + }, + ], + } ); + } + }, [ attributes.prompt, setAttributes ] ); + + const handleBaseVariantChange = useCallback( ( newVariant ) => { + setBaseVariant( newVariant ); + + if ( attrsRef.current.conversation_id ) { + sendChat( `Change the base layer variant to ${ newVariant }.`, 'base_variant', { variant: newVariant } ); + return; + } + + const postId = wp.data.select( 'core/editor' ).getCurrentPostId(); + if ( ! postId ) { + return; + } + + try { + apiFetch( { + path: '/jeo/v1/minimap/setup', + method: 'POST', + data: { post_id: postId }, + } ).then( ( response ) => { + if ( response.success && response.base_layer ) { + let chosen = response.base_layer; + + if ( newVariant !== 'none' && response.base_layer.variant !== newVariant ) { + apiFetch( { + path: '/jeo/v1/minimap/setup', + method: 'POST', + data: { post_id: postId, base_variant: newVariant }, + } ).then( ( r2 ) => { + if ( r2.success && r2.base_layer ) { + setAttributes( { base_layer: r2.base_layer } ); + } + } ); + } else { + setAttributes( { base_layer: chosen } ); + } + } + } ); + } catch ( syncError ) { + // eslint-disable-next-line no-console + console.error( '[JEO Minimap] Synchronous error during base variant change:', syncError ); + } + }, [ sendChat, setAttributes ] ); + + const closeModal = useCallback( () => setModal( false ), [] ); + const openModal = useCallback( () => setModal( true ), [] ); + + // The minimap preview renders inside the editor iframe while the block + // sidebar (InspectorControls) lives in the parent document — resolve the + // top window and dispatch there, mirroring the iframe bridge pattern used + // by map-editor-preview.js. + const openSidebarToLayers = useCallback( () => { + let topWin; + try { + topWin = window.parent !== window ? window.parent : window; + topWin.wp.data.dispatch( 'core/edit-post' ).openGeneralSidebar( 'edit-post/block' ); + } catch ( e ) { + return; + } + + const scrollToLayersPanel = () => { + const panel = topWin.document.querySelector( '.jeo-layers-panel' ); + if ( panel ) { + panel.scrollIntoView( { behavior: 'smooth', block: 'start' } ); + return true; + } + return false; + }; + + topWin.setTimeout( () => { + if ( scrollToLayersPanel() ) { + return; + } + // The inspector may be showing the "Styles" tab, which unmounts the + // Settings panels — switch back to "Settings" and try again. + const settingsTab = Array.from( + topWin.document.querySelectorAll( + '.block-editor-block-inspector [role="tab"]' + ) + ).find( ( tab ) => /settings/i.test( tab.textContent ) ); + if ( settingsTab ) { + settingsTab.click(); + topWin.setTimeout( scrollToLayersPanel, 150 ); + } + }, 100 ); + }, [] ); + + const setPanLimitsFromMap = () => { + const map = mapRef.current; + if ( map ) { + const bounds = map.getBounds(); + const ne = bounds.getNorthEast(); + const sw = bounds.getSouthWest(); + setAttributes( { + pan_limits: { + east: ne.lng, + north: ne.lat, + south: sw.lat, + west: sw.lng, + }, + } ); + } + }; + + const [ zoomState, setZoomState ] = useState( 'initial_zoom' ); + const currentZoom = normalizedAttributes[ zoomState ]; + const mapRef = useRef( undefined ); + + const debouncedOnMove = useMemo( + () => debounce( ( { viewState } ) => { + setAttributes( { + center_lat: viewState.latitude, + center_lon: viewState.longitude, + } ); + }, 300 ), + [ setAttributes ] + ); + + const debouncedOnZoom = useMemo( + () => debounce( ( { viewState } ) => { + const zoom = Math.round( viewState.zoom * 10 ) / 10; + setAttributes( { [ zoomState ]: zoom } ); + }, 300 ), + [ setAttributes, zoomState ] + ); + + useEffect( () => { + return () => { + debouncedOnMove.cancel(); + debouncedOnZoom.cancel(); + }; + }, [ debouncedOnMove, debouncedOnZoom ] ); + + const [ generationMode, setGenerationMode ] = useState( + () => attributes.prompt?.trim() ? 'prompt' : 'content' + ); + + const handleGenerate = useCallback( () => { + if ( generationMode === 'prompt' ) { + generateFromPrompt(); + } else { + generate(); + } + }, [ generationMode, generate, generateFromPrompt ] ); + + if ( attributes.status === 'loading' ) { + return ( +
    + +
    + + + { LOADING_MESSAGES[ loadingMsgIndex ] } + +
    +
    +
    + ); + } + + if ( attributes.status === 'idle' || attributes.status === 'error' ) { + return ( +
    + + { AI_CONFIGURED && ( +

    + { __( 'AI will analyze your content and suggest relevant map layers, center point, and zoom level.', 'jeowp' ) } +

    + ) } + { attributes.status === 'error' && ( + + { attributes.message } + + ) } + { AI_CONFIGURED && ( + <> + + { generationMode === 'prompt' && ( + setAttributes( { prompt: v } ) } + /> + ) } + + + ) } +
    +
    + ); + } + + return ( +
    + { modal && ( + { + if ( update.layers ) { + const baseId = attributes.base_layer?.id; + const newBase = update.layers.find( ( l ) => l.id === baseId ); + const newThematic = update.layers.filter( ( l ) => l.id !== baseId ); + setAttributes( { + base_layer: newBase || attributes.base_layer, + layers: newThematic, + } ); + } else { + setAttributes( update ); + } + } } + loadedLayers={ loadedLayers } + loadingLayers={ loadingLayers } + /> + ) } + + + + + + + + + setAttributes( { show_pins: v === 'yes' } ) } + /> + + { AI_CONFIGURED && ( + + { attributes.conversation?.length > 0 && ( +
    + { attributes.conversation.map( ( msg, i ) => ( +
    + + { 'assistant' === msg.role ? __( 'Assistant', 'jeowp' ) : __( 'You', 'jeowp' ) } + + { msg.text } +
    + ) ) } + { chatLoading && ( +
    + { __( 'Assistant', 'jeowp' ) } + + + +
    + ) } +
    +
    + ) } +
    + handleChatKeyDown( e, () => sendChat( chatInput ) ) } + placeholder={ __( 'Ask the AI to refine the map… (Shift+Enter for a line break)', 'jeowp' ) } + /> +
    +
    + + +
    + { chatPromptVisible && ( +
    + handleChatKeyDown( e, generateFromChatPrompt ) } + placeholder={ __( 'Describe the map you want… (Shift+Enter for a line break)', 'jeowp' ) } + /> + +
    + ) } + + ) } + + + { attributes.message && ( + + { attributes.message } + + ) } + +
    + { isPreparingComposedPreview && } + { ( styleBase || useComposedPreview ) && ( +
    + { __( 'Vector style preview', 'jeowp' ) } +
    + ) } + { ! isPreparingComposedPreview && ( + { + if ( useComposedPreview ) { + applyComposedVisibility(); + } else if ( styleBase ) { + applyStyleLayerFiltering( + mapRef.current, + styleBase.instance + ); + } + } } + > + { loadedLayers.length > 0 && + allLayers.map( ( layer ) => { + const layerRecord = loadedLayers.find( + ( { id } ) => id === layer.id + ); + + if ( ! layerRecord?.meta ) { + return null; + } + + if ( useComposedPreview ) { + return null; + } + + if ( hasMapboxLayers && layerRecord.meta.type === 'mapbox' ) { + return null; + } + + return ; + } ) } + { attributes.show_pins && + ( attributes.pins || [] ).map( ( pin, i ) => ( + + ) ) } + + ) } +
    + +
    + + { allLayers.length > 0 && ( + + ) } +
    +
    + ); +} diff --git a/src/js/src/map-blocks/onetime-map-editor.css b/src/js/src/map-blocks/onetime-map-editor.css index 3b577e3a7..b4453800f 100644 --- a/src/js/src/map-blocks/onetime-map-editor.css +++ b/src/js/src/map-blocks/onetime-map-editor.css @@ -366,6 +366,195 @@ ul.jeo-layers-list .components-card { .jeo-setting + button { margin-top: 1em; } +.jeo-setting-layer { + line-height: 1.35; +} .jeo-setting-layer h2 { + font-size: 1em; + line-height: inherit; + margin-bottom: 0.5em; +} + +/** + * Minimap Placeholder + */ +.jeo-minimap-placeholder { + padding: 2em; + text-align: center; +} + +.jeo-minimap-placeholder .components-placeholder__label { + font-size: 18px; + font-weight: 600; +} + +.jeo-minimap-placeholder__description { + color: #757575; + font-size: 13px; + line-height: 1.5; +} + +.jeo-minimap-placeholder__error { + margin-bottom: 16px; +} + +.jeo-minimap-placeholder__generate { + margin-top: 16px; +} + +/** + * Minimap Preview Controls + */ +.jeo-preview-controls { + display: flex; + flex-direction: row; + align-items: center; + gap: 8px; +} + +.jeo-minimap-placeholder__loading { + display: flex; + flex-direction: column; + align-items: center; + gap: 16px; + padding: 24px 0; +} + +.jeo-minimap-placeholder__loading-text { + color: #757575; + font-size: 13px; + transition: opacity 0.3s ease-in-out; +} + +/** + * Minimap Chat Panel + */ +.jeo-minimap-chat-panel .jeo-chat-messages { + max-height: 400px; + overflow-y: auto; + margin-bottom: 12px; + border: 1px solid #e0e0e0; + border-radius: 4px; + padding: 8px; + background: #fafafa; +} + +.jeo-chat-message { + margin-bottom: 8px; + padding: 6px 10px; + border-radius: 6px; + font-size: 13px; + line-height: 1.4; + max-width: 85%; + word-wrap: break-word; +} + +.jeo-chat-message:last-child { margin-bottom: 0; } + +.jeo-chat-message--user { + margin-left: auto; + background: #007cba; + color: #fff; +} + +.jeo-chat-message--assistant { + margin-right: auto; + background: #f0f0f0; + color: #1e1e1e; +} + +.jeo-chat-message--typing { + display: flex; + align-items: center; + gap: 6px; +} + +.jeo-chat-typing-dots { + display: inline-flex; + gap: 3px; + align-items: center; + padding: 2px 0; +} + +.jeo-chat-typing-dots span { + display: inline-block; + width: 6px; + height: 6px; + border-radius: 50%; + background: #757575; + animation: jeo-typing-bounce 1.2s ease-in-out infinite; +} + +.jeo-chat-typing-dots span:nth-child( 2 ) { + animation-delay: 0.15s; +} + +.jeo-chat-typing-dots span:nth-child( 3 ) { + animation-delay: 0.3s; +} + +@keyframes jeo-typing-bounce { + 0%, 60%, 100% { + transform: translateY( 0 ); + opacity: 0.4; + } + 30% { + transform: translateY( -4px ); + opacity: 1; + } +} + +.jeo-chat-message__role { + display: block; + font-size: 11px; + font-weight: 600; + margin-bottom: 2px; + opacity: 0.7; +} + +.jeo-chat-message--user .jeo-chat-message__role { + color: rgba( 255, 255, 255, 0.8 ); +} + +.jeo-chat-input-row { + display: flex; + gap: 6px; + align-items: flex-end; + margin-bottom: 8px; +} + +.jeo-chat-input-row .components-base-control { + flex: 1; + margin-bottom: 0; +} + +.jeo-chat-input-row .components-base-control__field { + margin-bottom: 0; +} + +.jeo-chat-input-row textarea { + min-height: 36px; + resize: vertical; +} + +.jeo-chat-actions { + display: flex; + gap: 8px; + margin-bottom: 8px; +} + +.jeo-chat-prompt-area { + margin-top: 8px; +} + +.jeo-chat-prompt-area textarea { + min-height: 60px; + resize: vertical; +} + +.jeo-chat-prompt-area .components-button { + margin-top: 8px; + display: block; +} diff --git a/src/js/src/map-blocks/onetime-map-editor.js b/src/js/src/map-blocks/onetime-map-editor.js index fcb0f1ffd..6be98cdd0 100644 --- a/src/js/src/map-blocks/onetime-map-editor.js +++ b/src/js/src/map-blocks/onetime-map-editor.js @@ -7,6 +7,11 @@ import { __ } from '@wordpress/i18n'; import { Map } from '../lib/mapgl-react'; import LayersSettingsModal from './layers-settings-modal'; import { renderLayer } from './map-preview-layer'; +import { + applyStyleLayerFiltering, + findStyleLayer, + styleLayerMapProps, +} from './use-style-layer'; import { coerceOnetimeMapAttributes } from './onetime-map-config'; import MapPanel from './map-panel'; import LayersPanel from './layers-panel'; @@ -60,7 +65,7 @@ export default function OnetimeMapEditor ( { attributes, setAttributes, clientId const layerIds = useMemo( () => { return normalizedAttributes.layers.map( ( layer ) => layer.id ); - }, [ normalizedAttributes.layers ] ); + }, [ JSON.stringify( normalizedAttributes.layers.map( ( l ) => l.id ) ) ] ); const layerSettingsKey = useMemo( () => JSON.stringify( normalizedAttributes.layers || [] ), [ normalizedAttributes.layers ] @@ -120,6 +125,11 @@ export default function OnetimeMapEditor ( { attributes, setAttributes, clientId applyComposedVisibility(); }, [ applyComposedVisibility ] ); + const styleBase = useMemo( + () => findStyleLayer( loadedLayers, normalizedAttributes.layers ), + [ loadedLayers, normalizedAttributes.layers ] + ); + const setPanLimitsFromMap = () => { const { current: map } = mapRef; if ( map ) { @@ -157,6 +167,7 @@ export default function OnetimeMapEditor ( { attributes, setAttributes, clientId renderPanel={ PanelBody } setZoomState={ setZoomState } setPanLimitsFromMap={ setPanLimitsFromMap } + isMapBlock={ true } /> { + return !! select( 'core/blocks' ).getBlockType( 'newspack-blocks/homepage-articles' ); + }, [] ); + + const categoryList = useSelect( ( select ) => { + return select( 'core' ).getEntityRecords( 'taxonomy', 'category', { + per_page: 100, + hide_empty: true, + } ); + }, [] ); + + const tagList = useSelect( ( select ) => { + return select( 'core' ).getEntityRecords( 'taxonomy', 'post_tag', { + per_page: 100, + hide_empty: true, + } ); + }, [] ); + + const enabledPostTypes = useSelect( ( select ) => { + const types = select( 'core' ).getPostTypes( { per_page: -1 } ); + if ( ! types ) { + return []; + } + const jeoTypes = globalThis.jeo_settings?.enabled_post_types || [ 'post' ]; + return types.filter( + ( t ) => jeoTypes.includes( t.slug ) && t.rest_base + ); + }, [] ); + + const imageSizeOptions = useSelect( ( select ) => { + return ( select( 'core/block-editor' ).getSettings().imageSizes || [] ).map( + ( s ) => ( { label: s.name, value: s.slug } ) + ); + }, [] ); + + const categoryOptions = [ + { label: __( '— None —', 'jeowp' ), value: '' }, + ...( categoryList || [] ).map( ( c ) => ( { + label: c.name, + value: String( c.id ), + } ) ), + ]; + + const tagOptions = [ + { label: __( '— None —', 'jeowp' ), value: '' }, + ...( tagList || [] ).map( ( t ) => ( { + label: t.name, + value: String( t.id ), + } ) ), + ]; + + const postTypeOptions = [ + { label: __( 'All geo-enabled types', 'jeowp' ), value: '' }, + ...enabledPostTypes.map( ( t ) => ( { + label: t.labels?.name || t.slug, + value: t.slug, + } ) ), + ]; + + const parseMultiSelect = ( val ) => { + if ( ! val || ! val.length ) { + return ''; + } + return val.map( ( v ) => String( v ) ).join( ',' ); + }; + + const selectedCategories = categories + ? categories.split( ',' ).filter( Boolean ) + : []; + const selectedTags = tags + ? tags.split( ',' ).filter( Boolean ) + : []; + const selectedCatExcl = categoryExclusions + ? categoryExclusions.split( ',' ).filter( Boolean ) + : []; + const selectedTagExcl = tagExclusions + ? tagExclusions.split( ',' ).filter( Boolean ) + : []; + + return ( +
    + + + setPreviewLat( Number.parseFloat( val ) || 0 ) } + /> + setPreviewLng( Number.parseFloat( val ) || 0 ) } + /> + + + + setAttributes( { postLayout: val } ) } + /> + { postLayout === 'grid' && ( + setAttributes( { postsPerRow: val } ) } + min={ 1 } + max={ 6 } + /> + ) } + { hasNewspack && ( + <> + setAttributes( { mediaPosition: val } ) } + /> + setAttributes( { imageShape: val } ) } + /> + + ) } + { ! hasNewspack && ( + <> + setAttributes( { imageSize: val } ) } + /> + setAttributes( { imageAsLink: val } ) } + /> + + ) } + + + + setAttributes( { postsPerPage: val } ) } + min={ 1 } + max={ 36 } + /> + setAttributes( { radius: val } ) } + min={ 1 } + max={ 2000 } + /> + setAttributes( { orderBy: val } ) } + /> + setAttributes( { maxAgeDays: val } ) } + min={ 0 } + max={ 3650 } + /> + { orderBy === 'relevance' && ( + <> + setAttributes( { distanceWeight: val } ) } + min={ 0 } + max={ 10 } + step={ 0.1 } + /> + setAttributes( { dateWeight: val } ) } + min={ 0 } + max={ 10 } + step={ 0.1 } + /> + + ) } + { enabledPostTypes.length > 1 && ( + setAttributes( { postType: val } ) } + /> + ) } + + + + setAttributes( { categories: parseMultiSelect( val ) } ) } + /> + setAttributes( { tags: parseMultiSelect( val ) } ) } + /> + setAttributes( { categoryExclusions: parseMultiSelect( val ) } ) } + /> + setAttributes( { tagExclusions: parseMultiSelect( val ) } ) } + /> + + + + setAttributes( { showThumbnail: val } ) } + /> + { hasNewspack && ( + setAttributes( { showCategory: val } ) } + /> + ) } + setAttributes( { showDate: val } ) } + /> + setAttributes( { showExcerpt: val } ) } + /> + { showExcerpt && ( + setAttributes( { excerptLength: val } ) } + min={ 5 } + max={ 200 } + /> + ) } + setAttributes( { showAuthor: val } ) } + /> + { hasNewspack && showAuthor && ( + setAttributes( { showAvatar: val } ) } + /> + ) } + { hasNewspack && ( + <> + setAttributes( { showReadMore: val } ) } + /> + { showReadMore && ( + setAttributes( { readMoreLabel: val } ) } + /> + ) } + + ) } + + + { hasNewspack && ( + + setAttributes( { typeScale: val } ) } + min={ 1 } + max={ 10 } + /> + setAttributes( { imageScale: val } ) } + min={ 1 } + max={ 4 } + /> + setAttributes( { colGap: val } ) } + min={ 1 } + max={ 3 } + /> + setAttributes( { minHeight: val } ) } + min={ 0 } + max={ 100 } + /> + + ) } + + + +
    + ); +} diff --git a/src/js/src/map-blocks/storymap-editor.js b/src/js/src/map-blocks/storymap-editor.js index f3f4cdba0..61b3ecb52 100755 --- a/src/js/src/map-blocks/storymap-editor.js +++ b/src/js/src/map-blocks/storymap-editor.js @@ -38,6 +38,11 @@ import { createUploadAdapter } from './cke5-image-upload'; import { baseColors } from './color-palettes'; import { Map as MapPreview } from '../lib/mapgl-react'; import { renderLayer } from './map-preview-layer'; +import { + applyStyleLayerFiltering, + findStyleLayer, + styleLayerMapProps, +} from './use-style-layer'; import JeoAutosuggest from './jeo-autosuggest'; import JeoGeoAutoComplete from '../posts-sidebar/geo-auto-complete'; import { @@ -49,6 +54,7 @@ import { } from './storymap-ordering'; import { useRecordsByIds } from '../shared/rest-records'; import { computeInlineEnd } from '../shared/direction'; +import { decodeHtmlEntity } from '../shared/html'; import { getCKEditorLanguage } from '../shared/locale'; import { applyComposedVisibilityFromSelection, @@ -96,10 +102,6 @@ function createInitialViewState () { }; } -function decodeHtmlEntity ( str ) { - return str.replace( /&#(\d+);/g, ( match, dec ) => String.fromCharCode( dec ) ); -}; - function removeTags(str) { if ((str == null) || (str === '')) { return false; @@ -109,11 +111,13 @@ function removeTags(str) { } function flyTo ( map, location ) { + const lat = parseFloat( location.lat ); + const lng = parseFloat( location.lng || location.lon ); + + if ( isNaN( lat ) || isNaN( lng ) ) return; + map.flyTo({ - center: [ - parseFloat( location.lon ), - parseFloat( location.lat ), - ] + center: [ lng, lat ] }); } @@ -550,7 +554,7 @@ export default function StoryMapEditor ( { attributes, setAttributes } ) { return []; } return loadedMap.meta.layers.map( ( layer ) => layer.id ); - }, [ loadedMap?.meta.layers ] ); + }, [ JSON.stringify( loadedMap?.meta.layers?.map( ( l ) => l.id ) || [] ) ] ); const { records: loadedLayers = [], @@ -614,6 +618,17 @@ export default function StoryMapEditor ( { attributes, setAttributes } ) { useComposedPreview, ] ); + const currentSlideLayers = + attributes.slides?.[ currentSlideIndex ]?.selectedLayers || []; + const styleBase = useMemo( + () => + findStyleLayer( + attributes.navigateMapLayers || [], + currentSlideLayers + ), + [ attributes.navigateMapLayers, currentSlideLayers ] + ); + useEffect( () => { const currentSlide = attributes.slides?.[ currentSlideIndex ]; setViewState( { diff --git a/src/js/src/map-blocks/use-style-layer.js b/src/js/src/map-blocks/use-style-layer.js new file mode 100644 index 000000000..0f82419af --- /dev/null +++ b/src/js/src/map-blocks/use-style-layer.js @@ -0,0 +1,200 @@ +import { + mapboxToken, + transformRequest as defaultTransformRequest, +} from '../lib/mapgl-loader'; + +function getInlineStyle( options ) { + const raw = ( options.inline_style || '' ).trim(); + + if ( ! raw ) { + return null; + } + + try { + const parsed = JSON.parse( raw ); + return parsed && typeof parsed === 'object' ? parsed : null; + } catch ( error ) { + console.warn( '[JEO] Invalid inline style JSON.', error ); + return null; + } +} + +/** + * Resolve a style-json layer's style from its options, without requiring a + * layer instance. Inline JSON takes precedence over the style URL. Both are + * valid `mapStyle` inputs for the map preview. + * + * @param {Object} options layer_type_options. + * @return {string|Object|null} + */ +export function getStyleJsonStyle( options = {} ) { + return getInlineStyle( options ) || ( options.style_url || '' ).trim() || null; +} + +/** + * Resolve the base style of a style-type layer instance (mapbox, style-json, + * or any future provider following the same conventions). + * + * Returns a style URL string or an inline style object — both are valid + * `map.setStyle()` / `mapStyle` inputs. Resolution order mirrors the + * geojson layer type: inline JSON takes precedence over the URL. + * + * @param {Object} layer Layer CPT record meta. + * @param {Object} instance Layer instance settings from the map config. + * @return {string|Object|null} + */ +export function getStyleLayerStyle( layer, instance ) { + if ( ! instance?.load_as_style || ! layer ) { + return null; + } + + const options = layer.layer_type_options || {}; + + const inline = getInlineStyle( options ); + if ( inline ) { + return inline; + } + + // Generic style-URL convention: any style-type layer (style-json and + // future providers) can point directly to a style JSON URL. + if ( options.style_url ) { + return options.style_url.trim(); + } + + // Legacy Mapbox style: resolve through the Mapbox Styles API. + if ( 'mapbox' === layer.type && options.style_id ) { + const accessToken = options.access_token || mapboxToken; + const styleId = options.style_id.replace( 'mapbox://styles/', '' ); + + if ( styleId && accessToken ) { + return `https://api.mapbox.com/styles/v1/${ styleId }?access_token=${ accessToken }`; + } + } + + return null; +} + +function getCustomTokens( layer, instance ) { + if ( ! instance?.load_as_style || layer?.type !== 'mapbox' ) { + return null; + } + + const options = layer.layer_type_options || {}; + if ( ! options.access_token || ! options.style_id ) { + return null; + } + + const styleId = options.style_id.replace( 'mapbox://styles/', '' ); + const mapboxUser = styleId.split( '/' )[ 0 ]; + + return { [ mapboxUser ]: options.access_token }; +} + +function createTokenAwareTransformRequest( customTokens ) { + if ( ! customTokens ) { + return null; + } + + const users = Object.keys( customTokens ); + + return ( url, resourceType ) => { + let resolvedUrl = url; + + for ( const user of users ) { + if ( + url.includes( `${ user }/` ) || + url.includes( `${ user }.` ) + ) { + const accessToken = customTokens[ user ]; + try { + const parsedUrl = new URL( url ); + const parsedParams = new URLSearchParams( + parsedUrl.search + ); + if ( + parsedParams.get( 'access_token' ) !== accessToken + ) { + parsedParams.set( 'access_token', accessToken ); + parsedUrl.search = + '?' + parsedParams.toString(); + resolvedUrl = parsedUrl.toString(); + } + } catch ( e ) { + // URL parsing may fail for non-standard protocols + } + break; + } + } + + if ( defaultTransformRequest ) { + return defaultTransformRequest( resolvedUrl, resourceType ); + } + return { url: resolvedUrl }; + }; +} + +export function findStyleLayer( loadedLayers, instances ) { + if ( ! loadedLayers || ! instances ) { + return null; + } + + for ( const instance of instances ) { + const record = loadedLayers.find( ( r ) => r.id === instance.id ); + if ( ! record?.meta ) { + continue; + } + + const style = getStyleLayerStyle( record.meta, instance ); + if ( style ) { + const customTokens = getCustomTokens( record.meta, instance ); + return { + instance, + style, + transformRequest: createTokenAwareTransformRequest( + customTokens + ), + }; + } + } + + return null; +} + +export function styleLayerMapProps( styleBase ) { + if ( ! styleBase ) { + return {}; + } + + const props = { mapStyle: styleBase.style }; + if ( styleBase.transformRequest ) { + props.transformRequest = styleBase.transformRequest; + } + return props; +} + +function hasMapLayer( map, layerId ) { + return typeof map.getLayer === 'function' && map.getLayer( layerId ); +} + +export function applyStyleLayerFiltering( map, instance ) { + if ( ! map || ! instance?.style_layers?.length ) { + return; + } + + instance.style_layers.forEach( ( styleLayer ) => { + const layerId = styleLayer.id; + + if ( ! styleLayer.show ) { + if ( hasMapLayer( map, layerId ) ) { + map.removeLayer( layerId ); + } + return; + } + + if ( ! instance.default ) { + if ( hasMapLayer( map, layerId ) ) { + map.setLayoutProperty( layerId, 'visibility', 'none' ); + } + } + } ); +} diff --git a/src/js/src/map-blocks/use-style-layer.test.js b/src/js/src/map-blocks/use-style-layer.test.js new file mode 100644 index 000000000..c7c3ea684 --- /dev/null +++ b/src/js/src/map-blocks/use-style-layer.test.js @@ -0,0 +1,191 @@ +import { + findStyleLayer, + getStyleJsonStyle, + getStyleLayerStyle, + styleLayerMapProps, +} from './use-style-layer'; + +jest.mock( '../lib/mapgl-loader', () => ( { + mapboxToken: 'global-test-token', + transformRequest: null, +} ) ); + +describe( 'getStyleLayerStyle', () => { + it( 'returns null when the instance does not load as style', () => { + expect( + getStyleLayerStyle( + { type: 'style-json', layer_type_options: { style_url: 'https://example.com/style.json' } }, + { load_as_style: false } + ) + ).toBeNull(); + } ); + + it( 'resolves a style-json URL without requiring a token', () => { + expect( + getStyleLayerStyle( + { type: 'style-json', layer_type_options: { style_url: 'https://tiles.openfreemap.org/styles/dark' } }, + { load_as_style: true } + ) + ).toBe( 'https://tiles.openfreemap.org/styles/dark' ); + } ); + + it( 'gives precedence to inline style JSON over the URL', () => { + const inline = { version: 8, sources: {}, layers: [] }; + + expect( + getStyleLayerStyle( + { + type: 'style-json', + layer_type_options: { + style_url: 'https://example.com/style.json', + inline_style: JSON.stringify( inline ), + }, + }, + { load_as_style: true } + ) + ).toEqual( inline ); + } ); + + it( 'falls back to the URL when inline style JSON is invalid', () => { + expect( + getStyleLayerStyle( + { + type: 'style-json', + layer_type_options: { + style_url: 'https://example.com/style.json', + inline_style: '{invalid json', + }, + }, + { load_as_style: true } + ) + ).toBe( 'https://example.com/style.json' ); + expect( console ).toHaveWarned(); + } ); + + it( 'resolves a legacy mapbox style through the Mapbox API', () => { + expect( + getStyleLayerStyle( + { type: 'mapbox', layer_type_options: { style_id: 'mapbox/dark-v11' } }, + { load_as_style: true } + ) + ).toBe( + 'https://api.mapbox.com/styles/v1/mapbox/dark-v11?access_token=global-test-token' + ); + } ); + + it( 'honors a per-layer mapbox access token', () => { + expect( + getStyleLayerStyle( + { + type: 'mapbox', + layer_type_options: { + style_id: 'mapbox://styles/custom/user-style', + access_token: 'layer-token', + }, + }, + { load_as_style: true } + ) + ).toBe( + 'https://api.mapbox.com/styles/v1/custom/user-style?access_token=layer-token' + ); + } ); + + it( 'returns null for a mapbox style without any token', () => { + const loader = require( '../lib/mapgl-loader' ); + loader.mapboxToken = ''; + + expect( + getStyleLayerStyle( + { type: 'mapbox', layer_type_options: { style_id: 'mapbox/dark-v11' } }, + { load_as_style: true } + ) + ).toBeNull(); + + loader.mapboxToken = 'global-test-token'; + } ); +} ); + +describe( 'getStyleJsonStyle', () => { + it( 'returns inline style when present', () => { + const inline = { version: 8, sources: {}, layers: [] }; + + expect( + getStyleJsonStyle( { + style_url: 'https://example.com/style.json', + inline_style: JSON.stringify( inline ), + } ) + ).toEqual( inline ); + } ); + + it( 'returns the URL when no inline style is set', () => { + expect( + getStyleJsonStyle( { style_url: ' https://example.com/style.json ' } ) + ).toBe( 'https://example.com/style.json' ); + } ); + + it( 'returns null when neither is set', () => { + expect( getStyleJsonStyle( {} ) ).toBeNull(); + } ); +} ); + +describe( 'findStyleLayer', () => { + it( 'hoists a style-json base without a transformRequest', () => { + const styleBase = findStyleLayer( + [ + { + id: 7, + meta: { + type: 'style-json', + layer_type_options: { style_url: 'https://tiles.openfreemap.org/styles/dark' }, + }, + }, + ], + [ { id: 7, load_as_style: true } ] + ); + + expect( styleBase ).not.toBeNull(); + expect( styleBase.style ).toBe( 'https://tiles.openfreemap.org/styles/dark' ); + expect( styleBase.transformRequest ).toBeNull(); + } ); + + it( 'ignores layers that are not loaded as style', () => { + expect( + findStyleLayer( + [ + { + id: 7, + meta: { + type: 'style-json', + layer_type_options: { style_url: 'https://example.com/style.json' }, + }, + }, + ], + [ { id: 7, load_as_style: false } ] + ) + ).toBeNull(); + } ); + + it( 'returns null when nothing resolves', () => { + expect( findStyleLayer( [], [] ) ).toBeNull(); + } ); +} ); + +describe( 'styleLayerMapProps', () => { + it( 'returns an empty object without a style base', () => { + expect( styleLayerMapProps( null ) ).toEqual( {} ); + } ); + + it( 'passes a style URL as mapStyle', () => { + expect( + styleLayerMapProps( { style: 'https://example.com/style.json' } ) + ).toEqual( { mapStyle: 'https://example.com/style.json' } ); + } ); + + it( 'passes an inline style object as mapStyle', () => { + const inline = { version: 8, sources: {}, layers: [] }; + + expect( styleLayerMapProps( { style: inline } ) ).toEqual( { + mapStyle: inline, + } ); + } ); +} ); diff --git a/src/js/src/map-blocks/utils.js b/src/js/src/map-blocks/utils.js index 3dc365d51..f28970cb7 100644 --- a/src/js/src/map-blocks/utils.js +++ b/src/js/src/map-blocks/utils.js @@ -1,4 +1,4 @@ -import { __ } from '@wordpress/i18n'; +import { _x } from '@wordpress/i18n'; export function loadLayer ( layers, settings ) { const layer = ( layers || [] ).find( ( layer ) => layer.id === settings.id ); @@ -6,7 +6,7 @@ export function loadLayer ( layers, settings ) { } export const layerUseLabels = { - fixed: __( 'Fixed', 'jeowp' ), - swappable: __( 'Swappable', 'jeowp' ), - switchable: __( 'Switchable', 'jeowp' ), + fixed: _x( 'Fixed', 'layer', 'jeowp' ), + swappable: _x( 'Swappable', 'layer', 'jeowp' ), + switchable: _x( 'Switchable', 'layer', 'jeowp' ), }; diff --git a/src/js/src/maps-sidebar/map-preview-portal.js b/src/js/src/maps-sidebar/map-preview-portal.js deleted file mode 100644 index fbcbbb919..000000000 --- a/src/js/src/maps-sidebar/map-preview-portal.js +++ /dev/null @@ -1,13 +0,0 @@ -import { createPortal } from '@wordpress/element'; - -export default function MapPreviewPortal( { children } ) { - // Only look in the parent document. In Block API v3 the editor - // content lives inside an iframe — createPortal cannot reliably - // render across document boundaries, so we fall back to inline - // rendering (the preview shows in the sidebar panel instead). - const target = document.getElementById( 'map-preview' ); - if ( ! target ) { - return children; - } - return createPortal( children, target ); -} diff --git a/src/js/src/maps-sidebar/maps-sidebar.js b/src/js/src/maps-sidebar/maps-sidebar.js index db6de14b8..cf87e5979 100644 --- a/src/js/src/maps-sidebar/maps-sidebar.js +++ b/src/js/src/maps-sidebar/maps-sidebar.js @@ -1,7 +1,7 @@ +import { useEntityRecords } from '@wordpress/core-data'; import { select, withDispatch, withSelect } from '@wordpress/data'; import { PluginDocumentSettingPanel } from '@wordpress/editor'; import { useCallback, useMemo, useState } from '@wordpress/element'; -import { __ } from '@wordpress/i18n'; import LayersPanel from '../map-blocks/layers-panel'; import LayersSettingsModal from '../map-blocks/layers-settings-modal'; @@ -9,9 +9,6 @@ import MapPanel from '../map-blocks/map-panel'; import MapEmbedUrl from './map-embed-url'; import PostsSelector from '../posts-selector'; import { normalizeRelatedPosts } from '../posts-selector/defaults'; -import { useRecordsByIds } from '../shared/rest-records'; - -import './maps-sidebar.scss'; function MapsSidebar( { postId, @@ -25,15 +22,19 @@ function MapsSidebar( { return ( postMeta.layers || [] ).map( ( layer ) => layer.id ); }, [ postMeta.layers ] ); - const { records: loadedLayers = [], isLoading: loadingLayers } = useRecordsByIds( { - path: '/jeo/v1/map-layer', - ids: layerIds, - enabled: layerIds.length > 0, - query: { context: 'edit' }, - } ); + const { records: loadedLayers, isResolving: loadingLayers } = useEntityRecords( + 'postType', + 'map-layer', + { + include: layerIds, + per_page: -1, + }, + { enabled: layerIds.length > 0 } + ); + + const closeModal = useCallback( () => setModal( false ), [] ); + const openModal = useCallback( () => setModal( true ), [] ); - const closeModal = useCallback( () => setModal( false ), [ setModal ] ); - const openModal = useCallback( () => setModal( true ), [ setModal ] ); const setRelatedPosts = useCallback( ( value ) => { setPostMeta( { @@ -43,10 +44,15 @@ function MapsSidebar( { [ setPostMeta ] ); + const setPanLimitsFromMap = useCallback( () => { + if ( typeof window.__jeoSetPanLimitsFromMap === 'function' ) { + window.__jeoSetPanLimitsFromMap(); + } + }, [] ); + const embedUrl = postId && `${ jeo_settings.site_url }/embed/?map_id=${ postId }`; - return ( <> { modal && ( @@ -59,12 +65,15 @@ function MapsSidebar( { /> ) } - { embedUrl && !postMeta.disable_embed && } + { embedUrl && ! postMeta.disable_embed && ( + + ) } ( { setPostMeta: ( meta ) => { const currentMeta = select( 'core/editor' ).getEditedPostAttribute( 'meta' ) || {}; + dispatch( 'core/editor' ).editPost( { meta: { ...currentMeta, ...meta }, } ); diff --git a/src/js/src/maps-sidebar/maps-sidebar.scss b/src/js/src/maps-sidebar/maps-sidebar.scss deleted file mode 100644 index 2beaf471b..000000000 --- a/src/js/src/maps-sidebar/maps-sidebar.scss +++ /dev/null @@ -1,21 +0,0 @@ -#map-preview { - - .mapboxgl-map, .maplibregl-map { - margin: auto; - } -} - -.zoom-button { - display: flex; - align-items: center; - justify-content: center; - border-radius: 3px; - margin: 1px !important; -} - -.button-group-div { - display: inline-flex; - background-color: rgb(182, 182, 182); - padding: 0px 1px 3px; - border-radius: 4px; -} diff --git a/src/js/src/posts-sidebar/geo-posts-ai.js b/src/js/src/posts-sidebar/geo-posts-ai.js new file mode 100644 index 000000000..d55c83fb9 --- /dev/null +++ b/src/js/src/posts-sidebar/geo-posts-ai.js @@ -0,0 +1,343 @@ +import { Button, CheckboxControl, ToggleControl, Spinner } from '@wordpress/components'; +import { Fragment, useState, useEffect, useRef } from '@wordpress/element'; +import { useDraggable } from './use-draggable'; +import { __, sprintf, _n } from '@wordpress/i18n'; +import { MapContainer, Marker, TileLayer } from 'react-leaflet'; +import L from 'leaflet'; +import 'leaflet/dist/leaflet.css'; +import './geo-posts.css'; + +export function JeoGeocodePostsAI ({ aiSuggestedLocations, isAIProcessing, onCancel, saveAiLocations, toggleAiLocation, changeRelevance, onRetry }) { + const [ mapInstance, setMapInstance ] = useState( null ); + const [ mapReady, setMapReady ] = useState( false ); + const [ enriching, setEnriching ] = useState( {} ); // Track which items are being enriched + const [ isMinimized, setIsMinimized ] = useState( false ); + const { position: panelPos, bindDrag: panelDrag } = useDraggable(); + + // Handle map creation + const mapCreated = ( map ) => { + setMapInstance( map ); + }; + + // Handle map loaded to fit bounds + const mapLoaded = () => { + setMapReady( true ); + }; + + // Function to enrich data via reverse geocode + const handleEnrich = async ( index, lat, lng ) => { + setEnriching( prev => ( { ...prev, [ index ]: true } ) ); + + try { + const response = await window.fetch( + globalThis.jeo?.ajax_url + '?action=jeo_reverse_geocode&nonce=' + ( globalThis.jeo?.geocode_nonce || '' ) + '&lat=' + lat + '&lon=' + lng + ); + const result = await response.json(); + + if ( result && ! result.raw?.error ) { + // Format a pretty full address string with more details + let displayAddress = result.full_address || aiSuggestedLocations[ index ]._geocode_full_address; + + // Build structured detail parts + const parts = []; + if ( result.address ) { + parts.push( result.address + ( result.address_number ? ', ' + result.address_number : '' ) ); + } + if ( result.city_level_1 ) parts.push( result.city_level_1 ); + if ( result.city ) parts.push( result.city ); + if ( result.region_level_2 ) parts.push( result.region_level_2 ); + if ( result.postcode ) parts.push( result.postcode ); + if ( result.country ) parts.push( result.country ); + + if ( parts.length > 0 ) { + displayAddress = parts.join( ' - ' ); + } + + const enrichedData = { + _geocode_full_address: displayAddress, + _geocode_country: result.country || '', + _geocode_country_code: result.country_code || '', + _geocode_region_level_1: result.region_level_1 || '', + _geocode_region_level_2: result.region_level_2 || '', + _geocode_region_level_3: result.region_level_3 || '', + _geocode_city: result.city || '', + _geocode_city_level_1: result.city_level_1 || '', + _geocode_address: result.address || '', + _geocode_address_number: result.address_number || '', + _geocode_postcode: result.postcode || '', + _is_enriched: true + }; + + // Using a trick: changeRelevance now accepts an object for enrichment + changeRelevance( index, aiSuggestedLocations[ index ].relevance, enrichedData ); + } + } catch ( error ) { + console.error( 'JEO: Enrichment failed', error ); + } finally { + setEnriching( prev => ( { ...prev, [ index ]: false } ) ); + } + }; + + // Fit map bounds when map is ready and locations exist + useEffect( () => { + if ( mapInstance && mapReady && aiSuggestedLocations.length > 0 ) { + const coords = aiSuggestedLocations + .map( ( loc ) => { + const lat = parseFloat( loc._geocode_lat ); + const lng = parseFloat( loc._geocode_lon ); + return ( isNaN( lat ) || isNaN( lng ) ) ? null : [ lat, lng ]; + } ) + .filter( c => c !== null ); + + if ( coords.length === 1 ) { + mapInstance.setZoom( 10 ); + mapInstance.panTo( coords[ 0 ] ); + } else if ( coords.length > 1 ) { + mapInstance.fitBounds( coords ); + } + } + }, [ mapInstance, mapReady, aiSuggestedLocations ] ); + + // Create marker icon + const pinUrls = window.jeo?.pin_urls || { + primary: 'https://cdn.jsdelivr.net/gh/pointhi/leaflet-color-markers/img/marker-icon-blue.png', + secondary: 'https://cdn.jsdelivr.net/gh/pointhi/leaflet-color-markers/img/marker-icon-grey.png', + }; + const createMarkerIcon = ( isSelected ) => { + return new L.Icon( { + iconUrl: isSelected ? pinUrls.primary : pinUrls.secondary, + iconSize: [ 25, 41 ], + iconAnchor: [ 12, 41 ], + } ); + }; + + // Handle marker click to fly to location + const handleMarkerClick = ( lat, lon ) => { + const latitude = parseFloat( lat ); + const longitude = parseFloat( lon ); + if ( mapInstance && ! isNaN( latitude ) && ! isNaN( longitude ) ) { + mapInstance.flyTo( [ latitude, longitude ], 10 ); + } + }; + + const selectedCount = aiSuggestedLocations.filter( l => l._selected ).length; + + return ( + +
    + {/* Fullscreen map */} +
    + + + { aiSuggestedLocations.map( ( loc, index ) => { + const lat = parseFloat( loc._geocode_lat ); + const lng = parseFloat( loc._geocode_lon ); + if ( isNaN( lat ) || isNaN( lng ) ) return null; + + return ( + handleMarkerClick( loc._geocode_lat, loc._geocode_lon ) } + /> + ); + } ) } + +
    + + {/* Floating overlay panel */} +
    +
    +

    + { isMinimized + ? sprintf( + /* translators: %d: number of selected AI suggestions. */ + _n( 'AI Suggestion (%d selected)', 'AI Suggestions (%d selected)', selectedCount, 'jeowp' ), + selectedCount + ) + : __( 'Review AI Suggestions', 'jeowp' ) + } +

    + +
    + + { ! isMinimized && ( +
    +

    + { __( 'Select locations to add to the map:', 'jeowp' ) } +

    +
    + { aiSuggestedLocations.map( ( loc, index ) => { + const confidenceColor = loc.confidence >= 80 ? '#46b450' : (loc.confidence >= 50 ? '#ffb900' : '#d63638'); + const isPrimary = loc.relevance === 'primary'; + + return ( +
    handleMarkerClick( loc._geocode_lat, loc._geocode_lon ) } + > +
    +
    + toggleAiLocation( index ) } + disabled={ loc._disabled } + /> + + { loc._geocode_full_address } + { loc._is_enriched && ✅ } + { isPrimary && ★ { __( 'Primary', 'jeowp' ) } } + +
    +
    + { loc.confidence }% +
    +
    + +
    +
    +

    + { `${ Number.parseFloat(loc._geocode_lat).toFixed(4) }, ${ Number.parseFloat(loc._geocode_lon).toFixed(4) }` } +

    + +
    + + +
    e.stopPropagation() }> + changeRelevance( index, isPrimary ? 'secondary' : 'primary' ) } + __nextHasNoMarginBottom + /> +
    +
    +
    + + { loc._ai_quote ? ( +
    + { __( 'Context:', 'jeowp' ) } "{ loc._ai_quote }" +
    + ) : null } + + { loc._is_enriched && ( +
    + { __( 'Verified:', 'jeowp' ) } { loc._geocode_full_address } +
    + ) } + + { loc._disabled && ( +

    + { __( 'Score too low for reliable mapping.', 'jeowp' ) } +

    + ) } +
    +
    + ); + } ) } +
    + + + +
    +
    +
    + ) } +
    +
    +
    + ); +} diff --git a/src/js/src/posts-sidebar/geo-posts.css b/src/js/src/posts-sidebar/geo-posts.css index d86bc9937..143be9f31 100644 --- a/src/js/src/posts-sidebar/geo-posts.css +++ b/src/js/src/posts-sidebar/geo-posts.css @@ -113,6 +113,145 @@ } } +/* Fullscreen modal layout: map fills viewport, overlay panel floats on top. + The WordPress Modal component already sets display:flex on the frame and, + when isFullScreen, display:flex on .components-modal__content with + > :last-child { flex: 1 }. We only need to override sizing and ensure + the map container has explicit dimensions. */ +.jeo-geocode-modal .components-modal__frame { + border-radius: 0 !important; + margin: 0 !important; + max-height: 100vh !important; + max-width: 100vw !important; + width: 100vw !important; + height: 100vh !important; +} + +.jeo-geocode-modal .components-modal__content { + padding: 0 !important; + overflow: hidden !important; +} + +.jeo-geocode-modal .components-modal__header { + background: rgba(255, 255, 255, 0.95); +} + +.jeo-geocode-modal__container { + position: relative; + width: 100%; + height: calc(100vh - 72px); + overflow: hidden; +} + +.jeo-geocode-modal__map { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + z-index: 1; +} + +.jeo-geocode-modal__map .leaflet-container { + width: 100% !important; + height: 100% !important; +} + +/* Floating overlay panel */ +.jeo-geocode-modal__panel { + position: absolute; + bottom: 100px; + left: 16px; + width: 420px; + max-height: calc(100% - 96px); + background: rgba(255, 255, 255, 0.96); + border-radius: 8px; + box-shadow: 0 4px 20px rgba(0, 0, 0, 0.25); + backdrop-filter: blur(4px); + z-index: 500; + display: flex; + flex-direction: column; +} + +.jeo-geocode-modal__panel.is-collapsed { + width: auto; + min-width: 180px; + max-height: 48px; + overflow: hidden; +} + +.jeo-geocode-modal__panel.is-collapsed .jeo-geocode-modal__panel-content { + display: none; +} + +.jeo-geocode-modal__panel-header { + display: flex; + align-items: center; + justify-content: space-between; + padding: 10px 14px; + border-bottom: 1px solid #e0e0e0; + flex-shrink: 0; + cursor: move; + user-select: none; +} + +.jeo-geocode-modal__panel-header:hover { + background: rgba(0, 0, 0, 0.02); +} + +.jeo-geocode-modal__panel-header h3 { + margin: 0; + font-size: 14px; + font-weight: 600; + color: #1e1e1e; +} + +.jeo-geocode-modal__panel-toggle { + background: none; + border: none; + cursor: pointer; + padding: 4px 8px; + font-size: 16px; + line-height: 1; + color: #50575e; + border-radius: 4px; + transition: background 0.15s; +} + +.jeo-geocode-modal__panel-toggle:hover { + background: #f0f0f1; + color: #1e1e1e; +} + +.jeo-geocode-modal__panel-content { + padding: 12px 14px; + overflow-y: auto; + flex: 1; +} + +/* Action bar at bottom of modal */ +.jeo-geocode-modal__actions { + position: absolute; + bottom: 16px; + right: 16px; + z-index: 500; + display: flex; + gap: 10px; + background: rgba(255, 255, 255, 0.95); + padding: 10px 16px; + border-radius: 8px; + box-shadow: 0 2px 12px rgba(0, 0, 0, 0.15); + backdrop-filter: blur(4px); +} + +/* Responsive: panel stacks on narrow screens */ +@media screen and (max-width: 600px) { + .jeo-geocode-modal__panel { + width: calc(100% - 32px); + max-height: 45%; + } +} + @media screen and (min-width: 1000px) { .jeo-geocode-posts { flex-direction: row; diff --git a/src/js/src/posts-sidebar/geo-posts.js b/src/js/src/posts-sidebar/geo-posts.js index c6e097463..deb2975b0 100644 --- a/src/js/src/posts-sidebar/geo-posts.js +++ b/src/js/src/posts-sidebar/geo-posts.js @@ -1,57 +1,49 @@ -import { Button, RadioControl } from '@wordpress/components'; +import { Button, CheckboxControl, RadioControl } from '@wordpress/components'; import { Component, createRef, Fragment } from '@wordpress/element'; -import { __ } from '@wordpress/i18n'; +import { __, sprintf, _n } from '@wordpress/i18n'; import classNames from 'classnames'; import { MapContainer, Marker, TileLayer } from 'react-leaflet'; import L from 'leaflet'; -import { CheckboxControl } from '../shared/wp-form-controls'; -import primaryMarkerIconUrl from '../icons/editor-marker-primary.svg'; -import secondaryMarkerIconUrl from '../icons/editor-marker-secondary.svg'; import JeoGeoAutoComplete from './geo-auto-complete'; import 'leaflet/dist/leaflet.css'; import './geo-posts.css'; import { isEqual } from 'lodash-es'; -function clonePoints( points = [] ) { - return Array.isArray( points ) - ? points.map( ( point ) => ( point ? { ...point } : point ) ) - : []; -} - -const PRIMARY_MARKER_ICON = new L.Icon( { - iconUrl: primaryMarkerIconUrl, - iconSize: [ 25, 41 ], - iconAnchor: [ 12, 41 ], - popupAnchor: [ 0, -34 ], -} ); - -const SECONDARY_MARKER_ICON = new L.Icon( { - iconUrl: secondaryMarkerIconUrl, - iconSize: [ 25, 41 ], - iconAnchor: [ 12, 41 ], - popupAnchor: [ 0, -34 ], -} ); - class JeoGeocodePosts extends Component { constructor() { super(); const metadata = wp.data .select( 'core/editor' ) .getEditedPostAttribute( 'meta' ); - const initialPoints = clonePoints( metadata?._related_point ); + + const rawPoints = metadata._related_point || []; + const sanitizedPoints = rawPoints.map( p => { + const lat = parseFloat( String( p._geocode_lat ).replace( ',', '.' ) ); + const lon = parseFloat( String( p._geocode_lon || '' ).replace( ',', '.' ) ); + return { + ...p, + _geocode_lat: isNaN( lat ) ? 0 : lat, + _geocode_lon: isNaN( lon ) ? 0 : lon + }; + } ); this.state = { pointsCheckpoint: [], formMode: 'view', searchValue: '', zoom: 1, - points: initialPoints, + points: sanitizedPoints, currentMarkerIndex: 0, loadStatus: 'pending', magneticMarkers: true, + isPanelMinimized: false, + panelOffset: { x: 0, y: 0 }, }; + this.isDraggingPanel = false; + this.dragStartPos = { x: 0, y: 0 }; + this.onLocationFound = this.onLocationFound.bind( this ); this.onMarkerDragged = this.onMarkerDragged.bind( this ); this.getProperty = this.getProperty.bind( this ); @@ -74,6 +66,10 @@ class JeoGeocodePosts extends Component { this.onClickNewPoint = this.onClickNewPoint.bind( this ); this.onClickCancel = this.onClickCancel.bind( this ); this.toggleMagnet = this.toggleMagnet.bind( this ); + this.togglePanel = this.togglePanel.bind( this ); + this.startPanelDrag = this.startPanelDrag.bind( this ); + this.movePanelDrag = this.movePanelDrag.bind( this ); + this.stopPanelDrag = this.stopPanelDrag.bind( this ); this.refMap = createRef(); } @@ -85,14 +81,18 @@ class JeoGeocodePosts extends Component { } resetForm() { + this.setState( { + formMode: 'view', + searchValue: '', + } ); + + const { pointsCheckpoint } = this.state; this.setState( - ( prevState ) => ( { - formMode: 'view', - searchValue: '', - points: clonePoints( prevState.pointsCheckpoint ), + { + points: pointsCheckpoint, pointsCheckpoint: [], currentMarkerIndex: 0, - } ), + }, () => { const { points, currentMarkerIndex } = this.state; if ( points.length > 0 ) { @@ -117,9 +117,15 @@ class JeoGeocodePosts extends Component { } toggleMagnet () { - this.setState( ( prevState ) => ( { - magneticMarkers: ! prevState.magneticMarkers, - } ) ); + this.setState( { + magneticMarkers: !this.state.magneticMarkers, + } ); + } + + togglePanel () { + this.setState( { + isPanelMinimized: !this.state.isPanelMinimized, + } ); } /** @@ -129,14 +135,16 @@ class JeoGeocodePosts extends Component { * @param {Object} data the new data. Only attributes that change */ updatePoint( point, data ) { - this.setState( ( prevState ) => ( { - points: prevState.points.map( ( value, i ) => { + const { points } = this.state; + this.setState( { + ...this.state, + points: points.map( ( value, i ) => { if ( i === point ) { return { ...value, ...data }; } return value; } ), - } ) ); + } ); } updateCurrentPoint( data ) { @@ -183,14 +191,10 @@ class JeoGeocodePosts extends Component { } fetchReverseGeocode( lat, lng ) { - const requestUrl = new URL( jeo.ajax_url ); - requestUrl.searchParams.set( 'action', 'jeo_reverse_geocode' ); - requestUrl.searchParams.set( 'nonce', jeo.geocode_nonce ); - requestUrl.searchParams.set( 'lat', lat ); - requestUrl.searchParams.set( 'lon', lng ); - return window - .fetch( requestUrl ) + .fetch( + jeo.ajax_url + '?action=jeo_reverse_geocode&nonce=' + jeo.geocode_nonce + '&lat=' + lat + '&lon=' + lng + ) .then( ( response ) => { if ( ! response.ok ) { throw new Error( @@ -217,32 +221,26 @@ class JeoGeocodePosts extends Component { ); } - onClickDelete( e ) { - e.preventDefault(); - e.stopPropagation(); - - const index = parseInt( e.target.attributes.marker_index.value ); + onClickDelete( index ) { + const { points } = this.state; this.setState( - ( prevState ) => ( { - points: prevState.points.filter( ( el, i ) => i !== index ), + { + ...this.state, + points: points.filter( ( el, i ) => i !== index ), currentMarkerIndex: 0, - } ), + }, () => this.save() ); } - onClickEdit( e ) { - e.preventDefault(); - e.stopPropagation(); - + onClickEdit( index ) { const { points } = this.state; - const index = parseInt( e.target.attributes.marker_index.value ); const point = points[ index ]; this.setState( { - pointsCheckpoint: clonePoints( points ), + pointsCheckpoint: points, currentMarkerIndex: index, - searchValue: point._geocode_full_address, + searchValue: point._geocode_full_address || '', formMode: 'edit', loadStatus: 'resolved', }, @@ -257,7 +255,7 @@ class JeoGeocodePosts extends Component { this.setState( { formMode: 'new', - pointsCheckpoint: clonePoints( points ), + pointsCheckpoint: points, loadStatus: 'pending', } ); } @@ -270,74 +268,64 @@ class JeoGeocodePosts extends Component { } onLocationFound( location ) { - this.flyToLocation( location.lat, location.lon ); - this.fetchReverseGeocode( location.lat, location.lon ).then( ( result ) => { - if ( result.raw.error ) { + const lat = parseFloat( location.lat ); + const lng = parseFloat( location.lon ); + + if ( isNaN( lat ) || isNaN( lng ) ) { + console.error( 'JEO: Invalid coordinates received', location ); + return; + } + + this.flyToLocation( lat, lng ); + this.fetchReverseGeocode( lat, lng ).then( ( result ) => { + if ( ! result || ( result.raw && result.raw.error ) ) { return this.resetForm(); } - const fullAddress = - this.getProperty( result, 'full_address' ) || - this.getProperty( result.raw || {}, 'display_name' ) || - this.getProperty( location, 'full_address' ) || - ''; - - this.setState( ( prevState ) => { - const foundPoint = { - _geocode_lat: prevState.magneticMarkers - ? this.getProperty( result, 'lat' ) - : String( location.lat ), - _geocode_lon: prevState.magneticMarkers - ? this.getProperty( result, 'lon' ) - : String( location.lon ), - _geocode_full_address: fullAddress, - _geocode_country: this.getProperty( result, 'country' ), - _geocode_country_code: this.getProperty( result, 'country_code' ), - _geocode_region_level_1: this.getProperty( result, 'region_level_1' ), - _geocode_region_level_2: this.getProperty( result, 'region_level_2' ), - _geocode_region_level_3: this.getProperty( result, 'region_level_3' ), - _geocode_city: this.getProperty( result, 'city' ), - _geocode_city_level_1: this.getProperty( result, 'city_level_1' ), - }; - const nextState = { - searchValue: fullAddress, - loadStatus: 'resolved', - }; - - if ( - prevState.formMode === 'new' && - prevState.points.length === prevState.pointsCheckpoint.length - ) { - const existingPoint = prevState.points.filter( ( point ) => - isEqual( point, foundPoint ) - ); - - if ( ! existingPoint.length ) { - nextState.points = [ ...prevState.points, foundPoint ]; - nextState.currentMarkerIndex = prevState.points.length; - } + const rawLat = this.state.magneticMarkers ? this.getProperty( result, 'lat' ) : lat; + const rawLon = this.state.magneticMarkers ? this.getProperty( result, 'lon' ) : lng; + + const foundPoint = { + _geocode_lat: parseFloat( String( rawLat || 0 ).replace( ',', '.' ) ), + _geocode_lon: parseFloat( String( rawLon || 0 ).replace( ',', '.' ) ), + _geocode_full_address: this.getProperty( result, 'full_address' ), + _geocode_country: this.getProperty( result, 'country' ), + _geocode_country_code: this.getProperty( result, 'country_code' ), + _geocode_region_level_1: this.getProperty( result, 'region_level_1' ), + _geocode_region_level_2: this.getProperty( result, 'region_level_2' ), + _geocode_region_level_3: this.getProperty( result, 'region_level_3' ), + _geocode_city: this.getProperty( result, 'city' ), + _geocode_city_level_1: this.getProperty( result, 'city_level_1' ), + }; + + this.setState( { + searchValue: foundPoint._geocode_full_address, + loadStatus: 'resolved', + } ); - return nextState; + const { formMode, points, pointsCheckpoint } = this.state; + if ( formMode === 'new' && points.length === pointsCheckpoint.length ) { + const existingPoint = points.filter( ( p ) => + isEqual( p, foundPoint ) + ); + if ( ! existingPoint.length ) { + this.setState( { + points: [ ...points, foundPoint ], + currentMarkerIndex: points.length, + } ); } - - nextState.points = prevState.points.map( ( point, index ) => { - if ( index === prevState.currentMarkerIndex ) { - return { ...point, ...foundPoint }; - } - - return point; - } ); - - return nextState; - } ); + } else { + this.updateCurrentPoint( foundPoint ); + } } ); } - flyToLocation( lat, lon ) { - this.refMap.current.flyTo( [ - parseFloat( lat ), - parseFloat( lon ), - ] ); + flyToLocation( lat, lng ) { + const latitude = parseFloat( lat ); + const longitude = parseFloat( lng ); + if ( this.refMap.current && ! isNaN( latitude ) && ! isNaN( longitude ) ) { + this.refMap.current.flyTo( [ latitude, longitude ] ); + } } mapCreated( mapInstance ) { @@ -347,12 +335,13 @@ class JeoGeocodePosts extends Component { mapLoaded( e ) { const { points } = this.state; - const coords = points.map( ( point ) => { - return [ - parseFloat( point._geocode_lat ), - parseFloat( point._geocode_lon ), - ]; - } ); + const coords = points + .map( ( point ) => { + const lat = parseFloat( point._geocode_lat ); + const lng = parseFloat( point._geocode_lon ); + return ( isNaN( lat ) || isNaN( lng ) ) ? null : [ lat, lng ]; + } ) + .filter( c => c !== null ); const map = e.target; @@ -367,8 +356,8 @@ class JeoGeocodePosts extends Component { onMarkerDragged( e ) { const marker = e.target; const latLng = marker.getLatLng(); - this.setState( { loadStatus: 'pending' } ); this.onLocationFound( { lat: latLng.lat, lon: latLng.lng } ); + this.setState( { loadStatus: 'pending' } ); } save() { @@ -391,6 +380,45 @@ class JeoGeocodePosts extends Component { } } + startPanelDrag( e ) { + if ( e.target.tagName === 'BUTTON' || e.target.closest( 'button' ) ) { + return; + } + this.isDraggingPanel = true; + const clientX = e.clientX ?? e.touches?.[ 0 ]?.clientX ?? 0; + const clientY = e.clientY ?? e.touches?.[ 0 ]?.clientY ?? 0; + this.dragStartPos = { x: clientX, y: clientY }; + document.addEventListener( 'mousemove', this.movePanelDrag ); + document.addEventListener( 'mouseup', this.stopPanelDrag ); + document.addEventListener( 'touchmove', this.movePanelDrag, { passive: false } ); + document.addEventListener( 'touchend', this.stopPanelDrag ); + } + + movePanelDrag( e ) { + if ( ! this.isDraggingPanel ) { + return; + } + const clientX = e.clientX ?? e.touches?.[ 0 ]?.clientX ?? 0; + const clientY = e.clientY ?? e.touches?.[ 0 ]?.clientY ?? 0; + const dx = clientX - this.dragStartPos.x; + const dy = clientY - this.dragStartPos.y; + this.setState( ( prev ) => ( { + panelOffset: { + x: prev.panelOffset.x + dx, + y: prev.panelOffset.y + dy, + }, + } ) ); + this.dragStartPos = { x: clientX, y: clientY }; + } + + stopPanelDrag() { + this.isDraggingPanel = false; + document.removeEventListener( 'mousemove', this.movePanelDrag ); + document.removeEventListener( 'mouseup', this.stopPanelDrag ); + document.removeEventListener( 'touchmove', this.movePanelDrag ); + document.removeEventListener( 'touchend', this.stopPanelDrag ); + } + renderForm() { const { currentMarkerIndex, @@ -400,22 +428,20 @@ class JeoGeocodePosts extends Component { loadStatus, magneticMarkers, } = this.state; - const selectedPoint = points[ currentMarkerIndex ]; - const currentSearchValue = typeof searchValue === 'string' - ? searchValue - : selectedPoint?._geocode_full_address || ''; + + const currentSearchValue = searchValue || ''; const isDisabled = ! ( - loadStatus === 'resolved' && - currentSearchValue.replace( /\s/g, '' ).length + loadStatus === 'resolved' && currentSearchValue.replace( /\s/g, '' ).length ); + + const selectedPoint = points[ currentMarkerIndex ]; return (
    this.setState( { loadStatus: 'pending' } ) } /> { __( 'Type an address, then click Search or press Enter. You can also drag the marker across the map.', 'jeowp' ) } @@ -464,119 +490,166 @@ class JeoGeocodePosts extends Component { } render() { - const { currentMarkerIndex, formMode, zoom } = this.state; + const { currentMarkerIndex, formMode, zoom, isPanelMinimized } = this.state; const pointsList = formMode !== 'new' ? this.state.points : this.state.pointsCheckpoint; const pointsMap = this.state.points; + const panelClass = `jeo-geocode-modal__panel ${ isPanelMinimized ? 'is-collapsed' : '' }`; return ( -
    -
    -
    -

    { __( 'Add new point', 'jeowp' ) }

    -
    - { formMode === 'view' ? ( - - ) : formMode === 'new' ? ( - this.renderForm() - ) : null } -
    -
    -
    -

    { __( 'Current points', 'jeowp' ) }

    - { pointsList.length === 0 ? ( - __( 'No points', 'jeowp' ) - ) : ( -
      - { pointsList.map( ( point, i ) => ( -
    • - { point._geocode_full_address }{ ' ' } - { formMode === 'view' ? ( - - - | - - - ) : ( - formMode === 'edit' && - currentMarkerIndex === i && - this.renderForm() - ) } -
    • - ) ) } -
    - ) } -
    +
    + {/* Fullscreen map */} +
    + + + { pointsMap.map( ( point, i ) => { + let icon; + + const pinUrls = window.jeo?.pin_urls || { + primary: 'https://cdn.jsdelivr.net/gh/pointhi/leaflet-color-markers/img/marker-icon-blue.png', + secondary: 'https://cdn.jsdelivr.net/gh/pointhi/leaflet-color-markers/img/marker-icon-grey.png', + }; + if ( ! point.relevance || point.relevance === 'primary' ) { + icon = new L.Icon( { + iconUrl: pinUrls.primary, + iconSize: [ 25, 41 ], + iconAnchor: [ 12, 41 ], + } ); + } else { + icon = new L.Icon( { + iconUrl: pinUrls.secondary, + iconSize: [ 25, 41 ], + iconAnchor: [ 12, 41 ], + } ); + } + + const lat = parseFloat( point._geocode_lat ); + const lng = parseFloat( point._geocode_lon ); + if ( isNaN( lat ) || isNaN( lng ) ) return null; + + return ( + + ); + } ) } +
    -
    -
    - +
    +

    - - { pointsMap.map( ( point, i ) => { - const icon = - ! point.relevance || point.relevance === 'primary' - ? PRIMARY_MARKER_ICON - : SECONDARY_MARKER_ICON; - - return ( - - ); - } ) } - + { isPanelMinimized + ? sprintf( + /* translators: %d: number of geolocation points. */ + _n( 'Point (%d)', 'Points (%d)', pointsList.length, 'jeowp' ), + pointsList.length + ) + : __( 'Geolocate this post', 'jeowp' ) + } +

    +
    + + { ! isPanelMinimized && ( +
    +
    +

    { __( 'Add new point', 'jeowp' ) }

    +
    + { formMode === 'view' ? ( + + ) : formMode === 'new' ? ( + this.renderForm() + ) : null } +
    +
    +
    +

    { __( 'Current points', 'jeowp' ) }

    + { pointsList.length === 0 ? ( + __( 'No points', 'jeowp' ) + ) : ( +
      + { pointsList.map( ( point, i ) => ( +
    • + { point._geocode_full_address }{ ' ' } + { formMode === 'view' ? ( + + + | + + + ) : ( + formMode === 'edit' && + currentMarkerIndex === i && + this.renderForm() + ) } +
    • + ) ) } +
    + ) } +
    +
    + ) }
    ); diff --git a/src/js/src/posts-sidebar/index.js b/src/js/src/posts-sidebar/index.js index ec46142bd..a0942e329 100644 --- a/src/js/src/posts-sidebar/index.js +++ b/src/js/src/posts-sidebar/index.js @@ -1,56 +1,360 @@ import { registerPlugin } from '@wordpress/plugins'; import { PluginDocumentSettingPanel } from '@wordpress/editor'; import { Component, Fragment } from '@wordpress/element'; -import { Modal, Button } from '@wordpress/components'; -import { __ } from '@wordpress/i18n'; -import JeoGeocodePosts from './geo-posts'; +import { Modal, Button, Notice } from '@wordpress/components'; +import { sprintf, __, _x, _n } from '@wordpress/i18n'; import { useSelect } from '@wordpress/data'; +import apiFetch from '@wordpress/api-fetch'; +import JeoGeocodePosts from './geo-posts'; +import { JeoGeocodePostsAI } from './geo-posts-ai'; - -const JeoGeocodePanel = class JeoGeocodePanel extends Component { - constructor() { - super(); - this.state = { - isOpen: false, - }; +/** + * Apply configured primary/secondary point limits to a list of formatted locations. + * + * @param {Array} points Formatted location points. + * @return {Array} Points respecting max limits when configured. + */ +const applyPointLimits = ( points ) => { + const limits = window.jeo?.ai_limits || {}; + if ( ! limits.use_primary_limit && ! limits.use_secondary_limit ) { + return points; } - render() { - const isOpen = this.state.isOpen; - return ( - - + + ) } + +
    + - { isOpen && ( - this.setState( { isOpen: false } ) } + { aiConfigured && ( + ) } - - ); - } +
    + + { aiError && ( + setState( ( prev ) => ( { ...prev, aiError: null } ) ) }> + { aiError } + + ) } + + { isApprovalModalOpen && ( + setState( ( prev ) => ( { ...prev, isApprovalModalOpen: false } ) ) } + className="jeo-geocode-modal" + isFullScreen={ true } + > + setState( ( prev ) => ( { ...prev, isApprovalModalOpen: false }) ) } + onRetry={ handleAIGeoreference } + /> + + ) } + + { isOpen && ( + setState( ( prev ) => ( { ...prev, isOpen: false } ) ) } + className="jeo-geocode-modal" + isFullScreen={ true } + > + setState( ( prev ) => ( { ...prev, isOpen: false } ) ) } + onCancel={ () => setState( ( prev ) => ( { ...prev, isOpen: false } ) ) } + /> + + ) } +
    + ); }; registerPlugin( 'jeo-posts-sidebar', { icon: null, render: () => { - const currentPostType = useSelect( ( select ) => { - return select( 'core/editor' ).getCurrentPostType(); + const postData = useSelect( ( select ) => { + const editor = select( 'core/editor' ); + return { + postId: editor.getCurrentPostId(), + title: editor.getEditedPostAttribute( 'title' ), + content: editor.getEditedPostAttribute( 'content' ), + postType: editor.getCurrentPostType(), + }; }, [] ); return (
    - { currentPostType ? + { postData.postType ? - + - : null }; + : null }
    ); }, diff --git a/src/js/src/posts-sidebar/use-draggable.js b/src/js/src/posts-sidebar/use-draggable.js new file mode 100644 index 000000000..8cd2ff352 --- /dev/null +++ b/src/js/src/posts-sidebar/use-draggable.js @@ -0,0 +1,86 @@ +import { useState, useCallback, useRef, useEffect } from '@wordpress/element'; + +/** + * Hook to make an element draggable via its handle. + * + * @param {Object} options + * @param {number} options.initialX Initial X offset in pixels (default 0). + * @param {number} options.initialY Initial Y offset in pixels (default 0). + * + * @return {Object} { position: {x,y}, bindDrag: {onMouseDown, onTouchStart} } + */ +export function useDraggable( { initialX = 0, initialY = 0 } = {} ) { + const [ position, setPosition ] = useState( { x: initialX, y: initialY } ); + const isDragging = useRef( false ); + const startPos = useRef( { x: 0, y: 0 } ); + const currentPos = useRef( { x: initialX, y: initialY } ); + + const handleMouseMove = useCallback( ( e ) => { + if ( ! isDragging.current ) { + return; + } + const dx = e.clientX - startPos.current.x; + const dy = e.clientY - startPos.current.y; + currentPos.current = { + x: currentPos.current.x + dx, + y: currentPos.current.y + dy, + }; + startPos.current = { x: e.clientX, y: e.clientY }; + setPosition( { ...currentPos.current } ); + }, [] ); + + const handleTouchMove = useCallback( ( e ) => { + if ( ! isDragging.current || e.touches.length !== 1 ) { + return; + } + const touch = e.touches[ 0 ]; + const dx = touch.clientX - startPos.current.x; + const dy = touch.clientY - startPos.current.y; + currentPos.current = { + x: currentPos.current.x + dx, + y: currentPos.current.y + dy, + }; + startPos.current = { x: touch.clientX, y: touch.clientY }; + setPosition( { ...currentPos.current } ); + }, [] ); + + const stopDragging = useCallback( () => { + isDragging.current = false; + document.removeEventListener( 'mousemove', handleMouseMove ); + document.removeEventListener( 'mouseup', stopDragging ); + document.removeEventListener( 'touchmove', handleTouchMove ); + document.removeEventListener( 'touchend', stopDragging ); + }, [ handleMouseMove, handleTouchMove ] ); + + const startDragging = useCallback( ( e ) => { + // Ignore if clicking a button inside the header. + if ( e.target.tagName === 'BUTTON' || e.target.closest( 'button' ) ) { + return; + } + isDragging.current = true; + const clientX = e.clientX ?? e.touches?.[ 0 ]?.clientX ?? 0; + const clientY = e.clientY ?? e.touches?.[ 0 ]?.clientY ?? 0; + startPos.current = { x: clientX, y: clientY }; + document.addEventListener( 'mousemove', handleMouseMove ); + document.addEventListener( 'mouseup', stopDragging ); + document.addEventListener( 'touchmove', handleTouchMove, { passive: false } ); + document.addEventListener( 'touchend', stopDragging ); + }, [ handleMouseMove, handleTouchMove, stopDragging ] ); + + useEffect( () => { + return () => { + document.removeEventListener( 'mousemove', handleMouseMove ); + document.removeEventListener( 'mouseup', stopDragging ); + document.removeEventListener( 'touchmove', handleTouchMove ); + document.removeEventListener( 'touchend', stopDragging ); + }; + }, [ handleMouseMove, handleTouchMove, stopDragging ] ); + + return { + position, + bindDrag: { + onMouseDown: startDragging, + onTouchStart: startDragging, + }, + }; +} diff --git a/src/js/src/shared/chat-input-keys.js b/src/js/src/shared/chat-input-keys.js new file mode 100644 index 000000000..4ea015d0f --- /dev/null +++ b/src/js/src/shared/chat-input-keys.js @@ -0,0 +1,27 @@ +/** + * Shared Enter-to-submit behavior for chat textareas. + * + * Plain Enter submits the message; Shift+Enter inserts a line break + * (left to the browser default); Enter combined with Ctrl, Cmd, or Alt + * is ignored; Enter during IME composition is left to the input method + * editor. + * + * @param {KeyboardEvent} event Keydown event from the textarea. + * @param {Function} submit Callback invoked on plain Enter. + */ +export function handleChatKeyDown( event, submit ) { + if ( event.key !== 'Enter' ) { + return; + } + + if ( event.nativeEvent?.isComposing ) { + return; + } + + if ( event.shiftKey || event.ctrlKey || event.metaKey || event.altKey ) { + return; + } + + event.preventDefault(); + submit(); +} diff --git a/src/js/src/shared/chat-input-keys.test.js b/src/js/src/shared/chat-input-keys.test.js new file mode 100644 index 000000000..2020fdec9 --- /dev/null +++ b/src/js/src/shared/chat-input-keys.test.js @@ -0,0 +1,89 @@ +import { handleChatKeyDown } from './chat-input-keys'; + +const keydownEvent = ( { + key = 'Enter', + shiftKey = false, + ctrlKey = false, + metaKey = false, + altKey = false, + isComposing = false, +} = {} ) => ( { + key, + shiftKey, + ctrlKey, + metaKey, + altKey, + preventDefault: jest.fn(), + nativeEvent: { isComposing }, +} ); + +describe( 'handleChatKeyDown', () => { + it( 'submits on plain Enter', () => { + const submit = jest.fn(); + const event = keydownEvent(); + + handleChatKeyDown( event, submit ); + + expect( submit ).toHaveBeenCalledTimes( 1 ); + expect( event.preventDefault ).toHaveBeenCalledTimes( 1 ); + } ); + + it( 'lets Shift+Enter insert a line break', () => { + const submit = jest.fn(); + const event = keydownEvent( { shiftKey: true } ); + + handleChatKeyDown( event, submit ); + + expect( submit ).not.toHaveBeenCalled(); + expect( event.preventDefault ).not.toHaveBeenCalled(); + } ); + + it( 'ignores Ctrl, Cmd, and Alt+Enter', () => { + for ( const modifiers of [ { ctrlKey: true }, { metaKey: true }, { altKey: true } ] ) { + const submit = jest.fn(); + const event = keydownEvent( modifiers ); + + handleChatKeyDown( event, submit ); + + expect( submit ).not.toHaveBeenCalled(); + expect( event.preventDefault ).not.toHaveBeenCalled(); + } + } ); + + it( 'ignores non-Enter keys', () => { + const submit = jest.fn(); + const event = keydownEvent( { key: 'a' } ); + + handleChatKeyDown( event, submit ); + + expect( submit ).not.toHaveBeenCalled(); + expect( event.preventDefault ).not.toHaveBeenCalled(); + } ); + + it( 'ignores Enter while IME is composing', () => { + const submit = jest.fn(); + const event = keydownEvent( { isComposing: true } ); + + handleChatKeyDown( event, submit ); + + expect( submit ).not.toHaveBeenCalled(); + expect( event.preventDefault ).not.toHaveBeenCalled(); + } ); + + it( 'tolerates events without a nativeEvent', () => { + const submit = jest.fn(); + const event = { + key: 'Enter', + shiftKey: false, + ctrlKey: false, + metaKey: false, + altKey: false, + preventDefault: jest.fn(), + }; + + handleChatKeyDown( event, submit ); + + expect( submit ).toHaveBeenCalledTimes( 1 ); + expect( event.preventDefault ).toHaveBeenCalledTimes( 1 ); + } ); +} ); diff --git a/src/js/src/shared/html.js b/src/js/src/shared/html.js new file mode 100644 index 000000000..90bcace20 --- /dev/null +++ b/src/js/src/shared/html.js @@ -0,0 +1,65 @@ +export function decodeHtmlEntity ( str ) { + return str.replaceAll( /&#(\d+);/g, ( match, dec ) => { + return String.fromCodePoint( dec ); + } ); +} + +export function decodeHtmlEntities ( input ) { + const textarea = document.createElement( 'textarea' ); + textarea.innerHTML = input; + return textarea.value; +} + +const SANITIZE_ALLOWED_TAGS = new Set( [ 'strong', 'b', 'em', 'i', 'br', 'a', 'span' ] ); + +export function sanitizeHtml ( rawHtml ) { + const decoded = decodeHtmlEntities( rawHtml ); + const div = document.createElement( 'div' ); + div.innerHTML = decoded; + + const walk = ( node ) => { + if ( node.nodeType === Node.TEXT_NODE ) { + return node.textContent; + } + + if ( node.nodeType === Node.ELEMENT_NODE ) { + const tag = node.tagName.toLowerCase(); + + if ( ! SANITIZE_ALLOWED_TAGS.has( tag ) ) { + return Array.from( node.childNodes ) + .map( walk ) + .join( '' ); + } + + if ( tag === 'br' ) { + return '
    '; + } + + const inner = Array.from( node.childNodes ) + .map( walk ) + .join( '' ); + + if ( tag === 'a' ) { + const href = node.getAttribute( 'href' ) || ''; + const safeHref = href.replace( /"/g, '"' ); + return `${ inner }`; + } + + if ( tag === 'span' ) { + const attrs = Array.from( node.attributes ) + .filter( ( attr ) => ! attr.name.toLowerCase().startsWith( 'on' ) ) + .map( ( attr ) => `${ attr.name }="${ attr.value.replace( /"/g, '"' ) }"` ) + .join( ' ' ); + return attrs ? `${ inner }` : `${ inner }`; + } + + return `<${ tag }>${ inner }`; + } + + return ''; + }; + + return Array.from( div.childNodes ) + .map( walk ) + .join( '' ); +} diff --git a/src/js/src/shared/html.test.js b/src/js/src/shared/html.test.js new file mode 100644 index 000000000..5069f18a7 --- /dev/null +++ b/src/js/src/shared/html.test.js @@ -0,0 +1,69 @@ +import { decodeHtmlEntity, decodeHtmlEntities, sanitizeHtml } from './html'; + +describe( 'decodeHtmlEntity', () => { + it( 'decodes numeric entities to their code points', () => { + expect( decodeHtmlEntity( '© 2026' ) ).toBe( '© 2026' ); + } ); +} ); + +describe( 'decodeHtmlEntities', () => { + it( 'decodes named and numeric entities', () => { + expect( decodeHtmlEntities( '© & ©' ) ).toBe( '© & ©' ); + } ); + + it( 'decodes named entities embedded in AI layer explanations', () => { + // Shape returned by the WP REST API (`excerpt.rendered`) for AI-generated + // layers whose excerpt embeds the raw layer attribution. + const description = + '

    AI-generated layer: Rivers. © OpenStreetMap Data source: osm.

    '; + expect( + decodeHtmlEntities( description.replace( /<[^>]+>/g, '' ) ) + ).toBe( + 'AI-generated layer: Rivers. © OpenStreetMap Data source: osm.' + ); + } ); +} ); + +describe( 'sanitizeHtml', () => { + it( 'keeps allowed link markup and forces safe target/rel', () => { + expect( + sanitizeHtml( + '© OpenFreeMap' + ) + ).toBe( + '© OpenFreeMap' + ); + } ); + + it( 'keeps multiple links and inline emphasis (layer attribution shape)', () => { + expect( + sanitizeHtml( + '© OpenFreeMap © OpenStreetMap' + ) + ).toBe( + '© OpenFreeMap © OpenStreetMap' + ); + } ); + + it( 'passes plain URLs through unchanged', () => { + expect( sanitizeHtml( 'http://www.ibge.gov.br/' ) ).toBe( + 'http://www.ibge.gov.br/' + ); + } ); + + it( 'strips disallowed tags but keeps their text', () => { + expect( sanitizeHtml( '
    Source: IBGE
    ' ) ).toBe( + 'Source: IBGE' + ); + } ); + + it( 'keeps anchors without an href as empty links', () => { + expect( sanitizeHtml( 'OpenFreeMap' ) ).toBe( + 'OpenFreeMap' + ); + } ); + + it( 'keeps br tags', () => { + expect( sanitizeHtml( 'one
    two' ) ).toBe( 'one
    two' ); + } ); +} ); diff --git a/src/js/src/shared/layer-style-editor.js b/src/js/src/shared/layer-style-editor.js new file mode 100644 index 000000000..e77573769 --- /dev/null +++ b/src/js/src/shared/layer-style-editor.js @@ -0,0 +1,250 @@ +import { useEffect, useRef, useState } from '@wordpress/element'; +import { CheckboxControl, Modal } from '@wordpress/components'; +import { __ } from '@wordpress/i18n'; +import { RangeControl, TextControl } from './wp-form-controls'; + +const COLOR_FIELDS = { + fill: [ + { key: 'fill-color', label: __( 'Fill Color', 'jeowp' ), default: '#000000' }, + { key: 'fill-outline-color', label: __( 'Outline Color', 'jeowp' ), default: '#000000' }, + ], + line: [ + { key: 'line-color', label: __( 'Line Color', 'jeowp' ), default: '#000000' }, + ], + circle: [ + { key: 'circle-color', label: __( 'Circle Color', 'jeowp' ), default: '#000000' }, + { key: 'circle-stroke-color', label: __( 'Stroke Color', 'jeowp' ), default: '#000000' }, + ], +}; + +const OPACITY_FIELDS = { + fill: [ + { key: 'fill-opacity', label: __( 'Fill Opacity', 'jeowp' ), min: 0, max: 1, step: 0.05, default: 0.6 }, + ], + line: [ + { key: 'line-opacity', label: __( 'Line Opacity', 'jeowp' ), min: 0, max: 1, step: 0.05, default: 1 }, + ], + circle: [ + { key: 'circle-opacity', label: __( 'Circle Opacity', 'jeowp' ), min: 0, max: 1, step: 0.05, default: 1 }, + ], +}; + +const NUMERIC_FIELDS = { + line: [ + { key: 'line-width', label: __( 'Line Width', 'jeowp' ), min: 0, max: 20, step: 0.5, default: 1 }, + ], + circle: [ + { key: 'circle-radius', label: __( 'Circle Radius', 'jeowp' ), min: 0, max: 50, step: 0.5, default: 5 }, + { key: 'circle-stroke-width', label: __( 'Stroke Width', 'jeowp' ), min: 0, max: 20, step: 0.5, default: 0 }, + ], +}; + +const SUPPORTED_TYPES = [ 'fill', 'line', 'circle' ]; + +export function isStyleableLayerType( type ) { + return SUPPORTED_TYPES.includes( type ); +} + +function getNestedValue( obj, path ) { + return path.split( '.' ).reduce( ( acc, part ) => acc?.[ part ], undefined ); +} + +function setNestedValue( obj, path, value ) { + const parts = path.split( '.' ); + const result = { ...obj }; + let current = result; + for ( let i = 0; i < parts.length - 1; i++ ) { + current[ parts[ i ] ] = { ...( current[ parts[ i ] ] || {} ) }; + current = current[ parts[ i ] ]; + } + current[ parts[ parts.length - 1 ] ] = value; + return result; +} + +function ColorPickerField( { label, value, onChange, disabled } ) { + return ( +
    + + onChange( e.target.value ) } + disabled={ disabled } + style={ { width: 44, height: 30, border: '1px solid #ddd', borderRadius: 4, padding: 1, cursor: disabled ? 'not-allowed' : 'pointer', opacity: disabled ? 0.5 : 1 } } + /> + +
    + ); +} + +const COMMIT_DEBOUNCE_MS = 300; + +export default function LayerStyleEditor( { style, layerType, defaultStyle, onChange, onClose } ) { + const [ useDefault, setUseDefault ] = useState( !! ( defaultStyle && style?.use_default ) ); + + const [ draftStyle, setDraftStyle ] = useState( style || {} ); + const draftRef = useRef( draftStyle ); + const debounceRef = useRef( null ); + + const paint = draftStyle.paint || {}; + + // Sync the draft when the parent style changes externally (e.g. AI + // refinement or another control editing the same layer). + useEffect( () => { + const external = style || {}; + if ( JSON.stringify( external ) !== JSON.stringify( draftRef.current ) ) { + draftRef.current = external; + setDraftStyle( external ); + } + }, [ style ] ); + + const commit = ( next ) => { + draftRef.current = next; + setDraftStyle( next ); + clearTimeout( debounceRef.current ); + debounceRef.current = setTimeout( () => onChange( next ), COMMIT_DEBOUNCE_MS ); + }; + + // Flush any pending commit when the editor closes so no edit is lost. + useEffect( () => { + return () => { + clearTimeout( debounceRef.current ); + if ( JSON.stringify( draftRef.current ) !== JSON.stringify( style || {} ) ) { + onChange( draftRef.current ); + } + }; + // eslint-disable-next-line react-hooks/exhaustive-deps -- flush-on-unmount only + }, [] ); + + const handleToggleDefault = ( checked ) => { + setUseDefault( checked ); + commit( checked ? { use_default: true } : {} ); + }; + + const updatePaint = ( key, value ) => { + commit( { ...draftStyle, use_default: false, paint: { ...paint, [ key ]: value } } ); + }; + + const removePaint = ( key ) => { + const { [ key ]: _, ...rest } = paint; + const nextStyle = { ...draftStyle, use_default: false, paint: rest }; + if ( Object.keys( rest ).length === 0 ) { + delete nextStyle.paint; + } + commit( nextStyle ); + }; + + const resetField = ( key, defaultValue ) => { + updatePaint( key, defaultValue ); + }; + + if ( ! isStyleableLayerType( layerType ) ) { + return ( + +

    { __( 'Styling is only supported for fill, line, and circle layer types.', 'jeowp' ) }

    +
    + ); + } + + const colorFields = COLOR_FIELDS[ layerType ] || []; + const opacityFields = OPACITY_FIELDS[ layerType ] || []; + const numericFields = NUMERIC_FIELDS[ layerType ] || []; + + return ( + +
    + { defaultStyle && ( +
    + + { __( 'AI Default Style', 'jeowp' ) } + + + { useDefault && ( +
    + { defaultStyle.filter && ( +

    + { __( 'Filter:', 'jeowp' ) } { JSON.stringify( defaultStyle.filter ) } +

    + ) } + { defaultStyle.paint && ( +

    + { __( 'Paint:', 'jeowp' ) } { JSON.stringify( defaultStyle.paint ) } +

    + ) } +
    + ) } +
    + ) } + + { colorFields.length > 0 && ( +
    + { __( 'Colors', 'jeowp' ) } + { colorFields.map( ( field ) => ( + updatePaint( field.key, v ) } + disabled={ useDefault } + /> + ) ) } +
    + ) } + + { opacityFields.length > 0 && ( +
    + { __( 'Opacity', 'jeowp' ) } + { opacityFields.map( ( field ) => ( + updatePaint( field.key, v ) } + disabled={ useDefault } + /> + ) ) } +
    + ) } + + { numericFields.length > 0 && ( +
    + { __( 'Size', 'jeowp' ) } + { numericFields.map( ( field ) => ( + updatePaint( field.key, v ) } + disabled={ useDefault } + /> + ) ) } +
    + ) } +
    +
    + ); +} diff --git a/src/js/src/shared/rest-records.js b/src/js/src/shared/rest-records.js index 319cfb133..414ef60ab 100644 --- a/src/js/src/shared/rest-records.js +++ b/src/js/src/shared/rest-records.js @@ -20,14 +20,17 @@ const appendLanguageQuery = ( query = {} ) => { return nextQuery; }; -export const chunkRecordIds = ( ids = [], chunkSize = DEFAULT_ID_CHUNK_SIZE ) => { - const normalizedIds = Array.from( +export const normalizeRecordIds = ( ids = [] ) => + Array.from( new Set( ids .map( ( id ) => Number.parseInt( id, 10 ) ) .filter( ( id ) => Number.isFinite( id ) && id > 0 ) ) ); + +export const chunkRecordIds = ( ids = [], chunkSize = DEFAULT_ID_CHUNK_SIZE ) => { + const normalizedIds = normalizeRecordIds( ids ); const chunks = []; for ( let index = 0; index < normalizedIds.length; index += chunkSize ) { @@ -91,16 +94,17 @@ export const useRecordsByIds = ( { query = {}, chunkSize = DEFAULT_ID_CHUNK_SIZE, } ) => { + // Keyed by content, not array identity: callers frequently rebuild the IDs + // array on every render, and re-running the fetch effect would flip + // `isLoading` and unmount consumers (e.g. the layers list with open modals). + const normalizedIdsKey = normalizeRecordIds( ids ).join( ',' ); const normalizedIds = useMemo( () => - Array.from( - new Set( - ids - .map( ( id ) => Number.parseInt( id, 10 ) ) - .filter( ( id ) => Number.isFinite( id ) && id > 0 ) - ) - ), - [ ids ] + normalizedIdsKey + ? normalizedIdsKey.split( ',' ).map( ( id ) => Number.parseInt( id, 10 ) ) + : [], + // eslint-disable-next-line react-hooks/exhaustive-deps -- content-keyed memo, see above + [ normalizedIdsKey ] ); const idChunks = useMemo( () => chunkRecordIds( normalizedIds, chunkSize ), diff --git a/src/js/src/shared/rest-records.test.js b/src/js/src/shared/rest-records.test.js index acfdb94c1..6036d8f1d 100644 --- a/src/js/src/shared/rest-records.test.js +++ b/src/js/src/shared/rest-records.test.js @@ -1,9 +1,26 @@ import { chunkRecordIds, mergeRecordsByIdOrder, + normalizeRecordIds, } from './rest-records'; describe( 'rest-records helpers', () => { + it( 'normalizes record IDs, dropping invalid values and duplicates', () => { + expect( normalizeRecordIds( [ 1, '2', 2, 3, null, 'foo', 0, -4 ] ) ).toEqual( [ + 1, 2, 3, + ] ); + } ); + + it( 'returns an empty list for missing input', () => { + expect( normalizeRecordIds() ).toEqual( [] ); + } ); + + it( 'produces a stable content key regardless of array identity', () => { + const first = normalizeRecordIds( [ 1, 2, 3 ] ); + const second = normalizeRecordIds( [ 1, 2, 3 ] ); + expect( first.join( ',' ) ).toBe( second.join( ',' ) ); + } ); + it( 'chunks normalized IDs into stable groups', () => { expect( chunkRecordIds( [ 1, '2', 2, 3, null, 'foo', 4 ], 2 ) diff --git a/src/js/src/shared/schema-form.js b/src/js/src/shared/schema-form.js index aa2b278be..f402d280f 100644 --- a/src/js/src/shared/schema-form.js +++ b/src/js/src/shared/schema-form.js @@ -5,6 +5,7 @@ import { __ } from '@wordpress/i18n'; import { CheckboxControl, SelectControl, + TextareaControl, TextControl, } from './wp-form-controls'; @@ -113,6 +114,29 @@ function TextWidget( { ); } +function TextareaWidget( { + id, + value, + disabled, + readonly, + required, + autofocus, + placeholder, + onChange, +} ) { + return ( + + ); +} + function SelectWidget( { id, value, @@ -157,6 +181,7 @@ function SelectWidget( { const widgets = { CheckboxWidget, SelectWidget, + TextareaWidget, TextWidget, }; diff --git a/src/js/src/shared/style-layer-types.js b/src/js/src/shared/style-layer-types.js new file mode 100644 index 000000000..6dbc57430 --- /dev/null +++ b/src/js/src/shared/style-layer-types.js @@ -0,0 +1,26 @@ +/** + * Generic style-layer-type detection. + * + * Style layer types (mapbox, style-json, or any type registered with + * `isStyle: true` on the frontend JeoLayerTypes registry) load a whole map + * style instead of being added as individual GL layers. Consumers should key + * off this helper instead of hardcoding type slugs so future providers work + * without changes. + * + * The registry is loaded both on the frontend and inside the block-editor + * iframe (`Layer_Types::enqueue_iframe_assets()`), so it is expected to be + * available; when it is missing the helper degrades to false rather than + * guessing from a hardcoded list. + * + * @param {string} slug Layer type slug. + * @param {Object} registry Optional JeoLayerTypes-like registry (mainly for + * tests). Defaults to `window.JeoLayerTypes`. + * @return {boolean} Whether the type loads a whole map style. + */ +export function isStyleLayerType( slug, registry = window.JeoLayerTypes ) { + if ( ! slug ) { + return false; + } + + return registry?.isStyle?.( slug ) === true; +} diff --git a/src/js/src/shared/style-layer-types.test.js b/src/js/src/shared/style-layer-types.test.js new file mode 100644 index 000000000..0851a8977 --- /dev/null +++ b/src/js/src/shared/style-layer-types.test.js @@ -0,0 +1,32 @@ +import { isStyleLayerType } from './style-layer-types'; + +describe( 'isStyleLayerType', () => { + it( 'defers to the JeoLayerTypes registry', () => { + const registry = { isStyle: ( slug ) => 'custom-style' === slug }; + + expect( isStyleLayerType( 'custom-style', registry ) ).toBe( true ); + expect( isStyleLayerType( 'mapbox', registry ) ).toBe( false ); + } ); + + it( 'returns false when the registry is unavailable', () => { + expect( isStyleLayerType( 'mapbox', undefined ) ).toBe( false ); + expect( isStyleLayerType( 'mapbox', {} ) ).toBe( false ); + } ); + + it( 'returns false for empty slugs', () => { + const registry = { isStyle: () => true }; + + expect( isStyleLayerType( '', registry ) ).toBe( false ); + expect( isStyleLayerType( null, registry ) ).toBe( false ); + expect( isStyleLayerType( undefined, registry ) ).toBe( false ); + } ); + + it( 'defaults to the window registry', () => { + window.JeoLayerTypes = { isStyle: ( slug ) => 'window-style' === slug }; + + expect( isStyleLayerType( 'window-style' ) ).toBe( true ); + expect( isStyleLayerType( 'other' ) ).toBe( false ); + + delete window.JeoLayerTypes; + } ); +} ); diff --git a/src/js/src/shared/styles.js b/src/js/src/shared/styles.js index b2d457a0e..2ee6b62d2 100644 --- a/src/js/src/shared/styles.js +++ b/src/js/src/shared/styles.js @@ -1,3 +1,7 @@ +export function resolveTileUrl( url ) { + return url.replace( /\{r\}/g, window.devicePixelRatio >= 2 ? '@2x' : '' ); +} + export const EMPTY_STYLE = { version: 8, name: 'Empty', diff --git a/src/js/src/shared/wp-form-controls.js b/src/js/src/shared/wp-form-controls.js index 2f0f70e1f..97469653d 100644 --- a/src/js/src/shared/wp-form-controls.js +++ b/src/js/src/shared/wp-form-controls.js @@ -2,9 +2,11 @@ import { CheckboxControl as WPCheckboxControl, ComboboxControl as WPComboboxControl, FormTokenField as WPFormTokenField, + RadioControl as WPRadioControl, RangeControl as WPRangeControl, SelectControl as WPSelectControl, TextControl as WPTextControl, + TextareaControl as WPTextareaControl, } from '@wordpress/components'; const nextInputControlProps = { @@ -39,3 +41,11 @@ export function RangeControl( props ) { export function FormTokenField( props ) { return ; } + +export function RadioControl( props ) { + return ; +} + +export function TextareaControl( props ) { + return ; +} diff --git a/src/js/src/stories-near-you/stories-near-you-frontend.js b/src/js/src/stories-near-you/stories-near-you-frontend.js new file mode 100644 index 000000000..b71bb22d8 --- /dev/null +++ b/src/js/src/stories-near-you/stories-near-you-frontend.js @@ -0,0 +1,450 @@ +import { __ } from '@wordpress/i18n'; + +( function () { + const CONTAINER_SELECTOR = '.wp-block-jeo-stories-near-you'; + const REST_ENDPOINT = '/wp-json/jeo/v1/stories-near-you'; + const GEOLOCATION_TIMEOUT = 15000; + const GEOLOCATION_OVERALL_TIMEOUT = 25000; + const CONSENT_KEY = 'jeo_stories_near_you_consent'; + + class BrowserGeolocationProvider { + getLocation() { + if ( ! navigator.geolocation ) { + return Promise.reject( { error: 'not_supported' } ); + } + + const attemptPromise = this.tryGetCurrentPosition( true ).catch( ( e ) => { + if ( e.error === 'denied' ) { + throw e; + } + return this.tryGetCurrentPosition( false ); + } ); + + const overallTimeoutPromise = new Promise( ( resolve, reject ) => { + setTimeout( () => reject( { error: 'timeout' } ), GEOLOCATION_OVERALL_TIMEOUT ); + } ); + + return Promise.race( [ attemptPromise, overallTimeoutPromise ] ); + } + + tryGetCurrentPosition( enableHighAccuracy ) { + const precision = globalThis.jeo_snu_config?.geolocationPrecision || 2; + + return new Promise( ( resolve, reject ) => { + navigator.geolocation.getCurrentPosition( + ( position ) => { + resolve( { + lat: Number.parseFloat( position.coords.latitude.toFixed( precision ) ), + lng: Number.parseFloat( position.coords.longitude.toFixed( precision ) ), + } ); + }, + ( error ) => { + if ( error.code === error.PERMISSION_DENIED ) { + localStorage.removeItem( CONSENT_KEY ); + reject( { error: 'denied' } ); + } else { + reject( { error: 'unavailable' } ); + } + }, + { + enableHighAccuracy, + timeout: GEOLOCATION_TIMEOUT, + maximumAge: 300000, + } + ); + } ); + } + } + + class StoriesNearYou { + constructor( element, geolocationProvider ) { + this.element = element; + this.geolocationProvider = geolocationProvider; + this.attrs = this.parseAttributes(); + this.excludeIds = []; + this.renderedPostIds = []; + this.hasRendered = false; + this.waterfallTrigger = null; + } + + parseAttributes() { + const script = this.element.querySelector( + '.jeo-stories-near-you-attrs' + ); + if ( ! script ) { + return {}; + } + try { + return JSON.parse( script.textContent ); + } catch ( e ) { + return {}; + } + } + + setExcludeIds( ids ) { + this.excludeIds = ids; + } + + setWaterfallTrigger( fn ) { + this.waterfallTrigger = fn; + } + + getRenderedPostIds() { + return this.renderedPostIds; + } + + getManualLocation() { + const lat = parseFloat( this.attrs.lat ); + const lng = parseFloat( this.attrs.lng ); + if ( Number.isNaN( lat ) || Number.isNaN( lng ) ) { + return null; + } + if ( lat === 0 && lng === 0 ) { + return null; + } + return { lat, lng }; + } + + async init( location ) { + if ( location !== undefined ) { + await this.fetchAndRender( location ); + return; + } + + const manualLocation = this.getManualLocation(); + if ( manualLocation ) { + await this.fetchAndRender( manualLocation ); + return; + } + + if ( ! navigator.geolocation ) { + this.showError( 'not_supported' ); + return; + } + + if ( localStorage.getItem( CONSENT_KEY ) === '1' ) { + try { + const loc = await this.geolocationProvider.getLocation(); + await this.fetchAndRender( loc ); + } catch ( e ) { + this.showError( e?.error ); + } + return; + } + + this.renderConsentPrompt(); + } + + renderConsentPrompt() { + const skeleton = this.element.querySelector( + '.jeo-stories-near-you__skeleton' + ); + if ( skeleton ) { + skeleton.remove(); + } + + const consentEl = document.createElement( 'div' ); + consentEl.className = 'jeo-stories-near-you__consent'; + const hasManualLocation = this.getManualLocation() !== null; + consentEl.innerHTML = ` + + + + `; + + this.element.insertBefore( consentEl, this.element.querySelector( '.jeo-stories-near-you__error' ) ); + + consentEl.querySelector( '.jeo-stories-near-you__consent-button' ).addEventListener( 'click', async () => { + if ( this.waterfallTrigger ) { + await this.waterfallTrigger(); + return; + } + localStorage.setItem( CONSENT_KEY, '1' ); + consentEl.remove(); + this.showSkeleton(); + try { + const location = await this.geolocationProvider.getLocation(); + await this.fetchAndRender( location ); + } catch ( e ) { + this.showError( e?.error ); + } + } ); + + consentEl.querySelector( '.jeo-stories-near-you__consent-skip' ).addEventListener( 'click', async () => { + consentEl.remove(); + const manualLocation = this.getManualLocation(); + await this.fetchAndRender( manualLocation ); + } ); + } + + showSkeleton() { + const skeleton = document.createElement( 'ul' ); + skeleton.className = 'wp-block-latest-posts__list jeo-stories-near-you__skeleton'; + if ( this.attrs.postLayout !== 'list' && this.attrs.postsPerRow ) { + skeleton.classList.add( 'is-grid', 'columns-' + this.attrs.postsPerRow ); + } + for ( let i = 0; i < ( this.attrs.postsPerPage || 3 ); i++ ) { + const card = document.createElement( 'li' ); + card.className = 'jeo-stories-near-you__skeleton-card'; + card.innerHTML = '
    '; + skeleton.appendChild( card ); + } + this.element.insertBefore( skeleton, this.element.querySelector( '.jeo-stories-near-you__error' ) ); + } + + async fetchAndRender( location ) { + const params = new URLSearchParams(); + + if ( location ) { + params.set( 'lat', location.lat ); + params.set( 'lng', location.lng ); + } + + const keys = [ + 'postsPerPage', + 'postsPerRow', + 'showThumbnail', + 'showCategory', + 'showDate', + 'showExcerpt', + 'showAuthor', + 'postLayout', + 'mediaPosition', + 'imageShape', + 'excerptLength', + 'showReadMore', + 'readMoreLabel', + 'showAvatar', + 'colGap', + 'typeScale', + 'imageScale', + 'minHeight', + 'categories', + 'tags', + 'categoryExclusions', + 'tagExclusions', + 'customTaxonomies', + 'postType', + 'imageSize', + 'imageAsLink', + 'radius', + 'orderBy', + 'maxAgeDays', + 'distanceWeight', + 'dateWeight', + ]; + + keys.forEach( ( key ) => { + if ( this.attrs[ key ] !== undefined && this.attrs[ key ] !== '' ) { + params.set( key, this.attrs[ key ] ); + } + } ); + + const serverExcludeIds = this.attrs.excludeIds || []; + const allExclude = [ ...serverExcludeIds, ...this.excludeIds ]; + const uniqueExclude = [ ...new Set( allExclude.map( ( id ) => Number.parseInt( id, 10 ) ) ) ]; + if ( uniqueExclude.length ) { + params.set( 'excludeIds', uniqueExclude.join( ',' ) ); + } + + try { + const response = await fetch( + REST_ENDPOINT + '?' + params.toString() + ); + + if ( ! response.ok ) { + throw new Error( response.statusText ); + } + + const data = await response.json(); + this.renderedPostIds = [ ...this.renderedPostIds, ...( data.postIds || [] ) ]; + this.hasRendered = true; + this.renderResponse( data.html ); + } catch ( e ) { + this.showError( 'network' ); + } + } + + renderResponse( html ) { + const skeleton = this.element.querySelector( + '.jeo-stories-near-you__skeleton' + ); + const errorEl = this.element.querySelector( + '.jeo-stories-near-you__error' + ); + + if ( skeleton ) { + skeleton.remove(); + } + + if ( errorEl ) { + errorEl.classList.add( 'hidden' ); + } + + if ( html ) { + const temp = document.createElement( 'div' ); + temp.innerHTML = html; + const errorRef = this.element.querySelector( + '.jeo-stories-near-you__error' + ); + while ( temp.firstChild ) { + this.element.insertBefore( temp.firstChild, errorRef ); + } + } + } + + getErrorMessage( errorType ) { + const messages = { + denied: __( 'You denied location access. Please enable location permissions in your browser settings.', 'jeowp' ), + unavailable: __( 'Your browser could not determine your location. This may be due to strict privacy settings or because GPS is unavailable on your device.', 'jeowp' ), + timeout: __( 'Location request timed out. Please try again or use the configured location.', 'jeowp' ), + not_supported: __( 'This browser does not support location services.', 'jeowp' ), + network: __( 'Unable to load stories. Please check your connection and try again.', 'jeowp' ), + }; + return messages[ errorType ] || __( 'Unable to load stories near you.', 'jeowp' ); + } + + showError( errorType ) { + const skeleton = this.element.querySelector( + '.jeo-stories-near-you__skeleton' + ); + const errorEl = this.element.querySelector( + '.jeo-stories-near-you__error' + ); + const consentEl = this.element.querySelector( + '.jeo-stories-near-you__consent' + ); + + if ( skeleton ) { + skeleton.remove(); + } + if ( consentEl ) { + consentEl.remove(); + } + + if ( errorEl ) { + const messageEl = errorEl.querySelector( 'p' ); + if ( messageEl ) { + messageEl.textContent = this.getErrorMessage( errorType ); + } + errorEl.classList.remove( 'hidden' ); + } + } + } + + async function resolveSharedLocation( provider ) { + if ( ! navigator.geolocation ) { + throw { error: 'not_supported' }; + } + + if ( localStorage.getItem( CONSENT_KEY ) === '1' ) { + return provider.getLocation(); + } + + return undefined; + } + + async function initAll() { + const elements = document.querySelectorAll( CONTAINER_SELECTOR ); + const provider = new BrowserGeolocationProvider(); + + if ( elements.length <= 1 ) { + elements.forEach( ( element ) => { + const instance = new StoriesNearYou( element, provider ); + const manual = instance.getManualLocation(); + instance.init( manual !== null ? manual : undefined ); + } ); + return; + } + + const instances = []; + let waterfallPromise = null; + + const triggerWaterfall = async () => { + if ( waterfallPromise ) { + return waterfallPromise; + } + + localStorage.setItem( CONSENT_KEY, '1' ); + + waterfallPromise = ( async () => { + let location; + try { + location = await provider.getLocation(); + } catch ( e ) { + for ( const inst of instances ) { + const consentEl = inst.element.querySelector( '.jeo-stories-near-you__consent' ); + if ( consentEl ) { + consentEl.remove(); + } + const skeleton = inst.element.querySelector( '.jeo-stories-near-you__skeleton' ); + if ( skeleton ) { + skeleton.remove(); + } + inst.showError( e?.error ); + } + return; + } + + for ( const inst of instances ) { + const consentEl = inst.element.querySelector( '.jeo-stories-near-you__consent' ); + if ( consentEl ) { + consentEl.remove(); + } + if ( ! inst.hasRendered && ! inst.element.querySelector( '.jeo-stories-near-you__skeleton' ) ) { + inst.showSkeleton(); + } + } + + const allRenderedIds = []; + for ( const inst of instances ) { + if ( inst.hasRendered ) { + allRenderedIds.push( ...inst.getRenderedPostIds() ); + continue; + } + inst.setExcludeIds( allRenderedIds ); + await inst.fetchAndRender( location ); + const ids = inst.getRenderedPostIds(); + allRenderedIds.push( ...ids ); + } + } )(); + + return waterfallPromise; + }; + + for ( const element of elements ) { + const instance = new StoriesNearYou( element, provider ); + instance.setWaterfallTrigger( triggerWaterfall ); + instances.push( instance ); + } + + let sharedError = null; + const sharedLocation = await resolveSharedLocation( provider ).catch( ( error ) => { + console.error( error ); + sharedError = error?.error || 'not_supported'; + return null; + } ); + const allRenderedIds = []; + + if ( sharedLocation === null ) { + for ( const instance of instances ) { + instance.showError( sharedError ); + } + return; + } + + for ( const instance of instances ) { + instance.setExcludeIds( allRenderedIds ); + const manual = instance.getManualLocation(); + await instance.init( manual !== null ? manual : sharedLocation ); + const ids = instance.getRenderedPostIds(); + allRenderedIds.push( ...ids ); + } + } + + if ( document.readyState === 'loading' ) { + document.addEventListener( 'DOMContentLoaded', initAll ); + } else { + initAll(); + } +} )(); diff --git a/src/languages/jeowp-es_CO.po b/src/languages/jeowp-es_CO.po index f298e10e8..e600ee9a7 100755 --- a/src/languages/jeowp-es_CO.po +++ b/src/languages/jeowp-es_CO.po @@ -1,25 +1,20 @@ msgid "" msgstr "" -"Project-Id-Version: JEO 3.1.2\n" +"Project-Id-Version: \n" "Report-Msgid-Bugs-To: https://wordpress.org/support/plugin/jeowp\n" "Last-Translator: Stefano Wrobleski\n" -"Language-Team: InfoAmazonia\n" +"Language-Team: InfoAmazoniadevelop1.0\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "POT-Creation-Date: 2026-06-09T19:53:28+00:00\n" -"PO-Revision-Date: 2026-06-09T19:53:28+00:00\n" +"PO-Revision-Date: 2026-07-17 12:00-0300\n" "Language: es_CO\n" "Plural-Forms: nplurals=2; plural=n != 1;\n" -"X-Generator: Poedit 3.9\n" "X-Domain: jeowp\n" +"X-Generator: Poedit 3.9\n" "X-Loco-Version: 2.5.0; wp-5.7\n" -#: includes/menu/class-menu.php:37 -#: includes/menu/class-menu.php:38 -msgid "JEO" -msgstr "JEO" - #. Plugin URI of the plugin #. Author URI of the plugin #: jeo.php @@ -36,63 +31,58 @@ msgstr "Mapas interactivos para el editor de bloques de WordPress" msgid "InfoAmazonia" msgstr "InfoAmazonia" -#: includes/class-jeo.php:583 +#: includes/class-jeo.php:931 msgid "Mapbox was selected as the rendering library, but no Mapbox API key is configured. Falling back to MapLibre." msgstr "Se seleccionó Mapbox como biblioteca de renderizado, pero no hay ninguna clave de API de Mapbox configurada. Se volverá a MapLibre." -#: includes/class-jeo.php:584 +#: includes/class-jeo.php:932 msgid "Mapbox was selected as the rendering library, but the external Mapbox SDK could not be loaded. Falling back to MapLibre." msgstr "Se seleccionó Mapbox como biblioteca de renderizado, pero no se pudo cargar el SDK externo de Mapbox. Se volverá a MapLibre." -#: includes/class-jeo.php:1107 +#: includes/class-jeo.php:1470 +#: js/build/mapBlocks.js:3 msgid "Read more" msgstr "Leer más" -#: includes/geocode/class-geocode-handler.php:130 -msgid "Geocoding service provided by OpenStreetMap at https://nominatim.openstreetmap.org/." -msgstr "Servicio de geocodificación ofrecido por OpenStreetMap en https://nominatim.openstreetmap.org/." - -#: includes/geocode/class-geocode-handler.php:231 -msgid "Metadata storing the locations related to the post. Each location is an object with latitude, longitude, and geocode attributes." -msgstr "Metadatos que almacenan las ubicaciones relacionadas con la entrada. Cada ubicación es un objeto con atributos de latitud, longitud y geocodificación." - -#: includes/geocode/class-geocode-handler.php:247 +#: includes/ai/settings/tab-embedded.php:74 +#: includes/geocode/class-geocode-handler.php:265 #: js/build/mapBlocks.js:3 #: js/build/mapsSidebar.js:1 msgid "Latitude" msgstr "Latitud" -#: includes/geocode/class-geocode-handler.php:261 +#: includes/ai/settings/tab-embedded.php:75 +#: includes/geocode/class-geocode-handler.php:282 #: js/build/mapBlocks.js:3 #: js/build/mapsSidebar.js:1 msgid "Longitude" msgstr "Longitud" -#: includes/geocode/class-geocode-handler.php:275 +#: includes/geocode/class-geocode-handler.php:296 msgid "Country" msgstr "País" -#: includes/geocode/class-geocode-handler.php:289 +#: includes/geocode/class-geocode-handler.php:310 msgid "Country code" msgstr "Código de país" -#: includes/geocode/class-geocode-handler.php:303 +#: includes/geocode/class-geocode-handler.php:324 msgid "Region level 1" msgstr "Región nivel 1" -#: includes/geocode/class-geocode-handler.php:317 +#: includes/geocode/class-geocode-handler.php:338 msgid "Region level 2" msgstr "Región nivel 2" -#: includes/geocode/class-geocode-handler.php:331 +#: includes/geocode/class-geocode-handler.php:352 msgid "Region level 3" msgstr "Región nivel 3" -#: includes/geocode/class-geocode-handler.php:345 +#: includes/geocode/class-geocode-handler.php:366 msgid "City" msgstr "Ciudad" -#: includes/geocode/class-geocode-handler.php:359 +#: includes/geocode/class-geocode-handler.php:380 msgid "City sub-level 1" msgstr "Ciudad sub-nivel 1" @@ -164,50 +154,50 @@ msgstr "Capa actualizada." msgid "JEO Layers" msgstr "Capas JEO" -#: includes/layers/class-layers.php:108 +#: includes/layers/class-layers.php:131 msgid "The layer type" msgstr "Tipo de capa" -#: includes/layers/class-layers.php:120 +#: includes/layers/class-layers.php:143 msgid "Layer attribution as text or HTML with a link" msgstr "Atribución de la capa como texto o HTML con un enlace" -#: includes/layers/class-layers.php:137 +#: includes/layers/class-layers.php:160 msgid "Layer type-specific options" msgstr "Opciones específicas del tipo de capa" -#: includes/layers/class-layers.php:149 +#: includes/layers/class-layers.php:172 msgid "The URL to download the source data of the layer" msgstr "URL para descargar los datos de la fuente de la capa" -#: includes/layers/class-layers.php:161 +#: includes/layers/class-layers.php:184 msgid "Label for the attribution URL link" msgstr "Etiqueta del enlace de URL de atribución" -#: includes/layers/class-layers.php:173 +#: includes/layers/class-layers.php:196 msgid "The legend type" msgstr "Tipo de leyenda" -#: includes/layers/class-layers.php:190 +#: includes/layers/class-layers.php:213 msgid "Legend type-specific options" msgstr "Opciones específicas del tipo de leyenda" -#: includes/layers/class-layers.php:202 +#: includes/layers/class-layers.php:225 #: js/build/layersSidebar.js:7 msgid "Use legend" msgstr "Usar leyenda" -#: includes/layers/class-layers.php:214 +#: includes/layers/class-layers.php:237 #: js/build/layersSidebar.js:7 msgid "Legend title" msgstr "Título de leyenda" -#: includes/layers/class-layers.php:265 +#: includes/layers/class-layers.php:335 msgid "Layer type not registered" msgstr "Tipo de capa no registrada" #. translators: Explore is the name of JEO's discovery page feature. -#: includes/loaders.php:535 +#: includes/loaders.php:591 #: templates/embed-discovery.php:18 #: js/build/discovery.js:6 msgid "Explore" @@ -219,7 +209,6 @@ msgid "Maps" msgstr "Mapas" #: includes/maps/class-maps.php:55 -#: includes/settings/settings-page.php:34 msgid "Map" msgstr "Mapa" @@ -273,6 +262,7 @@ msgid "Map scheduled." msgstr "Mapa programado." #: includes/maps/class-maps.php:70 +#: js/build/mapBlocks.js:1 msgid "Map updated." msgstr "Mapa actualizado." @@ -323,304 +313,174 @@ msgstr "Indica si esta capa es visible por defecto" msgid "Indicates if the legend of this layer should be displayed or not" msgstr "Indica si la leyenda de esta capa debería ser mostrada o no" -#: includes/maps/class-maps.php:198 +#: includes/maps/class-maps.php:220 msgid "The map Layers" msgstr "Capas del mapa" -#: includes/maps/class-maps.php:211 +#: includes/maps/class-maps.php:233 #: js/build/mapBlocks.js:1 msgid "East pan limit" msgstr "Límite de panorámica este" -#: includes/maps/class-maps.php:215 +#: includes/maps/class-maps.php:237 #: js/build/mapBlocks.js:1 msgid "North pan limit" msgstr "Límite de panorámica norte" -#: includes/maps/class-maps.php:219 +#: includes/maps/class-maps.php:241 #: js/build/mapBlocks.js:1 msgid "South pan limit" msgstr "Límite de panorámica sur" -#: includes/maps/class-maps.php:223 +#: includes/maps/class-maps.php:245 #: js/build/mapBlocks.js:1 msgid "West pan limit" msgstr "Límite de panorámica oeste" -#: includes/maps/class-maps.php:233 +#: includes/maps/class-maps.php:255 msgid "Map pan limits" msgstr "Limites de panorámica del mapa" -#: includes/maps/class-maps.php:246 +#: includes/maps/class-maps.php:268 msgid "List of category IDs related posts must have" msgstr "Lista de IDs de categorías que los post relacionados deben tener" -#: includes/maps/class-maps.php:253 +#: includes/maps/class-maps.php:275 msgid "List of tag IDs related posts must have" msgstr "Lista de IDs de etiquetas que los post relacionados deben tener" -#: includes/maps/class-maps.php:260 +#: includes/maps/class-maps.php:282 msgid "Maximum date for related posts" msgstr "Máxima fecha para posts relacionados" -#: includes/maps/class-maps.php:264 +#: includes/maps/class-maps.php:286 msgid "Minimum date for related posts" msgstr "Mínima fecha para posts relacionados" -#: includes/maps/class-maps.php:268 +#: includes/maps/class-maps.php:290 msgid "List of meta values related posts must have" msgstr "Lista de meta-values que los post relacionados deben tener" -#: includes/maps/class-maps.php:292 +#: includes/maps/class-maps.php:314 msgid "The map criteria to get related posts" msgstr "Criterios de mapa para obtener posts relacionados" -#: includes/maps/class-maps.php:305 +#: includes/maps/class-maps.php:327 msgid "Disable scroll zoom" msgstr "Desactivar scroll zoom" -#: includes/maps/class-maps.php:318 +#: includes/maps/class-maps.php:340 msgid "Disable drag pan" msgstr "Desactivar arrastrar y desplazar" -#: includes/maps/class-maps.php:331 +#: includes/maps/class-maps.php:353 msgid "Disable drag rotation" msgstr "Desactivar rotación por arrastre" -#: includes/maps/class-maps.php:344 +#: includes/maps/class-maps.php:366 msgid "Enable fullscreen button" msgstr "Activar botón de pantalla completa" -#: includes/maps/class-maps.php:357 +#: includes/maps/class-maps.php:379 msgid "Show all posts on map" msgstr "Mostrar todos los posts en el mapa" -#: includes/maps/class-maps.php:370 +#: includes/maps/class-maps.php:392 msgid "Minimum Zoom level" msgstr "Mínimo nivel de zoom" -#: includes/maps/class-maps.php:383 +#: includes/maps/class-maps.php:405 msgid "Maximum zoom level" msgstr "Nivel máximo de zoom" -#: includes/maps/class-maps.php:396 +#: includes/maps/class-maps.php:418 #: js/build/mapBlocks.js:3 #: js/build/mapsSidebar.js:1 msgid "Hide in discovery" msgstr "Ocultar en la página Explorar" -#: includes/maps/class-maps.php:409 +#: includes/maps/class-maps.php:431 msgid "Disable embed" msgstr "Desactivar incrustación" -#: includes/maps/class-maps.php:565 +#: includes/maps/class-maps.php:587 msgid "Map zoom must be between 1 and 14" msgstr "El zoom del mapa debe estar entre 1 y 14" -#: includes/settings/class-settings.php:179 -msgid "Mapbox requires a valid API key. The rendering library setting was not changed." -msgstr "Mapbox requiere una clave de API válida. La configuración de la biblioteca de renderizado no se modificó." - -#: includes/settings/class-settings.php:347 -#: includes/settings/class-settings.php:348 +#: includes/ai/class-ai-settings.php:50 +#: includes/settings/class-settings.php:685 +#: includes/settings/class-settings.php:686 #: js/build/layersSidebar.js:11 msgid "Settings" msgstr "Configuraciones" -#: includes/settings/settings-page.php:16 -msgid "JEO Settings" -msgstr "Configuraciones de JEO" - -#: includes/settings/settings-page.php:20 +#: includes/settings/class-settings.php:709 msgid "General" msgstr "General" -#: includes/settings/settings-page.php:23 +#: includes/settings/class-settings.php:710 msgid "Geocoders" msgstr "Geocodificadores" -#: includes/settings/settings-page.php:26 -msgid "Customize" -msgstr "Personalización" - -#: includes/settings/settings-page.php:40 +#: includes/settings/settings-page.php:28 msgid "Rendering library" msgstr "Biblioteca de renderizado" -#: includes/settings/settings-page.php:50 -msgid "Default map latitute" -msgstr "Latitud por defecto del mapa" - #: includes/settings/settings-page.php:57 -msgid "Default map longitude" -msgstr "Longitud por defecto del mapa" - -#: includes/settings/settings-page.php:64 msgid "Default map zoom" msgstr "Zoom por defecto del mapa" -#: includes/settings/settings-page.php:71 -msgid "Post types" -msgstr "Tipos de contenido" - -#: includes/settings/settings-page.php:77 -msgid "Show story maps on post archives pages" -msgstr "Mostrar mapas narrativos en las páginas de archivo de entradas" - -#: includes/settings/settings-page.php:84 -msgid "Enabled Post Types. Default: post,storymap" -msgstr "Tipos de contenido habilitados. Predeterminado: post,storymap" - -#: includes/settings/settings-page.php:86 -msgid "Post types separated by comma, Ex: map,post,page" -msgstr "Tipos de contenido separados por comas. Ej.: map,post,page" - -#: includes/settings/settings-page.php:91 +#: includes/settings/settings-page.php:137 msgid "Mapbox" msgstr "Mapbox" -#: includes/settings/settings-page.php:94 -msgid "When selected, Mapbox loads the official Mapbox GL JS assets from api.mapbox.com and uses Mapbox terms and privacy practices." -msgstr "Cuando se selecciona, Mapbox carga los recursos oficiales de Mapbox GL JS desde api.mapbox.com y usa los términos y prácticas de privacidad de Mapbox." - -#: includes/settings/settings-page.php:97 -msgid "Visitors load Mapbox assets in the browser, and Mapbox receives the configured access token plus the visitor IP address and requested map resources." -msgstr "Los visitantes cargan los recursos de Mapbox en el navegador, y Mapbox recibe el token de acceso configurado, además de la dirección IP del visitante y los recursos de mapa solicitados." - -#: includes/settings/settings-page.php:103 +#: includes/ai/settings/tab-provider.php:27 msgid "API Key" msgstr "Clave de API" -#: includes/settings/settings-page.php:105 +#: includes/settings/settings-page.php:143 msgid "Ex. pk.eyJ3..." msgstr "Ej.: pk.eyJ3..." -#: includes/settings/settings-page.php:107 -msgid "This key is required only when the rendering library is set to Mapbox." -msgstr "Esta clave solo es necesaria cuando la biblioteca de renderizado está configurada como Mapbox." - -#: includes/settings/settings-page.php:121 -msgid "Active Geocoder" -msgstr "Geocodificador activo" - -#: includes/settings/settings-page.php:134 -msgid "The built-in Nominatim geocoder sends the address you submit, or the coordinates selected on the map, to nominatim.openstreetmap.org only when you explicitly run a search or reverse-geocode a point." -msgstr "El geocodificador Nominatim integrado envía la dirección que ingresas, o las coordenadas seleccionadas en el mapa, a nominatim.openstreetmap.org solo cuando ejecutas explícitamente una búsqueda o una geocodificación inversa de un punto." - -#. translators: %s is the geocoder name. Example: Nominatim options. -#: includes/settings/settings-page.php:154 -#, php-format -msgctxt "geocoder_options" -msgid "%s options" -msgstr "%s opciones" - -#: includes/settings/settings-page.php:198 +#: includes/settings/settings-page.php:173 msgid "Typography" msgstr "Tipografía" -#: includes/settings/settings-page.php:201 -msgid "Typography URL" -msgstr "URL de la tipografía" - -#: includes/settings/settings-page.php:205 -#: includes/settings/settings-page.php:220 -msgid "You may use a local or external stylesheet URL. If you choose an external host, your visitors will request that asset directly from that provider." -msgstr "Puedes usar una URL local o externa de hoja de estilos. Si eliges un host externo, los visitantes solicitarán ese recurso directamente a ese proveedor." - #: includes/settings/settings-page.php:210 -msgid "Typography name" -msgstr "Nombre de la tipografía" - -#: includes/settings/settings-page.php:212 -msgid "Ex. Open Sans" -msgstr "Ej.: Open Sans" - -#: includes/settings/settings-page.php:216 -msgid "Secondary Typography URL" -msgstr "URL de la tipografía secundaria" - -#: includes/settings/settings-page.php:225 -msgid "Secondary Typography name" -msgstr "Nombre de la tipografía secundaria" - -#: includes/settings/settings-page.php:227 -msgid "Ex. Libre Baskerville" -msgstr "Ej.: Libre Baskerville" - -#: includes/settings/settings-page.php:231 -msgid "Info button font-size (rem)" -msgstr "Tamaño de fuente del botón Info (rem)" - -#: includes/settings/settings-page.php:238 +#: js/build/mapBlocks.js:3 +#: js/build/mapsSidebar.js:1 msgid "Colors" msgstr "Colores" -#: includes/settings/settings-page.php:242 -msgid "Primary color" -msgstr "Color primario" - -#: includes/settings/settings-page.php:244 -#: includes/settings/settings-page.php:258 -#: includes/settings/settings-page.php:264 -#: includes/settings/settings-page.php:276 -msgid "Ex. #ffffff" -msgstr "Ej. #ffffff" - -#: includes/settings/settings-page.php:249 -msgid "Text over primary color" -msgstr "Texto sobre color primario" - -#: includes/settings/settings-page.php:256 +#: includes/settings/settings-page.php:220 msgid "Info button background color" msgstr "Color de fondo del botón Info" -#: includes/settings/settings-page.php:262 -msgid "Info button color" -msgstr "Color del botón Info" - -#: includes/settings/settings-page.php:268 +#: includes/settings/settings-page.php:232 msgid "Close button background color" msgstr "Color de fondo del botón de cierre" -#: includes/settings/settings-page.php:274 -msgid "Close button color" -msgstr "Color del botón de cierre" - -#: includes/settings/settings-page.php:281 -msgid "Embed" -msgstr "Incrustación" - #: js/build/discovery.js:6 msgctxt "discovery share action" msgid "Embed" msgstr "Embed" -#: includes/settings/settings-page.php:285 +#: includes/settings/settings-page.php:296 msgid "Company logo" msgstr "Logotipo de la empresa" -#: includes/settings/settings-page.php:289 +#: includes/settings/settings-page.php:300 msgid "You may use a local or external image URL. Large logos will be scaled down automatically in the embed footer." msgstr "Puedes usar una URL local o externa de imagen. Los logotipos grandes se reducirán automáticamente en el pie de la incrustación." -#. translators: 1: Mapbox terms URL, 2: Mapbox privacy URL, 3: Nominatim usage policy URL, 4: OSMF privacy URL, 5: OSM tile policy URL. -#: includes/loaders.php:351 -#, php-format -msgid "

    JEO can connect to third-party services depending on your configuration.

    • Mapbox: only used when Mapbox is selected as the rendering library or when a map uses Mapbox-hosted resources. In that case the site loads JavaScript/CSS from Mapbox and sends the configured access token plus the visitor’s IP address, browser details and requested map resources to Mapbox. Terms: Mapbox Terms of Service. Privacy Policy: Mapbox Privacy Policy.
    • Nominatim (OpenStreetMap): used when an editor explicitly runs an address search or reverse geocoding request in the post geolocation UI. The typed address or selected coordinates, the site URL in the user agent string and the server IP address are sent to the Nominatim service. Usage Policy: Nominatim Usage Policy. Privacy Policy: OpenStreetMap Foundation Privacy Policy.
    • OpenStreetMap raster tiles: the default MapLibre preview style requests map tiles from the OpenStreetMap tile service, which receives the visitor’s IP address, browser details and requested tile URLs. Tile Policy: OpenStreetMap Tile Usage Policy. Privacy Policy: OpenStreetMap Foundation Privacy Policy.
    • Optional external asset URLs configured by the site administrator: if you configure an external typography stylesheet or footer logo URL, visitors’ browsers will request that asset directly from the selected host. That host may receive the visitor’s IP address, browser details and referrer according to its own terms and privacy policy.
    " -msgstr "

    JEO puede conectarse a servicios de terceros según tu configuración.

    • Mapbox: solo se usa cuando Mapbox se selecciona como biblioteca de renderizado o cuando un mapa usa recursos alojados en Mapbox. En ese caso, el sitio carga JavaScript/CSS desde Mapbox y envía el token de acceso configurado, además de la dirección IP del visitante, detalles del navegador y los recursos de mapa solicitados a Mapbox. Términos: Términos de Servicio de Mapbox. Política de Privacidad: Política de Privacidad de Mapbox.
    • Nominatim (OpenStreetMap): se usa cuando un editor ejecuta explícitamente una búsqueda de dirección o una solicitud de geocodificación inversa en la interfaz de geolocalización de entradas. La dirección escrita o las coordenadas seleccionadas, la URL del sitio en la cadena del user agent y la dirección IP del servidor se envían al servicio Nominatim. Política de Uso: Política de Uso de Nominatim. Política de Privacidad: Política de Privacidad de la OpenStreetMap Foundation.
    • Teselas ráster de OpenStreetMap: el estilo de vista previa predeterminado de MapLibre solicita teselas al servicio de teselas de OpenStreetMap, que recibe la dirección IP del visitante, detalles del navegador y las URL de teselas solicitadas. Política de Teselas: Política de Uso de Teselas de OpenStreetMap. Política de Privacidad: Política de Privacidad de la OpenStreetMap Foundation.
    • URLs externas opcionales de recursos configuradas por el administrador del sitio: si configuras una URL externa para la hoja de estilos tipográfica o para el logotipo del pie, los navegadores de los visitantes solicitarán ese recurso directamente al host seleccionado. Ese host puede recibir la dirección IP del visitante, detalles del navegador y el referente, según sus propios términos y política de privacidad.
    " - -#: includes/settings/settings-page.php:291 +#: includes/settings/settings-page.php:302 msgid "Insert Image" msgstr "Insertar imagen" -#: includes/settings/settings-page.php:301 +#: includes/settings/settings-page.php:331 msgid "Save Changes" msgstr "Guardar cambios" -#: includes/settings/class-settings.php:317 -msgid "JEO could not save this asset URL. Use a valid http(s) or root-relative URL." -msgstr "JEO no pudo guardar esta URL de recurso. Usa una URL http(s) válida o una URL relativa a la raíz." - #. translators: Story Map is the name of JEO's storytelling map feature. #: includes/storymap/class-storymap.php:69 #: includes/storymap/class-storymap.php:70 @@ -703,11 +563,12 @@ msgstr "Volver al inicio" #: templates/map-content-layers-list.php:18 #: templates/map-content-layers-list.php:20 +#: templates/map-content-layers-list.php:22 #: js/build/jeoMap.js:1 msgid "Attribution:" msgstr "Atribución:" -#: templates/map-content-layers-list.php:25 +#: templates/map-content-layers-list.php:27 #: js/build/jeoMap.js:1 msgid "Download from source" msgstr "Descargar desde la fuente" @@ -720,44 +581,44 @@ msgstr "Descargar desde la fuente" msgid "Raster Mapbox Tiled Source" msgstr "Fuente ráster de teselas de Mapbox" -#: includes/layer-types/mapbox-tileset-raster.js:68 -#: includes/layer-types/mapbox-tileset-vector.js:70 +#: includes/layer-types/mapbox-tileset-raster.js:72 +#: includes/layer-types/mapbox-tileset-vector.js:109 #: js/build/layersSidebar.js:11 #: js/build/mapBlocks.js:1 msgid "Tileset ID" msgstr "ID del tileset" -#: includes/layer-types/mapbox-tileset-raster.js:69 -#: includes/layer-types/mapbox-tileset-vector.js:71 +#: includes/layer-types/mapbox-tileset-raster.js:73 +#: includes/layer-types/mapbox-tileset-vector.js:110 #: js/build/layersSidebar.js:11 #: js/build/mapBlocks.js:1 msgid "Example: username.tilesetid" msgstr "Ejemplo: username.tilesetid" -#: includes/layer-types/mapbox-tileset-raster.js:72 -#: includes/layer-types/mapbox-tileset-vector.js:99 -#: includes/layer-types/mvt.js:101 +#: includes/layer-types/mapbox-tileset-raster.js:76 +#: includes/layer-types/mapbox-tileset-vector.js:138 +#: includes/layer-types/mvt.js:133 #: js/build/layersSidebar.js:11 #: js/build/mapBlocks.js:1 msgid "Style Source Type" msgstr "Tipo de fuente del estilo" -#: includes/layer-types/mapbox-tileset-raster.js:73 -#: includes/layer-types/mvt.js:102 +#: includes/layer-types/mapbox-tileset-raster.js:77 +#: includes/layer-types/mvt.js:134 #: js/build/layersSidebar.js:11 #: js/build/mapBlocks.js:1 msgid "Which data the map should display" msgstr "Qué datos debe mostrar el mapa" -#: includes/layer-types/mapbox-tileset-raster.js:79 -#: includes/layer-types/mapbox-tileset-vector.js:79 +#: includes/layer-types/mapbox-tileset-raster.js:83 +#: includes/layer-types/mapbox-tileset-vector.js:118 #: js/build/layersSidebar.js:11 #: js/build/mapBlocks.js:1 msgid "Layer Type" msgstr "Tipo de capa" -#: includes/layer-types/mapbox-tileset-raster.js:81 -#: includes/layer-types/mapbox-tileset-vector.js:81 +#: includes/layer-types/mapbox-tileset-raster.js:85 +#: includes/layer-types/mapbox-tileset-vector.js:120 #: js/build/layersSidebar.js:11 #: js/build/mapBlocks.js:1 msgid "Layers take the data that they get from a source, optionally filter features, and then define how those features are styled." @@ -771,20 +632,20 @@ msgstr "Las capas toman los datos que reciben de una fuente, opcionalmente filtr msgid "Vector Mapbox Tiled Source" msgstr "Fuente vectorial de teselas de Mapbox" -#: includes/layer-types/mapbox-tileset-vector.js:75 -#: includes/layer-types/mvt.js:83 +#: includes/layer-types/mapbox-tileset-vector.js:114 +#: includes/layer-types/mvt.js:115 #: js/build/layersSidebar.js:11 #: js/build/mapBlocks.js:1 msgid "Source layer" msgstr "Capa de origen" -#: includes/layer-types/mapbox-tileset-vector.js:76 +#: includes/layer-types/mapbox-tileset-vector.js:115 #: js/build/layersSidebar.js:11 #: js/build/mapBlocks.js:1 msgid "Which data the map should display." msgstr "Qué datos debe mostrar el mapa." -#: includes/layer-types/mapbox-tileset-vector.js:100 +#: includes/layer-types/mapbox-tileset-vector.js:139 #: js/build/layersSidebar.js:11 #: js/build/mapBlocks.js:1 msgid "The layer source type style" @@ -798,25 +659,31 @@ msgstr "El tipo de fuente del estilo de la capa" msgid "Mapbox Style" msgstr "Estilo de Mapbox" -#: includes/layer-types/mapbox.js:117 +#: includes/layer-types/mapbox.js:119 #: js/build/layersSidebar.js:11 #: js/build/mapBlocks.js:1 msgid "Style ID" msgstr "ID del estilo" -#: includes/layer-types/mapbox.js:119 +#: includes/layer-types/mapbox.js:121 #: js/build/layersSidebar.js:11 #: js/build/mapBlocks.js:1 msgid "The Mapbox Style ID includes the user name and id. Example: username/id or mapbox://styles/username/id" msgstr "El ID del estilo de Mapbox incluye el nombre de usuario y el ID. Ejemplo: username/id o mapbox://styles/username/id" -#: includes/layer-types/mapbox.js:126 +#: includes/layer-types/mapbox-tileset-raster.js:95 +#: includes/layer-types/mapbox-tileset-vector.js:146 +#: includes/layer-types/mapbox.js:128 +#: includes/layer-types/mvt.js:141 #: js/build/layersSidebar.js:11 #: js/build/mapBlocks.js:1 msgid "Access token" msgstr "Token de acceso" -#: includes/layer-types/mapbox.js:128 +#: includes/layer-types/mapbox-tileset-raster.js:96 +#: includes/layer-types/mapbox-tileset-vector.js:147 +#: includes/layer-types/mapbox.js:130 +#: includes/layer-types/mvt.js:142 #: js/build/layersSidebar.js:11 #: js/build/mapBlocks.js:1 msgid "Optional. If this layer needs a different access token from the one set in Settings, inform it here." @@ -830,8 +697,9 @@ msgstr "Opcional. Si esta capa necesita un token de acceso diferente del configu msgid "Mapbox Vector Tiles (MVT)" msgstr "Teselas vectoriales de Mapbox (MVT)" -#: includes/layer-types/mvt.js:79 -#: includes/layer-types/tilelayer.js:57 +#: includes/ai/settings/tab-provider.php:27 +#: includes/layer-types/mvt.js:111 +#: includes/layer-types/tilelayer.js:61 #: includes/legend-types/circles.js:25 #: includes/legend-types/icons.js:25 #: js/build/layersSidebar.js:11 @@ -839,7 +707,7 @@ msgstr "Teselas vectoriales de Mapbox (MVT)" msgid "URL" msgstr "URL" -#: includes/layer-types/mvt.js:84 +#: includes/layer-types/mvt.js:116 #: js/build/layersSidebar.js:11 #: js/build/mapBlocks.js:1 msgid "Layer to use from a vector tile source." @@ -853,25 +721,25 @@ msgstr "Capa que se usará de una fuente de teselas vectoriales." msgid "Raster Tiled Source" msgstr "Fuente ráster de teselas" -#: includes/layer-types/tilelayer.js:61 +#: includes/layer-types/tilelayer.js:65 #: js/build/layersSidebar.js:11 #: js/build/mapBlocks.js:1 msgid "Scheme" msgstr "Esquema" -#: includes/layer-types/tilelayer.js:62 +#: includes/layer-types/tilelayer.js:66 #: js/build/layersSidebar.js:11 #: js/build/mapBlocks.js:1 msgid "Influences the Y direction of the tile coordinates." msgstr "Influye en la dirección Y de las coordenadas de las teselas." -#: includes/layer-types/tilelayer.js:65 +#: includes/layer-types/tilelayer.js:69 #: js/build/layersSidebar.js:11 #: js/build/mapBlocks.js:1 msgid "Slippy Map tilenames (XYZ)" msgstr "Nombres de teselas de Slippy Map (XYZ)" -#: includes/layer-types/tilelayer.js:66 +#: includes/layer-types/tilelayer.js:70 #: js/build/layersSidebar.js:11 #: js/build/mapBlocks.js:1 msgid "OSGeo spec (TMS)" @@ -916,6 +784,7 @@ msgstr "Un arreglo de etiquetas e íconos" #: includes/legend-types/JeoLegend.js:181 #: includes/legend-types/JeoLegend.js:186 #: js/build/JeoLegend.js:1 +#: js/build/layersSidebar.js:1 #: js/build/layersSidebar.js:7 msgid "Default label" msgstr "Etiqueta predeterminada" @@ -929,11 +798,7 @@ msgstr "Categorías de color" msgid "An array of labels and colors" msgstr "Un arreglo de etiquetas y colores" -#: includes/settings/settings-page.js:37 -#: includes/settings/settings-page.js:39 -msgid "Choose Image" -msgstr "Elegir imagen" - +#: js/build/contextSidebar.js:1 #: js/build/discovery.js:1 #: js/build/discovery.js:6 #: js/build/mapBlocks.js:3 @@ -987,6 +852,8 @@ msgstr "MM/DD/AAAA" msgid " - " msgstr " - " +#: includes/admin/uninstall-page.php:76 +#: includes/ai/class-bulk-processor.php:671 #: js/build/discovery.js:2 #: js/build/mapBlocks.js:1 #: js/build/postsSidebar.js:1 @@ -1121,12 +988,6 @@ msgstr "Rango de fechas" msgid "Date range: %s" msgstr "Rango de fechas: %s" -#. translators: %d is the number of stories currently displayed. -#: js/build/discovery.js:3 -#, js-format -msgid "Displayed stories: %d" -msgstr "Historias mostradas: %d" - #. translators: %s is the list of active story tags. #: js/build/discovery.js:6 #, js-format @@ -1169,14 +1030,6 @@ msgstr "No se pueden cargar las capas del mapa en este momento." msgid "Search map" msgstr "Buscar mapa" -#: js/build/discovery.js:6 -msgid "Changes applied" -msgstr "Cambios aplicados" - -#: js/build/discovery.js:6 -msgid "Not applied" -msgstr "No aplicado" - #: js/build/discovery.js:6 #: js/build/mapBlocks.js:3 #: js/build/mapsSidebar.js:1 @@ -1229,22 +1082,6 @@ msgstr "Cerrar ventana emergente" msgid "Info" msgstr "Información" -#: js/build/jeoMap.js:1 -msgid "Show previous post" -msgstr "Mostrar la entrada anterior" - -#: js/build/jeoMap.js:1 -msgid "Show next post" -msgstr "Mostrar la entrada siguiente" - -#. translators: %d: number of markers in the current map area. -#: js/build/jeoMap.js:2 -#, js-format -msgid "%d marker in this area" -msgid_plural "%d markers in this area" -msgstr[0] "%d marcador en esta área" -msgstr[1] "%d marcadores en esta área" - #: js/build/jeoStorymap.js:3 msgid "at" msgstr "en" @@ -1308,6 +1145,7 @@ msgstr "Remover" msgid "See preview" msgstr "Ver previsualización" +#: includes/ai/settings/tab-embedded.php:54 #: js/build/layersSidebar.js:7 #: js/build/mapBlocks.js:1 msgid "Preview" @@ -1344,6 +1182,7 @@ msgid "Type" msgstr "Tipo" #: js/build/layersSidebar.js:7 +#: js/build/mapBlocks.js:1 msgid "No" msgstr "No" @@ -1393,30 +1232,22 @@ msgstr "Hecho" msgid "Edit interactions" msgstr "Editar interacciones" -#: js/build/layersSidebar.js:11 +#: js/build/mapBlocks.js:1 msgid "Please fill all required fields, you will not be able to publish or update until that." msgstr "Complete todos los campos obligatorios, no podrá publicar ni actualizar hasta ese momento." -#: js/build/layersSidebar.js:11 +#: js/build/mapBlocks.js:1 msgid "Your Mapbox access token may be invalid. You will not be able to publish or update. Please check your settings." msgstr "Es posible que su token de acceso de Mapbox no sea válido. No podrá publicar ni actualizar. Por favor revise su configuración." -#: js/build/layersSidebar.js:11 +#: js/build/mapBlocks.js:1 msgid "Your layer was not found. You will not be able to publish or update. Please check your settings." msgstr "No se encontró la capa. No podrá publicar ni actualizar. Por favor revise su configuración." -#: js/build/layersSidebar.js:11 +#: js/build/mapBlocks.js:1 msgid "Error loading your layer, you will not be able to publish or update. Please check your settings." msgstr "Error al cargar la capa, no podrá publicar ni actualizar. Compruebe la configuración." -#: js/build/layersSidebar.js:11 -msgid "Your Mapbox API Key was not found in your JEO Settings. You will not be able to publish or update." -msgstr "No se encontró la clave de API de Mapbox en la configuración de JEO. No podrá publicar ni actualizar." - -#: js/build/layersSidebar.js:11 -msgid "Please, check your settings." -msgstr "Por favor, revise su configuración." - #: js/build/layersSidebar.js:11 msgid "Attributions" msgstr "Atribuciones" @@ -1577,6 +1408,8 @@ msgstr "No fue posible cargar las sugerencias de direcciones." msgid "Search address" msgstr "Buscar dirección" +#: includes/ai/settings/tab-knowledge.php:299 +#: includes/settings/class-settings.php:642 #: js/build/mapBlocks.js:1 #: js/build/postsSidebar.js:1 msgid "Search" @@ -1658,32 +1491,11 @@ msgstr "Mostrar mapas con narrativa" msgid "Embedded Story Map" msgstr "Mapa Narrativo incrustado" -#: js/build/mapBlocks.js:3 -msgid "Map Editor Preview" -msgstr "Vista previa del editor de mapas" - -#: js/build/mapBlocks.js:3 -msgid "Interactive map preview for the Map post type editor." -msgstr "Vista previa interactiva del mapa para el editor del tipo de contenido Mapa." - -#: js/build/mapBlocks.js:3 -msgid "Layer Editor Preview" -msgstr "Vista previa del editor de capas" - -#: js/build/mapBlocks.js:3 -msgid "Interactive layer preview for the Map Layer post type editor." -msgstr "Vista previa interactiva de la capa para el editor del tipo de contenido Capa de mapa." - #: js/build/mapBlocks.js:3 #: js/build/mapsSidebar.js:1 msgid "Loading layers data..." msgstr "Cargando datos de las capas..." -#: js/build/mapBlocks.js:3 -#: js/build/mapsSidebar.js:1 -msgid "Default" -msgstr "Defecto" - #: js/build/mapBlocks.js:3 #: js/build/mapsSidebar.js:1 msgid "Base layer should be fixed" @@ -1864,21 +1676,6 @@ msgstr "Desactivar mapa incorporado" msgid "Map settings" msgstr "Configuración del mapa" -#: js/build/mapBlocks.js:3 -#: js/build/mapsSidebar.js:1 -msgid "Fixed" -msgstr "Fijo" - -#: js/build/mapBlocks.js:3 -#: js/build/mapsSidebar.js:1 -msgid "Swappable" -msgstr "Intercambiable" - -#: js/build/mapBlocks.js:3 -#: js/build/mapsSidebar.js:1 -msgid "Switchable" -msgstr "Conmutable" - #: js/build/mapBlocks.js:3 #: js/build/mapsSidebar.js:1 msgid "Key" @@ -1928,14 +1725,6 @@ msgstr "Consultas de metadatos" msgid "Embed URL" msgstr "URL de incrustación" -#: js/build/postsSidebar.js:1 -msgid "Unable to retrieve the address for the selected point." -msgstr "No fue posible obtener la dirección del punto seleccionado." - -#: js/build/postsSidebar.js:1 -msgid "Type an address, then click Search or press Enter. You can also drag the marker across the map." -msgstr "Escribe una dirección y luego haz clic en Buscar o presiona Enter. También puedes arrastrar el marcador por el mapa." - #: js/build/postsSidebar.js:1 msgid "Move marker to nearest address" msgstr "Mover el marcador a la dirección más cercana" @@ -1945,10 +1734,12 @@ msgid "Relevance" msgstr "Relevancia" #: js/build/postsSidebar.js:1 +#: js/build/postsSidebar.js:3 msgid "Primary" msgstr "Primario" #: js/build/postsSidebar.js:1 +#: js/build/postsSidebar.js:3 msgid "Secondary" msgstr "Secundario" @@ -1960,178 +1751,3139 @@ msgstr "Actualizar punto seleccionado" msgid "Save new point" msgstr "Guardar nuevo punto" -#: js/build/postsSidebar.js:1 +#: js/build/postsSidebar.js:2 msgid "Add new point" msgstr "Añadir nuevo punto" -#: js/build/postsSidebar.js:1 +#: js/build/postsSidebar.js:2 msgid "Current points" msgstr "Puntos actuales" -#: js/build/postsSidebar.js:1 +#: js/build/postsSidebar.js:2 msgid "No points" msgstr "Sin puntos" -#: js/build/postsSidebar.js:1 +#: includes/settings/class-settings.php:604 +#: js/build/postsSidebar.js:2 msgid "Delete" msgstr "Borrar" -#: js/build/postsSidebar.js:1 +#: includes/settings/class-settings.php:641 +#: js/build/postsSidebar.js:2 msgid "Edit" msgstr "Editar" -#: js/build/postsSidebar.js:1 +#: js/build/postsSidebar.js:2 +#: js/build/postsSidebar.js:4 msgid "Geolocate this post" msgstr "Geolocalizar este post" -#: js/build/postsSidebar.js:1 +#: js/build/postsSidebar.js:4 msgid "Geolocation" msgstr "Geolocalización" -#: includes/class-jeo.php:382 +#: includes/class-jeo.php:672 msgid "Ordered list of map-layer IDs to return." msgstr "Lista ordenada de IDs de capas de mapa que se devolverán." -#: includes/class-jeo.php:387 +#: includes/class-jeo.php:677 msgid "Request context." msgstr "Contexto de la solicitud." -#: includes/settings/settings-page.php:203 -msgid "Ex. https://example.com/wp-content/uploads/fonts/jeo-font.css" -msgstr "Ej. https://ejemplo.com/wp-content/uploads/fonts/jeo-font.css" - -#: includes/settings/settings-page.php:218 -msgid "Ex. https://example.com/wp-content/uploads/fonts/jeo-story-font.css" -msgstr "Ej. https://ejemplo.com/wp-content/uploads/fonts/jeo-story-font.css" - #. translators: %s is the authors list #: js/build/jeoStorymap.js:3 #, js-format msgid "By %s" msgstr "Por %s" -#: includes/class-jeo.php:1332 -msgid "This JEO embed is not available." -msgstr "Este embed de JEO no está disponible." +#: includes/admin/dashboard-page.php:306 +msgid "Toggle panel" +msgstr "Alternar panel" + +#: includes/admin/dashboard-page.php:308 +msgid "JEO Platform" +msgstr "Plataforma JEO" + +#: includes/admin/dashboard-page.php:309 +msgid "Loading geodata..." +msgstr "Cargando geodatos..." + +#: includes/admin/dashboard-page.php:310 +msgid "Minimize Dashboard" +msgstr "Minimizar panel" + +#: includes/admin/dashboard-page.php:317 +msgid "Search terms" +msgstr "Términos de búsqueda" + +#: includes/admin/dashboard-page.php:318 +msgid "Title or content..." +msgstr "Título o contenido..." + +#: includes/admin/dashboard-page.php:322 +msgid "Post Type" +msgstr "Tipo de contenido" + +#: includes/admin/dashboard-page.php:324 +msgid "All Types" +msgstr "Todos los tipos" + +#: includes/admin/dashboard-page.php:329 +msgid "Taxonomy" +msgstr "Taxonomía" + +#: includes/admin/dashboard-page.php:334 +msgid "Term" +msgstr "Término" + +#: includes/admin/dashboard-page.php:339 +msgid "Apply Filters" +msgstr "Aplicar filtros" + +#: includes/admin/dashboard-page.php:344 +msgid "Timeline Range" +msgstr "Rango de línea de tiempo" + +#: includes/admin/dashboard-page.php:361 +msgid "Gathering geodata and initializing JEO Engine..." +msgstr "Recopilando geodatos e inicializando JEO Engine..." + +#: includes/admin/dashboard-page.php:524 +#: includes/settings/class-settings.php:654 +msgid "Select Taxonomy..." +msgstr "Seleccionar taxonomía..." + +#: includes/admin/dashboard-page.php:542 +#: includes/settings/class-settings.php:655 +msgid "Select Term..." +msgstr "Seleccionar término..." + +#: includes/admin/dashboard-page.php:605 +#: includes/settings/class-settings.php:656 +msgid " locations found." +msgstr " ubicaciones encontradas." + +#: includes/admin/dashboard-page.php:617 +#: includes/admin/dashboard-page.php:704 +msgid "Untitled" +msgstr "Sin título" + +#: includes/admin/dashboard-page.php:707 +#: includes/settings/class-settings.php:652 +msgid "View Post" +msgstr "Ver entrada" + +#: includes/admin/dashboard-page.php:707 +#: includes/settings/class-settings.php:653 +msgid "Edit Post" +msgstr "Editar entrada" + +#: includes/admin/uninstall-handler.php:30 +#: includes/admin/uninstall-handler.php:48 +#: includes/admin/uninstall-handler.php:49 +msgid "Uninstall JEO" +msgstr "Desinstalar JEO" + +#: includes/admin/uninstall-handler.php:31 +msgid "Uninstall" +msgstr "Desinstalar" + +#: includes/admin/uninstall-handler.php:63 +#: includes/admin/uninstall-page.php:23 +msgid "You do not have permission to uninstall plugins." +msgstr "No tiene permisos para desinstalar plugins." + +#: includes/admin/uninstall-page.php:50 +msgid "Warning: This action cannot be undone." +msgstr "Advertencia: Esta acción no se puede deshacer." + +#: includes/admin/uninstall-page.php:53 +msgid "Uninstalling JEO will permanently delete the following data from your site:" +msgstr "Desinstalar JEO eliminará permanentemente los siguientes datos de su sitio:" + +#: includes/admin/uninstall-page.php:56 +msgid "All plugin settings stored in the database (map defaults, API keys, appearance settings, etc.)." +msgstr "Todas las configuraciones del plugin almacenadas en la base de datos (valores predeterminados de mapa, claves de API, configuraciones de apariencia, etc.)." + +#: includes/admin/uninstall-page.php:57 +msgid "All AI usage logs (private posts of type \"jeo-ai-log\")." +msgstr "Todos los registros de uso de IA (entradas privadas del tipo \"jeo-ai-log\")." + +#: includes/admin/uninstall-page.php:58 +msgid "All geolocation metadata attached to posts (_related_point, _geocode_* index fields)." +msgstr "Todos los metadatos de geolocalización adjuntos a las entradas (_related_point, campos de índice _geocode_*)." + +#: includes/admin/uninstall-page.php:59 +msgid "Cached geocoding results (Nominatim transients)." +msgstr "Resultados de geocodificación en caché (transitorios de Nominatim)." + +#: includes/admin/uninstall-page.php:60 +msgid "Bulk AI cron logs and embedding token counters." +msgstr "Registros de Cron de IA masiva y contadores de Tokens de Embedding." + +#: includes/admin/uninstall-page.php:61 +msgid "RAG vector store files from the uploads directory (wp-content/uploads/jeo-ai-store/)." +msgstr "Archivos del Almacén de Vectores RAG del directorio de uploads (wp-content/uploads/jeo-ai-store/)." + +#: includes/admin/uninstall-page.php:62 +msgid "Scheduled cron jobs created by the plugin." +msgstr "Tareas Cron programadas creadas por el plugin." + +#: includes/admin/uninstall-page.php:65 +msgid "Your regular posts, pages, maps, layers, and story maps will remain, but any geolocation data associated with them will be lost." +msgstr "Sus entradas, páginas, mapas, capas y mapas narrativos normales permanecerán, pero cualquier dato de geolocalización asociado con ellos se perderá." + +#: includes/admin/uninstall-page.php:72 +msgid "I understand that all data listed above will be permanently deleted." +msgstr "Entiendo que todos los datos listados arriba serán eliminados permanentemente." + +#: includes/admin/uninstall-page.php:75 +msgid "Confirm and Uninstall JEO" +msgstr "Confirmar y desinstalar JEO" + +#: includes/admin/uninstall-page.php:75 +msgid "Are you absolutely sure? This cannot be undone." +msgstr "¿Está absolutamente seguro? Esto no se puede deshacer." + +#: includes/ai/class-ai-logger.php:39 +msgctxt "Post Type General Name" +msgid "AI Usage Logs" +msgstr "Registros de uso de IA" + +#: includes/ai/class-ai-logger.php:40 +msgctxt "Post Type Singular Name" +msgid "AI Usage Log" +msgstr "Registro de uso de IA" + +#: includes/ai/class-ai-logger.php:41 +msgid "AI Logs (Costs)" +msgstr "Registros de IA (Costos)" + +#: includes/ai/class-ai-logger.php:42 +msgid "All AI Logs" +msgstr "Todos los registros de IA" + +#: includes/ai/class-ai-logger.php:43 +msgid "View Log" +msgstr "Ver registro" + +#: includes/ai/class-ai-logger.php:47 +msgid "AI Usage Log" +msgstr "Registro de uso de IA" + +#: includes/ai/class-ai-logger.php:48 +msgid "Internal logs for AI token usage and costs." +msgstr "Registros internos para uso de Tokens y costos de IA." + +#: includes/ai/class-ai-settings.php:46 +#: includes/ai/settings/tab-provider.php:12 +msgid "AI Provider" +msgstr "Proveedor de IA" + +#: includes/ai/class-ai-settings.php:47 +msgid "Knowledge Base" +msgstr "Base de conocimiento" + +#: includes/ai/class-ai-settings.php:48 +msgid "Bulk Geolocation" +msgstr "Geolocalización masiva" + +#: includes/ai/class-ai-settings.php:49 +msgid "Context Assistant" +msgstr "Asistente de contexto" + +#: includes/ai/class-bulk-processor.php:170 +msgid "Bulk clearing started (Background)." +msgstr "Limpieza masiva iniciada (en segundo plano)." + +#: includes/ai/class-bulk-processor.php:174 +msgid "Bulk clearing started in background. Posts will be reset in batches." +msgstr "Limpieza masiva iniciada en segundo plano. Las entradas se restablecerán en lotes." + +#: includes/ai/class-bulk-processor.php:194 +#: includes/ai/class-rag-worker.php:101 +#: includes/settings/class-settings.php:623 +msgid "Error" +msgstr "Error" + +#: includes/ai/class-bulk-processor.php:194 +#: includes/ai/class-rag-worker.php:101 +msgid "Success" +msgstr "Éxito" + +#: includes/ai/class-bulk-processor.php:223 +#: includes/ai/class-rag-worker.php:68 +#: includes/ai/settings/tab-bulk.php:49 +#: includes/ai/settings/tab-knowledge.php:155 +msgid "Every Minute" +msgstr "Cada minuto" + +#: includes/ai/class-bulk-processor.php:229 +#: includes/ai/class-rag-worker.php:74 +#: includes/ai/settings/tab-bulk.php:50 +#: includes/ai/settings/tab-knowledge.php:156 +msgid "Every 5 Minutes" +msgstr "Cada 5 minutos" + +#: includes/ai/class-bulk-processor.php:235 +#: includes/ai/class-rag-worker.php:80 +#: includes/ai/settings/tab-bulk.php:51 +#: includes/ai/settings/tab-knowledge.php:157 +msgid "Every 15 Minutes" +msgstr "Cada 15 minutos" + +#: includes/ai/class-bulk-processor.php:344 +msgid "No post types selected for bulk processing." +msgstr "No se seleccionaron tipos de contenido para procesamiento masivo." + +#: includes/ai/class-bulk-processor.php:364 +msgid "No more posts to process. Deactivating worker." +msgstr "No hay más entradas para procesar. Desactivando el worker." + +#: includes/ai/class-bulk-processor.php:381 +msgid "CRITICAL: No active AI adapter found for bulk processing." +msgstr "CRÍTICO: No se encontró ningún adaptador de IA activo para procesamiento masivo." + +#: includes/ai/class-bulk-processor.php:434 +msgid "No more posts to clear." +msgstr "No hay más entradas para limpiar." + +#: includes/ai/class-bulk-processor.php:602 +msgid "No posts selected." +msgstr "No se seleccionaron entradas." + +#: includes/ai/class-bulk-processor.php:664 +msgid "Review Bulk AI Approval" +msgstr "Revisar aprobación masiva de IA" + +#: includes/ai/class-bulk-processor.php:667 +#: includes/ai/class-bulk-processor.php:703 +msgid "Analyzing selected posts..." +msgstr "Analizando entradas seleccionadas..." + +#: includes/ai/class-bulk-processor.php:672 +msgid "Confirm & Apply" +msgstr "Confirmar y aplicar" + +#: includes/ai/class-bulk-processor.php:698 +msgid "Please select at least one post." +msgstr "Seleccione al menos una entrada." + +#: includes/ai/class-bulk-processor.php:772 +msgid "JEO AI Status" +msgstr "Estado de IA de JEO" + +#: includes/ai/class-bulk-processor.php:797 +msgid "No Locations Found" +msgstr "No se encontraron ubicaciones" + +#: includes/ai/class-bulk-processor.php:799 +msgid "AI Error" +msgstr "Error de IA" + +#: includes/ai/class-bulk-processor.php:841 +msgid "Approve AI" +msgstr "Aprobar IA" + +#: includes/ai/class-bulk-processor.php:852 +msgid "Approve JEO AI Geolocations" +msgstr "Aprobar geolocalizaciones de IA de JEO" + +#: includes/ai/class-context-handler.php:231 +#: includes/ai/class-context-handler.php:350 +#: includes/ai/class-context-handler.php:819 +#: includes/ai/class-context-handler.php:895 +#: includes/ai/class-rag-worker.php:223 +#: includes/minimap/class-minimap.php:162 +#: includes/minimap/class-minimap.php:273 +#: includes/minimap/class-minimap.php:374 +msgid "Post not found." +msgstr "Entrada no encontrada." + +#: includes/ai/class-context-handler.php:242 +#: includes/ai/class-context-handler.php:371 +#: includes/ai/class-minilayer-classifier.php:35 +#: includes/ai/class-minilayer-handler.php:91 +#: includes/ai/class-minilayer-service.php:41 +#: includes/minimap/class-minimap.php:172 +#: includes/minimap/class-minimap.php:283 +#: includes/minimap/class-minimap.php:394 +msgid "No AI provider configured. Set one in JEO AI Settings." +msgstr "Ningún proveedor de IA configurado. Configure uno en los ajustes de IA de JEO." + +#: includes/ai/class-context-handler.php:254 +msgid "The article seems to have very little content. Please write a bit more about the topic, or tell me what you would like suggestions about (e.g. territory, entities, angles)." +msgstr "El artículo parece tener muy poco contenido. Escriba un poco más sobre el tema, o indique sobre qué le gustaría recibir sugerencias (ej.: territorio, entidades, ángulos)." + +#: includes/ai/class-context-handler.php:255 +#: includes/ai/class-context-handler.php:293 +msgid "Generate editorial suggestions for this post based on its content." +msgstr "Generar sugerencias editoriales para esta entrada basadas en su contenido." + +#: includes/ai/class-context-handler.php:263 +msgid "Waiting for more content or directions." +msgstr "Esperando más contenido o instrucciones." + +#: includes/ai/class-context-handler.php:299 +msgid "Suggestions generated." +msgstr "Sugerencias generadas." + +#: includes/ai/class-context-handler.php:330 +#: includes/minimap/class-minimap.php:354 +msgid "conversation_id is required." +msgstr "conversation_id es obligatorio." + +#: includes/ai/class-context-handler.php:340 +#: includes/minimap/class-minimap.php:262 +#: includes/minimap/class-minimap.php:364 +msgid "A post ID is required." +msgstr "Un ID de entrada es obligatorio." + +#: includes/ai/class-context-handler.php:360 +#: includes/minimap/class-minimap.php:384 +msgid "A message is required." +msgstr "Un mensaje es obligatorio." + +#: includes/ai/class-context-handler.php:410 +msgid "Suggestions updated." +msgstr "Sugerencias actualizadas." + +#: includes/ai/class-context-handler.php:502 +msgid "The AI did not respond after multiple attempts. Please try again or rephrase your request." +msgstr "La IA no respondió después de varios intentos. Intente nuevamente o reformule su solicitud." + +#: includes/ai/class-context-handler.php:916 +msgid "Conversation cleared." +msgstr "Conversación borrada." + +#: includes/ai/class-minilayer-handler.php:72 +#: includes/cli/class-ai-cli.php:52 +#: includes/cli/class-ai-cli.php:157 +#: includes/minimap/class-minimap.php:252 +msgid "A prompt is required." +msgstr "Un prompt es obligatorio." + +#: includes/ai/class-minilayer-handler.php:90 +#: includes/ai/class-minilayer-service.php:33 +msgid "Mapbox API key is not configured. Set it in JEO Settings." +msgstr "La clave de API de Mapbox no está configurada. Configúrela en los ajustes de JEO." + +#: includes/ai/class-minilayer-handler.php:92 +msgid "Could not generate the map style. Please try again." +msgstr "No se pudo generar el estilo del mapa. Intente nuevamente." + +#: includes/ai/class-minilayer-handler.php:93 +msgid "The AI returned an unexpected response. Please try again." +msgstr "La IA devolvió una respuesta inesperada. Intente nuevamente." + +#: includes/ai/class-minilayer-handler.php:94 +msgid "The AI did not create a valid style. Please try again." +msgstr "La IA no creó un estilo válido. Intente nuevamente." + +#: includes/ai/class-neuron-factory.php:216 +msgid "No Embedding Model configured. Please select one in the JEO AI settings." +msgstr "Ningún modelo de Embedding configurado. Seleccione uno en los ajustes de IA de JEO." + +#. translators: %s: directory path +#: includes/ai/class-rag-agent.php:64 +#, php-format +msgid "The RAG Vector Store directory (%s) could not be created. Please check your uploads folder permissions." +msgstr "No se pudo crear el directorio del Almacén de Vectores RAG (%s). Verifique los permisos de su carpeta de uploads." -#: includes/class-jeo.php:1333 -msgid "Embed not available" -msgstr "Embed no disponible" +#. translators: %s: directory path +#: includes/ai/class-rag-agent.php:70 +#, php-format +msgid "The RAG Vector Store directory (%s) is not writable. Local Vectorization is disabled." +msgstr "El directorio del Almacén de Vectores RAG (%s) no tiene permiso de escritura. La Vectorización local está desactivada." -#: includes/maps/class-map-style-composer.php:511 -#: includes/maps/class-map-style-composer.php:564 -msgid "Mapbox composed styles are disabled." -msgstr "Los estilos compuestos de Mapbox están desactivados." +#: includes/ai/class-rag-agent.php:76 +msgid "No AI Provider is configured. Please set up a provider (Gemini, OpenAI, etc.) before using RAG." +msgstr "Ningún proveedor de IA configurado. Configure un proveedor (Gemini, OpenAI, etc.) antes de usar RAG." -#: includes/maps/class-map-style-composer.php:536 -#: includes/maps/class-map-style-composer.php:588 -msgid "Could not create the Mapbox composed style cache directory." -msgstr "No fue posible crear el directorio de caché de estilos compuestos de Mapbox." +#: includes/ai/class-rag-agent.php:89 +msgid "No Embedding Model selected. Please choose a model in the Knowledge Base tab." +msgstr "Ningún modelo de Embedding seleccionado. Elija un modelo en la pestaña Base de conocimiento." -#: includes/maps/class-map-style-composer.php:684 -msgid "Map not found." -msgstr "Mapa no encontrado." +#. translators: %s: provider name +#: includes/ai/class-rag-agent.php:108 +#, php-format +msgid "The selected embedding provider (%s) does not support native Embeddings in JEO yet. Please use OpenAI, Gemini or Ollama for Vector Store features." +msgstr "El proveedor de Embedding seleccionado (%s) aún no soporta Embeddings nativos en JEO. Use OpenAI, Gemini u Ollama para las funciones del Almacén de Vectores." -#: includes/maps/class-map-style-composer.php:694 -#: includes/maps/class-map-style-composer.php:757 -msgid "A Mapbox access token is required to compose Mapbox styles." -msgstr "Se requiere un token de acceso de Mapbox para componer estilos de Mapbox." +#. translators: %s: provider name +#: includes/ai/class-rag-agent.php:119 +#, php-format +msgid "The API Key for the Embedding provider (%s) is missing. RAG requires a valid connection to generate embeddings." +msgstr "Falta la clave de API del proveedor de Embedding (%s). RAG requiere una conexión válida para generar Embeddings." -#: includes/maps/class-map-style-composer.php:734 -msgid "Public payload composition is only available for one-time maps." -msgstr "La composición con datos públicos solo está disponible para mapas de un solo uso." +#: includes/ai/class-rag-backup.php:41 +msgid "The PHP ZipArchive extension is required to create backups." +msgstr "La extensión ZipArchive de PHP es necesaria para crear copias de seguridad." -#: includes/maps/class-map-style-composer.php:740 -msgid "The map preview payload is too large." -msgstr "Los datos de la vista previa del mapa son demasiado grandes." +#: includes/ai/class-rag-backup.php:92 +msgid "No vector store found to backup." +msgstr "No se encontró ningún almacén de vectores para respaldar." -#: includes/maps/class-map-style-composer.php:752 -msgid "The map preview has too many layers to compose." -msgstr "La vista previa del mapa tiene demasiadas capas para componer." +#: includes/ai/class-rag-backup.php:99 +msgid "Failed to create the ZIP archive." +msgstr "Error al crear el archivo ZIP." -#: includes/maps/class-map-style-composer.php:763 -msgid "The map preview does not include any renderable layers." -msgstr "La vista previa del mapa no incluye ninguna capa renderizable." +#: includes/ai/class-rag-worker.php:209 +msgid "Either post_id or query is required." +msgstr "Se requiere post_id o query." -#: includes/maps/class-map-style-composer.php:785 -msgid "Map preview" -msgstr "Vista previa del mapa" +#. translators: %s: pipeline name +#: includes/ai/class-rag-worker.php:347 +#, php-format +msgid "Unknown pipeline: %s" +msgstr "Pipeline desconocido: %s" -#: includes/maps/class-map-style-composer.php:1026 -msgid "Invalid composed style hash." -msgstr "Hash de estilo compuesto no válido." +#. translators: 1: locked model name, 2: current model name +#: includes/ai/class-rag-worker.php:377 +#, php-format +msgid "Vector Store mismatch for %1$s! Expected %2$s, found %3$s." +msgstr "Incompatibilidad de Almacén de Vectores para %1$s! Se esperaba %2$s, se encontró %3$s." -#: includes/maps/class-map-style-composer.php:1114 -msgid "One or more Mapbox styles could not be fetched." -msgstr "No fue posible obtener uno o más estilos de Mapbox." +#. translators: %s: pipeline name +#: includes/ai/class-rag-worker.php:403 +#, php-format +msgid "No more %s to vectorize." +msgstr "No hay más %s para vectorizar." -#: includes/maps/class-map-style-composer.php:1515 -msgid "The PHP GD extension is required to merge Mapbox sprites." -msgstr "Se requiere la extensión PHP GD para fusionar sprites de Mapbox." +#. translators: %d: number of items vectorized +#: includes/ai/class-rag-worker.php:428 +#, php-format +msgid "Successfully vectorized %1$d %2$s." +msgstr "%1$d %2$s vectorizados correctamente." -#: includes/maps/class-map-style-composer.php:1634 -msgid "Could not decode a Mapbox sprite image." -msgstr "No fue posible decodificar una imagen de sprite de Mapbox." +#. translators: %s: pipeline name +#: includes/ai/class-rag-worker.php:437 +#, php-format +msgid "Batch skipped for %s (no content found)." +msgstr "Lote omitido para %s (no se encontró contenido)." + +#: includes/ai/settings/main-page.php:10 +msgid "JEO AI Configuration" +msgstr "Configuración de IA de JEO" + +#: includes/ai/settings/main-page.php:99 +msgid "Save AI Settings" +msgstr "Guardar ajustes de IA" + +#: includes/ai/settings/main-page.php:107 +#: includes/ai/settings/tab-general.php:22 +msgid "JEO AI API Debugger" +msgstr "Depurador de API de IA de JEO" + +#: includes/ai/settings/main-page.php:109 +msgid "Clear logs" +msgstr "Borrar registros" + +#: includes/ai/settings/main-page.php:115 +msgid "[System] Console initialized. Awaiting API activity..." +msgstr "[Sistema] Consola inicializada. Esperando actividad de la API..." + +#: includes/ai/settings/tab-bulk.php:12 +msgid "Background Processing" +msgstr "Procesamiento en segundo plano" + +#: includes/ai/settings/tab-bulk.php:15 +msgid "Enable background AI geolocalization" +msgstr "Activar geolocalización por IA en segundo plano" + +#: includes/ai/settings/tab-bulk.php:16 +msgid "When active, JEO will scan legacy posts and suggest locations based on their content." +msgstr "Cuando esté activo, JEO examinará las entradas existentes y sugerirá ubicaciones basadas en su contenido." + +#: includes/ai/settings/tab-bulk.php:20 +#: includes/ai/settings/tab-knowledge.php:149 +msgid "Batch Size" +msgstr "Tamaño del lote" + +#: includes/ai/settings/tab-bulk.php:23 +msgid "Number of posts to process per batch." +msgstr "Número de entradas a procesar por lote." + +#: includes/ai/settings/tab-bulk.php:27 +msgid "Target Post Types" +msgstr "Tipos de contenido objetivo" + +#: includes/ai/settings/tab-bulk.php:46 +#: includes/ai/settings/tab-knowledge.php:153 +msgid "Cron Interval" +msgstr "Intervalo del Cron" + +#: includes/ai/settings/tab-bulk.php:52 +#: includes/ai/settings/tab-knowledge.php:158 +msgid "Hourly" +msgstr "Cada hora" + +#: includes/ai/settings/tab-bulk.php:53 +msgid "Twice Daily" +msgstr "Dos veces al día" + +#: includes/ai/settings/tab-bulk.php:59 +msgid "Execution Logs" +msgstr "Registros de ejecución" + +#: includes/ai/settings/tab-bulk.php:62 +msgid "Recent Background Activity" +msgstr "Actividad reciente en segundo plano" + +#: includes/ai/settings/tab-bulk.php:68 +msgid "No recent activity recorded." +msgstr "No hay actividad reciente registrada." + +#: includes/ai/settings/tab-bulk.php:87 +msgid "Manual Control" +msgstr "Control manual" + +#: includes/ai/settings/tab-bulk.php:91 +#: includes/settings/class-settings.php:636 +msgid "Process 1 Batch Now" +msgstr "Procesar 1 lote ahora" + +#: includes/ai/settings/tab-bulk.php:94 +#: includes/settings/class-settings.php:637 +msgid "Clear 1 Batch" +msgstr "Limpiar 1 lote" + +#: includes/ai/settings/tab-bulk.php:97 +msgid "Reset All Posts" +msgstr "Restablecer todas las entradas" + +#: includes/ai/settings/tab-bulk.php:100 +msgid "Manual Batch: Processes legacy posts immediately. Clear Batch: Resets the \"processed\" flag for a small group. Reset All: Schedules a background task to clear all posts (Double confirmation required)." +msgstr "Lote manual: procesa entradas existentes inmediatamente. Limpiar lote: restablece el indicador \"procesado\" para un pequeño grupo. Restablecer todo: programa una tarea en segundo plano para limpiar todas las entradas (requiere doble confirmación)." + +#: includes/ai/settings/tab-bulk.php:105 +msgid "Processing Status" +msgstr "Estado de procesamiento" + +#. translators: %1$d: processed posts, %2$d: total posts, %3$d: percentage +#: includes/ai/settings/tab-bulk.php:139 +#, php-format +msgid "Geolocalization Progress: %1$d of %2$d posts marked as processed (%3$d%%)." +msgstr "Progreso de geolocalización: %1$d de %2$d entradas marcadas como procesadas (%3$d%%)." + +#: includes/ai/settings/tab-bulk.php:146 +msgid "Auto-Approval Threshold" +msgstr "Umbral de aprobación automática" + +#: includes/ai/settings/tab-bulk.php:149 +msgid "Minimum average confidence required to auto-approve locations when using bulk approval actions in the post list." +msgstr "Confianza media mínima requerida para aprobar automáticamente ubicaciones al usar acciones de aprobación masiva en la lista de entradas." + +#: includes/ai/settings/tab-bulk.php:154 +msgid "Internal Debug Log" +msgstr "Registro de Debug interno" + +#: includes/ai/settings/tab-bulk.php:157 +msgid "Enable detailed file-based logging (jeo-bulk-ai.log) for technical troubleshooting." +msgstr "Activar registro detallado en archivo (jeo-bulk-ai.log) para solución de problemas técnicos." + +#: includes/ai/settings/tab-bulk.php:161 +msgid "Open Log File" +msgstr "Abrir archivo de registro" + +#: includes/ai/settings/tab-bulk.php:162 +msgid "Delete Log File" +msgstr "Eliminar archivo de registro" + +#: includes/ai/settings/tab-context.php:18 +msgid "Context Assistant System Prompt" +msgstr "Prompt del sistema del Asistente de contexto" + +#: includes/ai/settings/tab-context.php:31 +msgid "Check this to use a custom system prompt. Uncheck to use the built-in default prompt." +msgstr "Marque para usar un prompt del sistema personalizado. Desmarque para usar el prompt predeterminado integrado." + +#: includes/ai/settings/tab-context.php:55 +msgid "Default System Prompt (reference)" +msgstr "Prompt del sistema predeterminado (referencia)" + +#: includes/ai/settings/tab-context.php:65 +msgid "This is the built-in default prompt. Enable the custom prompt checkbox above to override it." +msgstr "Este es el prompt predeterminado integrado. Active la casilla de prompt personalizado de arriba para reemplazarlo." + +#: includes/ai/settings/tab-embedded.php:10 +msgid "Embedded Brazilian Territories" +msgstr "Territorios brasileños incorporados" + +#: includes/ai/settings/tab-embedded.php:11 +msgid "The following geographic dictionaries are available locally to improve AI precision:" +msgstr "Los siguientes diccionarios geográficos están disponibles localmente para mejorar la precisión de la IA:" + +#: includes/ai/settings/tab-embedded.php:18 +#: includes/settings/class-settings.php:660 +msgid "Brazilian Biomes" +msgstr "Biomas brasileños" + +#: includes/ai/settings/tab-embedded.php:19 +#: includes/settings/class-settings.php:661 +msgid "Indigenous Territories" +msgstr "Territorios indígenas" + +#: includes/ai/settings/tab-embedded.php:20 +#: includes/settings/class-settings.php:662 +msgid "Quilombola Territories" +msgstr "Territorios quilombolas" + +#: includes/ai/settings/tab-embedded.php:21 +#: includes/settings/class-settings.php:663 +msgid "Extractive Reserves (Resex)" +msgstr "Reservas extractivas (Resex)" + +#: includes/ai/settings/tab-embedded.php:22 +#: includes/settings/class-settings.php:664 +msgid "Conservation Units" +msgstr "Unidades de conservación" + +#: includes/ai/settings/tab-embedded.php:23 +#: includes/settings/class-settings.php:665 +msgid "Riverside Communities" +msgstr "Comunidades ribereñas" + +#: includes/ai/settings/tab-embedded.php:24 +#: includes/settings/class-settings.php:666 +msgid "Agrarian Reform Settlements" +msgstr "Asentamientos de reforma agraria" + +#: includes/ai/settings/tab-embedded.php:25 +#: includes/settings/class-settings.php:667 +msgid "Indigenous Peoples (Ethnicities)" +msgstr "Pueblos indígenas (Etnias)" + +#: includes/ai/settings/tab-embedded.php:26 +#: includes/settings/class-settings.php:668 +msgid "Legal Amazon and Boundaries" +msgstr "Amazonía legal y límites" + +#: includes/ai/settings/tab-embedded.php:27 +#: includes/settings/class-settings.php:669 +msgid "Hydrographic Basins" +msgstr "Cuencas hidrográficas" + +#: includes/ai/settings/tab-embedded.php:46 +msgid "File:" +msgstr "Archivo:" + +#: includes/ai/settings/tab-embedded.php:47 +msgid "Entries found:" +msgstr "Entradas encontradas:" + +#: includes/ai/settings/tab-embedded.php:57 +#: includes/settings/class-settings.php:603 +msgid "Download" +msgstr "Descargar" + +#: includes/ai/settings/tab-embedded.php:65 +#: includes/ai/settings/tab-knowledge.php:289 +#: includes/settings/ai-logs-page.php:250 +msgid "Close" +msgstr "Cerrar" + +#: includes/ai/settings/tab-embedded.php:69 +msgid "Item List" +msgstr "Lista de elementos" + +#: includes/ai/settings/tab-embedded.php:73 +msgid "Name" +msgstr "Nombre" + +#: includes/ai/settings/tab-embedded.php:89 +msgid "JSON Raw" +msgstr "JSON sin procesar" + +#: includes/ai/settings/tab-general.php:14 +msgid "Debug Mode" +msgstr "Modo Debug" + +#: includes/ai/settings/tab-general.php:17 +msgid "Enable internal logging of all AI calls (Inputs/Outputs) for 24 hours. Useful for debugging prompt regressions." +msgstr "Activar registro interno de todas las llamadas de IA (entradas/salidas) durante 24 horas. Útil para depurar regresiones de prompt." + +#: includes/ai/settings/tab-general.php:25 +msgid "Show the floating API debugger console on AI settings pages. Displays real-time request/response logs." +msgstr "Mostrar la consola flotante del depurador de API en las páginas de configuración de IA. Muestra registros de solicitud/respuesta en tiempo real." + +#: includes/ai/settings/tab-general.php:30 +msgid "NeuronAI Structured Output" +msgstr "Salida estructurada de NeuronAI" + +#: includes/ai/settings/tab-general.php:33 +msgid "Use native schema enforcement instead of prompt-based JSON extraction. Falls back to text parsing if structured output fails." +msgstr "Usar imposición nativa de esquema en lugar de extracción de JSON basada en prompt. Recurre al análisis de texto si la salida estructurada falla." + +#: includes/ai/settings/tab-general.php:36 +msgid "Limitations:" +msgstr "Limitaciones:" + +#: includes/ai/settings/tab-general.php:38 +msgid "Token usage tracking is unavailable when structured output is active (shows 0 in logs)." +msgstr "El seguimiento de uso de Tokens no está disponible cuando la salida estructurada está activa (muestra 0 en los registros)." + +#: includes/ai/settings/tab-general.php:39 +msgid "If the provider does not support native schema enforcement, the system will automatically fall back to text parsing." +msgstr "Si el proveedor no soporta la imposición nativa de esquema, el sistema recurrirá automáticamente al análisis de texto." + +#: includes/ai/settings/tab-general.php:40 +msgid "Some internal tools (e.g. prompt generator test connection) automatically bypass structured output." +msgstr "Algunas herramientas internas (ej.: prueba de conexión del generador de prompt) omiten automáticamente la salida estructurada." + +#: includes/ai/settings/tab-general.php:45 +msgid "Recommendations by provider:" +msgstr "Recomendaciones por proveedor:" + +#: includes/ai/settings/tab-general.php:47 +msgid "Full support. Recommended to keep this option enabled." +msgstr "Soporte total. Recomendado mantener esta opción activada." + +#: includes/ai/settings/tab-general.php:48 +msgid "Supported via NeuronAI. Test with your model to confirm." +msgstr "Soportado vía NeuronAI. Pruebe con su modelo para confirmar." + +#: includes/ai/settings/tab-general.php:49 +msgid "Variable support depending on model and local configuration. Disable if you encounter errors." +msgstr "Soporte variable dependiendo del modelo y configuración local. Desactive si encuentra errores." + +#: includes/ai/settings/tab-general.php:56 +msgid "Embedded Data" +msgstr "Datos incorporados" + +#: includes/ai/settings/tab-general.php:59 +msgid "View locally embedded geographic dictionaries used to improve AI precision. This feature is experimental and not yet fully integrated." +msgstr "Ver diccionarios geográficos incorporados localmente usados para mejorar la precisión de la IA. Esta función es experimental y aún no está totalmente integrada." + +#: includes/ai/settings/tab-general.php:63 +msgid "View Embedded Data" +msgstr "Ver datos incorporados" -#: includes/maps/class-map-style-composer.php:2351 -msgid "Composed style artifact was not found." -msgstr "No se encontró el artefacto del estilo compuesto." +#: includes/ai/settings/tab-knowledge.php:17 +msgid "AI Provider Required" +msgstr "Proveedor de IA requerido" -#: js/build/discovery.js:6 -msgid "Mapbox style composition is unavailable." -msgstr "La composición de estilos de Mapbox no está disponible." +#: includes/ai/settings/tab-knowledge.php:19 +msgid "You need to configure an AI Provider before you can use the Knowledge Base. Set up your API key and choose a model in the Provider tab first." +msgstr "Necesita configurar un proveedor de IA antes de poder usar la Base de conocimiento. Configure su clave de API y elija un modelo en la pestaña Proveedor primero." -#: js/build/discovery.js:6 -#: js/build/jeoMap.js:1 -msgid "Mapbox style composition is unavailable for this map." -msgstr "La composición de estilos de Mapbox no está disponible para este mapa." +#: includes/ai/settings/tab-knowledge.php:22 +msgid "Configure AI Provider" +msgstr "Configurar proveedor de IA" -#: js/build/discovery.js:6 -msgid "Unable to load map information right now." -msgstr "No fue posible cargar la información del mapa en este momento." +#: includes/ai/settings/tab-knowledge.php:35 +msgid "RAG Knowledge Base (Vector Store)" +msgstr "Base de conocimiento RAG (Almacén de Vectores)" -#: js/build/discovery.js:6 -msgid "Map information" -msgstr "Información del mapa" +#: includes/ai/settings/tab-knowledge.php:36 +msgid "Vectorize your WordPress posts to allow the JEO AI to contextually answer questions and cross-reference territorial data." +msgstr "Vectorice sus entradas de WordPress para permitir que la IA de JEO responda preguntas contextualmente y haga referencia cruzada de datos territoriales." -#: js/build/jeoMap.js:1 -msgid "Mapbox style composition did not return renderable layers for this map." -msgstr "La composición de estilos de Mapbox no devolvió capas renderizables para este mapa." +#: includes/ai/settings/tab-knowledge.php:41 +msgid "Embedding Model" +msgstr "Modelo de Embedding" -#: js/build/jeoMap.js:1 -msgid "Unable to load composed Mapbox style for this map." -msgstr "No fue posible cargar el estilo compuesto de Mapbox para este mapa." +#: includes/ai/settings/tab-knowledge.php:57 +msgid "Model selection is locked." +msgstr "Selección de modelo bloqueada." -#: js/build/jeoMap.js:1 -msgid "Mapbox style composition is unavailable for this one-time map." -msgstr "La composición de estilos de Mapbox no está disponible para este mapa de un solo uso." +#. translators: %s: locked embedding model name +#: includes/ai/settings/tab-knowledge.php:62 +#, php-format +msgid "This Vector Store was initialized with %s. To use a different model, you must clear the current store first." +msgstr "Este Almacén de Vectores se inicializó con %s. Para usar un modelo diferente, primero debe limpiar el almacén actual." -#: js/build/jeoMap.js:1 -msgid "Mapbox style composition did not return renderable layers for this one-time map." -msgstr "La composición de estilos de Mapbox no devolvió capas renderizables para este mapa de un solo uso." +#: includes/ai/settings/tab-knowledge.php:70 +msgid "Select an embedding model..." +msgstr "Seleccione un modelo de Embedding..." -#: js/build/jeoMap.js:1 -msgid "Unable to load composed Mapbox style for this one-time map." -msgstr "No fue posible cargar el estilo compuesto de Mapbox para este mapa de un solo uso." +#: includes/ai/settings/tab-knowledge.php:98 +msgid "Select the specific embedding model. You can mix providers (e.g. use Gemini for chat and OpenAI for embeddings)." +msgstr "Seleccione el modelo de Embedding específico. Puede mezclar proveedores (ej.: usar Gemini para chat y OpenAI para Embeddings)." -#: js/build/jeoMap.js:1 -#: js/build/jeoStorymap.js:3 -msgid "Mapbox style layers could not be loaded because the composed style is unavailable." -msgstr "No fue posible cargar las capas de estilo de Mapbox porque el estilo compuesto no está disponible." +#: includes/ai/settings/tab-knowledge.php:110 +msgid "RAG Knowledge Base is not available" +msgstr "La Base de conocimiento RAG no está disponible" -#: js/build/jeoStorymap.js:3 -msgid "Mapbox style composition is unavailable for this storymap." -msgstr "La composición de estilos de Mapbox no está disponible para este mapa narrativo." +#: includes/ai/settings/tab-knowledge.php:115 +msgid "How to fix this:" +msgstr "Cómo solucionarlo:" -#: js/build/jeoStorymap.js:3 -msgid "Mapbox style composition did not return renderable layers for this storymap." -msgstr "La composición de estilos de Mapbox no devolvió capas renderizables para este mapa narrativo." +#: includes/ai/settings/tab-knowledge.php:118 +msgid "Please select an Embedding Model from the dropdown above and click \"Save AI Settings\"." +msgstr "Seleccione un modelo de Embedding en el menú desplegable de arriba y haga clic en \"Guardar ajustes de IA\"." -#: js/build/jeoStorymap.js:3 -msgid "Unable to load composed Mapbox style for this storymap." -msgstr "No fue posible cargar el estilo compuesto de Mapbox para este mapa narrativo." +#: includes/ai/settings/tab-knowledge.php:120 +msgid "Ensure you have an active AI Provider (Gemini, OpenAI, or Ollama) configured in the \"AI Provider\" tab." +msgstr "Asegúrese de tener un proveedor de IA activo (Gemini, OpenAI u Ollama) configurado en la pestaña \"Proveedor de IA\"." -#. translators: %d: number of maps whose composed style cache was refreshed. +#: includes/ai/settings/tab-knowledge.php:121 +msgid "Check if the \"wp-content/uploads\" directory exists and is writable." +msgstr "Verifique si el directorio \"wp-content/uploads\" existe y tiene permiso de escritura." + +#: includes/ai/settings/tab-knowledge.php:126 +msgid "Go to AI Provider Settings" +msgstr "Ir a ajustes del proveedor de IA" + +#: includes/ai/settings/tab-knowledge.php:134 +msgid "Background Indexing" +msgstr "Indexación en segundo plano" + +#: includes/ai/settings/tab-knowledge.php:135 +msgid "Automatically vectorize your posts in small batches using WP-Cron." +msgstr "Vectorice automáticamente sus entradas en pequeños lotes usando WP-Cron." + +#: includes/ai/settings/tab-knowledge.php:140 +msgid "Enable Auto-indexing" +msgstr "Activar indexación automática" + +#: includes/ai/settings/tab-knowledge.php:143 +msgid "Note: WP-Cron only runs when your site receives visitor traffic. If you are on a local development server with no traffic, indexing will pause until pages are loaded." +msgstr "Nota: WP-Cron solo se ejecuta cuando su sitio recibe tráfico de visitantes. Si está en un servidor de desarrollo local sin tráfico, la indexación se pausará hasta que se carguen las páginas." + +#: includes/ai/settings/tab-knowledge.php:162 +msgid "Search Results (topK)" +msgstr "Resultados de búsqueda (topK)" + +#: includes/ai/settings/tab-knowledge.php:164 +msgid "Max semantic matches returned per search." +msgstr "Máximo de coincidencias semánticas devueltas por búsqueda." + +#: includes/ai/settings/tab-knowledge.php:169 +msgid "Recent Background Logs" +msgstr "Registros recientes en segundo plano" + +#: includes/ai/settings/tab-knowledge.php:175 +msgid "No indexing jobs have run recently." +msgstr "No se han ejecutado trabajos de indexación recientemente." + +#: includes/ai/settings/tab-knowledge.php:192 +msgid "Manual Indexing" +msgstr "Indexación manual" + +#: includes/ai/settings/tab-knowledge.php:193 +msgid "Trigger a single batch vectorization immediately." +msgstr "Disparar una sola vectorización por lote inmediatamente." + +#: includes/ai/settings/tab-knowledge.php:196 +#: includes/ai/settings/tab-knowledge.php:319 +msgid "Vectorize 1 Batch Now" +msgstr "Vectorizar 1 lote ahora" + +#: includes/ai/settings/tab-knowledge.php:228 +msgid "Indexing Progress" +msgstr "Progreso de indexación" + +#. translators: %1$d: vectorized count, %2$d: total posts count +#: includes/ai/settings/tab-knowledge.php:237 +#, php-format +msgid "%1$d of %2$d posts indexed." +msgstr "%1$d de %2$d entradas indexadas." + +#: includes/ai/settings/tab-knowledge.php:246 +#: includes/ai/settings/tab-knowledge.php:403 +msgid "Status:" +msgstr "Estado:" + +#: includes/ai/settings/tab-knowledge.php:248 +msgid "Active (Local File Store)" +msgstr "Activo (Almacén de archivos local)" + +#: includes/ai/settings/tab-knowledge.php:252 +msgid "Alternative: Use WP-CLI for large databases:" +msgstr "Alternativa: Use WP-CLI para bases de datos grandes:" + +#: includes/ai/settings/tab-knowledge.php:257 +msgid "Backup Store" +msgstr "Almacén de copias de seguridad" + +#: includes/ai/settings/tab-knowledge.php:260 +msgid "Clear & Reset Model" +msgstr "Limpiar y restablecer modelo" + +#: includes/ai/settings/tab-knowledge.php:266 +msgid "Available Backups (Last 3)" +msgstr "Copias de seguridad disponibles (últimas 3)" + +#: includes/ai/settings/tab-knowledge.php:268 +#: includes/settings/class-settings.php:605 +msgid "No backups found." +msgstr "No se encontraron copias de seguridad." + +#: includes/ai/settings/tab-knowledge.php:275 +msgid "Test Embeddings & RAG Retrieval" +msgstr "Probar Embeddings y recuperación RAG" + +#: includes/ai/settings/tab-knowledge.php:276 +msgid "Test your configured Embedding Model on a random post to ensure it works correctly before indexing your entire database. This uses a temporary testing store." +msgstr "Pruebe su modelo de Embedding configurado en una entrada aleatoria para asegurar que funcione correctamente antes de indexar toda su base de datos. Usa un almacén de prueba temporal." + +#: includes/ai/settings/tab-knowledge.php:279 +#: includes/settings/class-settings.php:616 +msgid "Run Test on Random Post" +msgstr "Ejecutar prueba en entrada aleatoria" + +#: includes/ai/settings/tab-knowledge.php:280 +msgid "Test Vector Retrieval" +msgstr "Probar recuperación de vectores" + +#: includes/ai/settings/tab-knowledge.php:288 +msgid "Test Local Vector Retrieval" +msgstr "Probar recuperación local de vectores" + +#: includes/ai/settings/tab-knowledge.php:295 +msgid "Search Production Store" +msgstr "Buscar en almacén de producción" + +#: includes/ai/settings/tab-knowledge.php:296 +msgid "Search Test Store" +msgstr "Buscar en almacén de prueba" + +#: includes/ai/settings/tab-knowledge.php:298 +msgid "Search the Vector Store for semantic meaning (e.g., \"Indigenous lands in Amazon\")" +msgstr "Busque en el Almacén de Vectores por significado semántico (ej.: \"Tierras indígenas en la Amazonía\")" + +#: includes/ai/settings/tab-knowledge.php:302 +msgid "Enter a semantic search query to retrieve similar vectors from your local store." +msgstr "Ingrese una consulta de búsqueda semántica para recuperar vectores similares de su almacén local." + +#: includes/ai/settings/tab-knowledge.php:310 +msgid "Layer Store (Vectorized Map Layers)" +msgstr "Almacén de capas (capas de mapa vectorizadas)" + +#: includes/ai/settings/tab-knowledge.php:311 +msgid "Vectorize your map layers to enable AI-powered semantic search and layer suggestions based on post content." +msgstr "Vectorice sus capas de mapa para activar la búsqueda semántica con IA y sugerencias de capas basadas en el contenido de la entrada." + +#: includes/ai/settings/tab-knowledge.php:315 +msgid "Manual Layer Indexing" +msgstr "Indexación manual de capas" + +#: includes/ai/settings/tab-knowledge.php:316 +msgid "Trigger a single batch vectorization for map layers. Layers are also auto-indexed on the background cron schedule and when saved." +msgstr "Dispare una sola vectorización por lote para capas de mapa. Las capas también se indexan automáticamente en el Cron en segundo plano y al guardar." + +#: includes/ai/settings/tab-knowledge.php:344 +msgid "Layer Indexing Progress" +msgstr "Progreso de indexación de capas" + +#. translators: %1$d: vectorized count, %2$d: total layers count +#: includes/ai/settings/tab-knowledge.php:353 +#, php-format +msgid "%1$d of %2$d layers indexed." +msgstr "%1$d de %2$d capas indexadas." + +#: includes/ai/settings/tab-knowledge.php:359 +msgid "Recent Layer Logs" +msgstr "Registros recientes de capas" + +#: includes/ai/settings/tab-knowledge.php:365 +msgid "No layer indexing jobs have run recently." +msgstr "No se han ejecutado trabajos de indexación de capas recientemente." + +#: includes/ai/settings/tab-knowledge.php:382 +msgid "Suggest Layers" +msgstr "Sugerir capas" + +#: includes/ai/settings/tab-knowledge.php:383 +msgid "Test semantic layer matching. Enter a post ID or text to find layers that match the content." +msgstr "Pruebe la coincidencia semántica de capas. Ingrese un ID de entrada o texto para encontrar capas que coincidan con el contenido." + +#: includes/ai/settings/tab-knowledge.php:387 +msgid "Post ID" +msgstr "ID de entrada" + +#: includes/ai/settings/tab-knowledge.php:388 +msgid "e.g. 123" +msgstr "ej. 123" + +#: includes/ai/settings/tab-knowledge.php:391 +msgid "Or search text" +msgstr "O texto de búsqueda" + +#: includes/ai/settings/tab-knowledge.php:392 +msgid "e.g., deforestation in Amazon rainforest" +msgstr "ej.: deforestación en la selva amazónica" + +#: includes/ai/settings/tab-knowledge.php:394 +msgid "Find Layers" +msgstr "Encontrar capas" + +#: includes/ai/settings/tab-knowledge.php:405 +msgid "Active (Shared embedding model with Posts Store)" +msgstr "Activo (Modelo de Embedding compartido con el Almacén de entradas)" + +#: includes/ai/settings/tab-knowledge.php:409 +msgid "WP-CLI:" +msgstr "WP-CLI:" + +#: includes/ai/settings/tab-knowledge.php:414 +msgid "Clear Layer Store" +msgstr "Limpiar almacén de capas" + +#: includes/ai/settings/tab-provider.php:53 +msgid "Paste your API key here..." +msgstr "Pegue su clave de API aquí..." + +#: includes/ai/settings/tab-provider.php:58 +msgid "Set New Key" +msgstr "Establecer nueva clave" + +#: includes/ai/settings/tab-provider.php:63 +msgid "Status: Unknown" +msgstr "Estado: desconocido" + +#: includes/ai/settings/tab-provider.php:66 +msgid "Test Connection" +msgstr "Probar conexión" + +#: includes/ai/settings/tab-provider.php:73 +msgid "Model" +msgstr "Modelo" + +#: includes/ai/settings/tab-provider.php:79 +#: includes/settings/class-settings.php:581 +msgid "Change Model" +msgstr "Cambiar modelo" + +#: includes/ai/settings/tab-provider.php:87 +msgid "System Prompt Configuration" +msgstr "Configuración de Prompt del sistema" + +#: includes/ai/settings/tab-provider.php:91 +msgid "Check this to override the default behavior. Uncheck to temporarily disable and return to the optimized default prompt." +msgstr "Marque para reemplazar el comportamiento predeterminado. Desmarque para desactivar temporalmente y volver al prompt predeterminado optimizado." + +#: includes/ai/settings/tab-provider.php:95 +msgid "Include post categories and tags in the AI georeferencing request to improve location accuracy." +msgstr "Incluir categorías y etiquetas de la entrada en la solicitud de georreferenciación de IA para mejorar la precisión de la ubicación." + +#: includes/ai/settings/tab-provider.php:100 +#: includes/ai/settings/tab-provider.php:280 +#: includes/settings/class-settings.php:631 +msgid "Back" +msgstr "Atrás" + +#: includes/ai/settings/tab-provider.php:103 +#: includes/ai/settings/tab-provider.php:282 +#: includes/settings/class-settings.php:629 +#: js/build/contextSidebar.js:1 +msgid "Expand" +msgstr "Expandir" + +#: includes/ai/settings/tab-provider.php:104 +msgid "Validate Custom Prompt" +msgstr "Validar prompt personalizado" + +#: includes/ai/settings/tab-provider.php:105 +msgid "Clear Text" +msgstr "Limpiar texto" + +#: includes/ai/settings/tab-provider.php:116 +msgid "AI Prompt Engineer Assistant" +msgstr "Asistente de Ingeniería de Prompt de IA" + +#: includes/ai/settings/tab-provider.php:117 +msgid "Describe how you want the AI to behave (e.g., \"Only map cities in Brazil\" or \"Ignore street names\"). The active LLM will generate a highly optimized System Prompt for you, strictly adhering to JEO formatting rules." +msgstr "Describa cómo quiere que se comporte la IA (ej.: \"Mapear solo ciudades de Brasil\" o \"Ignorar nombres de calles\"). El LLM activo generará un Prompt del sistema altamente optimizado para usted, siguiendo estrictamente las reglas de formato de JEO." + +#: includes/ai/settings/tab-provider.php:279 +msgid "Ex: I want to map only locations inside Europe. Press Shift+Enter for new line, or Enter to generate." +msgstr "Ej.: Quiero mapear solo ubicaciones dentro de Europa. Presione Shift+Enter para nueva línea, o Enter para generar." + +#: includes/ai/settings/tab-provider.php:283 +msgid "Shift+Enter for new line" +msgstr "Shift+Enter para nueva línea" + +#: includes/ai/settings/tab-provider.php:288 +msgid "Georeferencing Calibration" +msgstr "Calibración de georreferenciación" + +#: includes/ai/settings/tab-provider.php:292 +#: includes/ai/settings/tab-provider.php:304 +#: includes/ai/settings/tab-provider.php:313 +#: includes/ai/settings/tab-provider.php:322 +#: includes/ai/settings/tab-provider.php:331 +#: includes/ai/settings/tab-provider.php:340 +#: includes/ai/settings/tab-provider.php:349 +#: includes/ai/settings/tab-provider.php:358 +msgid "Use" +msgstr "Usar" + +#: includes/ai/settings/tab-provider.php:294 +msgid "Location Granularity" +msgstr "Granularidad de ubicación" + +#: includes/ai/settings/tab-provider.php:296 +msgid "Broad (countries, regions, cities)" +msgstr "Amplia (países, regiones, ciudades)" + +#: includes/ai/settings/tab-provider.php:297 +msgid "Balanced (cities + neighborhoods)" +msgstr "Equilibrada (ciudades + barrios)" + +#: includes/ai/settings/tab-provider.php:298 +msgid "Fine (streets, landmarks, POIs)" +msgstr "Fina (calles, puntos de referencia, POIs)" + +#: includes/ai/settings/tab-provider.php:306 +msgid "Minimum Confidence" +msgstr "Confianza mínima" + +#: includes/ai/settings/tab-provider.php:315 +msgid "Primary Threshold" +msgstr "Umbral principal" + +#: includes/ai/settings/tab-provider.php:324 +msgid "Primary Points" +msgstr "Puntos principales" + +#: includes/ai/settings/tab-provider.php:333 +msgid "Secondary Threshold" +msgstr "Umbral secundario" + +#: includes/ai/settings/tab-provider.php:342 +msgid "Secondary Points" +msgstr "Puntos secundarios" + +#: includes/ai/settings/tab-provider.php:351 +msgid "Prioritize Title Mentions" +msgstr "Priorizar menciones en el título" + +#: includes/ai/settings/tab-provider.php:360 +msgid "Prompt Token Budget" +msgstr "Presupuesto de Tokens del Prompt" + +#: includes/ai/settings/tab-provider.php:366 +msgid "Higher budgets allow the AI to process longer articles but increase cost and latency." +msgstr "Presupuestos más altos permiten que la IA procese artículos más largos, pero aumentan el costo y la latencia." + +#: includes/ai/settings/tab-provider.php:373 +msgid "Balanced" +msgstr "Equilibrado" + +#: includes/ai/settings/tab-provider.php:377 +msgid "No token limit set. The LLM will use as many tokens as it deems necessary to produce the result." +msgstr "Sin límite de Tokens establecido. El LLM usará tantos Tokens como considere necesario para producir el resultado." + +#: includes/ai/settings/tab-provider.php:384 +msgid "Output Language" +msgstr "Idioma de salida" + +#: includes/ai/settings/tab-provider.php:386 +msgid "English (Optimized)" +msgstr "Inglés (optimizado)" + +#. translators: %s: site language +#: includes/ai/settings/tab-provider.php:387 +#, php-format +msgid "Site Language (%s)" +msgstr "Idioma del sitio (%s)" + +#: includes/ai/settings/tab-provider.php:390 +msgid "LLMs usually follow English instructions with higher precision and lower latency. We recommend English for complex rules, even if your posts are in other languages." +msgstr "Los LLMs suelen seguir instrucciones en inglés con mayor precisión y menor latencia. Recomendamos inglés para reglas complejas, incluso si sus entradas están en otros idiomas." + +#: includes/ai/settings/tab-provider.php:395 +#: includes/settings/class-settings.php:593 +msgid "Generate Prompt" +msgstr "Generar prompt" + +#: includes/class-ai-adapter.php:165 +msgid "Empty response from AI." +msgstr "Respuesta vacía de la IA." + +#: includes/class-ai-handler.php:48 +msgid "Warning: Deactivating JEO will permanently remove all API Keys and AI settings for security reasons. Do you want to proceed?" +msgstr "Advertencia: Desactivar JEO eliminará permanentemente todas las claves de API y configuraciones de IA por razones de seguridad. ¿Desea continuar?" + +#: includes/class-ai-handler.php:67 +#: includes/settings/ai-logs-page.php:16 +#: includes/settings/class-settings.php:657 +msgid "Unauthorized access." +msgstr "Acceso no autorizado." + +#: includes/class-ai-handler.php:74 +#: includes/settings/class-settings.php:658 +msgid "Invalid file." +msgstr "Archivo inválido." + +#: includes/class-ai-handler.php:79 +#: includes/settings/class-settings.php:659 +msgid "File not found." +msgstr "Archivo no encontrado." + +#: includes/class-ai-handler.php:106 +msgid "Google Gemini" +msgstr "Google Gemini" + +#: includes/class-ai-handler.php:107 +msgid "OpenAI" +msgstr "OpenAI" + +#: includes/class-ai-handler.php:108 +msgid "DeepSeek" +msgstr "DeepSeek" + +#: includes/class-ai-handler.php:109 +msgid "Anthropic Claude" +msgstr "Anthropic Claude" + +#: includes/class-ai-handler.php:110 +msgid "Ollama (Local/Custom)" +msgstr "Ollama (Local/Personalizado)" + +#: includes/class-ai-handler.php:111 +msgid "Mistral AI" +msgstr "Mistral AI" + +#: includes/class-ai-handler.php:112 +msgid "Zhipu AI (GLM)" +msgstr "Zhipu AI (GLM)" + +#: includes/class-ai-handler.php:113 +msgid "HuggingFace Inference" +msgstr "HuggingFace Inference" + +#: includes/class-ai-handler.php:114 +msgid "Grok (xAI)" +msgstr "Grok (xAI)" + +#: includes/class-ai-handler.php:115 +msgid "Cohere" +msgstr "Cohere" + +#. translators: %s: AI provider name +#: includes/class-ai-handler.php:169 +#, php-format +msgid "No API key configured for %s. Please add it in JEO → AI Settings." +msgstr "Ninguna clave de API configurada para %s. Añádala en JEO → Ajustes de IA." + +#: includes/class-ai-handler.php:422 +msgid "Invalid store type." +msgstr "Tipo de almacén inválido." + +#. translators: %s: Store type name. +#: includes/class-ai-handler.php:448 +#, php-format +msgid "%s store cleared successfully." +msgstr "Almacén %s limpiado correctamente." + +#: includes/class-ai-handler.php:491 +msgid "Layer store cleared successfully." +msgstr "Almacén de capas limpiado correctamente." + +#: includes/class-ai-handler.php:518 +msgid "Query is required." +msgstr "Se requiere una consulta." + +#: includes/class-ai-handler.php:539 +msgid "No matches found in the selected Vector Store. Try vectorizing some posts first." +msgstr "No se encontraron coincidencias en el Almacén de Vectores seleccionado. Intente vectorizar algunas entradas primero." + +#: includes/class-ai-handler.php:593 +msgid "No posts available to test." +msgstr "No hay entradas disponibles para probar." + +#: includes/class-ai-handler.php:601 +msgid "The selected random post has no usable text content." +msgstr "La entrada aleatoria seleccionada no tiene contenido de texto utilizable." + +#: includes/class-ai-handler.php:617 +msgid "The embedding provider returned an empty vector." +msgstr "El proveedor de Embedding devolvió un vector vacío." + +#: includes/class-ai-handler.php:632 +msgid "Embedding created and saved to Test Store!" +msgstr "¡Embedding creado y guardado en el Almacén de prueba!" + +#: includes/class-ai-handler.php:673 +msgid "API Key or URL is required to list models." +msgstr "Se requiere clave de API o URL para listar modelos." + +#: includes/class-ai-handler.php:706 +msgid "Model fetching not officially supported dynamically for Zhipu AI." +msgstr "La obtención de modelos no está soportada oficialmente de forma dinámica para Zhipu AI." + +#: includes/class-ai-handler.php:862 +msgid "Model fetching is not supported for this provider yet. Please enter the model ID manually." +msgstr "La obtención de modelos no está soportada para este proveedor aún. Ingrese el ID del modelo manualmente." + +#: includes/class-ai-handler.php:870 +msgid "No models found. Check your API key or permissions." +msgstr "No se encontraron modelos. Verifique su clave de API o permisos." + +#: includes/class-ai-handler.php:906 +msgid "No API Key provided or found in settings." +msgstr "No se proporcionó ni encontró ninguna clave de API en la configuración." + +#: includes/class-ai-handler.php:916 +msgid "Invalid provider." +msgstr "Proveedor inválido." + +#: includes/class-ai-handler.php:939 +msgid "API Key is valid and active!" +msgstr "¡La clave de API es válida y está activa!" + +#: includes/class-ai-handler.php:949 +msgid "The AI provider returned an unexpected response format." +msgstr "El proveedor de IA devolvió un formato de respuesta inesperado." + +#: includes/class-ai-handler.php:994 +msgid "Context is required." +msgstr "Se requiere contexto." + +#: includes/class-ai-handler.php:1026 +#: includes/class-ai-handler.php:1186 +#: includes/class-ai-handler.php:1274 +#: includes/class-ai-handler.php:1323 +msgid "No active AI adapter found." +msgstr "No se encontró ningún adaptador de IA activo." + +#: includes/class-ai-handler.php:1142 +msgid "AI failed to generate prompt." +msgstr "La IA falló al generar el prompt." + +#: includes/ai/class-context-handler.php:935 +#: includes/class-ai-handler.php:1160 +msgid "Prompt is required." +msgstr "Se requiere un prompt." + +#: includes/class-ai-handler.php:1208 +msgid "Prompt successfully validated! The AI understood your instructions and returned a valid JSON array." +msgstr "¡Prompt validado correctamente! La IA comprendió sus instrucciones y devolvió un array JSON válido." + +#: includes/class-ai-handler.php:1212 +msgid "Validation failed: The AI did not return a valid JSON array." +msgstr "Validación fallida: La IA no devolvió un array JSON válido." + +#: includes/class-ai-handler.php:1217 +msgid "Validation failed: The AI missed mandatory keys (name, lat, lon, quote) in its JSON objects." +msgstr "Validación fallida: La IA omitió claves obligatorias (name, lat, lon, quote) en sus objetos JSON." + +#: includes/class-ai-handler.php:1222 +msgid "Validation failed: The AI returned an invalid is_primary value (must be boolean)." +msgstr "Validación fallida: La IA devolvió un valor is_primary inválido (debe ser booleano)." + +#: includes/class-ai-handler.php:1228 +msgid "Prompt successfully validated! The AI returned an empty array, which means your filtering rules worked perfectly for the test text." +msgstr "¡Prompt validado correctamente! La IA devolvió un array vacío, lo que significa que sus reglas de filtrado funcionaron perfectamente para el texto de prueba." + +#: includes/class-ai-handler.php:1287 +msgid "An unexpected error occurred while processing the AI request." +msgstr "Ocurrió un error inesperado al procesar la solicitud de IA." + +#: includes/class-ai-handler.php:1356 +msgid "An unexpected error occurred while processing the AI chat request." +msgstr "Ocurrió un error inesperado al procesar la solicitud de chat de IA." + +#: includes/class-ai-handler.php:1485 +msgid "Backup created successfully!" +msgstr "¡Copia de seguridad creada correctamente!" + +#: includes/class-ai-handler.php:1506 +msgid "Missing filename" +msgstr "Falta el nombre del archivo" + +#: includes/class-ai-handler.php:1517 +msgid "File not found" +msgstr "Archivo no encontrado" + +#: includes/geocode/class-geocode-handler.php:132 +msgid "Service provided by Mapbox (requires API Key)." +msgstr "Servicio proporcionado por Mapbox (requiere clave de API)." + +#: includes/settings/class-settings.php:712 +msgid "Discovery" +msgstr "Discovery" + +#: includes/maps/class-maps.php:198 +msgid "Map paint properties" +msgstr "Propiedades de paint del mapa" + +#: includes/maps/class-maps.php:202 +msgid "Map layout properties" +msgstr "Propiedades de layout del mapa" + +#: includes/menu/class-menu.php:48 +#: includes/menu/class-menu.php:49 +msgid "Welcome" +msgstr "Bienvenido" + +#: includes/menu/class-menu.php:58 +#: includes/menu/class-menu.php:59 +msgid "Dashboard" +msgstr "Panel" + +#: includes/menu/class-menu.php:68 +#: includes/menu/class-menu.php:69 +msgid "AI (new)" +msgstr "IA (nuevo)" + +#: includes/minimap/class-minimap.php:201 +msgid "No matching layers found. Add layers manually or run the RAG indexer in JEO Settings." +msgstr "No se encontraron capas coincidentes. Añada capas manualmente o ejecute el indexador RAG en los ajustes de JEO." + +#: includes/minimap/class-minimap.php:486 +msgid "The AI returned an empty response. Please try again." +msgstr "La IA devolvió una respuesta vacía. Intente nuevamente." + +#: includes/minimap/class-minimap.php:1876 +msgid "Dark Base (Mapbox)" +msgstr "Base oscura (Mapbox)" + +#: includes/minimap/class-minimap.php:1883 +msgid "Terrain Base (Mapbox)" +msgstr "Base de terreno (Mapbox)" + +#: includes/minimap/class-minimap.php:1890 +msgid "Satellite Base (Mapbox)" +msgstr "Base de satélite (Mapbox)" + +#: includes/minimap/class-minimap.php:1901 +msgid "Dark Base" +msgstr "Base oscura" + +#: includes/minimap/class-minimap.php:1910 +msgid "Light Base" +msgstr "Base clara" + +#: includes/minimap/class-minimap.php:1919 +msgid "Satellite Base" +msgstr "Base de satélite" + +#: includes/privacy.php:20 +msgid "JEO collects and stores geolocation metadata for posts when authors explicitly geotag content. This may include latitude, longitude, and reverse-geocoded address components (city, region, country, postcode). This data is attached to the post and is available to anyone with access to the post." +msgstr "JEO recopila y almacena metadatos de geolocalización para las entradas cuando los autores georreferencian explícitamente el contenido. Esto puede incluir latitud, longitud y componentes de dirección geocodificados (ciudad, región, país, código postal). Estos datos se adjuntan a la entrada y están disponibles para cualquier persona con acceso a la entrada." + +#: includes/privacy.php:21 +msgid "When the \"Stories Near You\" feature is enabled, visitors may opt in to share their browser geolocation. This location is sent to the site server solely to retrieve nearby posts and is not stored." +msgstr "Cuando la función \"Historias cerca de ti\" está activada, los visitantes pueden optar por compartir su geolocalización del navegador. Esta ubicación se envía al servidor del sitio únicamente para recuperar entradas cercanas y no se almacena." + +#: includes/privacy.php:22 +msgid "If AI-powered georeferencing is enabled, post titles and content may be sent to external AI providers (e.g., OpenAI, Google Gemini). AI usage logs (provider name, token counts) are stored locally as private posts and are not shared externally." +msgstr "Si el georreferenciamiento por IA está activado, los títulos y contenido de las entradas pueden enviarse a proveedores de IA externos (ej.: OpenAI, Google Gemini). Los registros de uso de IA (nombre del proveedor, conteos de Tokens) se almacenan localmente como entradas privadas y no se comparten externamente." + +#: includes/privacy.php:23 +msgid "Geocoding requests may be sent to third-party services such as Nominatim (OpenStreetMap) or Mapbox, depending on the active geocoder." +msgstr "Las solicitudes de geocodificación pueden enviarse a servicios de terceros como Nominatim (OpenStreetMap) o Mapbox, dependiendo del geocodificador activo." + +#: includes/privacy.php:40 +#: includes/privacy.php:126 +msgid "JEO Geolocation Metadata" +msgstr "Metadatos de geolocalización de JEO" + +#: includes/privacy.php:106 +msgid "JEO Geolocation" +msgstr "Geolocalización de JEO" + +#: includes/settings/ai-logs-page.php:10 +msgid "AI Cost Dashboard & Debug Logs" +msgstr "Panel de costos de IA y registros de Debug" + +#: includes/settings/ai-logs-page.php:11 +msgid "Here you can view the most recent AI interactions powered by Neuron AI, along with their detailed token usage (Input/Output)." +msgstr "Aquí puede ver las interacciones más recientes de IA impulsadas por Neuron AI, junto con su uso detallado de Tokens (entrada/salida)." + +#: includes/settings/ai-logs-page.php:31 +msgid "All AI logs have been permanently deleted." +msgstr "Todos los registros de IA han sido eliminados permanentemente." + +#: includes/settings/ai-logs-page.php:75 +#: includes/settings/class-settings.php:646 +msgid "reqs" +msgstr "reqs" + +#: includes/settings/ai-logs-page.php:79 +#: includes/settings/ai-logs-page.php:104 +#: includes/settings/ai-logs-page.php:116 +#: includes/settings/class-settings.php:645 +msgid "Tokens" +msgstr "Tokens" + +#: includes/settings/ai-logs-page.php:82 +#: includes/settings/class-settings.php:647 +msgid "In (Prompt):" +msgstr "Entrada (Prompt):" + +#: includes/settings/ai-logs-page.php:83 +#: includes/settings/class-settings.php:648 +msgid "Out (Compl.):" +msgstr "Salida (Compl.):" + +#: includes/settings/ai-logs-page.php:94 +msgid "RAG & Vectorization Estimates" +msgstr "Estimaciones de RAG y vectorización" + +#: includes/settings/ai-logs-page.php:95 +msgid "These numbers represent the estimated tokens consumed to create and query your RAG Knowledge Base. Since embedding models do not natively report their specific usage, the system estimates the token count via mathematical approximation." +msgstr "Estos números representan los Tokens estimados consumidos para crear y consultar su Base de conocimiento RAG. Como los modelos de Embedding no reportan nativamente su uso específico, el sistema estima el conteo de Tokens mediante aproximación matemática." + +#: includes/settings/ai-logs-page.php:102 +msgid "Document Vectorization" +msgstr "Vectorización de documentos" + +#: includes/settings/ai-logs-page.php:107 +msgid "Tokens used inserting posts to Vector Store." +msgstr "Tokens usados insertando entradas al Almacén de Vectores." + +#: includes/settings/ai-logs-page.php:114 +msgid "Semantic Retrieval" +msgstr "Recuperación semántica" + +#: includes/settings/ai-logs-page.php:119 +msgid "Tokens used mapping user queries to vectors." +msgstr "Tokens usados mapeando consultas de usuarios a vectores." + +#: includes/settings/ai-logs-page.php:147 +msgid "Search logs:" +msgstr "Buscar registros:" + +#: includes/settings/ai-logs-page.php:149 +msgid "Search Logs" +msgstr "Buscar registros" + +#: includes/settings/ai-logs-page.php:156 +msgid "Are you sure you want to delete ALL logs?" +msgstr "¿Está seguro de que desea eliminar TODOS los registros?" + +#: includes/settings/ai-logs-page.php:157 +msgid "Clear All Logs" +msgstr "Borrar todos los registros" + +#. translators: %s: Number of log items. +#: includes/settings/ai-logs-page.php:164 +#, php-format +msgid "%s item" +msgid_plural "%s items" +msgstr[0] "%s elemento" +msgstr[1] "%s elementos" + +#: includes/settings/ai-logs-page.php:189 +msgid "Date/Time" +msgstr "Fecha/Hora" + +#: includes/settings/ai-logs-page.php:190 +msgid "Provider" +msgstr "Proveedor" + +#: includes/settings/ai-logs-page.php:191 +msgid "Tokens (In / Out)" +msgstr "Tokens (Entrada / Salida)" + +#: includes/settings/ai-logs-page.php:192 +msgid "Status / Output Preview" +msgstr "Estado / vista previa de salida" + +#: includes/settings/ai-logs-page.php:193 +#: includes/settings/class-settings.php:602 +msgid "Actions" +msgstr "Acciones" + +#: includes/settings/ai-logs-page.php:198 +msgid "No logs found yet. Ensure Debug Mode is enabled and georeference a post." +msgstr "Aún no se encontraron registros. Asegúrese de que el modo Debug esté activado y georreferencie una entrada." + +#: includes/settings/ai-logs-page.php:238 +#: includes/settings/class-settings.php:649 +msgid "Total" +msgstr "Total" + +#: includes/settings/ai-logs-page.php:239 +#: includes/settings/class-settings.php:650 +msgid "Prompt:" +msgstr "Prompt:" + +#: includes/settings/ai-logs-page.php:239 +#: includes/settings/class-settings.php:651 +msgid "Completion:" +msgstr "Completado:" + +#: includes/settings/ai-logs-page.php:244 +msgid "View Details" +msgstr "Ver detalles" + +#: includes/settings/ai-logs-page.php:249 +msgid "AI Interaction Details" +msgstr "Detalles de interacción de IA" + +#: includes/settings/ai-logs-page.php:254 +msgid "Timestamp" +msgstr "Marca de tiempo" + +#: includes/settings/ai-logs-page.php:255 +msgid "Input Tokens" +msgstr "Tokens de entrada" + +#: includes/settings/ai-logs-page.php:256 +msgid "Output Tokens" +msgstr "Tokens de salida" + +#: includes/settings/ai-logs-page.php:259 +msgid "Input (Prompt sent)" +msgstr "Entrada (Prompt enviado)" + +#: includes/settings/ai-logs-page.php:262 +msgid "Output (Raw Response)" +msgstr "Salida (respuesta sin procesar)" + +#: includes/settings/class-settings.php:573 +msgid "[System] Console cleared." +msgstr "[Sistema] Consola borrada." + +#: includes/settings/class-settings.php:574 +msgid "Missing Configuration" +msgstr "Configuración faltante" + +#: includes/settings/class-settings.php:575 +msgid "Checking..." +msgstr "Verificando..." + +#: includes/settings/class-settings.php:578 +msgid "Failed" +msgstr "Falló" + +#: includes/settings/class-settings.php:579 +msgid "Request Failed" +msgstr "Solicitud fallida" + +#: includes/settings/class-settings.php:580 +msgid "Loading..." +msgstr "Cargando..." + +#: includes/settings/class-settings.php:582 +msgid "Select or type a model..." +msgstr "Seleccione o escriba un modelo..." + +#: includes/settings/class-settings.php:583 +msgid " (Non-chat)" +msgstr " (No chat)" + +#: includes/settings/class-settings.php:584 +msgid "Please enter an API Key first." +msgstr "Ingrese una clave de API primero." + +#: includes/settings/class-settings.php:585 +msgid "Failed to fetch models: " +msgstr "Error al obtener modelos: " + +#: includes/settings/class-settings.php:586 +msgid "Unknown error" +msgstr "Error desconocido" + +#: includes/settings/class-settings.php:587 +msgid "Error fetching models. Check your key and connection." +msgstr "Error al obtener modelos. Verifique su clave y conexión." + +#: includes/settings/class-settings.php:588 +msgid "Please type a description first." +msgstr "Escriba una descripción primero." + +#: includes/settings/class-settings.php:589 +msgid "Asking AI..." +msgstr "Preguntando a la IA..." + +#: includes/ai/settings/tab-context.php:199 +#: includes/settings/class-settings.php:590 +msgid "Generating..." +msgstr "Generando..." + +#: includes/settings/class-settings.php:591 +msgid "✨ Applied above." +msgstr "✨ Aplicado arriba." + +#: includes/settings/class-settings.php:592 +msgid "Error generating prompt." +msgstr "Error al generar el prompt." + +#: includes/settings/class-settings.php:594 +msgid "Testing..." +msgstr "Probando..." + +#: includes/settings/class-settings.php:595 +msgid "✅ Valid" +msgstr "✅ Válido" + +#: includes/settings/class-settings.php:596 +msgid "❌ Invalid" +msgstr "❌ Inválido" + +#: includes/settings/class-settings.php:597 +msgid "Validate Prompt" +msgstr "Validar prompt" + +#: includes/settings/class-settings.php:598 +msgid "Loading backups..." +msgstr "Cargando copias de seguridad..." + +#: includes/settings/class-settings.php:599 +msgid "File" +msgstr "Archivo" + +#: includes/settings/class-settings.php:600 +msgid "Date" +msgstr "Fecha" + +#: includes/settings/class-settings.php:601 +#: js/build/mapBlocks.js:3 +#: js/build/mapsSidebar.js:1 +msgid "Size" +msgstr "Tamaño" + +#: includes/settings/class-settings.php:606 +msgid "Delete?" +msgstr "¿Eliminar?" + +#: includes/settings/class-settings.php:607 +msgid "Vectorizing..." +msgstr "Vectorizando..." + +#: includes/settings/class-settings.php:608 +msgid "Vectorize Now" +msgstr "Vectorizar ahora" + +#: includes/settings/class-settings.php:609 +msgid "Clear knowledge base?" +msgstr "¿Limpiar la base de conocimiento?" + +#: includes/settings/class-settings.php:610 +msgid "Fetching post and generating vector embeddings..." +msgstr "Obteniendo entrada y generando Embeddings de vectores..." + +#: includes/settings/class-settings.php:611 +msgid "Success!" +msgstr "¡Éxito!" + +#: includes/settings/class-settings.php:612 +msgid "Post Extracted:" +msgstr "Entrada extraída:" + +#: includes/settings/class-settings.php:613 +msgid "Vector Dimensions:" +msgstr "Dimensiones del vector:" + +#: includes/settings/class-settings.php:614 +msgid "Text Snippet:" +msgstr "Fragmento de texto:" + +#: includes/settings/class-settings.php:615 +msgid "Vector Preview:" +msgstr "Vista previa del vector:" + +#: includes/settings/class-settings.php:617 +msgid "Searching..." +msgstr "Buscando..." + +#: includes/settings/class-settings.php:618 +msgid "Searching the knowledge base " +msgstr "Buscando en la base de conocimiento " + +#: includes/settings/class-settings.php:619 +msgid "No documents found. Have you run vectorization?" +msgstr "No se encontraron documentos. ¿Ha ejecutado la vectorización?" + +#: includes/settings/class-settings.php:620 +msgid "Score / Relevance" +msgstr "Puntuación / Relevancia" + +#: includes/settings/class-settings.php:621 +msgid "Metadata" +msgstr "Metadatos" + +#: includes/settings/class-settings.php:624 +msgid "Processing..." +msgstr "Procesando..." + +#: includes/settings/class-settings.php:625 +msgid "Clearing..." +msgstr "Limpiando..." + +#: includes/settings/class-settings.php:626 +msgid "This will schedule clearing ALL AI-geolocated posts in the background. Continue?" +msgstr "Esto programará la limpieza de TODAS las entradas geolocalizadas por IA en segundo plano. ¿Continuar?" + +#: includes/settings/class-settings.php:627 +msgid "ARE YOU SURE? This cannot be undone and will require full re-vectorization for these posts." +msgstr "¿ESTÁ SEGURO? Esto no se puede deshacer y requerirá una re-vectorización completa para estas entradas." + +#: includes/settings/class-settings.php:628 +msgid "Bulk clearing started in background." +msgstr "Limpieza masiva iniciada en segundo plano." + +#: includes/settings/class-settings.php:630 +msgid "Collapse" +msgstr "Colapsar" + +#: includes/settings/class-settings.php:632 +msgid "Low" +msgstr "Bajo" + +#: includes/settings/class-settings.php:633 +msgid "Fair" +msgstr "Aceptable" + +#: includes/settings/class-settings.php:634 +msgid "Optimal" +msgstr "Óptimo" + +#: includes/settings/class-settings.php:635 +msgid "API call failed" +msgstr "Falló la llamada a la API" + +#: includes/settings/class-settings.php:638 +msgid "Delete log file?" +msgstr "¿Eliminar archivo de registro?" + +#: includes/settings/class-settings.php:639 +msgid "No matching layers found." +msgstr "No se encontraron capas coincidentes." + +#: includes/settings/class-settings.php:640 +msgid " matching layers:" +msgstr " capas coincidentes:" + +#: includes/settings/class-settings.php:643 +msgid "Clear layer store?" +msgstr "¿Limpiar almacén de capas?" + +#: includes/settings/class-settings.php:644 +msgid "Please enter a post ID or search text." +msgstr "Ingrese un ID de entrada o texto de búsqueda." + +#: includes/settings/class-settings.php:694 +#: includes/settings/class-settings.php:695 +msgid "AI Debug Logs" +msgstr "Registros de Debug de IA" + +#: includes/settings/class-settings.php:711 +msgid "Appearance" +msgstr "Apariencia" + +#: includes/settings/settings-page.php:10 +msgid "Jeo Settings" +msgstr "Ajustes de Jeo" + +#: includes/settings/settings-page.php:38 +msgid "Enabled Post Types" +msgstr "Tipos de contenido activados" + +#: includes/settings/settings-page.php:64 +msgid "Default map center (latitude)" +msgstr "Centro predeterminado del mapa (latitud)" + +#: includes/settings/settings-page.php:71 +msgid "Default map center (longitude)" +msgstr "Centro predeterminado del mapa (longitud)" + +#: includes/settings/settings-page.php:78 +msgid "User location precision" +msgstr "Precisión de ubicación del usuario" + +#: includes/settings/settings-page.php:83 +msgid "Lower = less precision, more privacy." +msgstr "Menor = menos precisión, más privacidad." + +#: includes/settings/settings-page.php:84 +msgid "https://en.wikipedia.org/wiki/Decimal_degrees#Precision" +msgstr "https://en.wikipedia.org/wiki/Decimal_degrees#Precision" + +#: includes/settings/settings-page.php:84 +msgid "Learn more about decimal degree precision." +msgstr "Más información sobre la precisión de grados decimales." + +#: includes/settings/settings-page.php:90 +msgid "Show Story Maps on Post Archives" +msgstr "Mostrar mapas narrativos en archivos de entradas" + +#: includes/settings/settings-page.php:93 +msgid "Enable this to display relevant story maps at the top of category and tag archive pages." +msgstr "Active esto para mostrar mapas narrativos relevantes en la parte superior de las páginas de archivo de categorías y etiquetas." + +#: includes/settings/settings-page.php:141 +msgid "Mapbox API Key" +msgstr "Clave de API de Mapbox" + +#: includes/settings/settings-page.php:160 +msgid "Geocoder service" +msgstr "Servicio de geocodificación" + +#: includes/settings/settings-page.php:167 +msgid "Select the service used to translate addresses into coordinates. Mapbox requires an API Key configured in the General tab." +msgstr "Seleccione el servicio usado para traducir direcciones en coordenadas. Mapbox requiere una clave de API configurada en la pestaña General." + +#: includes/settings/settings-page.php:177 +msgid "Font URL" +msgstr "URL de la fuente" + +#: includes/settings/settings-page.php:183 +msgid "Font Name" +msgstr "Nombre de la fuente" + +#: includes/settings/settings-page.php:189 +msgid "Secondary Font URL" +msgstr "URL de la fuente secundaria" + +#: includes/settings/settings-page.php:195 +msgid "Secondary Font Name" +msgstr "Nombre de la fuente secundaria" + +#: includes/settings/settings-page.php:201 +msgid "Info button font size (rem)" +msgstr "Tamaño de fuente del botón de información (rem)" + +#: includes/settings/settings-page.php:214 +msgid "Primary Color" +msgstr "Color primario" + +#: includes/settings/settings-page.php:226 +msgid "Info button text color" +msgstr "Color del texto del botón de información" + +#: includes/settings/settings-page.php:238 +msgid "Close button text color" +msgstr "Color del texto del botón de cerrar" + +#: includes/settings/settings-page.php:244 +msgid "Secondary Color" +msgstr "Color secundario" + +#: includes/settings/settings-page.php:250 +msgid "Map widgets background color" +msgstr "Color de fondo de los widgets del mapa" + +#: includes/settings/settings-page.php:256 +msgid "Map widgets text color" +msgstr "Color del texto de los widgets del mapa" + +#: includes/settings/settings-page.php:262 +msgid "Map widgets background color (hover)" +msgstr "Color de fondo de los widgets del mapa (al pasar el cursor)" + +#: includes/settings/settings-page.php:271 +msgid "Map Marker Icons" +msgstr "Íconos de marcadores del mapa" + +#: includes/settings/settings-page.php:275 +msgid "Primary Pin URL" +msgstr "URL del pin primario" + +#: includes/settings/settings-page.php:278 +msgid "URL for the primary location marker icon. Leave empty to use the default blue marker." +msgstr "URL del ícono del marcador de ubicación primario. Déjelo vacío para usar el marcador azul predeterminado." + +#: includes/settings/settings-page.php:282 +msgid "Secondary Pin URL" +msgstr "URL del pin secundario" + +#: includes/settings/settings-page.php:285 +msgid "URL for the secondary location marker icon. Leave empty to use the default grey marker." +msgstr "URL del ícono del marcador de ubicación secundario. Déjelo vacío para usar el marcador gris predeterminado." + +#: includes/settings/settings-page.php:311 +msgid "Discovery page" +msgstr "Página de Discovery" + +#: includes/settings/settings-page.php:318 +msgid "Select a page" +msgstr "Seleccione una página" + +#: includes/settings/settings-page.php:322 +msgid "Select the page where the JEO Discovery block is located." +msgstr "Seleccione la página donde se encuentra el bloque Discovery de JEO." + +#: includes/stories-near-you/class-stories-near-you.php:432 +#: js/build/storiesNearYou.js:1 +msgid "Unable to load stories near you." +msgstr "No se pudieron cargar historias cerca de usted." + +#: js/build/contextSidebar.js:1 +msgid "Suggested Paragraphs" +msgstr "Párrafos sugeridos" + +#: js/build/contextSidebar.js:1 +msgid "Inserted" +msgstr "Insertado" + +#: js/build/contextSidebar.js:1 +msgid "Insert into article" +msgstr "Insertar en el artículo" + +#: js/build/contextSidebar.js:1 +msgid "Copied!" +msgstr "¡Copiado!" + +#: js/build/contextSidebar.js:1 +msgid "Copy" +msgstr "Copiar" + +#: js/build/contextSidebar.js:1 +msgid "Failed to generate suggestions." +msgstr "Error al generar sugerencias." + +#: js/build/contextSidebar.js:1 +msgid "Error contacting AI." +msgstr "Error al contactar a la IA." + +#: js/build/contextSidebar.js:1 +msgid "Failed to process message." +msgstr "Error al procesar el mensaje." + +#: js/build/contextSidebar.js:1 +msgid "Generate new editorial suggestions based on the current post content." +msgstr "Generar nuevas sugerencias editoriales basadas en el contenido actual de la entrada." + +#: js/build/contextSidebar.js:1 +msgid "Failed to clear conversation." +msgstr "Error al borrar la conversación." + +#: js/build/contextSidebar.js:1 +msgid "Restoring conversation..." +msgstr "Restaurando conversación..." + +#: js/build/contextSidebar.js:1 +msgid "Use the AI Context Assistant to generate editorial suggestions based on your article and the site's knowledge base." +msgstr "Use el Asistente de contexto de IA para generar sugerencias editoriales basadas en su artículo y la base de conocimiento del sitio." + +#: js/build/contextSidebar.js:1 +msgid "Generate Suggestions" +msgstr "Generar sugerencias" + +#: js/build/contextSidebar.js:1 +msgid "Thinking..." +msgstr "Pensando..." + +#: js/build/contextSidebar.js:1 +msgid "Send" +msgstr "Enviar" + +#: js/build/contextSidebar.js:1 +msgid "Retry" +msgstr "Reintentar" + +#: js/build/contextSidebar.js:1 +msgid "AI Context" +msgstr "Contexto de IA" + +#: js/build/contextSidebar.js:1 +msgid "AI Context Assistant" +msgstr "Asistente de contexto de IA" + +#: js/build/mapBlocks.js:1 +msgid "Analyzing content…" +msgstr "Analizando contenido…" + +#: js/build/mapBlocks.js:1 +msgid "Searching for map layers…" +msgstr "Buscando capas de mapa…" + +#: js/build/mapBlocks.js:1 +msgid "Generating map configuration…" +msgstr "Generando configuración del mapa…" + +#: js/build/mapBlocks.js:1 +msgid "Map generated from post content." +msgstr "Mapa generado a partir del contenido de la entrada." + +#: js/build/mapBlocks.js:1 +msgid "Failed to suggest layers." +msgstr "Error al sugerir capas." + +#: js/build/mapBlocks.js:1 +msgid "Request failed." +msgstr "Solicitud fallida." + +#: js/build/mapBlocks.js:1 +msgid "Failed to update map." +msgstr "Error al actualizar el mapa." + +#: js/build/mapBlocks.js:1 +msgid "Map generated from prompt." +msgstr "Mapa generado a partir del prompt." + +#: js/build/mapBlocks.js:1 +msgid "Failed to generate from prompt." +msgstr "Error al generar a partir del prompt." + +#: js/build/mapBlocks.js:1 +msgid "Map regenerated from post content." +msgstr "Mapa regenerado a partir del contenido de la entrada." + +#: js/build/mapBlocks.js:1 +msgid "AI-Assisted Map" +msgstr "Mapa asistido por IA" + +#: js/build/mapBlocks.js:1 +msgid "AI will analyze your content and suggest relevant map layers, center point, and zoom level." +msgstr "La IA analizará su contenido y sugerirá capas de mapa relevantes, punto central y nivel de zoom." + +#: js/build/mapBlocks.js:1 +msgid "Generate from post content" +msgstr "Generar a partir del contenido de la entrada" + +#: js/build/mapBlocks.js:1 +msgid "Generate from prompt" +msgstr "Generar a partir del prompt" + +#: js/build/mapBlocks.js:1 +msgid "Map prompt" +msgstr "Prompt del mapa" + +#: js/build/mapBlocks.js:1 +msgid "Generate map" +msgstr "Generar mapa" + +#: js/build/mapBlocks.js:1 +msgid "Base Layer" +msgstr "Capa base" + +#: js/build/mapBlocks.js:1 +msgid "Base terrain" +msgstr "Terreno base" + +#: js/build/mapBlocks.js:1 +msgid "Geolocation Pins" +msgstr "Pines de geolocalización" + +#: js/build/mapBlocks.js:1 +msgid "Show pins" +msgstr "Mostrar pines" + +#: js/build/mapBlocks.js:1 +msgid "Yes" +msgstr "Sí" + +#: js/build/mapBlocks.js:1 +msgid "AI Assistant" +msgstr "Asistente de IA" + +#: js/build/mapBlocks.js:1 +msgid "Assistant" +msgstr "Asistente" + +#: js/build/mapBlocks.js:1 +msgid "You" +msgstr "Usted" + +#: js/build/mapBlocks.js:1 +msgid "Typing…" +msgstr "Escribiendo…" + +#: js/build/mapBlocks.js:1 +msgid "Send message" +msgstr "Enviar mensaje" + +#: js/build/mapBlocks.js:1 +msgid "Regenerate" +msgstr "Regenerar" + +#: js/build/mapBlocks.js:1 +msgid "New prompt" +msgstr "Nuevo prompt" + +#: js/build/mapBlocks.js:1 +msgid "Generate" +msgstr "Generar" + +#: js/build/mapBlocks.js:1 +msgid "Display an AI-generated contextual map with layers and geolocation pins" +msgstr "Muestra un mapa contextual generado por IA con capas y pines de geolocalización" + +#: js/build/mapBlocks.js:3 +msgid "Stories Near You" +msgstr "Historias cerca de ti" + +#: js/build/mapBlocks.js:3 +msgid "Display geolocated posts sorted by proximity to the reader" +msgstr "Muestra entradas geolocalizadas ordenadas por proximidad al lector" + +#: js/build/mapBlocks.js:3 +msgid "— None —" +msgstr "— Ninguno —" + +#: js/build/mapBlocks.js:3 +msgid "All geo-enabled types" +msgstr "Todos los tipos con geolocalización" + +#: js/build/mapBlocks.js:3 +msgid "Location Preview" +msgstr "Vista previa de ubicación" + +#: js/build/mapBlocks.js:3 +msgid "Post Card" +msgstr "Tarjeta de entrada" + +#: js/build/mapBlocks.js:3 +msgid "Layout" +msgstr "Layout" + +#: js/build/mapBlocks.js:3 +msgid "Grid" +msgstr "Cuadrícula" + +#: js/build/mapBlocks.js:3 +msgid "List" +msgstr "Lista" + +#: js/build/mapBlocks.js:3 +msgid "Columns" +msgstr "Columnas" + +#: js/build/mapBlocks.js:3 +msgid "Media position" +msgstr "Posición de la imagen" + +#: js/build/mapBlocks.js:3 +msgid "Top" +msgstr "Arriba" + +#: js/build/mapBlocks.js:3 +msgid "Left" +msgstr "Izquierda" + +#: js/build/mapBlocks.js:3 +msgid "Right" +msgstr "Derecha" + +#: js/build/mapBlocks.js:3 +msgid "Behind" +msgstr "Detrás" + +#: js/build/mapBlocks.js:3 +msgid "Image shape" +msgstr "Forma de imagen" + +#: js/build/mapBlocks.js:3 +msgid "Landscape" +msgstr "Horizontal" + +#: js/build/mapBlocks.js:3 +msgid "Portrait" +msgstr "Vertical" + +#: js/build/mapBlocks.js:3 +msgid "Square" +msgstr "Cuadrada" + +#: js/build/mapBlocks.js:3 +msgid "Uncropped" +msgstr "Sin recortar" + +#: js/build/mapBlocks.js:3 +msgid "Image size" +msgstr "Tamaño de imagen" + +#: js/build/mapBlocks.js:3 +msgid "Link featured image" +msgstr "Enlazar imagen destacada" + +#: js/build/mapBlocks.js:3 +msgid "Query Settings" +msgstr "Ajustes de consulta" + +#: js/build/mapBlocks.js:3 +msgid "Posts per page" +msgstr "Entradas por página" + +#: js/build/mapBlocks.js:3 +msgid "Post type" +msgstr "Tipo de contenido" + +#: js/build/mapBlocks.js:3 +msgid "Filters" +msgstr "Filtros" + +#: js/build/mapBlocks.js:3 +msgid "Exclude categories" +msgstr "Excluir categorías" + +#: js/build/mapBlocks.js:3 +msgid "Exclude tags" +msgstr "Excluir etiquetas" + +#: js/build/mapBlocks.js:3 +msgid "Display" +msgstr "Visualización" + +#: js/build/mapBlocks.js:3 +msgid "Show featured image" +msgstr "Mostrar imagen destacada" + +#: js/build/mapBlocks.js:3 +msgid "Show category" +msgstr "Mostrar categoría" + +#: js/build/mapBlocks.js:3 +msgid "Show date" +msgstr "Mostrar fecha" + +#: js/build/mapBlocks.js:3 +msgid "Show excerpt" +msgstr "Mostrar extracto" + +#: js/build/mapBlocks.js:3 +msgid "Excerpt length" +msgstr "Longitud del extracto" + +#: js/build/mapBlocks.js:3 +msgid "Show author" +msgstr "Mostrar autor" + +#: js/build/mapBlocks.js:3 +msgid "Show avatar" +msgstr "Mostrar avatar" + +#: js/build/mapBlocks.js:3 +msgid "Show read more link" +msgstr "Mostrar enlace 'leer más'" + +#: js/build/mapBlocks.js:3 +msgid "Read more label" +msgstr "Texto del enlace 'leer más'" + +#: js/build/mapBlocks.js:3 +msgid "Typography & Spacing" +msgstr "Tipografía y espaciado" + +#: js/build/mapBlocks.js:3 +msgid "Type scale" +msgstr "Escala de tipografía" + +#: js/build/mapBlocks.js:3 +msgid "Image scale" +msgstr "Escala de imagen" + +#: js/build/mapBlocks.js:3 +msgid "Column gap" +msgstr "Espacio entre columnas" + +#: js/build/mapBlocks.js:3 +msgid "Min height (vh)" +msgstr "Altura mínima (vh)" + +#: js/build/mapBlocks.js:3 +#: js/build/mapsSidebar.js:1 +msgid "Fill Color" +msgstr "Color de relleno" + +#: js/build/mapBlocks.js:3 +#: js/build/mapsSidebar.js:1 +msgid "Outline Color" +msgstr "Color del contorno" + +#: js/build/mapBlocks.js:3 +#: js/build/mapsSidebar.js:1 +msgid "Line Color" +msgstr "Color de línea" + +#: js/build/mapBlocks.js:3 +#: js/build/mapsSidebar.js:1 +msgid "Circle Color" +msgstr "Color del círculo" + +#: js/build/mapBlocks.js:3 +#: js/build/mapsSidebar.js:1 +msgid "Stroke Color" +msgstr "Color del trazo" + +#: js/build/mapBlocks.js:3 +#: js/build/mapsSidebar.js:1 +msgid "Fill Opacity" +msgstr "Opacidad del relleno" + +#: js/build/mapBlocks.js:3 +#: js/build/mapsSidebar.js:1 +msgid "Line Opacity" +msgstr "Opacidad de línea" + +#: js/build/mapBlocks.js:3 +#: js/build/mapsSidebar.js:1 +msgid "Circle Opacity" +msgstr "Opacidad del círculo" + +#: js/build/mapBlocks.js:3 +#: js/build/mapsSidebar.js:1 +msgid "Line Width" +msgstr "Ancho de línea" + +#: js/build/mapBlocks.js:3 +#: js/build/mapsSidebar.js:1 +msgid "Circle Radius" +msgstr "Radio del círculo" + +#: js/build/mapBlocks.js:3 +#: js/build/mapsSidebar.js:1 +msgid "Stroke Width" +msgstr "Ancho del trazo" + +#: js/build/mapBlocks.js:3 +#: js/build/mapsSidebar.js:1 +msgid "Layer Style" +msgstr "Estilo de capa" + +#: js/build/mapBlocks.js:3 +#: js/build/mapsSidebar.js:1 +msgid "Styling is only supported for fill, line, and circle layer types." +msgstr "El estilo solo es compatible con tipos de capa de relleno, línea y círculo." + +#: js/build/mapBlocks.js:3 +#: js/build/mapsSidebar.js:1 +msgid "AI Default Style" +msgstr "Estilo predeterminado de IA" + +#: js/build/mapBlocks.js:3 +#: js/build/mapsSidebar.js:1 +msgid "Use AI-suggested default style" +msgstr "Usar estilo predeterminado sugerido por IA" + +#: js/build/mapBlocks.js:3 +#: js/build/mapsSidebar.js:1 +msgid "Filter:" +msgstr "Filtro:" + +#: js/build/mapBlocks.js:3 +#: js/build/mapsSidebar.js:1 +msgid "Paint:" +msgstr "Paint:" + +#: js/build/mapBlocks.js:3 +#: js/build/mapsSidebar.js:1 +msgid "Opacity" +msgstr "Opacidad" + +#: js/build/mapBlocks.js:3 +#: js/build/mapsSidebar.js:1 +msgid "Style layer" +msgstr "Capa de estilo" + +#: js/build/postsSidebar.js:2 +#: js/build/postsSidebar.js:3 +msgid "Expand panel" +msgstr "Expandir panel" + +#: js/build/postsSidebar.js:2 +#: js/build/postsSidebar.js:3 +msgid "Minimize panel" +msgstr "Minimizar panel" + +#: js/build/postsSidebar.js:3 +#: js/build/postsSidebar.js:4 +msgid "Review AI Suggestions" +msgstr "Revisar sugerencias de IA" + +#: js/build/postsSidebar.js:3 +msgid "Select locations to add to the map:" +msgstr "Seleccione las ubicaciones para añadir al mapa:" + +#: js/build/postsSidebar.js:3 +msgid "Enriched via Geocoder" +msgstr "Enriquecido vía geocodificador" + +#: js/build/postsSidebar.js:3 +msgid "AI flagged this as primary location" +msgstr "La IA marcó esta como ubicación principal" + +#: js/build/postsSidebar.js:3 +msgid "Enrich" +msgstr "Enriquecer" + +#: js/build/postsSidebar.js:3 +msgid "Context:" +msgstr "Contexto:" + +#: js/build/postsSidebar.js:3 +msgid "Verified:" +msgstr "Verificado:" + +#: js/build/postsSidebar.js:3 +msgid "Score too low for reliable mapping." +msgstr "Puntuación demasiado baja para un mapeo confiable." + +#: js/build/postsSidebar.js:3 +msgid "Discard All" +msgstr "Descartar todo" + +#: js/build/postsSidebar.js:3 +msgid "Retrying..." +msgstr "Reintentando..." + +#: js/build/postsSidebar.js:3 +msgid "Retry AI" +msgstr "Reintentar IA" + +#: js/build/postsSidebar.js:3 +msgid "Add to Map" +msgstr "Añadir al mapa" + +#: js/build/postsSidebar.js:3 +msgid "Invalid response from AI." +msgstr "Respuesta inválida de la IA." + +#: js/build/postsSidebar.js:3 +msgid "No locations found by the AI." +msgstr "La IA no encontró ubicaciones." + +#: js/build/postsSidebar.js:3 +msgid "Error processing AI georeference." +msgstr "Error al procesar georreferenciación de IA." + +#: js/build/postsSidebar.js:3 +msgctxt "Artifical Intelligence" +msgid "AI" +msgstr "IA" + +#: js/build/postsSidebar.js:4 +msgid "Review Suggestions" +msgstr "Revisar sugerencias" + +#: js/build/postsSidebar.js:4 +msgid "Processing AI..." +msgstr "Procesando IA..." + +#: js/build/postsSidebar.js:4 +msgid "Geolocate with AI" +msgstr "Geolocalizar con IA" + +#. translators: %d: number of posts processed. +#: includes/ai/class-bulk-processor.php:407 +#, php-format +msgid "Processed batch of %d post." +msgid_plural "Processed batch of %d posts." +msgstr[0] "Lote de %d entrada procesado." +msgstr[1] "Lote de %d entradas procesado." + +#. translators: %d: number of posts cleared. +#: includes/ai/class-bulk-processor.php:452 +#, php-format +msgid "Cleared batch of %d post." +msgid_plural "Cleared batch of %d posts." +msgstr[0] "Lote de %d entrada limpiado." +msgstr[1] "Lote de %d entradas limpiado." + +#: includes/ai/class-bulk-processor.php:793 +msgctxt "post geolocation status" +msgid "Geolocated" +msgstr "Geolocalizado" + +#: includes/ai/class-bulk-processor.php:795 +msgctxt "post geolocation status" +msgid "Pending Approval" +msgstr "Pendiente de aprobación" + +#: includes/ai/class-bulk-processor.php:801 +msgctxt "post geolocation status" +msgid "Not Processed" +msgstr "No procesado" + +#: includes/settings/class-settings.php:576 +msgctxt "geocoder status" +msgid "Active" +msgstr "Activo" + +#: includes/settings/class-settings.php:577 +msgctxt "geocoder status" +msgid "Invalid" +msgstr "Inválido" + +#: includes/settings/class-settings.php:622 +msgctxt "fallback post title" +msgid "Untitled" +msgstr "Sin título" + +#. translators: %d: number of displayed stories. +#: js/build/discovery.js:3 +#, js-format +msgid "Displayed story: %d" +msgid_plural "Displayed stories: %d" +msgstr[0] "Story mostrada: %d" +msgstr[1] "Stories mostradas: %d" + +#: js/build/mapBlocks.js:1 +msgctxt "base terrain" +msgid "Dark" +msgstr "Oscuro" + +#: js/build/mapBlocks.js:1 +msgctxt "base terrain" +msgid "Light" +msgstr "Claro" + +#: js/build/mapBlocks.js:1 +msgctxt "base terrain" +msgid "Satellite" +msgstr "Satélite" + +#: js/build/mapBlocks.js:1 +msgctxt "base terrain" +msgid "None" +msgstr "Ninguno" + +#: js/build/mapBlocks.js:3 +#: js/build/mapsSidebar.js:1 +msgctxt "layer usage default label" +msgid "Default" +msgstr "Predeterminado" + +#: js/build/mapBlocks.js:3 +#: js/build/mapsSidebar.js:1 +msgctxt "layer" +msgid "Fixed" +msgstr "Fijo" + +#: js/build/mapBlocks.js:3 +#: js/build/mapsSidebar.js:1 +msgctxt "layer" +msgid "Swappable" +msgstr "Intercambiable" + +#: js/build/mapBlocks.js:3 +#: js/build/mapsSidebar.js:1 +msgctxt "layer" +msgid "Switchable" +msgstr "Conmutable" + +#. translators: %d: number of geolocation points. +#: js/build/postsSidebar.js:2 +#, js-format +msgid "Point (%d)" +msgid_plural "Points (%d)" +msgstr[0] "Punto (%d)" +msgstr[1] "Puntos (%d)" + +#. translators: %d: number of selected AI suggestions. +#: js/build/postsSidebar.js:3 +#, js-format +msgid "AI Suggestion (%d selected)" +msgid_plural "AI Suggestions (%d selected)" +msgstr[0] "Sugerencia de IA (%d seleccionada)" +msgstr[1] "Sugerencias de IA (%d seleccionadas)" + +#. translators: %d: number of locations found during bulk AI processing. +#: js/build/postsSidebar.js:4 +#, js-format +msgid "AI found %d location during bulk processing." +msgid_plural "AI found %d locations during bulk processing." +msgstr[0] "La IA encontró %d ubicación durante el procesamiento masivo." +msgstr[1] "La IA encontró %d ubicaciones durante el procesamiento masivo." + +#: includes/geocode/class-geocode-handler.php:123 +msgid "Geocoding service provided by OpenStreetMap at https://nominatim.openstreetmap.org/." +msgstr "Servicio de geocodificación proporcionado por OpenStreetMap en https://nominatim.openstreetmap.org/." + +#: includes/geocode/class-geocode-handler.php:249 +msgid "Metadata storing the locations related to the post. Each location is an object with latitude, longitude, and geocode attributes." +msgstr "Metadatos que almacenan las ubicaciones relacionadas con la entrada. Cada ubicación es un objeto con latitud, longitud y atributos de geocódigo." + +#: includes/menu/class-menu.php:35 +#: includes/menu/class-menu.php:36 +msgid "JEO" +msgstr "JEO" + +#: includes/settings/class-settings.php:538 +msgid "JEO could not save this asset URL. Use a valid http(s) or root-relative URL." +msgstr "JEO no pudo guardar esta URL de asset. Use una URL http(s) o relativa a la raíz válida." + +#. translators: %s is the geocoder name. Example: Nominatim options. +#: includes/settings/settings-page.php:111 +#, php-format +msgctxt "geocoder_options" +msgid "%s options" +msgstr "Opciones de %s" + +#: includes/settings/settings-page.php:292 +msgid "Embed" +msgstr "Embed" + +#: includes/settings/settings-page.js:916 +#: includes/settings/settings-page.js:918 +msgid "Choose Image" +msgstr "Elegir imagen" + +#: js/build/discovery.js:6 +msgid "Changes applied" +msgstr "Cambios aplicados" + +#: js/build/discovery.js:6 +msgid "Not applied" +msgstr "No aplicado" + +#: js/build/jeoMap.js:1 +msgid "Show previous post" +msgstr "Mostrar entrada anterior" + +#: js/build/jeoMap.js:1 +msgid "Show next post" +msgstr "Mostrar entrada siguiente" + +#. translators: %d: number of markers in the current map area. +#: js/build/jeoMap.js:2 +#, js-format +msgid "%d marker in this area" +msgid_plural "%d markers in this area" +msgstr[0] "%d marcador en esta área" +msgstr[1] "%d marcadores en esta área" + +#: js/build/mapBlocks.js:3 +msgid "Layer Editor Preview" +msgstr "Vista previa del editor de capas" + +#: js/build/mapBlocks.js:3 +msgid "Interactive layer preview for the Map Layer post type editor." +msgstr "Vista previa interactiva de capas para el editor del tipo de contenido Map Layer." + +#: js/build/mapBlocks.js:3 +msgid "Map Editor Preview" +msgstr "Vista previa del editor de mapas" + +#: js/build/mapBlocks.js:3 +msgid "Interactive map preview for the Map post type editor." +msgstr "Vista previa interactiva de mapa para el editor del tipo de contenido Map." + +#: js/build/postsSidebar.js:1 +msgid "Unable to retrieve the address for the selected point." +msgstr "No se pudo obtener la dirección para el punto seleccionado." + +#: includes/class-jeo.php:1711 +msgid "This JEO embed is not available." +msgstr "Este embed de JEO no está disponible." + +#: includes/class-jeo.php:1712 +msgid "Embed not available" +msgstr "Embed no disponible" + +#: includes/ai/class-context-agent.php:282 +msgid "No AI provider is configured. Set one in JEO AI Settings." +msgstr "Ningún proveedor de IA configurado. Configure uno en los ajustes de IA de JEO." + +#: includes/ai/class-context-agent.php:320 +msgid "The AI returned an empty prompt engineering response." +msgstr "La IA devolvió una respuesta vacía de ingeniería de prompt." + +#: includes/ai/class-context-handler.php:287 +#: js/build/contextSidebar.js:1 +msgid "Initial suggestions" +msgstr "Sugerencias iniciales" + +#: includes/ai/settings/tab-context.php:37 +msgid "Custom System Prompt" +msgstr "Prompt del sistema personalizado" + +#: includes/ai/settings/tab-context.php:49 +msgid "Custom system prompt used by the AI Context Assistant. Use the assistant below to generate or refine it. This value is stored as structured output JSON internally, but shown here as plain text for editing." +msgstr "Prompt del sistema personalizado usado por el Asistente de contexto de IA. Use el asistente de abajo para generarlo o refinario. Este valor se almacena internamente como JSON de salida estructurada, pero se muestra aquí como texto simple para edición." + +#: includes/ai/settings/tab-context.php:74 +msgid "AI Context Prompt Engineer Assistant" +msgstr "Asistente de Ingeniería de Prompt del Contexto de IA" + +#: includes/ai/settings/tab-context.php:76 +msgid "Describe how you want the Context Assistant to behave (e.g., \"Always link place names to the original article\" or \"Ground every claim with a verbatim quote\"). The active LLM will generate a highly optimized system prompt for you, strictly adhering to JEO formatting rules." +msgstr "Describa cómo quiere que se comporte el Asistente de contexto (ej.: \"Siempre enlazar nombres de lugares al artículo original\" o \"Basar cada afirmación con una cita textual\"). El LLM activo generará un prompt del sistema altamente optimizado para usted, siguiendo estrictamente las reglas de formato de JEO." + +#: includes/ai/settings/tab-context.php:113 +msgid "Ex: I want the assistant to always include inline contextual links and cite the exact sentence from the article that supports each location." +msgstr "Ej.: Quiero que el asistente siempre incluya enlaces contextuales insertados y cite la frase exacta del artículo que respalda cada ubicación." + +#: includes/ai/settings/tab-context.php:115 +msgid "Your description is saved automatically in this browser." +msgstr "Su descripción se guarda automáticamente en este navegador." + +#: includes/ai/settings/tab-context.php:121 +msgid "Generate Custom Prompt" +msgstr "Generar prompt personalizado" + +#: includes/ai/settings/tab-context.php:124 +msgid "Suggest initial prompt" +msgstr "Sugerir prompt inicial" + +#: includes/ai/settings/tab-context.php:182 +msgid "Initial prompt loaded. Edit and save the settings." +msgstr "Prompt inicial cargado. Edite y guarde los ajustes." + +#: includes/ai/settings/tab-context.php:192 +msgid "Please describe how you want the assistant to behave first." +msgstr "Describa cómo quiere que se comporte el asistente primero." + +#: includes/ai/settings/tab-context.php:217 +msgid "Custom prompt generated. Remember to save the settings." +msgstr "Prompt personalizado generado. Recuerde guardar los ajustes." + +#: includes/ai/settings/tab-context.php:220 +#: includes/ai/settings/tab-context.php:224 +msgid "Generation failed." +msgstr "Error en la generación." + +#: includes/layers/class-layers.php:101 +msgid "Layer Themes" +msgstr "Temas de capas" + +#: includes/layers/class-layers.php:102 +msgid "Layer Theme" +msgstr "Tema de capa" + +#: includes/layers/class-layers.php:103 +msgid "Add New Layer Theme" +msgstr "Añadir nuevo tema de capa" + +#: includes/layers/class-layers.php:104 +msgid "Edit Layer Theme" +msgstr "Editar tema de capa" + +#: includes/layers/class-layers.php:266 +msgid "Deforestation" +msgstr "Deforestación" + +#: includes/layers/class-layers.php:267 +msgid "Hydrography" +msgstr "Hidrografía" + +#: includes/layers/class-layers.php:268 +msgid "Indigenous Lands" +msgstr "Tierras indígenas" + +#: includes/layers/class-layers.php:269 +msgid "Protected Areas" +msgstr "Áreas protegidas" + +#: includes/layers/class-layers.php:270 +msgid "Mining" +msgstr "Minería" + +#: includes/layers/class-layers.php:271 +msgid "Oil and Gas" +msgstr "Petróleo y gas" + +#: includes/layers/class-layers.php:272 +msgid "Land Use" +msgstr "Uso del suelo" + +#: includes/layers/class-layers.php:273 +msgid "Agriculture" +msgstr "Agricultura" + +#: includes/layers/class-layers.php:274 +msgid "Infrastructure" +msgstr "Infraestructura" + +#: includes/layers/class-layers.php:275 +msgid "Administrative Boundaries" +msgstr "Límites administrativos" + +#: includes/layers/class-layers.php:276 +msgid "Socioeconomic" +msgstr "Socioeconómico" + +#: includes/layers/class-layers.php:277 +msgid "Biodiversity" +msgstr "Biodiversidad" + +#: includes/layers/class-layers.php:278 +msgid "Fire" +msgstr "Incendios" + +#: includes/layers/class-layers.php:279 +msgid "Climate" +msgstr "Clima" + +#: includes/minimap/class-minimap.php:229 +msgid "Generate a map for this post based on its content." +msgstr "Generar un mapa para esta entrada basado en su contenido." + +#. translators: %d: number of removed layers +#: includes/minimap/class-minimap.php:529 +#, php-format +msgid "%d layer(s) returned by the AI were discarded because they are not published map layers." +msgstr "%d capa(s) devuelta(s) por la IA fue(ron) descartada(s) porque no son capas de mapa publicadas." + +#: includes/minimap/class-minimap.php:1144 +msgid "The agent tried to remove several existing layers without an explicit request. They were kept to preserve the current map." +msgstr "El agente intentó eliminar varias capas existentes sin una solicitud explícita. Se mantuvieron para preservar el mapa actual." + +#. translators: %s: search radius in kilometers +#: includes/stories-near-you/class-stories-near-you.php:1123 +#, php-format +msgid "No stories found within %s km of your location." +msgstr "No se encontraron historias en un radio de %s km de su ubicación." + +#: js/build/contextSidebar.js:1 +msgid "References used" +msgstr "Referencias usadas" + +#: js/build/contextSidebar.js:1 +msgid "Previous suggestions" +msgstr "Sugerencias anteriores" + +#: js/build/contextSidebar.js:1 +msgid "View this version" +msgstr "Ver esta versión" + +#: js/build/mapBlocks.js:1 +msgid "Vector style preview" +msgstr "Vista previa de estilo vectorial" + +#: js/build/mapBlocks.js:3 +msgid "Search radius (km)" +msgstr "Radio de búsqueda (km)" + +#: js/build/mapBlocks.js:3 +msgid "Order by" +msgstr "Ordenar por" + +#: js/build/mapBlocks.js:3 +msgid "Most recent nearby" +msgstr "Más recientes cercanas" + +#: js/build/mapBlocks.js:3 +msgid "Nearest first" +msgstr "Más cercanas primero" + +#: js/build/mapBlocks.js:3 +msgid "Relevance (distance + date)" +msgstr "Relevancia (distancia + fecha)" + +#: js/build/mapBlocks.js:3 +msgid "Max age (days, 0 = no limit)" +msgstr "Edad máxima (días, 0 = sin límite)" + +#: js/build/mapBlocks.js:3 +msgid "Distance weight" +msgstr "Peso de distancia" + +#: js/build/mapBlocks.js:3 +msgid "Date weight" +msgstr "Peso de fecha" + +#: js/build/mapBlocks.js:3 +#: js/build/mapsSidebar.js:1 +msgid "Automatic style applied (this layer had no saved style). Edit layer settings to customize it." +msgstr "Estilo automático aplicado (esta capa no tenía estilo guardado). Edite los ajustes de la capa para personalizarlo." + +#: js/build/mapBlocks.js:3 +#: js/build/mapsSidebar.js:1 +msgid "Themes:" +msgstr "Temas:" + +#: js/build/mapBlocks.js:3 +#: js/build/mapsSidebar.js:1 +msgid "Source:" +msgstr "Fuente:" + +#: js/build/postsSidebar.js:1 +msgid "Type an address, then click Search or press Enter. You can also drag the marker across the map." +msgstr "Escriba una dirección, luego haga clic en Buscar o presione Enter. También puede arrastrar el marcador por el mapa." + +#: includes/maps/class-map-style-composer.php:631 +#: includes/maps/class-map-style-composer.php:700 +msgid "Mapbox composed styles are disabled." +msgstr "Los estilos compuestos de Mapbox están desactivados." + +#: includes/maps/class-map-style-composer.php:656 +#: includes/maps/class-map-style-composer.php:724 +msgid "Could not create the Mapbox composed style cache directory." +msgstr "No fue posible crear el directorio de caché de estilos compuestos de Mapbox." + +#: includes/maps/class-map-style-composer.php:827 +msgid "Map not found." +msgstr "Mapa no encontrado." + +#: includes/maps/class-map-style-composer.php:837 +msgid "A Mapbox access token is required to compose Mapbox styles." +msgstr "Se requiere un token de acceso de Mapbox para componer estilos de Mapbox." + +#: includes/maps/class-map-style-composer.php:877 +msgid "Public payload composition is only available for one-time maps." +msgstr "La composición con datos públicos solo está disponible para mapas de un solo uso." + +#: includes/maps/class-map-style-composer.php:883 +msgid "The map preview payload is too large." +msgstr "Los datos de la vista previa del mapa son demasiado grandes." + +#: includes/maps/class-map-style-composer.php:895 +msgid "The map preview has too many layers to compose." +msgstr "La vista previa del mapa tiene demasiadas capas para componer." + +#: includes/maps/class-map-style-composer.php:902 +msgid "The map preview does not include any renderable layers." +msgstr "La vista previa del mapa no incluye ninguna capa renderizable." + +#: includes/maps/class-map-style-composer.php:924 +msgid "Map preview" +msgstr "Vista previa del mapa" + +#: includes/maps/class-map-style-composer.php:1300 +msgid "Invalid composed style hash." +msgstr "Hash de estilo compuesto no válido." + +#: includes/maps/class-map-style-composer.php:1456 +msgid "One or more Mapbox styles could not be fetched." +msgstr "No fue posible obtener uno o más estilos de Mapbox." + +#: includes/maps/class-map-style-composer.php:1970 +msgid "The PHP GD extension is required to merge Mapbox sprites." +msgstr "Se requiere la extensión PHP GD para fusionar sprites de Mapbox." + +#: includes/maps/class-map-style-composer.php:2095 +msgid "Could not decode a Mapbox sprite image." +msgstr "No fue posible decodificar una imagen de sprite de Mapbox." + +#: includes/maps/class-map-style-composer.php:3111 +msgid "Composed style artifact was not found." +msgstr "No se encontró el artefacto del estilo compuesto." + +#: js/build/discovery.js:6 +msgid "Mapbox style composition is unavailable." +msgstr "La composición de estilos de Mapbox no está disponible." + +#: js/build/discovery.js:6 +#: js/build/jeoMap.js:1 +msgid "Mapbox style composition is unavailable for this map." +msgstr "La composición de estilos de Mapbox no está disponible para este mapa." + +#: js/build/discovery.js:6 +msgid "Unable to load map information right now." +msgstr "No fue posible cargar la información del mapa en este momento." + +#: js/build/discovery.js:6 +msgid "Map information" +msgstr "Información del mapa" + +#: js/build/jeoMap.js:1 +msgid "Mapbox style composition did not return renderable layers for this map." +msgstr "La composición de estilos de Mapbox no devolvió capas renderizables para este mapa." + +#: js/build/jeoMap.js:1 +msgid "Unable to load composed Mapbox style for this map." +msgstr "No fue posible cargar el estilo compuesto de Mapbox para este mapa." + +#: js/build/jeoMap.js:1 +msgid "Mapbox style composition is unavailable for this one-time map." +msgstr "La composición de estilos de Mapbox no está disponible para este mapa de un solo uso." + +#: js/build/jeoMap.js:1 +msgid "Mapbox style composition did not return renderable layers for this one-time map." +msgstr "La composición de estilos de Mapbox no devolvió capas renderizables para este mapa de un solo uso." + +#: js/build/jeoMap.js:1 +msgid "Unable to load composed Mapbox style for this one-time map." +msgstr "No fue posible cargar el estilo compuesto de Mapbox para este mapa de un solo uso." + +#: js/build/jeoMap.js:1 +#: js/build/jeoStorymap.js:3 +msgid "Mapbox style layers could not be loaded because the composed style is unavailable." +msgstr "No fue posible cargar las capas de estilo de Mapbox porque el estilo compuesto no está disponible." + +#: js/build/jeoStorymap.js:3 +msgid "Mapbox style composition is unavailable for this storymap." +msgstr "La composición de estilos de Mapbox no está disponible para este mapa narrativo." + +#: js/build/jeoStorymap.js:3 +msgid "Mapbox style composition did not return renderable layers for this storymap." +msgstr "La composición de estilos de Mapbox no devolvió capas renderizables para este mapa narrativo." + +#: js/build/jeoStorymap.js:3 +msgid "Unable to load composed Mapbox style for this storymap." +msgstr "No fue posible cargar el estilo compuesto de Mapbox para este mapa narrativo." + +#. translators: %d: number of maps whose composed style cache was refreshed. #: js/build/layersSidebar.js:8 #, js-format msgid "%d map" @@ -2170,3 +4922,478 @@ msgstr "Actualizando caché de estilos compuestos" #: js/build/layersSidebar.js:11 msgid "Refresh composed style cache" msgstr "Actualizar caché de estilos compuestos" + +#. translators: %s: linked URL +#: includes/ai/class-context-handler.php:703 +#, php-format +msgid "Link to %s removed: not listed in references." +msgstr "Enlace a %s eliminado: no listado en las referencias." + +#: includes/ai/class-context-handler.php:720 +msgid "Verification notes:" +msgstr "Notas de verificación:" + +#: js/build/mapBlocks.js:1 +msgid "Save the post before generating the map." +msgstr "Guarde la entrada antes de generar el mapa." + +#: js/build/mapBlocks.js:1 +msgid "Could not start map generation." +msgstr "No se pudo iniciar la generación del mapa." + +#: js/build/mapBlocks.js:1 +msgid "Could not send message." +msgstr "No se pudo enviar el mensaje." + +#: js/build/mapBlocks.js:1 +msgid "Could not generate from prompt." +msgstr "No se pudo generar a partir del prompt." + +#: jeo.php:36 +msgid "JEO Plugin Error: Your PHP version is too old. JEO AI features require PHP 8.2 or higher. The plugin will remain deactivated." +msgstr "Error del Plugin JEO: Su versión de PHP es demasiado antigua. Las funciones de IA de JEO requieren PHP 8.2 o superior. El plugin permanecerá desactivado." + +#: js/build/storiesNearYou.js:1 +msgid "You denied location access. Please enable location permissions in your browser settings." +msgstr "Denegó el acceso a la ubicación. Habilite los permisos de ubicación en la configuración de su navegador." + +#: js/build/storiesNearYou.js:1 +msgid "Location request timed out. Please try again or use the configured location." +msgstr "La solicitud de ubicación expiró. Intente nuevamente o use la ubicación configurada." + +#: js/build/storiesNearYou.js:1 +msgid "This browser does not support location services." +msgstr "Este navegador no admite servicios de ubicación." + +#: js/build/storiesNearYou.js:1 +msgid "Unable to load stories. Please check your connection and try again." +msgstr "No fue posible cargar las historias. Verifique su conexión e inténtelo de nuevo." + +#: js/build/storiesNearYou.js:1 +msgid "Your browser could not determine your location. This may be due to strict privacy settings or because GPS is unavailable on your device." +msgstr "Su navegador no pudo determinar su ubicación. Esto puede deberse a una configuración de privacidad estricta o a que el GPS no está disponible en su dispositivo." + +#. translators: %d: number of preserved layers +#: includes/minimap/class-minimap.php:1388 +#, php-format +msgid "%d manually-added layer(s) were preserved during AI refinement." +msgstr "%d capa(s) añadida(s) manualmente preservada(s) durante el refinamiento de la IA." + +#: includes/class-jeo.php:2062 +msgid "A Mapbox style ID and access token are required." +msgstr "Se requiere un ID de estilo de Mapbox y un token de acceso." + +#: includes/class-jeo.php:2073 +#: includes/class-jeo.php:2111 +msgid "Unexpected Mapbox style payload." +msgstr "Respuesta de estilo de Mapbox inesperada." + +#: includes/minimap/class-minimap.php:230 +msgid "Initial generation from post content" +msgstr "Generación inicial a partir del contenido de la publicación" + +#. translators: %d: version number. +#: includes/minimap/class-minimap.php:933 +#, php-format +msgid "Version %d was not found in the stored history. The configuration reconstructed from the conversation context was applied instead." +msgstr "La versión %d no fue encontrada en el historial almacenado. Se aplicó la configuración reconstruida a partir del contexto de la conversación." + +#. translators: %d: version number. +#: includes/minimap/class-minimap.php:946 +#, php-format +msgid "Restored map version %d." +msgstr "Versión %d del mapa restaurada." + +#. translators: %d: HTTP status code. +#: includes/ai/class-funai-place-polygon-adapter.php:152 +#, php-format +msgid "FUNAI WFS returned HTTP %d." +msgstr "El WFS de FUNAI retornó HTTP %d." + +#: includes/ai/class-funai-place-polygon-adapter.php:162 +msgid "Could not parse FUNAI WFS response." +msgstr "No se pudo analizar la respuesta del WFS de FUNAI." + +#: includes/ai/class-funai-place-polygon-adapter.php:84 +msgid "Could not compute bounding box from FUNAI geometry." +msgstr "No se pudo calcular la caja delimitadora a partir de la geometría de FUNAI." + +#: includes/ai/class-generate-boundary-layer-tool.php:83 +msgid "You do not have permission to generate layers." +msgstr "No tienes permiso para generar capas." + +#: includes/ai/class-ibge-place-polygon-adapter.php:78 +msgid "Could not compute bounding box from IBGE geometry." +msgstr "No se pudo calcular la caja delimitadora a partir de la geometría del IBGE." + +#. translators: %d: HTTP status code. +#: includes/ai/class-ibge-place-polygon-adapter.php:289 +#, php-format +msgid "IBGE API returned HTTP %d." +msgstr "La API del IBGE retornó HTTP %d." + +#: includes/ai/class-ibge-place-polygon-adapter.php:299 +msgid "Could not parse IBGE geometry response." +msgstr "No se pudo analizar la respuesta de geometría del IBGE." + +#. translators: %d: HTTP status code. +#: includes/ai/class-mapbox-style-builder.php:74 +#, php-format +msgid "Mapbox Styles API returned HTTP %d." +msgstr "La API de Estilos de Mapbox retornó HTTP %d." + +#: includes/ai/class-mapbox-style-builder.php:103 +msgid "Mapbox returned an unexpected style payload." +msgstr "Mapbox retornó una respuesta de estilo inesperada." + +#: includes/ai/class-mapbox-style-builder.php:136 +msgid "Only \"mapbox\" layer specs can be built into a composed style." +msgstr "Solo las especificaciones de capa \"mapbox\" pueden construirse en un estilo compuesto." + +#: includes/ai/class-mapbox-style-builder.php:144 +msgid "Composed style spec is missing style_json." +msgstr "La especificación de estilo compuesto no tiene style_json." + +#. translators: %s: missing style key. +#: includes/ai/class-mapbox-style-builder.php:155 +#, php-format +msgid "Composed style is missing required key: %s." +msgstr "El estilo compuesto no tiene la clave requerida: %s." + +#: includes/ai/class-mapbox-style-builder.php:187 +msgid "Mapbox API key is not configured." +msgstr "La clave de API de Mapbox no está configurada." + +#: includes/ai/class-mapbox-style-builder.php:195 +msgid "Mapbox API key format is not recognized." +msgstr "El formato de la clave de API de Mapbox no es reconocido." + +#: includes/ai/class-mapbox-style-builder.php:205 +msgid "Could not decode Mapbox API key." +msgstr "No se pudo decodificar la clave de API de Mapbox." + +#: includes/ai/class-mapbox-style-builder.php:213 +msgid "Could not extract Mapbox username from API key." +msgstr "No se pudo extraer el nombre de usuario de Mapbox de la clave de API." + +#: includes/ai/class-minilayer-classifier.php:57 +msgid "The classifier returned an unexpected response." +msgstr "El clasificador retornó una respuesta inesperada." + +#: includes/ai/class-minilayer-handler.php:95 +msgid "Could not classify the layer request. Please try again." +msgstr "No se pudo clasificar la solicitud de capa. Inténtalo de nuevo." + +#: includes/ai/class-minilayer-handler.php:96 +msgid "The classifier returned an unexpected response. Please try again." +msgstr "El clasificador retornó una respuesta inesperada. Inténtalo de nuevo." + +#: includes/ai/class-minilayer-handler.php:97 +#: includes/ai/class-minilayer-service.php:55 +msgid "The request cannot be approximated with available map data." +msgstr "La solicitud no puede aproximarse con los datos de mapa disponibles." + +#: includes/ai/class-minilayer-handler.php:98 +msgid "The AI returned an unsupported layer type." +msgstr "La IA retornó un tipo de capa no soportado." + +#: includes/ai/class-minilayer-handler.php:99 +msgid "The AI response is missing a required tileset field." +msgstr "La respuesta de la IA no tiene un campo tileset requerido." + +#: includes/ai/class-minilayer-handler.php:100 +msgid "The AI returned an invalid geometry type." +msgstr "La IA retornó un tipo de geometría inválido." + +#: includes/ai/class-minilayer-handler.php:101 +msgid "The AI response is missing composed style JSON." +msgstr "La respuesta de la IA no tiene el JSON de estilo compuesto." + +#. translators: %s: layer type. +#: includes/ai/class-minilayer-service.php:71 +#, php-format +msgid "Unsupported layer type: %s." +msgstr "Tipo de capa no soportado: %s." + +#. translators: %s: missing field name. +#: includes/ai/class-minilayer-service.php:92 +#, php-format +msgid "Missing required tileset field: %s." +msgstr "Campo tileset requerido ausente: %s." + +#: includes/ai/class-minilayer-service.php:103 +msgid "Invalid layer geometry type for tileset-vector." +msgstr "Tipo de geometría de capa inválido para tileset-vector." + +#: includes/ai/class-minilayer-service.php:221 +msgid "External sources" +msgstr "Fuentes externas" + +#: includes/ai/class-minilayer-service.php:222 +msgid "Mapbox composed style" +msgstr "Estilo compuesto de Mapbox" + +#: includes/ai/class-minilayer-service.php:298 +msgid "Minilayer" +msgstr "Minilayer" + +#: includes/ai/class-osm-place-polygon-adapter.php:99 +msgid "Could not compute bounding box from OSM geometry." +msgstr "No se pudo calcular la caja delimitadora a partir de la geometría de OSM." + +#: includes/ai/class-osm-place-polygon-adapter.php:364 +msgid "Could not parse Overpass response." +msgstr "No se pudo analizar la respuesta de Overpass." + +#: includes/ai/class-osm-place-polygon-adapter.php:385 +msgid "Could not assemble closed rings from OSM relation." +msgstr "No se pudieron montar anillos cerrados a partir de la relación de OSM." + +#: includes/ai/class-osm-place-polygon-adapter.php:424 +msgid "No Overpass mirrors configured." +msgstr "No hay espejos de Overpass configurados." + +#. translators: %d: HTTP status code. +#: includes/ai/class-osm-place-polygon-adapter.php:460 +#, php-format +msgid "Overpass returned HTTP %d." +msgstr "Overpass retornó HTTP %d." + +#: includes/ai/class-osm-place-polygon-adapter.php:472 +msgid "All Overpass mirrors failed." +msgstr "Todos los espejos de Overpass fallaron." + +#. translators: %s: place name. +#: includes/ai/class-place-polygon-service.php:96 +#, php-format +msgid "Could not find a boundary polygon for \"%s\"." +msgstr "No se pudo encontrar un polígono de límite para \"%s\"." + +#. translators: %s: boundary display name. +#: includes/ai/class-place-polygon-service.php:126 +#, php-format +msgid "Boundary: %s" +msgstr "Límite: %s" + +#: includes/ai/class-place-polygon-service.php:238 +msgid "Could not write boundary GeoJSON file." +msgstr "No se pudo escribir el archivo GeoJSON de límite." + +#: includes/ai/class-place-polygon-service.php:256 +msgid "Could not create boundary GeoJSON attachment." +msgstr "No se pudo crear el adjunto GeoJSON de límite." + +#: includes/cli/class-ai-cli.php:58 +msgid "Classifying prompt..." +msgstr "Clasificando prompt..." + +#. translators: 1: layer title, 2: post ID. +#: includes/cli/class-ai-cli.php:68 +#, php-format +msgid "Layer created: %1$s (ID %2$d)" +msgstr "Capa creada: %1$s (ID %2$d)" + +#: includes/cli/class-ai-cli.php:107 +msgid "A place name is required." +msgstr "Se requiere un nombre de lugar." + +#: includes/cli/class-ai-cli.php:115 +msgid "Resolving boundary polygon..." +msgstr "Resolviendo polígono de límite..." + +#. translators: 1: layer title, 2: post ID. +#: includes/cli/class-ai-cli.php:126 +#, php-format +msgid "Boundary layer created: %1$s (ID %2$d)" +msgstr "Capa de límite creada: %1$s (ID %2$d)" + +#: includes/cli/class-ai-cli.php:161 +msgid "Classifying prompt (no layer will be created)..." +msgstr "Clasificando prompt (no se creará ninguna capa)..." + +#: includes/ai/class-mapbox-style-builder.php:79 +msgid "Mapbox denied the style publish request (HTTP 403): the configured Mapbox token likely lacks the styles:write scope. Use a secret token (sk.…) or add the styles:write scope to the token at mapbox.com/account/access-tokens, then update the key in JEO Settings." +msgstr "Mapbox rechazó la solicitud de publicación del estilo (HTTP 403): es probable que el token de Mapbox configurado no tenga el ámbito styles:write. Use un token secreto (sk.…) o agregue el ámbito styles:write al token en mapbox.com/account/access-tokens y luego actualice la clave en Configuraciones de JEO." + +#: includes/ai/class-mapbox-style-builder.php:84 +msgid "Mapbox rejected the access token (HTTP 401): the Mapbox API key configured in JEO Settings is invalid or has expired." +msgstr "Mapbox rechazó el token de acceso (HTTP 401): la clave de API de Mapbox configurada en Configuraciones de JEO no es válida o ha caducado." + +#: includes/settings/settings-page.php:144 +msgid "Public access token. Used for MapboxGL rendering, Mapbox layers, and AI layer generation. This token is exposed to site visitors (it is required to display maps), so keep its scopes read-only." +msgstr "Token de acceso público. Se usa para el renderizado con MapboxGL, las capas de Mapbox y la generación de capas con IA. Este token queda expuesto a los visitantes del sitio (es necesario para mostrar los mapas), así que mantenga sus ámbitos como de solo lectura." + +#: includes/settings/settings-page.php:148 +msgid "Mapbox Secret Key" +msgstr "Clave secreta de Mapbox" + +#: includes/settings/settings-page.php:150 +msgid "Ex. sk.eyJ3..." +msgstr "Ej. sk.eyJ3..." + +#: includes/settings/settings-page.php:151 +msgid "Used to create AI-assisted layers (requires the styles:write scope). For security, use a secret token — the public token is visible to site visitors. If empty, publishing falls back to the public token." +msgstr "Se usa para crear capas asistidas por IA (requiere el ámbito styles:write). Por seguridad, use un token secreto; el token público es visible para los visitantes del sitio. Si está vacío, la publicación recurre al token público." + +#: includes/layers/class-layers.php:254 +msgid "AI-suggested default style (filter/paint/layout) applied when a map layer instance sets style.use_default" +msgstr "Estilo predeterminado sugerido por IA (filter/paint/layout) que se aplica cuando una instancia de capa del mapa define style.use_default" + +#: includes/maps/class-maps.php:190 +msgid "Per-instance paint and layout overrides for vector and GeoJSON layers" +msgstr "Anulaciones de paint y layout por instancia para capas vectoriales y GeoJSON" + +#: includes/maps/class-maps.php:194 +msgid "Whether to use the layer default_style meta instead of the instance paint" +msgstr "Indica si se usa el metadato default_style de la capa en lugar del paint de la instancia" + +#: includes/maps/class-maps.php:206 +msgid "MapLibre filter expression" +msgstr "Expresión de filtro de MapLibre" + +#: includes/layer-types/geojson.js:188 +msgid "GeoJSON" +msgstr "GeoJSON" + +#: includes/layer-types/geojson.js:229 +msgid "GeoJSON URL" +msgstr "URL del GeoJSON" + +#: includes/layer-types/geojson.js:230 +msgid "Public URL of a GeoJSON file (usually a FeatureCollection). Used when no inline GeoJSON is provided." +msgstr "URL pública de un archivo GeoJSON (normalmente una FeatureCollection). Se usa cuando no se proporciona un GeoJSON incrustado." + +#: includes/layer-types/geojson.js:238 +msgid "Inline GeoJSON" +msgstr "GeoJSON incrustado" + +#: includes/layer-types/geojson.js:239 +msgid "Optional. Paste a raw GeoJSON object or FeatureCollection. Takes precedence over the URL — useful for small geometries." +msgstr "Opcional. Pegue un objeto GeoJSON o una FeatureCollection sin procesar. Tiene prioridad sobre la URL; útil para geometrías pequeñas." + +#: includes/layer-types/geojson.js:246 +msgid "Layer style" +msgstr "Estilo de la capa" + +#: includes/layer-types/geojson.js:247 +msgid "How the GeoJSON features are drawn." +msgstr "Cómo se dibujan las entidades del GeoJSON." + +#: includes/class-jeo.php:2169 +msgid "A valid http(s) style URL is required." +msgstr "Se requiere una URL de estilo http(s) válida." + +#: includes/class-jeo.php:2184 +#: includes/class-jeo.php:2217 +msgid "Unexpected style JSON payload." +msgstr "Contenido JSON de estilo inesperado." + +#: includes/layer-types/style-json.js:5 +msgid "Style JSON" +msgstr "JSON de estilo" + +#: includes/layer-types/style-json.js:30 +msgid "Style URL" +msgstr "URL del estilo" + +#: includes/layer-types/style-json.js:31 +msgid "Public URL of a MapLibre GL style JSON file. Used when no inline style is provided. Works with keyless providers such as OpenFreeMap (e.g. https://tiles.openfreemap.org/styles/dark)." +msgstr "URL pública de un archivo JSON de estilo de MapLibre GL. Se usa cuando no se proporciona un estilo incrustado. Funciona con proveedores sin clave como OpenFreeMap (p. ej., https://tiles.openfreemap.org/styles/dark)." + +#: includes/layer-types/style-json.js:39 +msgid "Inline style JSON" +msgstr "JSON de estilo incrustado" + +#: includes/layer-types/style-json.js:40 +msgid "Optional. Paste a raw MapLibre GL style object. Takes precedence over the URL — useful for small, self-contained styles." +msgstr "Opcional. Pegue un objeto de estilo de MapLibre GL sin procesar. Tiene prioridad sobre la URL; útil para estilos pequeños y autónomos." + +#: includes/ai/class-context-handler.php:496 +#: includes/minimap/class-minimap.php:321 +#: includes/minimap/class-minimap.php:442 +msgid "The previous request could not be processed. Please try again." +msgstr "No se pudo procesar la solicitud anterior. Intente nuevamente." + +#: includes/ai/class-funai-place-polygon-adapter.php:91 +msgid "FUNAI" +msgstr "FUNAI" + +#: includes/ai/class-ibge-place-polygon-adapter.php:85 +msgid "IBGE" +msgstr "IBGE" + +#. translators: %s: tileset ID. +#: includes/ai/class-minilayer-service.php:149 +#, php-format +msgid "Mapbox tileset %s" +msgstr "Tileset %s de Mapbox" + +#: includes/ai/class-osm-place-polygon-adapter.php:106 +msgid "OpenStreetMap contributors" +msgstr "Colaboradores de OpenStreetMap" + +#. translators: %s: municipality name with state abbreviation. +#: includes/ai/class-place-polygon-service.php:326 +#, php-format +msgid "Layer showing the area of the municipality of %s" +msgstr "Capa con el área del municipio de %s" + +#. translators: %s: state name. +#: includes/ai/class-place-polygon-service.php:332 +#, php-format +msgid "Layer showing the area of the state of %s" +msgstr "Capa con el área del departamento de %s" + +#. translators: %s: indigenous land name. +#: includes/ai/class-place-polygon-service.php:338 +#, php-format +msgid "Layer showing the indigenous land %s" +msgstr "Capa con el área de la tierra indígena %s" + +#. translators: %s: place display name. +#: includes/ai/class-place-polygon-service.php:344 +#, php-format +msgid "Layer showing the area of %s" +msgstr "Capa con el área de %s" + +#: js/build/mapBlocks.js:1 +msgid "Understand chosen layers" +msgstr "Entender capas elegidas" + +#: includes/ai/class-retrieve-knowledge-tool.php:112 +msgid "Provide either \"query\" (string) or \"queries\" (array of strings)." +msgstr "Proporcione \"query\" (cadena) o \"queries\" (matriz de cadenas)." + +#: js/build/contextSidebar.js:1 +msgid "Ask for changes or new paragraphs... (Shift+Enter for a line break)" +msgstr "Pida cambios o nuevos párrafos... (Shift+Enter para nueva línea)" + +#: js/build/mapBlocks.js:1 +msgid "Ask the AI to refine the map… (Shift+Enter for a line break)" +msgstr "Pida a la IA que refine el mapa… (Shift+Enter para nueva línea)" + +#: js/build/mapBlocks.js:1 +msgid "Describe the map you want… (Shift+Enter for a line break)" +msgstr "Describa el mapa que quiere… (Shift+Enter para nueva línea)" + +#. translators: %d: number of converted style values. +#: includes/maps/class-map-style-composer.php:1387 +#, php-format +msgid "Converted %d legacy Mapbox stop-function value(s) to style-spec expressions for MapLibre compatibility." +msgstr "Se convirtieron %d valor(es) de función de stops heredada de Mapbox en expresiones style-spec para compatibilidad con MapLibre." + +#. translators: %d: number of converted style values. +#: includes/maps/class-map-style-composer.php:1408 +#, php-format +msgid "Converted %d legacy Mapbox token string(s) to style-spec expressions for MapLibre compatibility." +msgstr "Se convirtieron %d cadena(s) de token heredada(s) de Mapbox en expresiones style-spec para compatibilidad con MapLibre." + +#: includes/maps/class-map-style-composer.php:3137 +msgid "Could not encode the composed style artifact as JSON." +msgstr "No se pudo codificar el artefacto del estilo compuesto como JSON." + +#. translators: %s: artifact file name. +#: includes/maps/class-map-style-composer.php:3148 +#, php-format +msgid "Could not write the composed style artifact %s. Check that the uploads directory is writable." +msgstr "No se pudo escribir el artefacto del estilo compuesto %s. Compruebe que el directorio de uploads permita la escritura." diff --git a/src/languages/jeowp-pt_BR.po b/src/languages/jeowp-pt_BR.po index 303d1c833..fc2c84f28 100755 --- a/src/languages/jeowp-pt_BR.po +++ b/src/languages/jeowp-pt_BR.po @@ -1,25 +1,20 @@ msgid "" msgstr "" -"Project-Id-Version: JEO 3.1.2\n" +"Project-Id-Version: \n" "Report-Msgid-Bugs-To: https://wordpress.org/support/plugin/jeowp\n" "Last-Translator: Stefano Wrobleski\n" -"Language-Team: InfoAmazonia\n" +"Language-Team: InfoAmazoniadevelop1.0\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "POT-Creation-Date: 2026-06-09T19:53:28+00:00\n" -"PO-Revision-Date: 2026-06-09T19:53:28+00:00\n" +"PO-Revision-Date: 2026-07-17 12:00-0300\n" "Language: pt_BR\n" "Plural-Forms: nplurals=2; plural=n != 1;\n" -"X-Generator: Poedit 3.9\n" "X-Domain: jeowp\n" +"X-Generator: Poedit 3.9\n" "X-Loco-Version: 2.5.0; wp-5.7\n" -#: includes/menu/class-menu.php:37 -#: includes/menu/class-menu.php:38 -msgid "JEO" -msgstr "JEO" - #. Plugin URI of the plugin #. Author URI of the plugin #: jeo.php @@ -36,63 +31,58 @@ msgstr "Mapas interativos para o editor de blocos do WordPress" msgid "InfoAmazonia" msgstr "InfoAmazonia" -#: includes/class-jeo.php:583 +#: includes/class-jeo.php:931 msgid "Mapbox was selected as the rendering library, but no Mapbox API key is configured. Falling back to MapLibre." msgstr "Mapbox foi selecionado como biblioteca de renderização, mas nenhuma chave de API do Mapbox está configurada. Voltando para MapLibre." -#: includes/class-jeo.php:584 +#: includes/class-jeo.php:932 msgid "Mapbox was selected as the rendering library, but the external Mapbox SDK could not be loaded. Falling back to MapLibre." msgstr "Mapbox foi selecionado como biblioteca de renderização, mas o SDK externo do Mapbox não pôde ser carregado. Voltando para MapLibre." -#: includes/class-jeo.php:1107 +#: includes/class-jeo.php:1470 +#: js/build/mapBlocks.js:3 msgid "Read more" msgstr "Ler mais" -#: includes/geocode/class-geocode-handler.php:130 -msgid "Geocoding service provided by OpenStreetMap at https://nominatim.openstreetmap.org/." -msgstr "Serviço de geocodificação fornecido pelo OpenStreetMap em https://nominatim.openstreetmap.org/." - -#: includes/geocode/class-geocode-handler.php:231 -msgid "Metadata storing the locations related to the post. Each location is an object with latitude, longitude, and geocode attributes." -msgstr "Metadados que armazenam as localizações relacionadas ao post. Cada localização é um objeto com atributos de latitude, longitude e geocodificação." - -#: includes/geocode/class-geocode-handler.php:247 +#: includes/ai/settings/tab-embedded.php:74 +#: includes/geocode/class-geocode-handler.php:265 #: js/build/mapBlocks.js:3 #: js/build/mapsSidebar.js:1 msgid "Latitude" msgstr "Latitude" -#: includes/geocode/class-geocode-handler.php:261 +#: includes/ai/settings/tab-embedded.php:75 +#: includes/geocode/class-geocode-handler.php:282 #: js/build/mapBlocks.js:3 #: js/build/mapsSidebar.js:1 msgid "Longitude" msgstr "Longitude" -#: includes/geocode/class-geocode-handler.php:275 +#: includes/geocode/class-geocode-handler.php:296 msgid "Country" msgstr "País" -#: includes/geocode/class-geocode-handler.php:289 +#: includes/geocode/class-geocode-handler.php:310 msgid "Country code" msgstr "Código do país" -#: includes/geocode/class-geocode-handler.php:303 +#: includes/geocode/class-geocode-handler.php:324 msgid "Region level 1" msgstr "Região nível 1" -#: includes/geocode/class-geocode-handler.php:317 +#: includes/geocode/class-geocode-handler.php:338 msgid "Region level 2" msgstr "Região nível 2" -#: includes/geocode/class-geocode-handler.php:331 +#: includes/geocode/class-geocode-handler.php:352 msgid "Region level 3" msgstr "Região nível 3" -#: includes/geocode/class-geocode-handler.php:345 +#: includes/geocode/class-geocode-handler.php:366 msgid "City" msgstr "Cidade" -#: includes/geocode/class-geocode-handler.php:359 +#: includes/geocode/class-geocode-handler.php:380 msgid "City sub-level 1" msgstr "Sub-nível de cidade 1" @@ -164,50 +154,50 @@ msgstr "Camada atualizada." msgid "JEO Layers" msgstr "Camadas JEO" -#: includes/layers/class-layers.php:108 +#: includes/layers/class-layers.php:131 msgid "The layer type" msgstr "Tipo da camada" -#: includes/layers/class-layers.php:120 +#: includes/layers/class-layers.php:143 msgid "Layer attribution as text or HTML with a link" msgstr "Atribuição da camada em texto ou HTML com um link" -#: includes/layers/class-layers.php:137 +#: includes/layers/class-layers.php:160 msgid "Layer type-specific options" msgstr "Opções específicas do tipo de camada" -#: includes/layers/class-layers.php:149 +#: includes/layers/class-layers.php:172 msgid "The URL to download the source data of the layer" msgstr "O URL para baixar os dados de origem da camada" -#: includes/layers/class-layers.php:161 +#: includes/layers/class-layers.php:184 msgid "Label for the attribution URL link" msgstr "Rótulo do link de URL da atribuição" -#: includes/layers/class-layers.php:173 +#: includes/layers/class-layers.php:196 msgid "The legend type" msgstr "Tipo da legenda" -#: includes/layers/class-layers.php:190 +#: includes/layers/class-layers.php:213 msgid "Legend type-specific options" msgstr "Opções específicas do tipo de legenda" -#: includes/layers/class-layers.php:202 +#: includes/layers/class-layers.php:225 #: js/build/layersSidebar.js:7 msgid "Use legend" msgstr "Usar legenda" -#: includes/layers/class-layers.php:214 +#: includes/layers/class-layers.php:237 #: js/build/layersSidebar.js:7 msgid "Legend title" msgstr "Título da legenda" -#: includes/layers/class-layers.php:265 +#: includes/layers/class-layers.php:335 msgid "Layer type not registered" msgstr "Tipo de camada não registrado" #. translators: Explore is the name of JEO's discovery page feature. -#: includes/loaders.php:535 +#: includes/loaders.php:591 #: templates/embed-discovery.php:18 #: js/build/discovery.js:6 msgid "Explore" @@ -219,7 +209,6 @@ msgid "Maps" msgstr "Mapas" #: includes/maps/class-maps.php:55 -#: includes/settings/settings-page.php:34 msgid "Map" msgstr "Mapa" @@ -273,6 +262,7 @@ msgid "Map scheduled." msgstr "Mapa agendado." #: includes/maps/class-maps.php:70 +#: js/build/mapBlocks.js:1 msgid "Map updated." msgstr "Mapa atualizado." @@ -323,304 +313,174 @@ msgstr "Indica se a legenda dessa camada deve ser exibida ou não" msgid "Indicates if the legend of this layer should be displayed or not" msgstr "Indica se a legenda dessa camada deve ser exibida ou não" -#: includes/maps/class-maps.php:198 +#: includes/maps/class-maps.php:220 msgid "The map Layers" msgstr "Camadas do mapa" -#: includes/maps/class-maps.php:211 +#: includes/maps/class-maps.php:233 #: js/build/mapBlocks.js:1 msgid "East pan limit" msgstr "Limite de panorâmica leste" -#: includes/maps/class-maps.php:215 +#: includes/maps/class-maps.php:237 #: js/build/mapBlocks.js:1 msgid "North pan limit" msgstr "Limite de panorâmica norte" -#: includes/maps/class-maps.php:219 +#: includes/maps/class-maps.php:241 #: js/build/mapBlocks.js:1 msgid "South pan limit" msgstr "Limite de panorâmica sul" -#: includes/maps/class-maps.php:223 +#: includes/maps/class-maps.php:245 #: js/build/mapBlocks.js:1 msgid "West pan limit" msgstr "Limite de panorâmica oeste" -#: includes/maps/class-maps.php:233 +#: includes/maps/class-maps.php:255 msgid "Map pan limits" msgstr "Limites de panorâmica do mapa" -#: includes/maps/class-maps.php:246 +#: includes/maps/class-maps.php:268 msgid "List of category IDs related posts must have" msgstr "Lista de IDs das categorias que os posts relacionados devem ter" -#: includes/maps/class-maps.php:253 +#: includes/maps/class-maps.php:275 msgid "List of tag IDs related posts must have" msgstr "Lista de IDs das etiquetas que os posts relacionados devem ter" -#: includes/maps/class-maps.php:260 +#: includes/maps/class-maps.php:282 msgid "Maximum date for related posts" msgstr "Data máxima para posts relacionados" -#: includes/maps/class-maps.php:264 +#: includes/maps/class-maps.php:286 msgid "Minimum date for related posts" msgstr "Data mínima para posts relacionados" -#: includes/maps/class-maps.php:268 +#: includes/maps/class-maps.php:290 msgid "List of meta values related posts must have" msgstr "Lista de IDs dos meta-values que os posts relacionados devem ter" -#: includes/maps/class-maps.php:292 +#: includes/maps/class-maps.php:314 msgid "The map criteria to get related posts" msgstr "Os critérios do mapa para obter posts relacionados" -#: includes/maps/class-maps.php:305 +#: includes/maps/class-maps.php:327 msgid "Disable scroll zoom" msgstr "Desabilitar zoom" -#: includes/maps/class-maps.php:318 +#: includes/maps/class-maps.php:340 msgid "Disable drag pan" msgstr "Desabilitar arrastar" -#: includes/maps/class-maps.php:331 +#: includes/maps/class-maps.php:353 msgid "Disable drag rotation" msgstr "Desabilitar rotação" -#: includes/maps/class-maps.php:344 +#: includes/maps/class-maps.php:366 msgid "Enable fullscreen button" msgstr "Habilitar botão de tela cheia" -#: includes/maps/class-maps.php:357 +#: includes/maps/class-maps.php:379 msgid "Show all posts on map" msgstr "Mostrar todos os posts no mapa" -#: includes/maps/class-maps.php:370 +#: includes/maps/class-maps.php:392 msgid "Minimum Zoom level" msgstr "Nível mínimo de zoom" -#: includes/maps/class-maps.php:383 +#: includes/maps/class-maps.php:405 msgid "Maximum zoom level" msgstr "Nível máximo de zoom" -#: includes/maps/class-maps.php:396 +#: includes/maps/class-maps.php:418 #: js/build/mapBlocks.js:3 #: js/build/mapsSidebar.js:1 msgid "Hide in discovery" msgstr "Ocultar na página Explorar" -#: includes/maps/class-maps.php:409 +#: includes/maps/class-maps.php:431 msgid "Disable embed" msgstr "Desabilitar incorporação" -#: includes/maps/class-maps.php:565 +#: includes/maps/class-maps.php:587 msgid "Map zoom must be between 1 and 14" msgstr "O Zoom do mapa deve estar entre 1 e 14" -#: includes/settings/class-settings.php:179 -msgid "Mapbox requires a valid API key. The rendering library setting was not changed." -msgstr "O Mapbox exige uma chave de API válida. A configuração da biblioteca de renderização não foi alterada." - -#: includes/settings/class-settings.php:347 -#: includes/settings/class-settings.php:348 +#: includes/ai/class-ai-settings.php:50 +#: includes/settings/class-settings.php:685 +#: includes/settings/class-settings.php:686 #: js/build/layersSidebar.js:11 msgid "Settings" msgstr "Configurações" -#: includes/settings/settings-page.php:16 -msgid "JEO Settings" -msgstr "Configurações do JEO" - -#: includes/settings/settings-page.php:20 +#: includes/settings/class-settings.php:709 msgid "General" msgstr "Geral" -#: includes/settings/settings-page.php:23 +#: includes/settings/class-settings.php:710 msgid "Geocoders" msgstr "Geocodificadores" -#: includes/settings/settings-page.php:26 -msgid "Customize" -msgstr "Personalização" - -#: includes/settings/settings-page.php:40 +#: includes/settings/settings-page.php:28 msgid "Rendering library" msgstr "Biblioteca de renderização" -#: includes/settings/settings-page.php:50 -msgid "Default map latitute" -msgstr "Latitude padrão do mapa" - #: includes/settings/settings-page.php:57 -msgid "Default map longitude" -msgstr "Longitude padrão do mapa" - -#: includes/settings/settings-page.php:64 msgid "Default map zoom" msgstr "Zoom padrão do mapa" -#: includes/settings/settings-page.php:71 -msgid "Post types" -msgstr "Tipos de post" - -#: includes/settings/settings-page.php:77 -msgid "Show story maps on post archives pages" -msgstr "Mostrar mapas narrativos nas páginas de arquivo de posts" - -#: includes/settings/settings-page.php:84 -msgid "Enabled Post Types. Default: post,storymap" -msgstr "Tipos de post habilitados. Padrão: post,storymap" - -#: includes/settings/settings-page.php:86 -msgid "Post types separated by comma, Ex: map,post,page" -msgstr "Tipos de post separados por vírgula. Ex.: map,post,page" - -#: includes/settings/settings-page.php:91 +#: includes/settings/settings-page.php:137 msgid "Mapbox" msgstr "Mapbox" -#: includes/settings/settings-page.php:94 -msgid "When selected, Mapbox loads the official Mapbox GL JS assets from api.mapbox.com and uses Mapbox terms and privacy practices." -msgstr "Quando selecionado, o Mapbox carrega os ativos oficiais do Mapbox GL JS de api.mapbox.com e usa os termos e práticas de privacidade do Mapbox." - -#: includes/settings/settings-page.php:97 -msgid "Visitors load Mapbox assets in the browser, and Mapbox receives the configured access token plus the visitor IP address and requested map resources." -msgstr "Os visitantes carregam os ativos do Mapbox no navegador, e o Mapbox recebe o token de acesso configurado, além do endereço IP do visitante e dos recursos de mapa solicitados." - -#: includes/settings/settings-page.php:103 +#: includes/ai/settings/tab-provider.php:27 msgid "API Key" msgstr "Chave de API" -#: includes/settings/settings-page.php:105 +#: includes/settings/settings-page.php:143 msgid "Ex. pk.eyJ3..." msgstr "Ex.: pk.eyJ3..." -#: includes/settings/settings-page.php:107 -msgid "This key is required only when the rendering library is set to Mapbox." -msgstr "Esta chave é necessária apenas quando a biblioteca de renderização está definida como Mapbox." - -#: includes/settings/settings-page.php:121 -msgid "Active Geocoder" -msgstr "Geocodificador ativo" - -#: includes/settings/settings-page.php:134 -msgid "The built-in Nominatim geocoder sends the address you submit, or the coordinates selected on the map, to nominatim.openstreetmap.org only when you explicitly run a search or reverse-geocode a point." -msgstr "O geocodificador Nominatim integrado envia o endereço informado, ou as coordenadas selecionadas no mapa, para nominatim.openstreetmap.org somente quando você executa explicitamente uma busca ou uma geocodificação reversa de um ponto." - -#. translators: %s is the geocoder name. Example: Nominatim options. -#: includes/settings/settings-page.php:154 -#, php-format -msgctxt "geocoder_options" -msgid "%s options" -msgstr "%s opções" - -#: includes/settings/settings-page.php:198 +#: includes/settings/settings-page.php:173 msgid "Typography" msgstr "Tipografia" -#: includes/settings/settings-page.php:201 -msgid "Typography URL" -msgstr "URL da tipografia" - -#: includes/settings/settings-page.php:205 -#: includes/settings/settings-page.php:220 -msgid "You may use a local or external stylesheet URL. If you choose an external host, your visitors will request that asset directly from that provider." -msgstr "Você pode usar uma URL local ou externa de folha de estilo. Se escolher um host externo, os visitantes solicitarão esse recurso diretamente a esse provedor." - #: includes/settings/settings-page.php:210 -msgid "Typography name" -msgstr "Nome da tipografia" - -#: includes/settings/settings-page.php:212 -msgid "Ex. Open Sans" -msgstr "Ex.: Open Sans" - -#: includes/settings/settings-page.php:216 -msgid "Secondary Typography URL" -msgstr "URL da tipografia secundária" - -#: includes/settings/settings-page.php:225 -msgid "Secondary Typography name" -msgstr "Nome da tipografia secundária" - -#: includes/settings/settings-page.php:227 -msgid "Ex. Libre Baskerville" -msgstr "Ex.: Libre Baskerville" - -#: includes/settings/settings-page.php:231 -msgid "Info button font-size (rem)" -msgstr "Tamanho da fonte do botão Info (rem)" - -#: includes/settings/settings-page.php:238 +#: js/build/mapBlocks.js:3 +#: js/build/mapsSidebar.js:1 msgid "Colors" msgstr "Cores" -#: includes/settings/settings-page.php:242 -msgid "Primary color" -msgstr "Cor primária" - -#: includes/settings/settings-page.php:244 -#: includes/settings/settings-page.php:258 -#: includes/settings/settings-page.php:264 -#: includes/settings/settings-page.php:276 -msgid "Ex. #ffffff" -msgstr "Ex. #ffffff" - -#: includes/settings/settings-page.php:249 -msgid "Text over primary color" -msgstr "Texto sobre a cor primária" - -#: includes/settings/settings-page.php:256 +#: includes/settings/settings-page.php:220 msgid "Info button background color" msgstr "Cor de fundo do botão Info" -#: includes/settings/settings-page.php:262 -msgid "Info button color" -msgstr "Cor do botão Info" - -#: includes/settings/settings-page.php:268 +#: includes/settings/settings-page.php:232 msgid "Close button background color" msgstr "Cor de fundo do botão de fechar" -#: includes/settings/settings-page.php:274 -msgid "Close button color" -msgstr "Cor do botão de fechar" - -#: includes/settings/settings-page.php:281 -msgid "Embed" -msgstr "Embed" - #: js/build/discovery.js:6 msgctxt "discovery share action" msgid "Embed" msgstr "Embed" -#: includes/settings/settings-page.php:285 +#: includes/settings/settings-page.php:296 msgid "Company logo" msgstr "Logotipo da empresa" -#: includes/settings/settings-page.php:289 +#: includes/settings/settings-page.php:300 msgid "You may use a local or external image URL. Large logos will be scaled down automatically in the embed footer." msgstr "Você pode usar uma URL local ou externa de imagem. Logos grandes serão reduzidos automaticamente no rodapé do embed." -#. translators: 1: Mapbox terms URL, 2: Mapbox privacy URL, 3: Nominatim usage policy URL, 4: OSMF privacy URL, 5: OSM tile policy URL. -#: includes/loaders.php:351 -#, php-format -msgid "

    JEO can connect to third-party services depending on your configuration.

    • Mapbox: only used when Mapbox is selected as the rendering library or when a map uses Mapbox-hosted resources. In that case the site loads JavaScript/CSS from Mapbox and sends the configured access token plus the visitor’s IP address, browser details and requested map resources to Mapbox. Terms: Mapbox Terms of Service. Privacy Policy: Mapbox Privacy Policy.
    • Nominatim (OpenStreetMap): used when an editor explicitly runs an address search or reverse geocoding request in the post geolocation UI. The typed address or selected coordinates, the site URL in the user agent string and the server IP address are sent to the Nominatim service. Usage Policy: Nominatim Usage Policy. Privacy Policy: OpenStreetMap Foundation Privacy Policy.
    • OpenStreetMap raster tiles: the default MapLibre preview style requests map tiles from the OpenStreetMap tile service, which receives the visitor’s IP address, browser details and requested tile URLs. Tile Policy: OpenStreetMap Tile Usage Policy. Privacy Policy: OpenStreetMap Foundation Privacy Policy.
    • Optional external asset URLs configured by the site administrator: if you configure an external typography stylesheet or footer logo URL, visitors’ browsers will request that asset directly from the selected host. That host may receive the visitor’s IP address, browser details and referrer according to its own terms and privacy policy.
    " -msgstr "

    O JEO pode se conectar a serviços de terceiros dependendo da sua configuração.

    • Mapbox: usado apenas quando o Mapbox é selecionado como biblioteca de renderização ou quando um mapa usa recursos hospedados no Mapbox. Nesse caso, o site carrega JavaScript/CSS do Mapbox e envia o token de acesso configurado, além do endereço IP do visitante, detalhes do navegador e recursos de mapa solicitados ao Mapbox. Termos: Termos de Serviço do Mapbox. Política de Privacidade: Política de Privacidade do Mapbox.
    • Nominatim (OpenStreetMap): usado quando um editor executa explicitamente uma busca de endereço ou uma solicitação de geocodificação reversa na interface de geolocalização de posts. O endereço digitado ou as coordenadas selecionadas, a URL do site na string do user agent e o endereço IP do servidor são enviados ao serviço Nominatim. Política de Uso: Política de Uso do Nominatim. Política de Privacidade: Política de Privacidade da OpenStreetMap Foundation.
    • Tiles raster do OpenStreetMap: o estilo padrão de pré-visualização do MapLibre solicita tiles ao serviço de tiles do OpenStreetMap, que recebe o endereço IP do visitante, detalhes do navegador e URLs de tiles solicitadas. Política de Tiles: Política de Uso de Tiles do OpenStreetMap. Política de Privacidade: Política de Privacidade da OpenStreetMap Foundation.
    • URLs externas opcionais de assets configuradas pelo administrador do site: se você configurar uma URL externa para a folha de estilo tipográfica ou para o logo do rodapé, os navegadores dos visitantes solicitarão esse recurso diretamente ao host selecionado. Esse host pode receber o endereço IP do visitante, detalhes do navegador e o referenciador, de acordo com seus próprios termos e política de privacidade.
    " - -#: includes/settings/settings-page.php:291 +#: includes/settings/settings-page.php:302 msgid "Insert Image" msgstr "Inserir imagem" -#: includes/settings/settings-page.php:301 +#: includes/settings/settings-page.php:331 msgid "Save Changes" msgstr "Salvar alterações" -#: includes/settings/class-settings.php:317 -msgid "JEO could not save this asset URL. Use a valid http(s) or root-relative URL." -msgstr "O JEO não pôde salvar esta URL de recurso. Use uma URL http(s) válida ou relativa à raiz." - #. translators: Story Map is the name of JEO's storytelling map feature. #: includes/storymap/class-storymap.php:69 #: includes/storymap/class-storymap.php:70 @@ -703,11 +563,12 @@ msgstr "Voltar para página inicial" #: templates/map-content-layers-list.php:18 #: templates/map-content-layers-list.php:20 +#: templates/map-content-layers-list.php:22 #: js/build/jeoMap.js:1 msgid "Attribution:" msgstr "Atribuição:" -#: templates/map-content-layers-list.php:25 +#: templates/map-content-layers-list.php:27 #: js/build/jeoMap.js:1 msgid "Download from source" msgstr "Baixar da fonte" @@ -720,44 +581,44 @@ msgstr "Baixar da fonte" msgid "Raster Mapbox Tiled Source" msgstr "Fonte raster de tiles do Mapbox" -#: includes/layer-types/mapbox-tileset-raster.js:68 -#: includes/layer-types/mapbox-tileset-vector.js:70 +#: includes/layer-types/mapbox-tileset-raster.js:72 +#: includes/layer-types/mapbox-tileset-vector.js:109 #: js/build/layersSidebar.js:11 #: js/build/mapBlocks.js:1 msgid "Tileset ID" msgstr "ID do tileset" -#: includes/layer-types/mapbox-tileset-raster.js:69 -#: includes/layer-types/mapbox-tileset-vector.js:71 +#: includes/layer-types/mapbox-tileset-raster.js:73 +#: includes/layer-types/mapbox-tileset-vector.js:110 #: js/build/layersSidebar.js:11 #: js/build/mapBlocks.js:1 msgid "Example: username.tilesetid" msgstr "Exemplo: username.tilesetid" -#: includes/layer-types/mapbox-tileset-raster.js:72 -#: includes/layer-types/mapbox-tileset-vector.js:99 -#: includes/layer-types/mvt.js:101 +#: includes/layer-types/mapbox-tileset-raster.js:76 +#: includes/layer-types/mapbox-tileset-vector.js:138 +#: includes/layer-types/mvt.js:133 #: js/build/layersSidebar.js:11 #: js/build/mapBlocks.js:1 msgid "Style Source Type" msgstr "Tipo de fonte do estilo" -#: includes/layer-types/mapbox-tileset-raster.js:73 -#: includes/layer-types/mvt.js:102 +#: includes/layer-types/mapbox-tileset-raster.js:77 +#: includes/layer-types/mvt.js:134 #: js/build/layersSidebar.js:11 #: js/build/mapBlocks.js:1 msgid "Which data the map should display" msgstr "Quais dados o mapa deve exibir" -#: includes/layer-types/mapbox-tileset-raster.js:79 -#: includes/layer-types/mapbox-tileset-vector.js:79 +#: includes/layer-types/mapbox-tileset-raster.js:83 +#: includes/layer-types/mapbox-tileset-vector.js:118 #: js/build/layersSidebar.js:11 #: js/build/mapBlocks.js:1 msgid "Layer Type" msgstr "Tipo da camada" -#: includes/layer-types/mapbox-tileset-raster.js:81 -#: includes/layer-types/mapbox-tileset-vector.js:81 +#: includes/layer-types/mapbox-tileset-raster.js:85 +#: includes/layer-types/mapbox-tileset-vector.js:120 #: js/build/layersSidebar.js:11 #: js/build/mapBlocks.js:1 msgid "Layers take the data that they get from a source, optionally filter features, and then define how those features are styled." @@ -771,20 +632,20 @@ msgstr "As camadas usam os dados que recebem de uma fonte, podem filtrar feiçõ msgid "Vector Mapbox Tiled Source" msgstr "Fonte vetorial de tiles do Mapbox" -#: includes/layer-types/mapbox-tileset-vector.js:75 -#: includes/layer-types/mvt.js:83 +#: includes/layer-types/mapbox-tileset-vector.js:114 +#: includes/layer-types/mvt.js:115 #: js/build/layersSidebar.js:11 #: js/build/mapBlocks.js:1 msgid "Source layer" msgstr "Camada de origem" -#: includes/layer-types/mapbox-tileset-vector.js:76 +#: includes/layer-types/mapbox-tileset-vector.js:115 #: js/build/layersSidebar.js:11 #: js/build/mapBlocks.js:1 msgid "Which data the map should display." msgstr "Quais dados o mapa deve exibir." -#: includes/layer-types/mapbox-tileset-vector.js:100 +#: includes/layer-types/mapbox-tileset-vector.js:139 #: js/build/layersSidebar.js:11 #: js/build/mapBlocks.js:1 msgid "The layer source type style" @@ -798,25 +659,31 @@ msgstr "O tipo de fonte do estilo da camada" msgid "Mapbox Style" msgstr "Estilo do Mapbox" -#: includes/layer-types/mapbox.js:117 +#: includes/layer-types/mapbox.js:119 #: js/build/layersSidebar.js:11 #: js/build/mapBlocks.js:1 msgid "Style ID" msgstr "ID do estilo" -#: includes/layer-types/mapbox.js:119 +#: includes/layer-types/mapbox.js:121 #: js/build/layersSidebar.js:11 #: js/build/mapBlocks.js:1 msgid "The Mapbox Style ID includes the user name and id. Example: username/id or mapbox://styles/username/id" msgstr "O ID do estilo do Mapbox inclui o nome de usuário e o ID. Exemplo: username/id ou mapbox://styles/username/id" -#: includes/layer-types/mapbox.js:126 +#: includes/layer-types/mapbox-tileset-raster.js:95 +#: includes/layer-types/mapbox-tileset-vector.js:146 +#: includes/layer-types/mapbox.js:128 +#: includes/layer-types/mvt.js:141 #: js/build/layersSidebar.js:11 #: js/build/mapBlocks.js:1 msgid "Access token" msgstr "Token de acesso" -#: includes/layer-types/mapbox.js:128 +#: includes/layer-types/mapbox-tileset-raster.js:96 +#: includes/layer-types/mapbox-tileset-vector.js:147 +#: includes/layer-types/mapbox.js:130 +#: includes/layer-types/mvt.js:142 #: js/build/layersSidebar.js:11 #: js/build/mapBlocks.js:1 msgid "Optional. If this layer needs a different access token from the one set in Settings, inform it here." @@ -830,8 +697,9 @@ msgstr "Opcional. Se esta camada precisar de um token de acesso diferente do def msgid "Mapbox Vector Tiles (MVT)" msgstr "Tiles vetoriais do Mapbox (MVT)" -#: includes/layer-types/mvt.js:79 -#: includes/layer-types/tilelayer.js:57 +#: includes/ai/settings/tab-provider.php:27 +#: includes/layer-types/mvt.js:111 +#: includes/layer-types/tilelayer.js:61 #: includes/legend-types/circles.js:25 #: includes/legend-types/icons.js:25 #: js/build/layersSidebar.js:11 @@ -839,7 +707,7 @@ msgstr "Tiles vetoriais do Mapbox (MVT)" msgid "URL" msgstr "URL" -#: includes/layer-types/mvt.js:84 +#: includes/layer-types/mvt.js:116 #: js/build/layersSidebar.js:11 #: js/build/mapBlocks.js:1 msgid "Layer to use from a vector tile source." @@ -853,25 +721,25 @@ msgstr "Camada a ser usada de uma fonte de tiles vetoriais." msgid "Raster Tiled Source" msgstr "Fonte raster de tiles" -#: includes/layer-types/tilelayer.js:61 +#: includes/layer-types/tilelayer.js:65 #: js/build/layersSidebar.js:11 #: js/build/mapBlocks.js:1 msgid "Scheme" msgstr "Esquema" -#: includes/layer-types/tilelayer.js:62 +#: includes/layer-types/tilelayer.js:66 #: js/build/layersSidebar.js:11 #: js/build/mapBlocks.js:1 msgid "Influences the Y direction of the tile coordinates." msgstr "Influencia a direção Y das coordenadas dos tiles." -#: includes/layer-types/tilelayer.js:65 +#: includes/layer-types/tilelayer.js:69 #: js/build/layersSidebar.js:11 #: js/build/mapBlocks.js:1 msgid "Slippy Map tilenames (XYZ)" msgstr "Nomes de tiles do Slippy Map (XYZ)" -#: includes/layer-types/tilelayer.js:66 +#: includes/layer-types/tilelayer.js:70 #: js/build/layersSidebar.js:11 #: js/build/mapBlocks.js:1 msgid "OSGeo spec (TMS)" @@ -916,6 +784,7 @@ msgstr "Uma lista de rótulos e ícones" #: includes/legend-types/JeoLegend.js:181 #: includes/legend-types/JeoLegend.js:186 #: js/build/JeoLegend.js:1 +#: js/build/layersSidebar.js:1 #: js/build/layersSidebar.js:7 msgid "Default label" msgstr "Rótulo padrão" @@ -929,11 +798,7 @@ msgstr "Categorias de cores" msgid "An array of labels and colors" msgstr "Uma lista de rótulos e cores" -#: includes/settings/settings-page.js:37 -#: includes/settings/settings-page.js:39 -msgid "Choose Image" -msgstr "Escolher imagem" - +#: js/build/contextSidebar.js:1 #: js/build/discovery.js:1 #: js/build/discovery.js:6 #: js/build/mapBlocks.js:3 @@ -987,6 +852,8 @@ msgstr "DD/MM/YYYY" msgid " - " msgstr " - " +#: includes/admin/uninstall-page.php:76 +#: includes/ai/class-bulk-processor.php:671 #: js/build/discovery.js:2 #: js/build/mapBlocks.js:1 #: js/build/postsSidebar.js:1 @@ -1121,12 +988,6 @@ msgstr "Intervalo de data" msgid "Date range: %s" msgstr "Intervalo de data: %s" -#. translators: %d is the number of stories currently displayed. -#: js/build/discovery.js:3 -#, js-format -msgid "Displayed stories: %d" -msgstr "Histórias exibidas: %d" - #. translators: %s is the list of active story tags. #: js/build/discovery.js:6 #, js-format @@ -1169,14 +1030,6 @@ msgstr "Não foi possível carregar as camadas do mapa agora." msgid "Search map" msgstr "Buscar mapa" -#: js/build/discovery.js:6 -msgid "Changes applied" -msgstr "Mudanças aplicadas" - -#: js/build/discovery.js:6 -msgid "Not applied" -msgstr "Não aplicado" - #: js/build/discovery.js:6 #: js/build/mapBlocks.js:3 #: js/build/mapsSidebar.js:1 @@ -1229,22 +1082,6 @@ msgstr "Fechar pop-up" msgid "Info" msgstr "Informações" -#: js/build/jeoMap.js:1 -msgid "Show previous post" -msgstr "Mostrar post anterior" - -#: js/build/jeoMap.js:1 -msgid "Show next post" -msgstr "Mostrar próximo post" - -#. translators: %d: number of markers in the current map area. -#: js/build/jeoMap.js:2 -#, js-format -msgid "%d marker in this area" -msgid_plural "%d markers in this area" -msgstr[0] "%d marcador nesta área" -msgstr[1] "%d marcadores nesta área" - #: js/build/jeoStorymap.js:3 msgid "at" msgstr "em" @@ -1308,6 +1145,7 @@ msgstr "Remover" msgid "See preview" msgstr "Ver prévisualização" +#: includes/ai/settings/tab-embedded.php:54 #: js/build/layersSidebar.js:7 #: js/build/mapBlocks.js:1 msgid "Preview" @@ -1344,6 +1182,7 @@ msgid "Type" msgstr "Tipo" #: js/build/layersSidebar.js:7 +#: js/build/mapBlocks.js:1 msgid "No" msgstr "Não" @@ -1393,30 +1232,22 @@ msgstr "Feito" msgid "Edit interactions" msgstr "Editar interações" -#: js/build/layersSidebar.js:11 +#: js/build/mapBlocks.js:1 msgid "Please fill all required fields, you will not be able to publish or update until that." msgstr "Por favor preencha os campos requeridos, você não poderá publicar ou atualizar até lá." -#: js/build/layersSidebar.js:11 +#: js/build/mapBlocks.js:1 msgid "Your Mapbox access token may be invalid. You will not be able to publish or update. Please check your settings." msgstr "Seu token de acesso do Mapbox pode ser inválido. Você não poderá publicar ou atualizar. Por favor, verifique suas configurações." -#: js/build/layersSidebar.js:11 +#: js/build/mapBlocks.js:1 msgid "Your layer was not found. You will not be able to publish or update. Please check your settings." msgstr "Sua camada não foi encontrada. Você não poderá publicar ou atualizar. Por favor, verifique suas configurações." -#: js/build/layersSidebar.js:11 +#: js/build/mapBlocks.js:1 msgid "Error loading your layer, you will not be able to publish or update. Please check your settings." msgstr "Erro ao carregar sua camada, você não poderá publicar ou atualizar. Por favor verifique as configurações." -#: js/build/layersSidebar.js:11 -msgid "Your Mapbox API Key was not found in your JEO Settings. You will not be able to publish or update." -msgstr "Sua chave API do Mapbox não foi encontrada nas configurações do JEO. Você não poderá publicar ou atualizar." - -#: js/build/layersSidebar.js:11 -msgid "Please, check your settings." -msgstr "Por favor, verifique suas configurações." - #: js/build/layersSidebar.js:11 msgid "Attributions" msgstr "Atribuições" @@ -1577,6 +1408,8 @@ msgstr "Não foi possível carregar as sugestões de endereço." msgid "Search address" msgstr "Pesquisar endereço" +#: includes/ai/settings/tab-knowledge.php:299 +#: includes/settings/class-settings.php:642 #: js/build/mapBlocks.js:1 #: js/build/postsSidebar.js:1 msgid "Search" @@ -1658,32 +1491,11 @@ msgstr "Exibir mapas com narrativa" msgid "Embedded Story Map" msgstr "Mapa Narrativo Incorporado" -#: js/build/mapBlocks.js:3 -msgid "Map Editor Preview" -msgstr "Prévia do editor de mapas" - -#: js/build/mapBlocks.js:3 -msgid "Interactive map preview for the Map post type editor." -msgstr "Prévia interativa do mapa para o editor do tipo de post Mapa." - -#: js/build/mapBlocks.js:3 -msgid "Layer Editor Preview" -msgstr "Prévia do editor de camadas" - -#: js/build/mapBlocks.js:3 -msgid "Interactive layer preview for the Map Layer post type editor." -msgstr "Prévia interativa da camada para o editor do tipo de post Camada de Mapa." - #: js/build/mapBlocks.js:3 #: js/build/mapsSidebar.js:1 msgid "Loading layers data..." msgstr "Carregando dados das camadas..." -#: js/build/mapBlocks.js:3 -#: js/build/mapsSidebar.js:1 -msgid "Default" -msgstr "Padrão" - #: js/build/mapBlocks.js:3 #: js/build/mapsSidebar.js:1 msgid "Base layer should be fixed" @@ -1864,21 +1676,6 @@ msgstr "Desabilitar incorporação" msgid "Map settings" msgstr "Configurações do mapa" -#: js/build/mapBlocks.js:3 -#: js/build/mapsSidebar.js:1 -msgid "Fixed" -msgstr "Fixado" - -#: js/build/mapBlocks.js:3 -#: js/build/mapsSidebar.js:1 -msgid "Swappable" -msgstr "Trocável" - -#: js/build/mapBlocks.js:3 -#: js/build/mapsSidebar.js:1 -msgid "Switchable" -msgstr "Comutável" - #: js/build/mapBlocks.js:3 #: js/build/mapsSidebar.js:1 msgid "Key" @@ -1928,14 +1725,6 @@ msgstr "Consultas de metadados" msgid "Embed URL" msgstr "URL de incorporação" -#: js/build/postsSidebar.js:1 -msgid "Unable to retrieve the address for the selected point." -msgstr "Não foi possível obter o endereço do ponto selecionado." - -#: js/build/postsSidebar.js:1 -msgid "Type an address, then click Search or press Enter. You can also drag the marker across the map." -msgstr "Digite um endereço e depois clique em Pesquisar ou pressione Enter. Você também pode arrastar o marcador pelo mapa." - #: js/build/postsSidebar.js:1 msgid "Move marker to nearest address" msgstr "Mover o marcador para o endereço mais próximo" @@ -1945,10 +1734,12 @@ msgid "Relevance" msgstr "Relevância" #: js/build/postsSidebar.js:1 +#: js/build/postsSidebar.js:3 msgid "Primary" msgstr "Primário" #: js/build/postsSidebar.js:1 +#: js/build/postsSidebar.js:3 msgid "Secondary" msgstr "Secundário" @@ -1960,179 +1751,3140 @@ msgstr "Atualizar ponto selecionado" msgid "Save new point" msgstr "Salvar novo ponto" -#: js/build/postsSidebar.js:1 +#: js/build/postsSidebar.js:2 msgid "Add new point" msgstr "Adicionar novo ponto" -#: js/build/postsSidebar.js:1 +#: js/build/postsSidebar.js:2 msgid "Current points" msgstr "Pontos atuais" -#: js/build/postsSidebar.js:1 +#: js/build/postsSidebar.js:2 msgid "No points" msgstr "Nenhum ponto" -#: js/build/postsSidebar.js:1 +#: includes/settings/class-settings.php:604 +#: js/build/postsSidebar.js:2 msgid "Delete" msgstr "Apagar" -#: js/build/postsSidebar.js:1 +#: includes/settings/class-settings.php:641 +#: js/build/postsSidebar.js:2 msgid "Edit" msgstr "Editar" -#: js/build/postsSidebar.js:1 +#: js/build/postsSidebar.js:2 +#: js/build/postsSidebar.js:4 msgid "Geolocate this post" msgstr "Geolocalizar este post" -#: js/build/postsSidebar.js:1 +#: js/build/postsSidebar.js:4 msgid "Geolocation" msgstr "Geolocalização" -#: includes/class-jeo.php:382 +#: includes/class-jeo.php:672 msgid "Ordered list of map-layer IDs to return." msgstr "Lista ordenada dos IDs das camadas a serem retornados." -#: includes/class-jeo.php:387 +#: includes/class-jeo.php:677 msgid "Request context." msgstr "Contexto da requisição." -#: includes/settings/settings-page.php:203 -msgid "Ex. https://example.com/wp-content/uploads/fonts/jeo-font.css" -msgstr "Ex. https://exemplo.com/wp-content/uploads/fonts/jeo-font.css" - -#: includes/settings/settings-page.php:218 -msgid "Ex. https://example.com/wp-content/uploads/fonts/jeo-story-font.css" -msgstr "Ex. https://exemplo.com/wp-content/uploads/fonts/jeo-story-font.css" - #. translators: %s is the authors list #: js/build/jeoStorymap.js:3 #, js-format msgid "By %s" msgstr "Por %s" -#: includes/class-jeo.php:1332 -msgid "This JEO embed is not available." -msgstr "Este embed JEO não está disponível." +#: includes/admin/dashboard-page.php:306 +msgid "Toggle panel" +msgstr "Alternar painel" + +#: includes/admin/dashboard-page.php:308 +msgid "JEO Platform" +msgstr "Plataforma JEO" + +#: includes/admin/dashboard-page.php:309 +msgid "Loading geodata..." +msgstr "Carregando geodados..." + +#: includes/admin/dashboard-page.php:310 +msgid "Minimize Dashboard" +msgstr "Minimizar painel" + +#: includes/admin/dashboard-page.php:317 +msgid "Search terms" +msgstr "Termos de busca" + +#: includes/admin/dashboard-page.php:318 +msgid "Title or content..." +msgstr "Título ou conteúdo..." + +#: includes/admin/dashboard-page.php:322 +msgid "Post Type" +msgstr "Tipo de post" + +#: includes/admin/dashboard-page.php:324 +msgid "All Types" +msgstr "Todos os tipos" + +#: includes/admin/dashboard-page.php:329 +msgid "Taxonomy" +msgstr "Taxonomia" + +#: includes/admin/dashboard-page.php:334 +msgid "Term" +msgstr "Termo" + +#: includes/admin/dashboard-page.php:339 +msgid "Apply Filters" +msgstr "Aplicar filtros" + +#: includes/admin/dashboard-page.php:344 +msgid "Timeline Range" +msgstr "Período da linha do tempo" + +#: includes/admin/dashboard-page.php:361 +msgid "Gathering geodata and initializing JEO Engine..." +msgstr "Coletando geodados e inicializando o JEO Engine..." + +#: includes/admin/dashboard-page.php:524 +#: includes/settings/class-settings.php:654 +msgid "Select Taxonomy..." +msgstr "Selecionar taxonomia..." + +#: includes/admin/dashboard-page.php:542 +#: includes/settings/class-settings.php:655 +msgid "Select Term..." +msgstr "Selecionar termo..." + +#: includes/admin/dashboard-page.php:605 +#: includes/settings/class-settings.php:656 +msgid " locations found." +msgstr " localizações encontradas." + +#: includes/admin/dashboard-page.php:617 +#: includes/admin/dashboard-page.php:704 +msgid "Untitled" +msgstr "Sem título" + +#: includes/admin/dashboard-page.php:707 +#: includes/settings/class-settings.php:652 +msgid "View Post" +msgstr "Ver post" + +#: includes/admin/dashboard-page.php:707 +#: includes/settings/class-settings.php:653 +msgid "Edit Post" +msgstr "Editar post" + +#: includes/admin/uninstall-handler.php:30 +#: includes/admin/uninstall-handler.php:48 +#: includes/admin/uninstall-handler.php:49 +msgid "Uninstall JEO" +msgstr "Desinstalar JEO" + +#: includes/admin/uninstall-handler.php:31 +msgid "Uninstall" +msgstr "Desinstalar" + +#: includes/admin/uninstall-handler.php:63 +#: includes/admin/uninstall-page.php:23 +msgid "You do not have permission to uninstall plugins." +msgstr "Você não tem permissão para desinstalar plugins." + +#: includes/admin/uninstall-page.php:50 +msgid "Warning: This action cannot be undone." +msgstr "Aviso: Esta ação não pode ser desfeita." + +#: includes/admin/uninstall-page.php:53 +msgid "Uninstalling JEO will permanently delete the following data from your site:" +msgstr "Desinstalar o JEO excluirá permanentemente os seguintes dados do seu site:" + +#: includes/admin/uninstall-page.php:56 +msgid "All plugin settings stored in the database (map defaults, API keys, appearance settings, etc.)." +msgstr "Todas as configurações do plugin armazenadas no banco de dados (padrões de mapa, chaves de API, configurações de aparência, etc.)." + +#: includes/admin/uninstall-page.php:57 +msgid "All AI usage logs (private posts of type \"jeo-ai-log\")." +msgstr "Todos os logs de uso da IA (posts privados do tipo \"jeo-ai-log\")." + +#: includes/admin/uninstall-page.php:58 +msgid "All geolocation metadata attached to posts (_related_point, _geocode_* index fields)." +msgstr "Todos os metadados de geolocalização anexados aos posts (_related_point, campos de índice _geocode_*)." + +#: includes/admin/uninstall-page.php:59 +msgid "Cached geocoding results (Nominatim transients)." +msgstr "Resultados de geocodificação em cache (transientes do Nominatim)." + +#: includes/admin/uninstall-page.php:60 +msgid "Bulk AI cron logs and embedding token counters." +msgstr "Logs de Cron da IA em lote e contadores de Tokens de Embedding." + +#: includes/admin/uninstall-page.php:61 +msgid "RAG vector store files from the uploads directory (wp-content/uploads/jeo-ai-store/)." +msgstr "Arquivos do Armazenamento de Vetores do RAG do diretório de uploads (wp-content/uploads/jeo-ai-store/)." + +#: includes/admin/uninstall-page.php:62 +msgid "Scheduled cron jobs created by the plugin." +msgstr "Tarefas Cron agendadas criadas pelo plugin." + +#: includes/admin/uninstall-page.php:65 +msgid "Your regular posts, pages, maps, layers, and story maps will remain, but any geolocation data associated with them will be lost." +msgstr "Seus posts, páginas, mapas, camadas e mapas narrativos normais permanecerão, mas qualquer dado de geolocalização associado a eles será perdido." + +#: includes/admin/uninstall-page.php:72 +msgid "I understand that all data listed above will be permanently deleted." +msgstr "Entendo que todos os dados listados acima serão permanentemente excluídos." + +#: includes/admin/uninstall-page.php:75 +msgid "Confirm and Uninstall JEO" +msgstr "Confirmar e Desinstalar JEO" + +#: includes/admin/uninstall-page.php:75 +msgid "Are you absolutely sure? This cannot be undone." +msgstr "Você tem certeza absoluta? Isso não pode ser desfeito." + +#: includes/ai/class-ai-logger.php:39 +msgctxt "Post Type General Name" +msgid "AI Usage Logs" +msgstr "Logs de Uso da IA" + +#: includes/ai/class-ai-logger.php:40 +msgctxt "Post Type Singular Name" +msgid "AI Usage Log" +msgstr "Log de Uso da IA" + +#: includes/ai/class-ai-logger.php:41 +msgid "AI Logs (Costs)" +msgstr "Logs da IA (Custos)" + +#: includes/ai/class-ai-logger.php:42 +msgid "All AI Logs" +msgstr "Todos os Logs da IA" + +#: includes/ai/class-ai-logger.php:43 +msgid "View Log" +msgstr "Ver log" + +#: includes/ai/class-ai-logger.php:47 +msgid "AI Usage Log" +msgstr "Log de Uso da IA" + +#: includes/ai/class-ai-logger.php:48 +msgid "Internal logs for AI token usage and costs." +msgstr "Logs internos para uso de Tokens e custos da IA." + +#: includes/ai/class-ai-settings.php:46 +#: includes/ai/settings/tab-provider.php:12 +msgid "AI Provider" +msgstr "Provedor de IA" + +#: includes/ai/class-ai-settings.php:47 +msgid "Knowledge Base" +msgstr "Base de Conhecimento" + +#: includes/ai/class-ai-settings.php:48 +msgid "Bulk Geolocation" +msgstr "Geolocalização em lote" + +#: includes/ai/class-ai-settings.php:49 +msgid "Context Assistant" +msgstr "Assistente de Contexto" + +#: includes/ai/class-bulk-processor.php:170 +msgid "Bulk clearing started (Background)." +msgstr "Limpeza em lote iniciada (segundo plano)." + +#: includes/ai/class-bulk-processor.php:174 +msgid "Bulk clearing started in background. Posts will be reset in batches." +msgstr "Limpeza em lote iniciada em segundo plano. Os posts serão redefinidos em lotes." + +#: includes/ai/class-bulk-processor.php:194 +#: includes/ai/class-rag-worker.php:101 +#: includes/settings/class-settings.php:623 +msgid "Error" +msgstr "Erro" + +#: includes/ai/class-bulk-processor.php:194 +#: includes/ai/class-rag-worker.php:101 +msgid "Success" +msgstr "Sucesso" + +#: includes/ai/class-bulk-processor.php:223 +#: includes/ai/class-rag-worker.php:68 +#: includes/ai/settings/tab-bulk.php:49 +#: includes/ai/settings/tab-knowledge.php:155 +msgid "Every Minute" +msgstr "A cada minuto" + +#: includes/ai/class-bulk-processor.php:229 +#: includes/ai/class-rag-worker.php:74 +#: includes/ai/settings/tab-bulk.php:50 +#: includes/ai/settings/tab-knowledge.php:156 +msgid "Every 5 Minutes" +msgstr "A cada 5 minutos" + +#: includes/ai/class-bulk-processor.php:235 +#: includes/ai/class-rag-worker.php:80 +#: includes/ai/settings/tab-bulk.php:51 +#: includes/ai/settings/tab-knowledge.php:157 +msgid "Every 15 Minutes" +msgstr "A cada 15 minutos" + +#: includes/ai/class-bulk-processor.php:344 +msgid "No post types selected for bulk processing." +msgstr "Nenhum tipo de post selecionado para processamento em lote." + +#: includes/ai/class-bulk-processor.php:364 +msgid "No more posts to process. Deactivating worker." +msgstr "Não há mais posts para processar. Desativando o worker." + +#: includes/ai/class-bulk-processor.php:381 +msgid "CRITICAL: No active AI adapter found for bulk processing." +msgstr "CRÍTICO: Nenhum adaptador de IA ativo encontrado para processamento em lote." + +#: includes/ai/class-bulk-processor.php:434 +msgid "No more posts to clear." +msgstr "Não há mais posts para limpar." + +#: includes/ai/class-bulk-processor.php:602 +msgid "No posts selected." +msgstr "Nenhum post selecionado." + +#: includes/ai/class-bulk-processor.php:664 +msgid "Review Bulk AI Approval" +msgstr "Revisar aprovação da IA em lote" + +#: includes/ai/class-bulk-processor.php:667 +#: includes/ai/class-bulk-processor.php:703 +msgid "Analyzing selected posts..." +msgstr "Analisando posts selecionados..." + +#: includes/ai/class-bulk-processor.php:672 +msgid "Confirm & Apply" +msgstr "Confirmar e aplicar" + +#: includes/ai/class-bulk-processor.php:698 +msgid "Please select at least one post." +msgstr "Selecione pelo menos um post." + +#: includes/ai/class-bulk-processor.php:772 +msgid "JEO AI Status" +msgstr "Status da IA do JEO" + +#: includes/ai/class-bulk-processor.php:797 +msgid "No Locations Found" +msgstr "Nenhuma localização encontrada" + +#: includes/ai/class-bulk-processor.php:799 +msgid "AI Error" +msgstr "Erro da IA" + +#: includes/ai/class-bulk-processor.php:841 +msgid "Approve AI" +msgstr "Aprovar IA" + +#: includes/ai/class-bulk-processor.php:852 +msgid "Approve JEO AI Geolocations" +msgstr "Aprovar geolocalizações da IA do JEO" + +#: includes/ai/class-context-handler.php:231 +#: includes/ai/class-context-handler.php:350 +#: includes/ai/class-context-handler.php:819 +#: includes/ai/class-context-handler.php:895 +#: includes/ai/class-rag-worker.php:223 +#: includes/minimap/class-minimap.php:162 +#: includes/minimap/class-minimap.php:273 +#: includes/minimap/class-minimap.php:374 +msgid "Post not found." +msgstr "Post não encontrado." + +#: includes/ai/class-context-handler.php:242 +#: includes/ai/class-context-handler.php:371 +#: includes/ai/class-minilayer-classifier.php:35 +#: includes/ai/class-minilayer-handler.php:91 +#: includes/ai/class-minilayer-service.php:41 +#: includes/minimap/class-minimap.php:172 +#: includes/minimap/class-minimap.php:283 +#: includes/minimap/class-minimap.php:394 +msgid "No AI provider configured. Set one in JEO AI Settings." +msgstr "Nenhum provedor de IA configurado. Defina um nas configurações de IA do JEO." + +#: includes/ai/class-context-handler.php:254 +msgid "The article seems to have very little content. Please write a bit more about the topic, or tell me what you would like suggestions about (e.g. territory, entities, angles)." +msgstr "O artigo parece ter muito pouco conteúdo. Escreva um pouco mais sobre o tema, ou diga sobre o que você gostaria de sugestões (ex.: território, entidades, ângulos)." + +#: includes/ai/class-context-handler.php:255 +#: includes/ai/class-context-handler.php:293 +msgid "Generate editorial suggestions for this post based on its content." +msgstr "Gerar sugestões editoriais para este post com base no seu conteúdo." + +#: includes/ai/class-context-handler.php:263 +msgid "Waiting for more content or directions." +msgstr "Aguardando mais conteúdo ou instruções." + +#: includes/ai/class-context-handler.php:299 +msgid "Suggestions generated." +msgstr "Sugestões geradas." + +#: includes/ai/class-context-handler.php:330 +#: includes/minimap/class-minimap.php:354 +msgid "conversation_id is required." +msgstr "conversation_id é obrigatório." + +#: includes/ai/class-context-handler.php:340 +#: includes/minimap/class-minimap.php:262 +#: includes/minimap/class-minimap.php:364 +msgid "A post ID is required." +msgstr "Um ID de post é obrigatório." + +#: includes/ai/class-context-handler.php:360 +#: includes/minimap/class-minimap.php:384 +msgid "A message is required." +msgstr "Uma mensagem é obrigatória." + +#: includes/ai/class-context-handler.php:410 +msgid "Suggestions updated." +msgstr "Sugestões atualizadas." + +#: includes/ai/class-context-handler.php:502 +msgid "The AI did not respond after multiple attempts. Please try again or rephrase your request." +msgstr "A IA não responde após várias tentativas. Tente novamente ou reformule sua solicitação." + +#: includes/ai/class-context-handler.php:916 +msgid "Conversation cleared." +msgstr "Conversa limpa." + +#: includes/ai/class-minilayer-handler.php:72 +#: includes/cli/class-ai-cli.php:52 +#: includes/cli/class-ai-cli.php:157 +#: includes/minimap/class-minimap.php:252 +msgid "A prompt is required." +msgstr "Um prompt é obrigatório." + +#: includes/ai/class-minilayer-handler.php:90 +#: includes/ai/class-minilayer-service.php:33 +msgid "Mapbox API key is not configured. Set it in JEO Settings." +msgstr "A chave de API do Mapbox não está configurada. Defina-a nas configurações do JEO." + +#: includes/ai/class-minilayer-handler.php:92 +msgid "Could not generate the map style. Please try again." +msgstr "Não foi possível gerar o estilo do mapa. Tente novamente." + +#: includes/ai/class-minilayer-handler.php:93 +msgid "The AI returned an unexpected response. Please try again." +msgstr "A IA retornou uma resposta inesperada. Tente novamente." + +#: includes/ai/class-minilayer-handler.php:94 +msgid "The AI did not create a valid style. Please try again." +msgstr "A IA não criou um estilo válido. Tente novamente." + +#: includes/ai/class-neuron-factory.php:216 +msgid "No Embedding Model configured. Please select one in the JEO AI settings." +msgstr "Nenhum modelo de Embedding configurado. Selecione um nas configurações de IA do JEO." + +#. translators: %s: directory path +#: includes/ai/class-rag-agent.php:64 +#, php-format +msgid "The RAG Vector Store directory (%s) could not be created. Please check your uploads folder permissions." +msgstr "O diretório do Armazenamento de Vetores do RAG (%s) não pôde ser criado. Verifique as permissões da pasta de uploads." -#: includes/class-jeo.php:1333 -msgid "Embed not available" -msgstr "Embed indisponível" +#. translators: %s: directory path +#: includes/ai/class-rag-agent.php:70 +#, php-format +msgid "The RAG Vector Store directory (%s) is not writable. Local Vectorization is disabled." +msgstr "O diretório do Armazenamento de Vetores do RAG (%s) não tem permissão de escrita. A Vetorização local está desativada." -#: includes/maps/class-map-style-composer.php:511 -#: includes/maps/class-map-style-composer.php:564 -msgid "Mapbox composed styles are disabled." -msgstr "Os estilos compostos do Mapbox estão desativados." +#: includes/ai/class-rag-agent.php:76 +msgid "No AI Provider is configured. Please set up a provider (Gemini, OpenAI, etc.) before using RAG." +msgstr "Nenhum provedor de IA configurado. Configure um provedor (Gemini, OpenAI, etc.) antes de usar o RAG." -#: includes/maps/class-map-style-composer.php:536 -#: includes/maps/class-map-style-composer.php:588 -msgid "Could not create the Mapbox composed style cache directory." -msgstr "Não foi possível criar o diretório de cache dos estilos compostos do Mapbox." +#: includes/ai/class-rag-agent.php:89 +msgid "No Embedding Model selected. Please choose a model in the Knowledge Base tab." +msgstr "Nenhum modelo de Embedding selecionado. Escolha um modelo na aba Base de Conhecimento." -#: includes/maps/class-map-style-composer.php:684 -msgid "Map not found." -msgstr "Mapa não encontrado." +#. translators: %s: provider name +#: includes/ai/class-rag-agent.php:108 +#, php-format +msgid "The selected embedding provider (%s) does not support native Embeddings in JEO yet. Please use OpenAI, Gemini or Ollama for Vector Store features." +msgstr "O provedor de Embedding selecionado (%s) ainda não suporta Embeddings nativos no JEO. Use OpenAI, Gemini ou Ollama para recursos do Armazenamento de Vetores." -#: includes/maps/class-map-style-composer.php:694 -#: includes/maps/class-map-style-composer.php:757 -msgid "A Mapbox access token is required to compose Mapbox styles." -msgstr "Um token de acesso do Mapbox é necessário para compor estilos do Mapbox." +#. translators: %s: provider name +#: includes/ai/class-rag-agent.php:119 +#, php-format +msgid "The API Key for the Embedding provider (%s) is missing. RAG requires a valid connection to generate embeddings." +msgstr "A chave de API do provedor de Embedding (%s) está ausente. O RAG requer uma conexão válida para gerar Embeddings." -#: includes/maps/class-map-style-composer.php:734 -msgid "Public payload composition is only available for one-time maps." -msgstr "A composição com dados públicos está disponível apenas para mapas de uso único." +#: includes/ai/class-rag-backup.php:41 +msgid "The PHP ZipArchive extension is required to create backups." +msgstr "A extensão ZipArchive do PHP é necessária para criar backups." -#: includes/maps/class-map-style-composer.php:740 -msgid "The map preview payload is too large." -msgstr "Os dados da prévia do mapa são grandes demais." +#: includes/ai/class-rag-backup.php:92 +msgid "No vector store found to backup." +msgstr "Nenhum armazenamento de vetores encontrado para backup." -#: includes/maps/class-map-style-composer.php:752 -msgid "The map preview has too many layers to compose." -msgstr "A prévia do mapa tem camadas demais para compor." +#: includes/ai/class-rag-backup.php:99 +msgid "Failed to create the ZIP archive." +msgstr "Falha ao criar o arquivo ZIP." -#: includes/maps/class-map-style-composer.php:763 -msgid "The map preview does not include any renderable layers." -msgstr "A prévia do mapa não inclui nenhuma camada renderizável." +#: includes/ai/class-rag-worker.php:209 +msgid "Either post_id or query is required." +msgstr "É obrigatório informar post_id ou query." -#: includes/maps/class-map-style-composer.php:785 -msgid "Map preview" -msgstr "Prévia do mapa" +#. translators: %s: pipeline name +#: includes/ai/class-rag-worker.php:347 +#, php-format +msgid "Unknown pipeline: %s" +msgstr "Pipeline desconhecido: %s" -#: includes/maps/class-map-style-composer.php:1026 -msgid "Invalid composed style hash." -msgstr "Hash do estilo composto inválido." +#. translators: 1: locked model name, 2: current model name +#: includes/ai/class-rag-worker.php:377 +#, php-format +msgid "Vector Store mismatch for %1$s! Expected %2$s, found %3$s." +msgstr "Incompatibilidade de Armazenamento de Vetores para %1$s! Esperado %2$s, encontrado %3$s." -#: includes/maps/class-map-style-composer.php:1114 -msgid "One or more Mapbox styles could not be fetched." -msgstr "Não foi possível obter um ou mais estilos do Mapbox." +#. translators: %s: pipeline name +#: includes/ai/class-rag-worker.php:403 +#, php-format +msgid "No more %s to vectorize." +msgstr "Não há mais %s para vetorizar." -#: includes/maps/class-map-style-composer.php:1515 -msgid "The PHP GD extension is required to merge Mapbox sprites." -msgstr "A extensão PHP GD é necessária para mesclar sprites do Mapbox." +#. translators: %d: number of items vectorized +#: includes/ai/class-rag-worker.php:428 +#, php-format +msgid "Successfully vectorized %1$d %2$s." +msgstr "%1$d %2$s vetorizados com sucesso." -#: includes/maps/class-map-style-composer.php:1634 -msgid "Could not decode a Mapbox sprite image." -msgstr "Não foi possível decodificar uma imagem de sprite do Mapbox." +#. translators: %s: pipeline name +#: includes/ai/class-rag-worker.php:437 +#, php-format +msgid "Batch skipped for %s (no content found)." +msgstr "Lote ignorado para %s (nenhum conteúdo encontrado)." + +#: includes/ai/settings/main-page.php:10 +msgid "JEO AI Configuration" +msgstr "Configuração de IA do JEO" + +#: includes/ai/settings/main-page.php:99 +msgid "Save AI Settings" +msgstr "Salvar configurações de IA" + +#: includes/ai/settings/main-page.php:107 +#: includes/ai/settings/tab-general.php:22 +msgid "JEO AI API Debugger" +msgstr "Depurador de API da IA do JEO" + +#: includes/ai/settings/main-page.php:109 +msgid "Clear logs" +msgstr "Limpar logs" + +#: includes/ai/settings/main-page.php:115 +msgid "[System] Console initialized. Awaiting API activity..." +msgstr "[Sistema] Console inicializado. Aguardando atividade da API..." + +#: includes/ai/settings/tab-bulk.php:12 +msgid "Background Processing" +msgstr "Processamento em segundo plano" + +#: includes/ai/settings/tab-bulk.php:15 +msgid "Enable background AI geolocalization" +msgstr "Ativar geolocalização por IA em segundo plano" + +#: includes/ai/settings/tab-bulk.php:16 +msgid "When active, JEO will scan legacy posts and suggest locations based on their content." +msgstr "Quando ativo, o JEO examinará posts existentes e sugerirá localizações com base no seu conteúdo." + +#: includes/ai/settings/tab-bulk.php:20 +#: includes/ai/settings/tab-knowledge.php:149 +msgid "Batch Size" +msgstr "Tamanho do lote" + +#: includes/ai/settings/tab-bulk.php:23 +msgid "Number of posts to process per batch." +msgstr "Número de posts a processar por lote." + +#: includes/ai/settings/tab-bulk.php:27 +msgid "Target Post Types" +msgstr "Tipos de post alvo" + +#: includes/ai/settings/tab-bulk.php:46 +#: includes/ai/settings/tab-knowledge.php:153 +msgid "Cron Interval" +msgstr "Intervalo do Cron" + +#: includes/ai/settings/tab-bulk.php:52 +#: includes/ai/settings/tab-knowledge.php:158 +msgid "Hourly" +msgstr "Horário" + +#: includes/ai/settings/tab-bulk.php:53 +msgid "Twice Daily" +msgstr "Duas vezes ao dia" + +#: includes/ai/settings/tab-bulk.php:59 +msgid "Execution Logs" +msgstr "Logs de execução" + +#: includes/ai/settings/tab-bulk.php:62 +msgid "Recent Background Activity" +msgstr "Atividade recente em segundo plano" + +#: includes/ai/settings/tab-bulk.php:68 +msgid "No recent activity recorded." +msgstr "Nenhuma atividade recente registrada." + +#: includes/ai/settings/tab-bulk.php:87 +msgid "Manual Control" +msgstr "Controle manual" + +#: includes/ai/settings/tab-bulk.php:91 +#: includes/settings/class-settings.php:636 +msgid "Process 1 Batch Now" +msgstr "Processar 1 lote agora" + +#: includes/ai/settings/tab-bulk.php:94 +#: includes/settings/class-settings.php:637 +msgid "Clear 1 Batch" +msgstr "Limpar 1 lote" + +#: includes/ai/settings/tab-bulk.php:97 +msgid "Reset All Posts" +msgstr "Redefinir todos os posts" + +#: includes/ai/settings/tab-bulk.php:100 +msgid "Manual Batch: Processes legacy posts immediately. Clear Batch: Resets the \"processed\" flag for a small group. Reset All: Schedules a background task to clear all posts (Double confirmation required)." +msgstr "Lote manual: processa posts existentes imediatamente. Limpar lote: redefine o sinalizador \"processado\" para um pequeno grupo. Redefinir tudo: agenda uma tarefa em segundo plano para limpar todos os posts (confirmação dupla necessária)." + +#: includes/ai/settings/tab-bulk.php:105 +msgid "Processing Status" +msgstr "Status de processamento" + +#. translators: %1$d: processed posts, %2$d: total posts, %3$d: percentage +#: includes/ai/settings/tab-bulk.php:139 +#, php-format +msgid "Geolocalization Progress: %1$d of %2$d posts marked as processed (%3$d%%)." +msgstr "Progresso da geolocalização: %1$d de %2$d posts marcados como processados (%3$d%%)." + +#: includes/ai/settings/tab-bulk.php:146 +msgid "Auto-Approval Threshold" +msgstr "Limite de aprovação automática" + +#: includes/ai/settings/tab-bulk.php:149 +msgid "Minimum average confidence required to auto-approve locations when using bulk approval actions in the post list." +msgstr "Confiança média mínima necessária para aprovar automaticamente localizações ao usar ações de aprovação em lote na lista de posts." + +#: includes/ai/settings/tab-bulk.php:154 +msgid "Internal Debug Log" +msgstr "Log de Debug interno" + +#: includes/ai/settings/tab-bulk.php:157 +msgid "Enable detailed file-based logging (jeo-bulk-ai.log) for technical troubleshooting." +msgstr "Ativar registro detalhado em arquivo (jeo-bulk-ai.log) para solução de problemas técnicos." + +#: includes/ai/settings/tab-bulk.php:161 +msgid "Open Log File" +msgstr "Abrir arquivo de log" + +#: includes/ai/settings/tab-bulk.php:162 +msgid "Delete Log File" +msgstr "Excluir arquivo de log" + +#: includes/ai/settings/tab-context.php:18 +msgid "Context Assistant System Prompt" +msgstr "Prompt do sistema do Assistente de Contexto" + +#: includes/ai/settings/tab-context.php:31 +msgid "Check this to use a custom system prompt. Uncheck to use the built-in default prompt." +msgstr "Marque para usar um prompt do sistema personalizado. Desmarque para usar o prompt padrão integrado." + +#: includes/ai/settings/tab-context.php:55 +msgid "Default System Prompt (reference)" +msgstr "Prompt do sistema padrão (referência)" + +#: includes/ai/settings/tab-context.php:65 +msgid "This is the built-in default prompt. Enable the custom prompt checkbox above to override it." +msgstr "Este é o prompt padrão integrado. Ative a caixa de seleção de prompt personalizado acima para substituí-lo." + +#: includes/ai/settings/tab-embedded.php:10 +msgid "Embedded Brazilian Territories" +msgstr "Territórios brasileiros incorporados" + +#: includes/ai/settings/tab-embedded.php:11 +msgid "The following geographic dictionaries are available locally to improve AI precision:" +msgstr "Os seguintes dicionários geográficos estão disponíveis localmente para melhorar a precisão da IA:" + +#: includes/ai/settings/tab-embedded.php:18 +#: includes/settings/class-settings.php:660 +msgid "Brazilian Biomes" +msgstr "Biomas brasileiros" + +#: includes/ai/settings/tab-embedded.php:19 +#: includes/settings/class-settings.php:661 +msgid "Indigenous Territories" +msgstr "Territórios indígenas" + +#: includes/ai/settings/tab-embedded.php:20 +#: includes/settings/class-settings.php:662 +msgid "Quilombola Territories" +msgstr "Territórios quilombolas" + +#: includes/ai/settings/tab-embedded.php:21 +#: includes/settings/class-settings.php:663 +msgid "Extractive Reserves (Resex)" +msgstr "Reservas extrativistas (Resex)" + +#: includes/ai/settings/tab-embedded.php:22 +#: includes/settings/class-settings.php:664 +msgid "Conservation Units" +msgstr "Unidades de conservação" + +#: includes/ai/settings/tab-embedded.php:23 +#: includes/settings/class-settings.php:665 +msgid "Riverside Communities" +msgstr "Comunidades ribeirinhas" + +#: includes/ai/settings/tab-embedded.php:24 +#: includes/settings/class-settings.php:666 +msgid "Agrarian Reform Settlements" +msgstr "Assentamentos de reforma agrária" + +#: includes/ai/settings/tab-embedded.php:25 +#: includes/settings/class-settings.php:667 +msgid "Indigenous Peoples (Ethnicities)" +msgstr "Povos indígenas (Etnias)" + +#: includes/ai/settings/tab-embedded.php:26 +#: includes/settings/class-settings.php:668 +msgid "Legal Amazon and Boundaries" +msgstr "Amazônia legal e limites" + +#: includes/ai/settings/tab-embedded.php:27 +#: includes/settings/class-settings.php:669 +msgid "Hydrographic Basins" +msgstr "Bacias hidrográficas" + +#: includes/ai/settings/tab-embedded.php:46 +msgid "File:" +msgstr "Arquivo:" + +#: includes/ai/settings/tab-embedded.php:47 +msgid "Entries found:" +msgstr "Entradas encontradas:" + +#: includes/ai/settings/tab-embedded.php:57 +#: includes/settings/class-settings.php:603 +msgid "Download" +msgstr "Baixar" + +#: includes/ai/settings/tab-embedded.php:65 +#: includes/ai/settings/tab-knowledge.php:289 +#: includes/settings/ai-logs-page.php:250 +msgid "Close" +msgstr "Fechar" + +#: includes/ai/settings/tab-embedded.php:69 +msgid "Item List" +msgstr "Lista de itens" + +#: includes/ai/settings/tab-embedded.php:73 +msgid "Name" +msgstr "Nome" + +#: includes/ai/settings/tab-embedded.php:89 +msgid "JSON Raw" +msgstr "JSON bruto" + +#: includes/ai/settings/tab-general.php:14 +msgid "Debug Mode" +msgstr "Modo de Debug" + +#: includes/ai/settings/tab-general.php:17 +msgid "Enable internal logging of all AI calls (Inputs/Outputs) for 24 hours. Useful for debugging prompt regressions." +msgstr "Ativar registro interno de todas as chamadas da IA (entradas/saídas) por 24 horas. Útil para depurar regressões de prompt." + +#: includes/ai/settings/tab-general.php:25 +msgid "Show the floating API debugger console on AI settings pages. Displays real-time request/response logs." +msgstr "Mostrar o console flutuante do depurador de API nas páginas de configuração da IA. Exibe logs de requisição/resposta em tempo real." + +#: includes/ai/settings/tab-general.php:30 +msgid "NeuronAI Structured Output" +msgstr "Saída estruturada do NeuronAI" + +#: includes/ai/settings/tab-general.php:33 +msgid "Use native schema enforcement instead of prompt-based JSON extraction. Falls back to text parsing if structured output fails." +msgstr "Usar imposição nativa de esquema em vez de extração de JSON baseada em prompt. Recorre à análise de texto se a saída estruturada falhar." + +#: includes/ai/settings/tab-general.php:36 +msgid "Limitations:" +msgstr "Limitações:" + +#: includes/ai/settings/tab-general.php:38 +msgid "Token usage tracking is unavailable when structured output is active (shows 0 in logs)." +msgstr "O rastreamento de uso de Tokens está indisponível quando a saída estruturada está ativa (mostra 0 nos logs)." + +#: includes/ai/settings/tab-general.php:39 +msgid "If the provider does not support native schema enforcement, the system will automatically fall back to text parsing." +msgstr "Se o provedor não suportar imposição nativa de esquema, o sistema recorrerá automaticamente à análise de texto." + +#: includes/ai/settings/tab-general.php:40 +msgid "Some internal tools (e.g. prompt generator test connection) automatically bypass structured output." +msgstr "Algumas ferramentas internas (ex.: teste de conexão do gerador de prompt) ignoram automaticamente a saída estruturada." + +#: includes/ai/settings/tab-general.php:45 +msgid "Recommendations by provider:" +msgstr "Recomendações por provedor:" + +#: includes/ai/settings/tab-general.php:47 +msgid "Full support. Recommended to keep this option enabled." +msgstr "Suporte total. Recomendado manter esta opção ativada." + +#: includes/ai/settings/tab-general.php:48 +msgid "Supported via NeuronAI. Test with your model to confirm." +msgstr "Suportado via NeuronAI. Teste com seu modelo para confirmar." + +#: includes/ai/settings/tab-general.php:49 +msgid "Variable support depending on model and local configuration. Disable if you encounter errors." +msgstr "Suporte variável dependendo do modelo e configuração local. Desative se encontrar erros." + +#: includes/ai/settings/tab-general.php:56 +msgid "Embedded Data" +msgstr "Dados incorporados" + +#: includes/ai/settings/tab-general.php:59 +msgid "View locally embedded geographic dictionaries used to improve AI precision. This feature is experimental and not yet fully integrated." +msgstr "Ver dicionários geográficos incorporados localmente usados para melhorar a precisão da IA. Este recurso é experimental e ainda não está totalmente integrado." + +#: includes/ai/settings/tab-general.php:63 +msgid "View Embedded Data" +msgstr "Ver dados incorporados" -#: includes/maps/class-map-style-composer.php:2351 -msgid "Composed style artifact was not found." -msgstr "O artefato do estilo composto não foi encontrado." +#: includes/ai/settings/tab-knowledge.php:17 +msgid "AI Provider Required" +msgstr "Provedor de IA necessário" -#: js/build/discovery.js:6 -msgid "Mapbox style composition is unavailable." -msgstr "A composição de estilos do Mapbox está indisponível." +#: includes/ai/settings/tab-knowledge.php:19 +msgid "You need to configure an AI Provider before you can use the Knowledge Base. Set up your API key and choose a model in the Provider tab first." +msgstr "Você precisa configurar um provedor de IA antes de usar a Base de Conhecimento. Configure sua chave de API e escolha um modelo na aba Provedor primeiro." -#: js/build/discovery.js:6 -#: js/build/jeoMap.js:1 -msgid "Mapbox style composition is unavailable for this map." -msgstr "A composição de estilos do Mapbox está indisponível para este mapa." +#: includes/ai/settings/tab-knowledge.php:22 +msgid "Configure AI Provider" +msgstr "Configurar provedor de IA" -#: js/build/discovery.js:6 -msgid "Unable to load map information right now." -msgstr "Não foi possível carregar as informações do mapa agora." +#: includes/ai/settings/tab-knowledge.php:35 +msgid "RAG Knowledge Base (Vector Store)" +msgstr "Base de Conhecimento RAG (Armazenamento de Vetores)" -#: js/build/discovery.js:6 -msgid "Map information" -msgstr "Informações do mapa" +#: includes/ai/settings/tab-knowledge.php:36 +msgid "Vectorize your WordPress posts to allow the JEO AI to contextually answer questions and cross-reference territorial data." +msgstr "Vetorize seus posts do WordPress para permitir que a IA do JEO responda perguntas contextualmente e faça referência cruzada de dados territoriais." -#: js/build/jeoMap.js:1 -msgid "Mapbox style composition did not return renderable layers for this map." -msgstr "A composição de estilos do Mapbox não retornou camadas renderizáveis para este mapa." +#: includes/ai/settings/tab-knowledge.php:41 +msgid "Embedding Model" +msgstr "Modelo de Embedding" -#: js/build/jeoMap.js:1 -msgid "Unable to load composed Mapbox style for this map." -msgstr "Não foi possível carregar o estilo composto do Mapbox para este mapa." +#: includes/ai/settings/tab-knowledge.php:57 +msgid "Model selection is locked." +msgstr "Seleção de modelo bloqueada." -#: js/build/jeoMap.js:1 -msgid "Mapbox style composition is unavailable for this one-time map." -msgstr "A composição de estilos do Mapbox está indisponível para este mapa de uso único." +#. translators: %s: locked embedding model name +#: includes/ai/settings/tab-knowledge.php:62 +#, php-format +msgid "This Vector Store was initialized with %s. To use a different model, you must clear the current store first." +msgstr "Este Armazenamento de Vetores foi inicializado com %s. Para usar um modelo diferente, você deve limpar o armazenamento atual primeiro." -#: js/build/jeoMap.js:1 -msgid "Mapbox style composition did not return renderable layers for this one-time map." -msgstr "A composição de estilos do Mapbox não retornou camadas renderizáveis para este mapa de uso único." +#: includes/ai/settings/tab-knowledge.php:70 +msgid "Select an embedding model..." +msgstr "Selecione um modelo de Embedding..." -#: js/build/jeoMap.js:1 -msgid "Unable to load composed Mapbox style for this one-time map." -msgstr "Não foi possível carregar o estilo composto do Mapbox para este mapa de uso único." +#: includes/ai/settings/tab-knowledge.php:98 +msgid "Select the specific embedding model. You can mix providers (e.g. use Gemini for chat and OpenAI for embeddings)." +msgstr "Selecione o modelo de Embedding específico. Você pode misturar provedores (ex.: usar Gemini para chat e OpenAI para Embeddings)." -#: js/build/jeoMap.js:1 -#: js/build/jeoStorymap.js:3 -msgid "Mapbox style layers could not be loaded because the composed style is unavailable." -msgstr "Não foi possível carregar as camadas de estilo do Mapbox porque o estilo composto está indisponível." +#: includes/ai/settings/tab-knowledge.php:110 +msgid "RAG Knowledge Base is not available" +msgstr "A Base de Conhecimento RAG não está disponível" -#: js/build/jeoStorymap.js:3 -msgid "Mapbox style composition is unavailable for this storymap." -msgstr "A composição de estilos do Mapbox está indisponível para este mapa narrativo." +#: includes/ai/settings/tab-knowledge.php:115 +msgid "How to fix this:" +msgstr "Como corrigir:" -#: js/build/jeoStorymap.js:3 -msgid "Mapbox style composition did not return renderable layers for this storymap." -msgstr "A composição de estilos do Mapbox não retornou camadas renderizáveis para este mapa narrativo." +#: includes/ai/settings/tab-knowledge.php:118 +msgid "Please select an Embedding Model from the dropdown above and click \"Save AI Settings\"." +msgstr "Selecione um modelo de Embedding no menu suspenso acima e clique em \"Salvar configurações de IA\"." -#: js/build/jeoStorymap.js:3 -msgid "Unable to load composed Mapbox style for this storymap." -msgstr "Não foi possível carregar o estilo composto do Mapbox para este mapa narrativo." +#: includes/ai/settings/tab-knowledge.php:120 +msgid "Ensure you have an active AI Provider (Gemini, OpenAI, or Ollama) configured in the \"AI Provider\" tab." +msgstr "Certifique-se de ter um provedor de IA ativo (Gemini, OpenAI ou Ollama) configurado na aba \"Provedor de IA\"." -#. translators: %d: number of maps whose composed style cache was refreshed. -#: js/build/layersSidebar.js:8 +#: includes/ai/settings/tab-knowledge.php:121 +msgid "Check if the \"wp-content/uploads\" directory exists and is writable." +msgstr "Verifique se o diretório \"wp-content/uploads\" existe e tem permissão de escrita." + +#: includes/ai/settings/tab-knowledge.php:126 +msgid "Go to AI Provider Settings" +msgstr "Ir para configurações do provedor de IA" + +#: includes/ai/settings/tab-knowledge.php:134 +msgid "Background Indexing" +msgstr "Indexação em segundo plano" + +#: includes/ai/settings/tab-knowledge.php:135 +msgid "Automatically vectorize your posts in small batches using WP-Cron." +msgstr "Vetorize automaticamente seus posts em pequenos lotes usando o WP-Cron." + +#: includes/ai/settings/tab-knowledge.php:140 +msgid "Enable Auto-indexing" +msgstr "Ativar indexação automática" + +#: includes/ai/settings/tab-knowledge.php:143 +msgid "Note: WP-Cron only runs when your site receives visitor traffic. If you are on a local development server with no traffic, indexing will pause until pages are loaded." +msgstr "Nota: O WP-Cron só é executado quando seu site recebe tráfego de visitantes. Se você estiver em um servidor de desenvolvimento local sem tráfego, a indexação será pausada até que as páginas sejam carregadas." + +#: includes/ai/settings/tab-knowledge.php:162 +msgid "Search Results (topK)" +msgstr "Resultados da busca (topK)" + +#: includes/ai/settings/tab-knowledge.php:164 +msgid "Max semantic matches returned per search." +msgstr "Máximo de correspondências semânticas retornadas por busca." + +#: includes/ai/settings/tab-knowledge.php:169 +msgid "Recent Background Logs" +msgstr "Logs recentes em segundo plano" + +#: includes/ai/settings/tab-knowledge.php:175 +msgid "No indexing jobs have run recently." +msgstr "Nenhum trabalho de indexação executado recentemente." + +#: includes/ai/settings/tab-knowledge.php:192 +msgid "Manual Indexing" +msgstr "Indexação manual" + +#: includes/ai/settings/tab-knowledge.php:193 +msgid "Trigger a single batch vectorization immediately." +msgstr "Disparar uma única vetorização em lote imediatamente." + +#: includes/ai/settings/tab-knowledge.php:196 +#: includes/ai/settings/tab-knowledge.php:319 +msgid "Vectorize 1 Batch Now" +msgstr "Vetorizar 1 lote agora" + +#: includes/ai/settings/tab-knowledge.php:228 +msgid "Indexing Progress" +msgstr "Progresso da indexação" + +#. translators: %1$d: vectorized count, %2$d: total posts count +#: includes/ai/settings/tab-knowledge.php:237 +#, php-format +msgid "%1$d of %2$d posts indexed." +msgstr "%1$d de %2$d posts indexados." + +#: includes/ai/settings/tab-knowledge.php:246 +#: includes/ai/settings/tab-knowledge.php:403 +msgid "Status:" +msgstr "Status:" + +#: includes/ai/settings/tab-knowledge.php:248 +msgid "Active (Local File Store)" +msgstr "Ativo (Armazenamento de arquivos local)" + +#: includes/ai/settings/tab-knowledge.php:252 +msgid "Alternative: Use WP-CLI for large databases:" +msgstr "Alternativa: Use WP-CLI para grandes bancos de dados:" + +#: includes/ai/settings/tab-knowledge.php:257 +msgid "Backup Store" +msgstr "Armazenamento de backup" + +#: includes/ai/settings/tab-knowledge.php:260 +msgid "Clear & Reset Model" +msgstr "Limpar e redefinir modelo" + +#: includes/ai/settings/tab-knowledge.php:266 +msgid "Available Backups (Last 3)" +msgstr "Backups disponíveis (últimos 3)" + +#: includes/ai/settings/tab-knowledge.php:268 +#: includes/settings/class-settings.php:605 +msgid "No backups found." +msgstr "Nenhum backup encontrado." + +#: includes/ai/settings/tab-knowledge.php:275 +msgid "Test Embeddings & RAG Retrieval" +msgstr "Testar Embeddings e recuperação RAG" + +#: includes/ai/settings/tab-knowledge.php:276 +msgid "Test your configured Embedding Model on a random post to ensure it works correctly before indexing your entire database. This uses a temporary testing store." +msgstr "Teste seu modelo de Embedding configurado em um post aleatório para garantir que funcione corretamente antes de indexar todo o seu banco de dados. Usa um armazenamento de teste temporário." + +#: includes/ai/settings/tab-knowledge.php:279 +#: includes/settings/class-settings.php:616 +msgid "Run Test on Random Post" +msgstr "Executar teste em post aleatório" + +#: includes/ai/settings/tab-knowledge.php:280 +msgid "Test Vector Retrieval" +msgstr "Testar recuperação de vetores" + +#: includes/ai/settings/tab-knowledge.php:288 +msgid "Test Local Vector Retrieval" +msgstr "Testar recuperação local de vetores" + +#: includes/ai/settings/tab-knowledge.php:295 +msgid "Search Production Store" +msgstr "Buscar no armazenamento de produção" + +#: includes/ai/settings/tab-knowledge.php:296 +msgid "Search Test Store" +msgstr "Buscar no armazenamento de teste" + +#: includes/ai/settings/tab-knowledge.php:298 +msgid "Search the Vector Store for semantic meaning (e.g., \"Indigenous lands in Amazon\")" +msgstr "Busque no Armazenamento de Vetores por significado semântico (ex.: \"Terras indígenas na Amazônia\")" + +#: includes/ai/settings/tab-knowledge.php:302 +msgid "Enter a semantic search query to retrieve similar vectors from your local store." +msgstr "Digite uma consulta de busca semântica para recuperar vetores similares do seu armazenamento local." + +#: includes/ai/settings/tab-knowledge.php:310 +msgid "Layer Store (Vectorized Map Layers)" +msgstr "Armazenamento de camadas (camadas de mapa vetorizadas)" + +#: includes/ai/settings/tab-knowledge.php:311 +msgid "Vectorize your map layers to enable AI-powered semantic search and layer suggestions based on post content." +msgstr "Vetorize suas camadas de mapa para ativar a busca semântica com IA e sugestões de camadas com base no conteúdo do post." + +#: includes/ai/settings/tab-knowledge.php:315 +msgid "Manual Layer Indexing" +msgstr "Indexação manual de camadas" + +#: includes/ai/settings/tab-knowledge.php:316 +msgid "Trigger a single batch vectorization for map layers. Layers are also auto-indexed on the background cron schedule and when saved." +msgstr "Dispare uma única vetorização em lote para camadas de mapa. As camadas também são indexadas automaticamente no Cron em segundo plano e ao salvar." + +#: includes/ai/settings/tab-knowledge.php:344 +msgid "Layer Indexing Progress" +msgstr "Progresso da indexação de camadas" + +#. translators: %1$d: vectorized count, %2$d: total layers count +#: includes/ai/settings/tab-knowledge.php:353 +#, php-format +msgid "%1$d of %2$d layers indexed." +msgstr "%1$d de %2$d camadas indexadas." + +#: includes/ai/settings/tab-knowledge.php:359 +msgid "Recent Layer Logs" +msgstr "Logs recentes de camadas" + +#: includes/ai/settings/tab-knowledge.php:365 +msgid "No layer indexing jobs have run recently." +msgstr "Nenhum trabalho de indexação de camadas executado recentemente." + +#: includes/ai/settings/tab-knowledge.php:382 +msgid "Suggest Layers" +msgstr "Sugerir camadas" + +#: includes/ai/settings/tab-knowledge.php:383 +msgid "Test semantic layer matching. Enter a post ID or text to find layers that match the content." +msgstr "Teste a correspondência semântica de camadas. Digite um ID de post ou texto para encontrar camadas que correspondam ao conteúdo." + +#: includes/ai/settings/tab-knowledge.php:387 +msgid "Post ID" +msgstr "ID do post" + +#: includes/ai/settings/tab-knowledge.php:388 +msgid "e.g. 123" +msgstr "ex. 123" + +#: includes/ai/settings/tab-knowledge.php:391 +msgid "Or search text" +msgstr "Ou texto de busca" + +#: includes/ai/settings/tab-knowledge.php:392 +msgid "e.g., deforestation in Amazon rainforest" +msgstr "ex.: desmatamento na floresta amazônica" + +#: includes/ai/settings/tab-knowledge.php:394 +msgid "Find Layers" +msgstr "Encontrar camadas" + +#: includes/ai/settings/tab-knowledge.php:405 +msgid "Active (Shared embedding model with Posts Store)" +msgstr "Ativo (Modelo de Embedding compartilhado com o Armazenamento de Posts)" + +#: includes/ai/settings/tab-knowledge.php:409 +msgid "WP-CLI:" +msgstr "WP-CLI:" + +#: includes/ai/settings/tab-knowledge.php:414 +msgid "Clear Layer Store" +msgstr "Limpar armazenamento de camadas" + +#: includes/ai/settings/tab-provider.php:53 +msgid "Paste your API key here..." +msgstr "Cole sua chave de API aqui..." + +#: includes/ai/settings/tab-provider.php:58 +msgid "Set New Key" +msgstr "Definir nova chave" + +#: includes/ai/settings/tab-provider.php:63 +msgid "Status: Unknown" +msgstr "Status: desconhecido" + +#: includes/ai/settings/tab-provider.php:66 +msgid "Test Connection" +msgstr "Testar conexão" + +#: includes/ai/settings/tab-provider.php:73 +msgid "Model" +msgstr "Modelo" + +#: includes/ai/settings/tab-provider.php:79 +#: includes/settings/class-settings.php:581 +msgid "Change Model" +msgstr "Alterar modelo" + +#: includes/ai/settings/tab-provider.php:87 +msgid "System Prompt Configuration" +msgstr "Configuração de Prompt do sistema" + +#: includes/ai/settings/tab-provider.php:91 +msgid "Check this to override the default behavior. Uncheck to temporarily disable and return to the optimized default prompt." +msgstr "Marque para substituir o comportamento padrão. Desmarque para desativar temporariamente e voltar ao prompt padrão otimizado." + +#: includes/ai/settings/tab-provider.php:95 +msgid "Include post categories and tags in the AI georeferencing request to improve location accuracy." +msgstr "Incluir categorias e tags do post na solicitação de georreferenciamento da IA para melhorar a precisão da localização." + +#: includes/ai/settings/tab-provider.php:100 +#: includes/ai/settings/tab-provider.php:280 +#: includes/settings/class-settings.php:631 +msgid "Back" +msgstr "Voltar" + +#: includes/ai/settings/tab-provider.php:103 +#: includes/ai/settings/tab-provider.php:282 +#: includes/settings/class-settings.php:629 +#: js/build/contextSidebar.js:1 +msgid "Expand" +msgstr "Expandir" + +#: includes/ai/settings/tab-provider.php:104 +msgid "Validate Custom Prompt" +msgstr "Validar prompt personalizado" + +#: includes/ai/settings/tab-provider.php:105 +msgid "Clear Text" +msgstr "Limpar texto" + +#: includes/ai/settings/tab-provider.php:116 +msgid "AI Prompt Engineer Assistant" +msgstr "Assistente de Engenharia de Prompt da IA" + +#: includes/ai/settings/tab-provider.php:117 +msgid "Describe how you want the AI to behave (e.g., \"Only map cities in Brazil\" or \"Ignore street names\"). The active LLM will generate a highly optimized System Prompt for you, strictly adhering to JEO formatting rules." +msgstr "Descreva como você quer que a IA se comporte (ex.: \"Mapear apenas cidades no Brasil\" ou \"Ignorar nomes de ruas\"). O LLM ativo gerará um Prompt do sistema altamente otimizado para você, seguindo estritamente as regras de formatação do JEO." + +#: includes/ai/settings/tab-provider.php:279 +msgid "Ex: I want to map only locations inside Europe. Press Shift+Enter for new line, or Enter to generate." +msgstr "Ex.: Quero mapear apenas localizações dentro da Europa. Pressione Shift+Enter para nova linha, ou Enter para gerar." + +#: includes/ai/settings/tab-provider.php:283 +msgid "Shift+Enter for new line" +msgstr "Shift+Enter para nova linha" + +#: includes/ai/settings/tab-provider.php:288 +msgid "Georeferencing Calibration" +msgstr "Calibração de georreferenciamento" + +#: includes/ai/settings/tab-provider.php:292 +#: includes/ai/settings/tab-provider.php:304 +#: includes/ai/settings/tab-provider.php:313 +#: includes/ai/settings/tab-provider.php:322 +#: includes/ai/settings/tab-provider.php:331 +#: includes/ai/settings/tab-provider.php:340 +#: includes/ai/settings/tab-provider.php:349 +#: includes/ai/settings/tab-provider.php:358 +msgid "Use" +msgstr "Usar" + +#: includes/ai/settings/tab-provider.php:294 +msgid "Location Granularity" +msgstr "Granularidade da localização" + +#: includes/ai/settings/tab-provider.php:296 +msgid "Broad (countries, regions, cities)" +msgstr "Ampla (países, regiões, cidades)" + +#: includes/ai/settings/tab-provider.php:297 +msgid "Balanced (cities + neighborhoods)" +msgstr "Equilibrada (cidades + bairros)" + +#: includes/ai/settings/tab-provider.php:298 +msgid "Fine (streets, landmarks, POIs)" +msgstr "Fina (ruas, pontos de referência, POIs)" + +#: includes/ai/settings/tab-provider.php:306 +msgid "Minimum Confidence" +msgstr "Confiança mínima" + +#: includes/ai/settings/tab-provider.php:315 +msgid "Primary Threshold" +msgstr "Limite principal" + +#: includes/ai/settings/tab-provider.php:324 +msgid "Primary Points" +msgstr "Pontos principais" + +#: includes/ai/settings/tab-provider.php:333 +msgid "Secondary Threshold" +msgstr "Limite secundário" + +#: includes/ai/settings/tab-provider.php:342 +msgid "Secondary Points" +msgstr "Pontos secundários" + +#: includes/ai/settings/tab-provider.php:351 +msgid "Prioritize Title Mentions" +msgstr "Priorizar menções no título" + +#: includes/ai/settings/tab-provider.php:360 +msgid "Prompt Token Budget" +msgstr "Orçamento de Tokens do Prompt" + +#: includes/ai/settings/tab-provider.php:366 +msgid "Higher budgets allow the AI to process longer articles but increase cost and latency." +msgstr "Orçamentos maiores permitem que a IA processe artigos mais longos, mas aumentam o custo e a latência." + +#: includes/ai/settings/tab-provider.php:373 +msgid "Balanced" +msgstr "Equilibrado" + +#: includes/ai/settings/tab-provider.php:377 +msgid "No token limit set. The LLM will use as many tokens as it deems necessary to produce the result." +msgstr "Nenhum limite de Tokens definido. O LLM usará tantos Tokens quanto julgar necessário para produzir o resultado." + +#: includes/ai/settings/tab-provider.php:384 +msgid "Output Language" +msgstr "Idioma de saída" + +#: includes/ai/settings/tab-provider.php:386 +msgid "English (Optimized)" +msgstr "Inglês (otimizado)" + +#. translators: %s: site language +#: includes/ai/settings/tab-provider.php:387 +#, php-format +msgid "Site Language (%s)" +msgstr "Idioma do site (%s)" + +#: includes/ai/settings/tab-provider.php:390 +msgid "LLMs usually follow English instructions with higher precision and lower latency. We recommend English for complex rules, even if your posts are in other languages." +msgstr "LLMs geralmente seguem instruções em inglês com maior precisão e menor latência. Recomendamos inglês para regras complexas, mesmo que seus posts estejam em outros idiomas." + +#: includes/ai/settings/tab-provider.php:395 +#: includes/settings/class-settings.php:593 +msgid "Generate Prompt" +msgstr "Gerar prompt" + +#: includes/class-ai-adapter.php:165 +msgid "Empty response from AI." +msgstr "Resposta vazia da IA." + +#: includes/class-ai-handler.php:48 +msgid "Warning: Deactivating JEO will permanently remove all API Keys and AI settings for security reasons. Do you want to proceed?" +msgstr "Aviso: Desativar o JEO removerá permanentemente todas as chaves de API e configurações de IA por motivos de segurança. Deseja continuar?" + +#: includes/class-ai-handler.php:67 +#: includes/settings/ai-logs-page.php:16 +#: includes/settings/class-settings.php:657 +msgid "Unauthorized access." +msgstr "Acesso não autorizado." + +#: includes/class-ai-handler.php:74 +#: includes/settings/class-settings.php:658 +msgid "Invalid file." +msgstr "Arquivo inválido." + +#: includes/class-ai-handler.php:79 +#: includes/settings/class-settings.php:659 +msgid "File not found." +msgstr "Arquivo não encontrado." + +#: includes/class-ai-handler.php:106 +msgid "Google Gemini" +msgstr "Google Gemini" + +#: includes/class-ai-handler.php:107 +msgid "OpenAI" +msgstr "OpenAI" + +#: includes/class-ai-handler.php:108 +msgid "DeepSeek" +msgstr "DeepSeek" + +#: includes/class-ai-handler.php:109 +msgid "Anthropic Claude" +msgstr "Anthropic Claude" + +#: includes/class-ai-handler.php:110 +msgid "Ollama (Local/Custom)" +msgstr "Ollama (Local/Personalizado)" + +#: includes/class-ai-handler.php:111 +msgid "Mistral AI" +msgstr "Mistral AI" + +#: includes/class-ai-handler.php:112 +msgid "Zhipu AI (GLM)" +msgstr "Zhipu AI (GLM)" + +#: includes/class-ai-handler.php:113 +msgid "HuggingFace Inference" +msgstr "HuggingFace Inference" + +#: includes/class-ai-handler.php:114 +msgid "Grok (xAI)" +msgstr "Grok (xAI)" + +#: includes/class-ai-handler.php:115 +msgid "Cohere" +msgstr "Cohere" + +#. translators: %s: AI provider name +#: includes/class-ai-handler.php:169 +#, php-format +msgid "No API key configured for %s. Please add it in JEO → AI Settings." +msgstr "Nenhuma chave de API configurada para %s. Adicione-a em JEO → Configurações de IA." + +#: includes/class-ai-handler.php:422 +msgid "Invalid store type." +msgstr "Tipo de armazenamento inválido." + +#. translators: %s: Store type name. +#: includes/class-ai-handler.php:448 +#, php-format +msgid "%s store cleared successfully." +msgstr "Armazenamento %s limpo com sucesso." + +#: includes/class-ai-handler.php:491 +msgid "Layer store cleared successfully." +msgstr "Armazenamento de camadas limpo com sucesso." + +#: includes/class-ai-handler.php:518 +msgid "Query is required." +msgstr "Consulta é obrigatória." + +#: includes/class-ai-handler.php:539 +msgid "No matches found in the selected Vector Store. Try vectorizing some posts first." +msgstr "Nenhuma correspondência encontrada no Armazenamento de Vetores selecionado. Tente vetorizar alguns posts primeiro." + +#: includes/class-ai-handler.php:593 +msgid "No posts available to test." +msgstr "Nenhum post disponível para teste." + +#: includes/class-ai-handler.php:601 +msgid "The selected random post has no usable text content." +msgstr "O post aleatório selecionado não tem conteúdo de texto utilizável." + +#: includes/class-ai-handler.php:617 +msgid "The embedding provider returned an empty vector." +msgstr "O provedor de Embedding retornou um vetor vazio." + +#: includes/class-ai-handler.php:632 +msgid "Embedding created and saved to Test Store!" +msgstr "Embedding criado e salvo no Armazenamento de teste!" + +#: includes/class-ai-handler.php:673 +msgid "API Key or URL is required to list models." +msgstr "Chave de API ou URL é obrigatória para listar modelos." + +#: includes/class-ai-handler.php:706 +msgid "Model fetching not officially supported dynamically for Zhipu AI." +msgstr "A busca de modelos não é oficialmente suportada dinamicamente para Zhipu AI." + +#: includes/class-ai-handler.php:862 +msgid "Model fetching is not supported for this provider yet. Please enter the model ID manually." +msgstr "A busca de modelos não é suportada para este provedor ainda. Insira o ID do modelo manualmente." + +#: includes/class-ai-handler.php:870 +msgid "No models found. Check your API key or permissions." +msgstr "Nenhum modelo encontrado. Verifique sua chave de API ou permissões." + +#: includes/class-ai-handler.php:906 +msgid "No API Key provided or found in settings." +msgstr "Nenhuma chave de API fornecida ou encontrada nas configurações." + +#: includes/class-ai-handler.php:916 +msgid "Invalid provider." +msgstr "Provedor inválido." + +#: includes/class-ai-handler.php:939 +msgid "API Key is valid and active!" +msgstr "A chave de API é válida e está ativa!" + +#: includes/class-ai-handler.php:949 +msgid "The AI provider returned an unexpected response format." +msgstr "O provedor de IA retornou um formato de resposta inesperado." + +#: includes/class-ai-handler.php:994 +msgid "Context is required." +msgstr "Contexto é obrigatório." + +#: includes/class-ai-handler.php:1026 +#: includes/class-ai-handler.php:1186 +#: includes/class-ai-handler.php:1274 +#: includes/class-ai-handler.php:1323 +msgid "No active AI adapter found." +msgstr "Nenhum adaptador de IA ativo encontrado." + +#: includes/class-ai-handler.php:1142 +msgid "AI failed to generate prompt." +msgstr "A IA falhou ao gerar o prompt." + +#: includes/ai/class-context-handler.php:935 +#: includes/class-ai-handler.php:1160 +msgid "Prompt is required." +msgstr "Prompt é obrigatório." + +#: includes/class-ai-handler.php:1208 +msgid "Prompt successfully validated! The AI understood your instructions and returned a valid JSON array." +msgstr "Prompt validado com sucesso! A IA compreendeu suas instruções e retornou um array JSON válido." + +#: includes/class-ai-handler.php:1212 +msgid "Validation failed: The AI did not return a valid JSON array." +msgstr "Falha na validação: A IA não retornou um array JSON válido." + +#: includes/class-ai-handler.php:1217 +msgid "Validation failed: The AI missed mandatory keys (name, lat, lon, quote) in its JSON objects." +msgstr "Falha na validação: A IA não incluiu chaves obrigatórias (name, lat, lon, quote) em seus objetos JSON." + +#: includes/class-ai-handler.php:1222 +msgid "Validation failed: The AI returned an invalid is_primary value (must be boolean)." +msgstr "Falha na validação: A IA retornou um valor is_primary inválido (deve ser booleano)." + +#: includes/class-ai-handler.php:1228 +msgid "Prompt successfully validated! The AI returned an empty array, which means your filtering rules worked perfectly for the test text." +msgstr "Prompt validado com sucesso! A IA retornou um array vazio, o que significa que suas regras de filtragem funcionaram perfeitamente para o texto de teste." + +#: includes/class-ai-handler.php:1287 +msgid "An unexpected error occurred while processing the AI request." +msgstr "Ocorreu um erro inesperado ao processar a solicitação da IA." + +#: includes/class-ai-handler.php:1356 +msgid "An unexpected error occurred while processing the AI chat request." +msgstr "Ocorreu um erro inesperado ao processar a solicitação de chat da IA." + +#: includes/class-ai-handler.php:1485 +msgid "Backup created successfully!" +msgstr "Backup criado com sucesso!" + +#: includes/class-ai-handler.php:1506 +msgid "Missing filename" +msgstr "Nome de arquivo ausente" + +#: includes/class-ai-handler.php:1517 +msgid "File not found" +msgstr "Arquivo não encontrado" + +#: includes/geocode/class-geocode-handler.php:132 +msgid "Service provided by Mapbox (requires API Key)." +msgstr "Serviço fornecido pelo Mapbox (requer chave de API)." + +#: includes/settings/class-settings.php:712 +msgid "Discovery" +msgstr "Discovery" + +#: includes/maps/class-maps.php:198 +msgid "Map paint properties" +msgstr "Propriedades de paint do mapa" + +#: includes/maps/class-maps.php:202 +msgid "Map layout properties" +msgstr "Propriedades de layout do mapa" + +#: includes/menu/class-menu.php:48 +#: includes/menu/class-menu.php:49 +msgid "Welcome" +msgstr "Bem-vindo" + +#: includes/menu/class-menu.php:58 +#: includes/menu/class-menu.php:59 +msgid "Dashboard" +msgstr "Painel" + +#: includes/menu/class-menu.php:68 +#: includes/menu/class-menu.php:69 +msgid "AI (new)" +msgstr "IA (novo)" + +#: includes/minimap/class-minimap.php:201 +msgid "No matching layers found. Add layers manually or run the RAG indexer in JEO Settings." +msgstr "Nenhuma camada correspondente encontrada. Adicione camadas manualmente ou execute o indexador RAG nas configurações do JEO." + +#: includes/minimap/class-minimap.php:486 +msgid "The AI returned an empty response. Please try again." +msgstr "A IA retornou uma resposta vazia. Tente novamente." + +#: includes/minimap/class-minimap.php:1876 +msgid "Dark Base (Mapbox)" +msgstr "Base escura (Mapbox)" + +#: includes/minimap/class-minimap.php:1883 +msgid "Terrain Base (Mapbox)" +msgstr "Base de terreno (Mapbox)" + +#: includes/minimap/class-minimap.php:1890 +msgid "Satellite Base (Mapbox)" +msgstr "Base de satélite (Mapbox)" + +#: includes/minimap/class-minimap.php:1901 +msgid "Dark Base" +msgstr "Base escura" + +#: includes/minimap/class-minimap.php:1910 +msgid "Light Base" +msgstr "Base clara" + +#: includes/minimap/class-minimap.php:1919 +msgid "Satellite Base" +msgstr "Base de satélite" + +#: includes/privacy.php:20 +msgid "JEO collects and stores geolocation metadata for posts when authors explicitly geotag content. This may include latitude, longitude, and reverse-geocoded address components (city, region, country, postcode). This data is attached to the post and is available to anyone with access to the post." +msgstr "O JEO coleta e armazena metadados de geolocalização para posts quando os autores georreferenciam explicitamente o conteúdo. Isso pode incluir latitude, longitude e componentes de endereço geocodificados (cidade, região, país, código postal). Esses dados são anexados ao post e estão disponíveis para qualquer pessoa com acesso ao post." + +#: includes/privacy.php:21 +msgid "When the \"Stories Near You\" feature is enabled, visitors may opt in to share their browser geolocation. This location is sent to the site server solely to retrieve nearby posts and is not stored." +msgstr "Quando o recurso \"Histórias próximas a você\" está ativado, os visitantes podem optar por compartilhar sua geolocalização do navegador. Essa localização é enviada ao servidor do site apenas para recuperar posts próximos e não é armazenada." + +#: includes/privacy.php:22 +msgid "If AI-powered georeferencing is enabled, post titles and content may be sent to external AI providers (e.g., OpenAI, Google Gemini). AI usage logs (provider name, token counts) are stored locally as private posts and are not shared externally." +msgstr "Se o georreferenciamento por IA estiver ativado, títulos e conteúdo de posts podem ser enviados a provedores de IA externos (ex.: OpenAI, Google Gemini). Logs de uso da IA (nome do provedor, contagens de Tokens) são armazenados localmente como posts privados e não são compartilhados externamente." + +#: includes/privacy.php:23 +msgid "Geocoding requests may be sent to third-party services such as Nominatim (OpenStreetMap) or Mapbox, depending on the active geocoder." +msgstr "Solicitações de geocodificação podem ser enviadas a serviços de terceiros como Nominatim (OpenStreetMap) ou Mapbox, dependendo do geocodificador ativo." + +#: includes/privacy.php:40 +#: includes/privacy.php:126 +msgid "JEO Geolocation Metadata" +msgstr "Metadados de geolocalização do JEO" + +#: includes/privacy.php:106 +msgid "JEO Geolocation" +msgstr "Geolocalização do JEO" + +#: includes/settings/ai-logs-page.php:10 +msgid "AI Cost Dashboard & Debug Logs" +msgstr "Painel de custos da IA e logs de Debug" + +#: includes/settings/ai-logs-page.php:11 +msgid "Here you can view the most recent AI interactions powered by Neuron AI, along with their detailed token usage (Input/Output)." +msgstr "Aqui você pode visualizar as interações mais recentes da IA com tecnologia Neuron AI, junto com o uso detalhado de Tokens (entrada/saída)." + +#: includes/settings/ai-logs-page.php:31 +msgid "All AI logs have been permanently deleted." +msgstr "Todos os logs da IA foram excluídos permanentemente." + +#: includes/settings/ai-logs-page.php:75 +#: includes/settings/class-settings.php:646 +msgid "reqs" +msgstr "reqs" + +#: includes/settings/ai-logs-page.php:79 +#: includes/settings/ai-logs-page.php:104 +#: includes/settings/ai-logs-page.php:116 +#: includes/settings/class-settings.php:645 +msgid "Tokens" +msgstr "Tokens" + +#: includes/settings/ai-logs-page.php:82 +#: includes/settings/class-settings.php:647 +msgid "In (Prompt):" +msgstr "Entrada (Prompt):" + +#: includes/settings/ai-logs-page.php:83 +#: includes/settings/class-settings.php:648 +msgid "Out (Compl.):" +msgstr "Saída (Compl.):" + +#: includes/settings/ai-logs-page.php:94 +msgid "RAG & Vectorization Estimates" +msgstr "Estimativas de RAG e vetorização" + +#: includes/settings/ai-logs-page.php:95 +msgid "These numbers represent the estimated tokens consumed to create and query your RAG Knowledge Base. Since embedding models do not natively report their specific usage, the system estimates the token count via mathematical approximation." +msgstr "Esses números representam os Tokens estimados consumidos para criar e consultar sua Base de Conhecimento RAG. Como os modelos de Embedding não relatam nativamente seu uso específico, o sistema estima a contagem de Tokens por aproximação matemática." + +#: includes/settings/ai-logs-page.php:102 +msgid "Document Vectorization" +msgstr "Vetorização de documentos" + +#: includes/settings/ai-logs-page.php:107 +msgid "Tokens used inserting posts to Vector Store." +msgstr "Tokens usados inserindo posts no Armazenamento de Vetores." + +#: includes/settings/ai-logs-page.php:114 +msgid "Semantic Retrieval" +msgstr "Recuperação semântica" + +#: includes/settings/ai-logs-page.php:119 +msgid "Tokens used mapping user queries to vectors." +msgstr "Tokens usados mapeando consultas de usuários para vetores." + +#: includes/settings/ai-logs-page.php:147 +msgid "Search logs:" +msgstr "Buscar logs:" + +#: includes/settings/ai-logs-page.php:149 +msgid "Search Logs" +msgstr "Buscar logs" + +#: includes/settings/ai-logs-page.php:156 +msgid "Are you sure you want to delete ALL logs?" +msgstr "Tem certeza de que deseja excluir TODOS os logs?" + +#: includes/settings/ai-logs-page.php:157 +msgid "Clear All Logs" +msgstr "Limpar todos os logs" + +#. translators: %s: Number of log items. +#: includes/settings/ai-logs-page.php:164 +#, php-format +msgid "%s item" +msgid_plural "%s items" +msgstr[0] "%s item" +msgstr[1] "%s itens" + +#: includes/settings/ai-logs-page.php:189 +msgid "Date/Time" +msgstr "Data/Hora" + +#: includes/settings/ai-logs-page.php:190 +msgid "Provider" +msgstr "Provedor" + +#: includes/settings/ai-logs-page.php:191 +msgid "Tokens (In / Out)" +msgstr "Tokens (Entrada / Saída)" + +#: includes/settings/ai-logs-page.php:192 +msgid "Status / Output Preview" +msgstr "Status / pré-visualização da saída" + +#: includes/settings/ai-logs-page.php:193 +#: includes/settings/class-settings.php:602 +msgid "Actions" +msgstr "Ações" + +#: includes/settings/ai-logs-page.php:198 +msgid "No logs found yet. Ensure Debug Mode is enabled and georeference a post." +msgstr "Nenhum log encontrado ainda. Certifique-se de que o modo de Debug está ativado e georreferencie um post." + +#: includes/settings/ai-logs-page.php:238 +#: includes/settings/class-settings.php:649 +msgid "Total" +msgstr "Total" + +#: includes/settings/ai-logs-page.php:239 +#: includes/settings/class-settings.php:650 +msgid "Prompt:" +msgstr "Prompt:" + +#: includes/settings/ai-logs-page.php:239 +#: includes/settings/class-settings.php:651 +msgid "Completion:" +msgstr "Conclusão:" + +#: includes/settings/ai-logs-page.php:244 +msgid "View Details" +msgstr "Ver detalhes" + +#: includes/settings/ai-logs-page.php:249 +msgid "AI Interaction Details" +msgstr "Detalhes da interação da IA" + +#: includes/settings/ai-logs-page.php:254 +msgid "Timestamp" +msgstr "Data/hora" + +#: includes/settings/ai-logs-page.php:255 +msgid "Input Tokens" +msgstr "Tokens de entrada" + +#: includes/settings/ai-logs-page.php:256 +msgid "Output Tokens" +msgstr "Tokens de saída" + +#: includes/settings/ai-logs-page.php:259 +msgid "Input (Prompt sent)" +msgstr "Entrada (Prompt enviado)" + +#: includes/settings/ai-logs-page.php:262 +msgid "Output (Raw Response)" +msgstr "Saída (resposta bruta)" + +#: includes/settings/class-settings.php:573 +msgid "[System] Console cleared." +msgstr "[Sistema] Console limpo." + +#: includes/settings/class-settings.php:574 +msgid "Missing Configuration" +msgstr "Configuração ausente" + +#: includes/settings/class-settings.php:575 +msgid "Checking..." +msgstr "Verificando..." + +#: includes/settings/class-settings.php:578 +msgid "Failed" +msgstr "Falhou" + +#: includes/settings/class-settings.php:579 +msgid "Request Failed" +msgstr "Falha na solicitação" + +#: includes/settings/class-settings.php:580 +msgid "Loading..." +msgstr "Carregando..." + +#: includes/settings/class-settings.php:582 +msgid "Select or type a model..." +msgstr "Selecione ou digite um modelo..." + +#: includes/settings/class-settings.php:583 +msgid " (Non-chat)" +msgstr " (Não-chat)" + +#: includes/settings/class-settings.php:584 +msgid "Please enter an API Key first." +msgstr "Insira uma chave de API primeiro." + +#: includes/settings/class-settings.php:585 +msgid "Failed to fetch models: " +msgstr "Falha ao buscar modelos: " + +#: includes/settings/class-settings.php:586 +msgid "Unknown error" +msgstr "Erro desconhecido" + +#: includes/settings/class-settings.php:587 +msgid "Error fetching models. Check your key and connection." +msgstr "Erro ao buscar modelos. Verifique sua chave e conexão." + +#: includes/settings/class-settings.php:588 +msgid "Please type a description first." +msgstr "Digite uma descrição primeiro." + +#: includes/settings/class-settings.php:589 +msgid "Asking AI..." +msgstr "Perguntando à IA..." + +#: includes/ai/settings/tab-context.php:199 +#: includes/settings/class-settings.php:590 +msgid "Generating..." +msgstr "Gerando..." + +#: includes/settings/class-settings.php:591 +msgid "✨ Applied above." +msgstr "✨ Aplicado acima." + +#: includes/settings/class-settings.php:592 +msgid "Error generating prompt." +msgstr "Erro ao gerar prompt." + +#: includes/settings/class-settings.php:594 +msgid "Testing..." +msgstr "Testando..." + +#: includes/settings/class-settings.php:595 +msgid "✅ Valid" +msgstr "✅ Válido" + +#: includes/settings/class-settings.php:596 +msgid "❌ Invalid" +msgstr "❌ Inválido" + +#: includes/settings/class-settings.php:597 +msgid "Validate Prompt" +msgstr "Validar prompt" + +#: includes/settings/class-settings.php:598 +msgid "Loading backups..." +msgstr "Carregando backups..." + +#: includes/settings/class-settings.php:599 +msgid "File" +msgstr "Arquivo" + +#: includes/settings/class-settings.php:600 +msgid "Date" +msgstr "Data" + +#: includes/settings/class-settings.php:601 +#: js/build/mapBlocks.js:3 +#: js/build/mapsSidebar.js:1 +msgid "Size" +msgstr "Tamanho" + +#: includes/settings/class-settings.php:606 +msgid "Delete?" +msgstr "Excluir?" + +#: includes/settings/class-settings.php:607 +msgid "Vectorizing..." +msgstr "Vetorizando..." + +#: includes/settings/class-settings.php:608 +msgid "Vectorize Now" +msgstr "Vetorizar agora" + +#: includes/settings/class-settings.php:609 +msgid "Clear knowledge base?" +msgstr "Limpar a base de conhecimento?" + +#: includes/settings/class-settings.php:610 +msgid "Fetching post and generating vector embeddings..." +msgstr "Obtendo post e gerando Embeddings de vetores..." + +#: includes/settings/class-settings.php:611 +msgid "Success!" +msgstr "Sucesso!" + +#: includes/settings/class-settings.php:612 +msgid "Post Extracted:" +msgstr "Post extraído:" + +#: includes/settings/class-settings.php:613 +msgid "Vector Dimensions:" +msgstr "Dimensões do vetor:" + +#: includes/settings/class-settings.php:614 +msgid "Text Snippet:" +msgstr "Trecho de texto:" + +#: includes/settings/class-settings.php:615 +msgid "Vector Preview:" +msgstr "Pré-visualização do vetor:" + +#: includes/settings/class-settings.php:617 +msgid "Searching..." +msgstr "Buscando..." + +#: includes/settings/class-settings.php:618 +msgid "Searching the knowledge base " +msgstr "Buscando na base de conhecimento " + +#: includes/settings/class-settings.php:619 +msgid "No documents found. Have you run vectorization?" +msgstr "Nenhum documento encontrado. Você executou a vetorização?" + +#: includes/settings/class-settings.php:620 +msgid "Score / Relevance" +msgstr "Pontuação / Relevância" + +#: includes/settings/class-settings.php:621 +msgid "Metadata" +msgstr "Metadados" + +#: includes/settings/class-settings.php:624 +msgid "Processing..." +msgstr "Processando..." + +#: includes/settings/class-settings.php:625 +msgid "Clearing..." +msgstr "Limpando..." + +#: includes/settings/class-settings.php:626 +msgid "This will schedule clearing ALL AI-geolocated posts in the background. Continue?" +msgstr "Isso agendará a limpeza de TODOS os posts geolocalizados pela IA em segundo plano. Continuar?" + +#: includes/settings/class-settings.php:627 +msgid "ARE YOU SURE? This cannot be undone and will require full re-vectorization for these posts." +msgstr "VOCÊ TEM CERTEZA? Isso não pode ser desfeito e exigirá nova vetorização completa para esses posts." + +#: includes/settings/class-settings.php:628 +msgid "Bulk clearing started in background." +msgstr "Limpeza em lote iniciada em segundo plano." + +#: includes/settings/class-settings.php:630 +msgid "Collapse" +msgstr "Recolher" + +#: includes/settings/class-settings.php:632 +msgid "Low" +msgstr "Baixo" + +#: includes/settings/class-settings.php:633 +msgid "Fair" +msgstr "Razoável" + +#: includes/settings/class-settings.php:634 +msgid "Optimal" +msgstr "Ótimo" + +#: includes/settings/class-settings.php:635 +msgid "API call failed" +msgstr "Falha na chamada da API" + +#: includes/settings/class-settings.php:638 +msgid "Delete log file?" +msgstr "Excluir arquivo de log?" + +#: includes/settings/class-settings.php:639 +msgid "No matching layers found." +msgstr "Nenhuma camada correspondente encontrada." + +#: includes/settings/class-settings.php:640 +msgid " matching layers:" +msgstr " camadas correspondentes:" + +#: includes/settings/class-settings.php:643 +msgid "Clear layer store?" +msgstr "Limpar armazenamento de camadas?" + +#: includes/settings/class-settings.php:644 +msgid "Please enter a post ID or search text." +msgstr "Digite um ID de post ou texto de busca." + +#: includes/settings/class-settings.php:694 +#: includes/settings/class-settings.php:695 +msgid "AI Debug Logs" +msgstr "Logs de Debug da IA" + +#: includes/settings/class-settings.php:711 +msgid "Appearance" +msgstr "Aparência" + +#: includes/settings/settings-page.php:10 +msgid "Jeo Settings" +msgstr "Configurações do Jeo" + +#: includes/settings/settings-page.php:38 +msgid "Enabled Post Types" +msgstr "Tipos de post ativados" + +#: includes/settings/settings-page.php:64 +msgid "Default map center (latitude)" +msgstr "Centro padrão do mapa (latitude)" + +#: includes/settings/settings-page.php:71 +msgid "Default map center (longitude)" +msgstr "Centro padrão do mapa (longitude)" + +#: includes/settings/settings-page.php:78 +msgid "User location precision" +msgstr "Precisão da localização do usuário" + +#: includes/settings/settings-page.php:83 +msgid "Lower = less precision, more privacy." +msgstr "Menor = menos precisão, mais privacidade." + +#: includes/settings/settings-page.php:84 +msgid "https://en.wikipedia.org/wiki/Decimal_degrees#Precision" +msgstr "https://en.wikipedia.org/wiki/Decimal_degrees#Precision" + +#: includes/settings/settings-page.php:84 +msgid "Learn more about decimal degree precision." +msgstr "Saiba mais sobre precisão de graus decimais." + +#: includes/settings/settings-page.php:90 +msgid "Show Story Maps on Post Archives" +msgstr "Mostrar mapas narrativos nos arquivos de posts" + +#: includes/settings/settings-page.php:93 +msgid "Enable this to display relevant story maps at the top of category and tag archive pages." +msgstr "Ative para exibir mapas narrativos relevantes no topo das páginas de arquivo de categorias e tags." + +#: includes/settings/settings-page.php:141 +msgid "Mapbox API Key" +msgstr "Chave de API do Mapbox" + +#: includes/settings/settings-page.php:160 +msgid "Geocoder service" +msgstr "Serviço de geocodificação" + +#: includes/settings/settings-page.php:167 +msgid "Select the service used to translate addresses into coordinates. Mapbox requires an API Key configured in the General tab." +msgstr "Selecione o serviço usado para traduzir endereços em coordenadas. O Mapbox requer uma chave de API configurada na aba Geral." + +#: includes/settings/settings-page.php:177 +msgid "Font URL" +msgstr "URL da fonte" + +#: includes/settings/settings-page.php:183 +msgid "Font Name" +msgstr "Nome da fonte" + +#: includes/settings/settings-page.php:189 +msgid "Secondary Font URL" +msgstr "URL da fonte secundária" + +#: includes/settings/settings-page.php:195 +msgid "Secondary Font Name" +msgstr "Nome da fonte secundária" + +#: includes/settings/settings-page.php:201 +msgid "Info button font size (rem)" +msgstr "Tamanho da fonte do botão de informação (rem)" + +#: includes/settings/settings-page.php:214 +msgid "Primary Color" +msgstr "Cor primária" + +#: includes/settings/settings-page.php:226 +msgid "Info button text color" +msgstr "Cor do texto do botão de informação" + +#: includes/settings/settings-page.php:238 +msgid "Close button text color" +msgstr "Cor do texto do botão de fechar" + +#: includes/settings/settings-page.php:244 +msgid "Secondary Color" +msgstr "Cor secundária" + +#: includes/settings/settings-page.php:250 +msgid "Map widgets background color" +msgstr "Cor de fundo dos widgets do mapa" + +#: includes/settings/settings-page.php:256 +msgid "Map widgets text color" +msgstr "Cor do texto dos widgets do mapa" + +#: includes/settings/settings-page.php:262 +msgid "Map widgets background color (hover)" +msgstr "Cor de fundo dos widgets do mapa (ao passar o mouse)" + +#: includes/settings/settings-page.php:271 +msgid "Map Marker Icons" +msgstr "Ícones de marcadores do mapa" + +#: includes/settings/settings-page.php:275 +msgid "Primary Pin URL" +msgstr "URL do pino primário" + +#: includes/settings/settings-page.php:278 +msgid "URL for the primary location marker icon. Leave empty to use the default blue marker." +msgstr "URL do ícone do marcador de localização primário. Deixe vazio para usar o marcador azul padrão." + +#: includes/settings/settings-page.php:282 +msgid "Secondary Pin URL" +msgstr "URL do pino secundário" + +#: includes/settings/settings-page.php:285 +msgid "URL for the secondary location marker icon. Leave empty to use the default grey marker." +msgstr "URL do ícone do marcador de localização secundário. Deixe vazio para usar o marcador cinza padrão." + +#: includes/settings/settings-page.php:311 +msgid "Discovery page" +msgstr "Página do Discovery" + +#: includes/settings/settings-page.php:318 +msgid "Select a page" +msgstr "Selecione uma página" + +#: includes/settings/settings-page.php:322 +msgid "Select the page where the JEO Discovery block is located." +msgstr "Selecione a página onde o bloco Discovery do JEO está localizado." + +#: includes/stories-near-you/class-stories-near-you.php:432 +#: js/build/storiesNearYou.js:1 +msgid "Unable to load stories near you." +msgstr "Não foi possível carregar histórias próximas a você." + +#: js/build/contextSidebar.js:1 +msgid "Suggested Paragraphs" +msgstr "Parágrafos sugeridos" + +#: js/build/contextSidebar.js:1 +msgid "Inserted" +msgstr "Inserido" + +#: js/build/contextSidebar.js:1 +msgid "Insert into article" +msgstr "Inserir no artigo" + +#: js/build/contextSidebar.js:1 +msgid "Copied!" +msgstr "Copiado!" + +#: js/build/contextSidebar.js:1 +msgid "Copy" +msgstr "Copiar" + +#: js/build/contextSidebar.js:1 +msgid "Failed to generate suggestions." +msgstr "Falha ao gerar sugestões." + +#: js/build/contextSidebar.js:1 +msgid "Error contacting AI." +msgstr "Erro ao contatar a IA." + +#: js/build/contextSidebar.js:1 +msgid "Failed to process message." +msgstr "Falha ao processar a mensagem." + +#: js/build/contextSidebar.js:1 +msgid "Generate new editorial suggestions based on the current post content." +msgstr "Gerar novas sugestões editoriais com base no conteúdo atual do post." + +#: js/build/contextSidebar.js:1 +msgid "Failed to clear conversation." +msgstr "Falha ao limpar a conversa." + +#: js/build/contextSidebar.js:1 +msgid "Restoring conversation..." +msgstr "Restaurando conversa..." + +#: js/build/contextSidebar.js:1 +msgid "Use the AI Context Assistant to generate editorial suggestions based on your article and the site's knowledge base." +msgstr "Use o Assistente de Contexto da IA para gerar sugestões editoriais com base no seu artigo e na base de conhecimento do site." + +#: js/build/contextSidebar.js:1 +msgid "Generate Suggestions" +msgstr "Gerar sugestões" + +#: js/build/contextSidebar.js:1 +msgid "Thinking..." +msgstr "Pensando..." + +#: js/build/contextSidebar.js:1 +msgid "Send" +msgstr "Enviar" + +#: js/build/contextSidebar.js:1 +msgid "Retry" +msgstr "Tentar novamente" + +#: js/build/contextSidebar.js:1 +msgid "AI Context" +msgstr "Contexto da IA" + +#: js/build/contextSidebar.js:1 +msgid "AI Context Assistant" +msgstr "Assistente de Contexto da IA" + +#: js/build/mapBlocks.js:1 +msgid "Analyzing content…" +msgstr "Analisando conteúdo…" + +#: js/build/mapBlocks.js:1 +msgid "Searching for map layers…" +msgstr "Buscando camadas de mapa…" + +#: js/build/mapBlocks.js:1 +msgid "Generating map configuration…" +msgstr "Gerando configuração do mapa…" + +#: js/build/mapBlocks.js:1 +msgid "Map generated from post content." +msgstr "Mapa gerado a partir do conteúdo do post." + +#: js/build/mapBlocks.js:1 +msgid "Failed to suggest layers." +msgstr "Falha ao sugerir camadas." + +#: js/build/mapBlocks.js:1 +msgid "Request failed." +msgstr "Falha na solicitação." + +#: js/build/mapBlocks.js:1 +msgid "Failed to update map." +msgstr "Falha ao atualizar o mapa." + +#: js/build/mapBlocks.js:1 +msgid "Map generated from prompt." +msgstr "Mapa gerado a partir do prompt." + +#: js/build/mapBlocks.js:1 +msgid "Failed to generate from prompt." +msgstr "Falha ao gerar a partir do prompt." + +#: js/build/mapBlocks.js:1 +msgid "Map regenerated from post content." +msgstr "Mapa regenerado a partir do conteúdo do post." + +#: js/build/mapBlocks.js:1 +msgid "AI-Assisted Map" +msgstr "Mapa assistido por IA" + +#: js/build/mapBlocks.js:1 +msgid "AI will analyze your content and suggest relevant map layers, center point, and zoom level." +msgstr "A IA analisará seu conteúdo e sugerirá camadas de mapa relevantes, ponto central e nível de zoom." + +#: js/build/mapBlocks.js:1 +msgid "Generate from post content" +msgstr "Gerar a partir do conteúdo do post" + +#: js/build/mapBlocks.js:1 +msgid "Generate from prompt" +msgstr "Gerar a partir do prompt" + +#: js/build/mapBlocks.js:1 +msgid "Map prompt" +msgstr "Prompt do mapa" + +#: js/build/mapBlocks.js:1 +msgid "Generate map" +msgstr "Gerar mapa" + +#: js/build/mapBlocks.js:1 +msgid "Base Layer" +msgstr "Camada base" + +#: js/build/mapBlocks.js:1 +msgid "Base terrain" +msgstr "Terreno base" + +#: js/build/mapBlocks.js:1 +msgid "Geolocation Pins" +msgstr "Pinos de geolocalização" + +#: js/build/mapBlocks.js:1 +msgid "Show pins" +msgstr "Mostrar pinos" + +#: js/build/mapBlocks.js:1 +msgid "Yes" +msgstr "Sim" + +#: js/build/mapBlocks.js:1 +msgid "AI Assistant" +msgstr "Assistente de IA" + +#: js/build/mapBlocks.js:1 +msgid "Assistant" +msgstr "Assistente" + +#: js/build/mapBlocks.js:1 +msgid "You" +msgstr "Você" + +#: js/build/mapBlocks.js:1 +msgid "Typing…" +msgstr "Digitando…" + +#: js/build/mapBlocks.js:1 +msgid "Send message" +msgstr "Enviar mensagem" + +#: js/build/mapBlocks.js:1 +msgid "Regenerate" +msgstr "Regenerar" + +#: js/build/mapBlocks.js:1 +msgid "New prompt" +msgstr "Novo prompt" + +#: js/build/mapBlocks.js:1 +msgid "Generate" +msgstr "Gerar" + +#: js/build/mapBlocks.js:1 +msgid "Display an AI-generated contextual map with layers and geolocation pins" +msgstr "Exibe um mapa contextual gerado por IA com camadas e pinos de geolocalização" + +#: js/build/mapBlocks.js:3 +msgid "Stories Near You" +msgstr "Histórias próximas a você" + +#: js/build/mapBlocks.js:3 +msgid "Display geolocated posts sorted by proximity to the reader" +msgstr "Exibe posts geolocalizados ordenados por proximidade com o leitor" + +#: js/build/mapBlocks.js:3 +msgid "— None —" +msgstr "— Nenhum —" + +#: js/build/mapBlocks.js:3 +msgid "All geo-enabled types" +msgstr "Todos os tipos com geolocalização" + +#: js/build/mapBlocks.js:3 +msgid "Location Preview" +msgstr "Pré-visualização da localização" + +#: js/build/mapBlocks.js:3 +msgid "Post Card" +msgstr "Cartão de post" + +#: js/build/mapBlocks.js:3 +msgid "Layout" +msgstr "Layout" + +#: js/build/mapBlocks.js:3 +msgid "Grid" +msgstr "Grade" + +#: js/build/mapBlocks.js:3 +msgid "List" +msgstr "Lista" + +#: js/build/mapBlocks.js:3 +msgid "Columns" +msgstr "Colunas" + +#: js/build/mapBlocks.js:3 +msgid "Media position" +msgstr "Posição da mídia" + +#: js/build/mapBlocks.js:3 +msgid "Top" +msgstr "Topo" + +#: js/build/mapBlocks.js:3 +msgid "Left" +msgstr "Esquerda" + +#: js/build/mapBlocks.js:3 +msgid "Right" +msgstr "Direita" + +#: js/build/mapBlocks.js:3 +msgid "Behind" +msgstr "Atrás" + +#: js/build/mapBlocks.js:3 +msgid "Image shape" +msgstr "Formato da imagem" + +#: js/build/mapBlocks.js:3 +msgid "Landscape" +msgstr "Paisagem" + +#: js/build/mapBlocks.js:3 +msgid "Portrait" +msgstr "Retrato" + +#: js/build/mapBlocks.js:3 +msgid "Square" +msgstr "Quadrado" + +#: js/build/mapBlocks.js:3 +msgid "Uncropped" +msgstr "Sem recorte" + +#: js/build/mapBlocks.js:3 +msgid "Image size" +msgstr "Tamanho da imagem" + +#: js/build/mapBlocks.js:3 +msgid "Link featured image" +msgstr "Vincular imagem destacada" + +#: js/build/mapBlocks.js:3 +msgid "Query Settings" +msgstr "Configurações de consulta" + +#: js/build/mapBlocks.js:3 +msgid "Posts per page" +msgstr "Posts por página" + +#: js/build/mapBlocks.js:3 +msgid "Post type" +msgstr "Tipo de post" + +#: js/build/mapBlocks.js:3 +msgid "Filters" +msgstr "Filtros" + +#: js/build/mapBlocks.js:3 +msgid "Exclude categories" +msgstr "Excluir categorias" + +#: js/build/mapBlocks.js:3 +msgid "Exclude tags" +msgstr "Excluir tags" + +#: js/build/mapBlocks.js:3 +msgid "Display" +msgstr "Exibição" + +#: js/build/mapBlocks.js:3 +msgid "Show featured image" +msgstr "Mostrar imagem destacada" + +#: js/build/mapBlocks.js:3 +msgid "Show category" +msgstr "Mostrar categoria" + +#: js/build/mapBlocks.js:3 +msgid "Show date" +msgstr "Mostrar data" + +#: js/build/mapBlocks.js:3 +msgid "Show excerpt" +msgstr "Mostrar resumo" + +#: js/build/mapBlocks.js:3 +msgid "Excerpt length" +msgstr "Comprimento do resumo" + +#: js/build/mapBlocks.js:3 +msgid "Show author" +msgstr "Mostrar autor" + +#: js/build/mapBlocks.js:3 +msgid "Show avatar" +msgstr "Mostrar avatar" + +#: js/build/mapBlocks.js:3 +msgid "Show read more link" +msgstr "Mostrar link 'ler mais'" + +#: js/build/mapBlocks.js:3 +msgid "Read more label" +msgstr "Texto do link 'ler mais'" + +#: js/build/mapBlocks.js:3 +msgid "Typography & Spacing" +msgstr "Tipografia e espaçamento" + +#: js/build/mapBlocks.js:3 +msgid "Type scale" +msgstr "Escala de tipografia" + +#: js/build/mapBlocks.js:3 +msgid "Image scale" +msgstr "Escala de imagem" + +#: js/build/mapBlocks.js:3 +msgid "Column gap" +msgstr "Espaço entre colunas" + +#: js/build/mapBlocks.js:3 +msgid "Min height (vh)" +msgstr "Altura mínima (vh)" + +#: js/build/mapBlocks.js:3 +#: js/build/mapsSidebar.js:1 +msgid "Fill Color" +msgstr "Cor de preenchimento" + +#: js/build/mapBlocks.js:3 +#: js/build/mapsSidebar.js:1 +msgid "Outline Color" +msgstr "Cor do contorno" + +#: js/build/mapBlocks.js:3 +#: js/build/mapsSidebar.js:1 +msgid "Line Color" +msgstr "Cor da linha" + +#: js/build/mapBlocks.js:3 +#: js/build/mapsSidebar.js:1 +msgid "Circle Color" +msgstr "Cor do círculo" + +#: js/build/mapBlocks.js:3 +#: js/build/mapsSidebar.js:1 +msgid "Stroke Color" +msgstr "Cor do traçado" + +#: js/build/mapBlocks.js:3 +#: js/build/mapsSidebar.js:1 +msgid "Fill Opacity" +msgstr "Opacidade do preenchimento" + +#: js/build/mapBlocks.js:3 +#: js/build/mapsSidebar.js:1 +msgid "Line Opacity" +msgstr "Opacidade da linha" + +#: js/build/mapBlocks.js:3 +#: js/build/mapsSidebar.js:1 +msgid "Circle Opacity" +msgstr "Opacidade do círculo" + +#: js/build/mapBlocks.js:3 +#: js/build/mapsSidebar.js:1 +msgid "Line Width" +msgstr "Largura da linha" + +#: js/build/mapBlocks.js:3 +#: js/build/mapsSidebar.js:1 +msgid "Circle Radius" +msgstr "Raio do círculo" + +#: js/build/mapBlocks.js:3 +#: js/build/mapsSidebar.js:1 +msgid "Stroke Width" +msgstr "Largura do traçado" + +#: js/build/mapBlocks.js:3 +#: js/build/mapsSidebar.js:1 +msgid "Layer Style" +msgstr "Estilo da camada" + +#: js/build/mapBlocks.js:3 +#: js/build/mapsSidebar.js:1 +msgid "Styling is only supported for fill, line, and circle layer types." +msgstr "A estilização é suportada apenas para tipos de camada de preenchimento, linha e círculo." + +#: js/build/mapBlocks.js:3 +#: js/build/mapsSidebar.js:1 +msgid "AI Default Style" +msgstr "Estilo padrão da IA" + +#: js/build/mapBlocks.js:3 +#: js/build/mapsSidebar.js:1 +msgid "Use AI-suggested default style" +msgstr "Usar estilo padrão sugerido pela IA" + +#: js/build/mapBlocks.js:3 +#: js/build/mapsSidebar.js:1 +msgid "Filter:" +msgstr "Filtro:" + +#: js/build/mapBlocks.js:3 +#: js/build/mapsSidebar.js:1 +msgid "Paint:" +msgstr "Paint:" + +#: js/build/mapBlocks.js:3 +#: js/build/mapsSidebar.js:1 +msgid "Opacity" +msgstr "Opacidade" + +#: js/build/mapBlocks.js:3 +#: js/build/mapsSidebar.js:1 +msgid "Style layer" +msgstr "Camada de estilo" + +#: js/build/postsSidebar.js:2 +#: js/build/postsSidebar.js:3 +msgid "Expand panel" +msgstr "Expandir painel" + +#: js/build/postsSidebar.js:2 +#: js/build/postsSidebar.js:3 +msgid "Minimize panel" +msgstr "Minimizar painel" + +#: js/build/postsSidebar.js:3 +#: js/build/postsSidebar.js:4 +msgid "Review AI Suggestions" +msgstr "Revisar sugestões da IA" + +#: js/build/postsSidebar.js:3 +msgid "Select locations to add to the map:" +msgstr "Selecione as localizações para adicionar ao mapa:" + +#: js/build/postsSidebar.js:3 +msgid "Enriched via Geocoder" +msgstr "Enriquecido via geocodificador" + +#: js/build/postsSidebar.js:3 +msgid "AI flagged this as primary location" +msgstr "A IA marcou esta como localização principal" + +#: js/build/postsSidebar.js:3 +msgid "Enrich" +msgstr "Enriquecer" + +#: js/build/postsSidebar.js:3 +msgid "Context:" +msgstr "Contexto:" + +#: js/build/postsSidebar.js:3 +msgid "Verified:" +msgstr "Verificado:" + +#: js/build/postsSidebar.js:3 +msgid "Score too low for reliable mapping." +msgstr "Pontuação baixa demais para mapeamento confiável." + +#: js/build/postsSidebar.js:3 +msgid "Discard All" +msgstr "Descartar tudo" + +#: js/build/postsSidebar.js:3 +msgid "Retrying..." +msgstr "Tentando novamente..." + +#: js/build/postsSidebar.js:3 +msgid "Retry AI" +msgstr "Tentar novamente a IA" + +#: js/build/postsSidebar.js:3 +msgid "Add to Map" +msgstr "Adicionar ao mapa" + +#: js/build/postsSidebar.js:3 +msgid "Invalid response from AI." +msgstr "Resposta inválida da IA." + +#: js/build/postsSidebar.js:3 +msgid "No locations found by the AI." +msgstr "Nenhuma localização encontrada pela IA." + +#: js/build/postsSidebar.js:3 +msgid "Error processing AI georeference." +msgstr "Erro ao processar georreferenciamento da IA." + +#: js/build/postsSidebar.js:3 +msgctxt "Artifical Intelligence" +msgid "AI" +msgstr "IA" + +#: js/build/postsSidebar.js:4 +msgid "Review Suggestions" +msgstr "Revisar sugestões" + +#: js/build/postsSidebar.js:4 +msgid "Processing AI..." +msgstr "Processando IA..." + +#: js/build/postsSidebar.js:4 +msgid "Geolocate with AI" +msgstr "Geolocalizar com IA" + +#. translators: %d: number of posts processed. +#: includes/ai/class-bulk-processor.php:407 +#, php-format +msgid "Processed batch of %d post." +msgid_plural "Processed batch of %d posts." +msgstr[0] "Lote de %d post processado." +msgstr[1] "Lote de %d posts processado." + +#. translators: %d: number of posts cleared. +#: includes/ai/class-bulk-processor.php:452 +#, php-format +msgid "Cleared batch of %d post." +msgid_plural "Cleared batch of %d posts." +msgstr[0] "Lote de %d post limpo." +msgstr[1] "Lote de %d posts limpo." + +#: includes/ai/class-bulk-processor.php:793 +msgctxt "post geolocation status" +msgid "Geolocated" +msgstr "Geolocalizado" + +#: includes/ai/class-bulk-processor.php:795 +msgctxt "post geolocation status" +msgid "Pending Approval" +msgstr "Aguardando aprovação" + +#: includes/ai/class-bulk-processor.php:801 +msgctxt "post geolocation status" +msgid "Not Processed" +msgstr "Não processado" + +#: includes/settings/class-settings.php:576 +msgctxt "geocoder status" +msgid "Active" +msgstr "Ativo" + +#: includes/settings/class-settings.php:577 +msgctxt "geocoder status" +msgid "Invalid" +msgstr "Inválido" + +#: includes/settings/class-settings.php:622 +msgctxt "fallback post title" +msgid "Untitled" +msgstr "Sem título" + +#. translators: %d: number of displayed stories. +#: js/build/discovery.js:3 +#, js-format +msgid "Displayed story: %d" +msgid_plural "Displayed stories: %d" +msgstr[0] "Story exibida: %d" +msgstr[1] "Stories exibidas: %d" + +#: js/build/mapBlocks.js:1 +msgctxt "base terrain" +msgid "Dark" +msgstr "Escuro" + +#: js/build/mapBlocks.js:1 +msgctxt "base terrain" +msgid "Light" +msgstr "Claro" + +#: js/build/mapBlocks.js:1 +msgctxt "base terrain" +msgid "Satellite" +msgstr "Satélite" + +#: js/build/mapBlocks.js:1 +msgctxt "base terrain" +msgid "None" +msgstr "Nenhum" + +#: js/build/mapBlocks.js:3 +#: js/build/mapsSidebar.js:1 +msgctxt "layer usage default label" +msgid "Default" +msgstr "Padrão" + +#: js/build/mapBlocks.js:3 +#: js/build/mapsSidebar.js:1 +msgctxt "layer" +msgid "Fixed" +msgstr "Fixo" + +#: js/build/mapBlocks.js:3 +#: js/build/mapsSidebar.js:1 +msgctxt "layer" +msgid "Swappable" +msgstr "Substituível" + +#: js/build/mapBlocks.js:3 +#: js/build/mapsSidebar.js:1 +msgctxt "layer" +msgid "Switchable" +msgstr "Alternável" + +#. translators: %d: number of geolocation points. +#: js/build/postsSidebar.js:2 +#, js-format +msgid "Point (%d)" +msgid_plural "Points (%d)" +msgstr[0] "Ponto (%d)" +msgstr[1] "Pontos (%d)" + +#. translators: %d: number of selected AI suggestions. +#: js/build/postsSidebar.js:3 +#, js-format +msgid "AI Suggestion (%d selected)" +msgid_plural "AI Suggestions (%d selected)" +msgstr[0] "Sugestão da IA (%d selecionada)" +msgstr[1] "Sugestões da IA (%d selecionadas)" + +#. translators: %d: number of locations found during bulk AI processing. +#: js/build/postsSidebar.js:4 +#, js-format +msgid "AI found %d location during bulk processing." +msgid_plural "AI found %d locations during bulk processing." +msgstr[0] "A IA encontrou %d localização durante o processamento em lote." +msgstr[1] "A IA encontrou %d localizações durante o processamento em lote." + +#: includes/geocode/class-geocode-handler.php:123 +msgid "Geocoding service provided by OpenStreetMap at https://nominatim.openstreetmap.org/." +msgstr "Serviço de geocodificação fornecido pelo OpenStreetMap em https://nominatim.openstreetmap.org/." + +#: includes/geocode/class-geocode-handler.php:249 +msgid "Metadata storing the locations related to the post. Each location is an object with latitude, longitude, and geocode attributes." +msgstr "Metadados que armazenam as localizações relacionadas ao post. Cada localização é um objeto com latitude, longitude e atributos de geocódigo." + +#: includes/menu/class-menu.php:35 +#: includes/menu/class-menu.php:36 +msgid "JEO" +msgstr "JEO" + +#: includes/settings/class-settings.php:538 +msgid "JEO could not save this asset URL. Use a valid http(s) or root-relative URL." +msgstr "O JEO não pôde salvar esta URL de asset. Use uma URL http(s) ou relativa à raiz válida." + +#. translators: %s is the geocoder name. Example: Nominatim options. +#: includes/settings/settings-page.php:111 +#, php-format +msgctxt "geocoder_options" +msgid "%s options" +msgstr "Opções de %s" + +#: includes/settings/settings-page.php:292 +msgid "Embed" +msgstr "Embed" + +#: includes/settings/settings-page.js:916 +#: includes/settings/settings-page.js:918 +msgid "Choose Image" +msgstr "Escolher imagem" + +#: js/build/discovery.js:6 +msgid "Changes applied" +msgstr "Alterações aplicadas" + +#: js/build/discovery.js:6 +msgid "Not applied" +msgstr "Não aplicado" + +#: js/build/jeoMap.js:1 +msgid "Show previous post" +msgstr "Mostrar post anterior" + +#: js/build/jeoMap.js:1 +msgid "Show next post" +msgstr "Mostrar próximo post" + +#. translators: %d: number of markers in the current map area. +#: js/build/jeoMap.js:2 +#, js-format +msgid "%d marker in this area" +msgid_plural "%d markers in this area" +msgstr[0] "%d marcador nesta área" +msgstr[1] "%d marcadores nesta área" + +#: js/build/mapBlocks.js:3 +msgid "Layer Editor Preview" +msgstr "Pré-visualização do editor de camadas" + +#: js/build/mapBlocks.js:3 +msgid "Interactive layer preview for the Map Layer post type editor." +msgstr "Pré-visualização interativa de camadas para o editor do tipo de post Map Layer." + +#: js/build/mapBlocks.js:3 +msgid "Map Editor Preview" +msgstr "Pré-visualização do editor de mapas" + +#: js/build/mapBlocks.js:3 +msgid "Interactive map preview for the Map post type editor." +msgstr "Pré-visualização interativa de mapa para o editor do tipo de post Map." + +#: js/build/postsSidebar.js:1 +msgid "Unable to retrieve the address for the selected point." +msgstr "Não foi possível obter o endereço para o ponto selecionado." + +#: includes/class-jeo.php:1711 +msgid "This JEO embed is not available." +msgstr "Este embed JEO não está disponível." + +#: includes/class-jeo.php:1712 +msgid "Embed not available" +msgstr "Embed indisponível" + +#: includes/ai/class-context-agent.php:282 +msgid "No AI provider is configured. Set one in JEO AI Settings." +msgstr "Nenhum provedor de IA configurado. Defina um nas configurações de IA do JEO." + +#: includes/ai/class-context-agent.php:320 +msgid "The AI returned an empty prompt engineering response." +msgstr "A IA retornou uma resposta vazia de engenharia de prompt." + +#: includes/ai/class-context-handler.php:287 +#: js/build/contextSidebar.js:1 +msgid "Initial suggestions" +msgstr "Sugestões iniciais" + +#: includes/ai/settings/tab-context.php:37 +msgid "Custom System Prompt" +msgstr "Prompt do sistema personalizado" + +#: includes/ai/settings/tab-context.php:49 +msgid "Custom system prompt used by the AI Context Assistant. Use the assistant below to generate or refine it. This value is stored as structured output JSON internally, but shown here as plain text for editing." +msgstr "Prompt do sistema personalizado usado pelo Assistente de Contexto da IA. Use o assistente abaixo para gerá-lo ou refiná-lo. Este valor é armazenado internamente como JSON de saída estruturada, mas exibido aqui como texto simples para edição." + +#: includes/ai/settings/tab-context.php:74 +msgid "AI Context Prompt Engineer Assistant" +msgstr "Assistente de Engenharia de Prompt do Contexto da IA" + +#: includes/ai/settings/tab-context.php:76 +msgid "Describe how you want the Context Assistant to behave (e.g., \"Always link place names to the original article\" or \"Ground every claim with a verbatim quote\"). The active LLM will generate a highly optimized system prompt for you, strictly adhering to JEO formatting rules." +msgstr "Descreva como você quer que o Assistente de Contexto se comporte (ex.: \"Sempre vincular nomes de lugares ao artigo original\" ou \"Embasar cada afirmação com uma citação literal\"). O LLM ativo gerará um prompt do sistema altamente otimizado para você, seguindo estritamente as regras de formatação do JEO." + +#: includes/ai/settings/tab-context.php:113 +msgid "Ex: I want the assistant to always include inline contextual links and cite the exact sentence from the article that supports each location." +msgstr "Ex.: Quero que o assistente sempre inclua links contextuais inline e cite a frase exata do artigo que sustenta cada localização." + +#: includes/ai/settings/tab-context.php:115 +msgid "Your description is saved automatically in this browser." +msgstr "Sua descrição é salva automaticamente neste navegador." + +#: includes/ai/settings/tab-context.php:121 +msgid "Generate Custom Prompt" +msgstr "Gerar prompt personalizado" + +#: includes/ai/settings/tab-context.php:124 +msgid "Suggest initial prompt" +msgstr "Sugerir prompt inicial" + +#: includes/ai/settings/tab-context.php:182 +msgid "Initial prompt loaded. Edit and save the settings." +msgstr "Prompt inicial carregado. Edite e salve as configurações." + +#: includes/ai/settings/tab-context.php:192 +msgid "Please describe how you want the assistant to behave first." +msgstr "Descreva como você quer que o assistente se comporte primeiro." + +#: includes/ai/settings/tab-context.php:217 +msgid "Custom prompt generated. Remember to save the settings." +msgstr "Prompt personalizado gerado. Lembre-se de salvar as configurações." + +#: includes/ai/settings/tab-context.php:220 +#: includes/ai/settings/tab-context.php:224 +msgid "Generation failed." +msgstr "Falha na geração." + +#: includes/layers/class-layers.php:101 +msgid "Layer Themes" +msgstr "Temas de camadas" + +#: includes/layers/class-layers.php:102 +msgid "Layer Theme" +msgstr "Tema de camada" + +#: includes/layers/class-layers.php:103 +msgid "Add New Layer Theme" +msgstr "Adicionar novo tema de camada" + +#: includes/layers/class-layers.php:104 +msgid "Edit Layer Theme" +msgstr "Editar tema de camada" + +#: includes/layers/class-layers.php:266 +msgid "Deforestation" +msgstr "Desmatamento" + +#: includes/layers/class-layers.php:267 +msgid "Hydrography" +msgstr "Hidrografia" + +#: includes/layers/class-layers.php:268 +msgid "Indigenous Lands" +msgstr "Terras indígenas" + +#: includes/layers/class-layers.php:269 +msgid "Protected Areas" +msgstr "Áreas protegidas" + +#: includes/layers/class-layers.php:270 +msgid "Mining" +msgstr "Mineração" + +#: includes/layers/class-layers.php:271 +msgid "Oil and Gas" +msgstr "Petróleo e gás" + +#: includes/layers/class-layers.php:272 +msgid "Land Use" +msgstr "Uso da terra" + +#: includes/layers/class-layers.php:273 +msgid "Agriculture" +msgstr "Agricultura" + +#: includes/layers/class-layers.php:274 +msgid "Infrastructure" +msgstr "Infraestrutura" + +#: includes/layers/class-layers.php:275 +msgid "Administrative Boundaries" +msgstr "Limites administrativos" + +#: includes/layers/class-layers.php:276 +msgid "Socioeconomic" +msgstr "Socioeconômico" + +#: includes/layers/class-layers.php:277 +msgid "Biodiversity" +msgstr "Biodiversidade" + +#: includes/layers/class-layers.php:278 +msgid "Fire" +msgstr "Queimadas" + +#: includes/layers/class-layers.php:279 +msgid "Climate" +msgstr "Clima" + +#: includes/minimap/class-minimap.php:229 +msgid "Generate a map for this post based on its content." +msgstr "Gerar um mapa para este post com base no seu conteúdo." + +#. translators: %d: number of removed layers +#: includes/minimap/class-minimap.php:529 +#, php-format +msgid "%d layer(s) returned by the AI were discarded because they are not published map layers." +msgstr "%d camada(s) retornada(s) pela IA foi(ram) descartada(s) porque não são camadas de mapa publicadas." + +#: includes/minimap/class-minimap.php:1144 +msgid "The agent tried to remove several existing layers without an explicit request. They were kept to preserve the current map." +msgstr "O agente tentou remover várias camadas existentes sem uma solicitação explícita. Elas foram mantidas para preservar o mapa atual." + +#. translators: %s: search radius in kilometers +#: includes/stories-near-you/class-stories-near-you.php:1123 +#, php-format +msgid "No stories found within %s km of your location." +msgstr "Nenhuma história encontrada num raio de %s km da sua localização." + +#: js/build/contextSidebar.js:1 +msgid "References used" +msgstr "Referências usadas" + +#: js/build/contextSidebar.js:1 +msgid "Previous suggestions" +msgstr "Sugestões anteriores" + +#: js/build/contextSidebar.js:1 +msgid "View this version" +msgstr "Ver esta versão" + +#: js/build/mapBlocks.js:1 +msgid "Vector style preview" +msgstr "Pré-visualização do estilo vetorial" + +#: js/build/mapBlocks.js:3 +msgid "Search radius (km)" +msgstr "Raio de busca (km)" + +#: js/build/mapBlocks.js:3 +msgid "Order by" +msgstr "Ordenar por" + +#: js/build/mapBlocks.js:3 +msgid "Most recent nearby" +msgstr "Mais recentes próximos" + +#: js/build/mapBlocks.js:3 +msgid "Nearest first" +msgstr "Mais próximos primeiro" + +#: js/build/mapBlocks.js:3 +msgid "Relevance (distance + date)" +msgstr "Relevância (distância + data)" + +#: js/build/mapBlocks.js:3 +msgid "Max age (days, 0 = no limit)" +msgstr "Idade máxima (dias, 0 = sem limite)" + +#: js/build/mapBlocks.js:3 +msgid "Distance weight" +msgstr "Peso da distância" + +#: js/build/mapBlocks.js:3 +msgid "Date weight" +msgstr "Peso da data" + +#: js/build/mapBlocks.js:3 +#: js/build/mapsSidebar.js:1 +msgid "Automatic style applied (this layer had no saved style). Edit layer settings to customize it." +msgstr "Estilo automático aplicado (esta camada não tinha estilo salvo). Edite as configurações da camada para personalizá-lo." + +#: js/build/mapBlocks.js:3 +#: js/build/mapsSidebar.js:1 +msgid "Themes:" +msgstr "Temas:" + +#: js/build/mapBlocks.js:3 +#: js/build/mapsSidebar.js:1 +msgid "Source:" +msgstr "Fonte:" + +#: js/build/postsSidebar.js:1 +msgid "Type an address, then click Search or press Enter. You can also drag the marker across the map." +msgstr "Digite um endereço, depois clique em Buscar ou pressione Enter. Você também pode arrastar o marcador pelo mapa." + +#: includes/maps/class-map-style-composer.php:631 +#: includes/maps/class-map-style-composer.php:700 +msgid "Mapbox composed styles are disabled." +msgstr "Os estilos compostos do Mapbox estão desativados." + +#: includes/maps/class-map-style-composer.php:656 +#: includes/maps/class-map-style-composer.php:724 +msgid "Could not create the Mapbox composed style cache directory." +msgstr "Não foi possível criar o diretório de cache dos estilos compostos do Mapbox." + +#: includes/maps/class-map-style-composer.php:827 +msgid "Map not found." +msgstr "Mapa não encontrado." + +#: includes/maps/class-map-style-composer.php:837 +msgid "A Mapbox access token is required to compose Mapbox styles." +msgstr "Um token de acesso do Mapbox é necessário para compor estilos do Mapbox." + +#: includes/maps/class-map-style-composer.php:877 +msgid "Public payload composition is only available for one-time maps." +msgstr "A composição com dados públicos está disponível apenas para mapas de uso único." + +#: includes/maps/class-map-style-composer.php:883 +msgid "The map preview payload is too large." +msgstr "Os dados da prévia do mapa são grandes demais." + +#: includes/maps/class-map-style-composer.php:895 +msgid "The map preview has too many layers to compose." +msgstr "A prévia do mapa tem camadas demais para compor." + +#: includes/maps/class-map-style-composer.php:902 +msgid "The map preview does not include any renderable layers." +msgstr "A prévia do mapa não inclui nenhuma camada renderizável." + +#: includes/maps/class-map-style-composer.php:924 +msgid "Map preview" +msgstr "Prévia do mapa" + +#: includes/maps/class-map-style-composer.php:1300 +msgid "Invalid composed style hash." +msgstr "Hash do estilo composto inválido." + +#: includes/maps/class-map-style-composer.php:1456 +msgid "One or more Mapbox styles could not be fetched." +msgstr "Não foi possível obter um ou mais estilos do Mapbox." + +#: includes/maps/class-map-style-composer.php:1970 +msgid "The PHP GD extension is required to merge Mapbox sprites." +msgstr "A extensão PHP GD é necessária para mesclar sprites do Mapbox." + +#: includes/maps/class-map-style-composer.php:2095 +msgid "Could not decode a Mapbox sprite image." +msgstr "Não foi possível decodificar uma imagem de sprite do Mapbox." + +#: includes/maps/class-map-style-composer.php:3111 +msgid "Composed style artifact was not found." +msgstr "O artefato do estilo composto não foi encontrado." + +#: js/build/discovery.js:6 +msgid "Mapbox style composition is unavailable." +msgstr "A composição de estilos do Mapbox está indisponível." + +#: js/build/discovery.js:6 +#: js/build/jeoMap.js:1 +msgid "Mapbox style composition is unavailable for this map." +msgstr "A composição de estilos do Mapbox está indisponível para este mapa." + +#: js/build/discovery.js:6 +msgid "Unable to load map information right now." +msgstr "Não foi possível carregar as informações do mapa agora." + +#: js/build/discovery.js:6 +msgid "Map information" +msgstr "Informações do mapa" + +#: js/build/jeoMap.js:1 +msgid "Mapbox style composition did not return renderable layers for this map." +msgstr "A composição de estilos do Mapbox não retornou camadas renderizáveis para este mapa." + +#: js/build/jeoMap.js:1 +msgid "Unable to load composed Mapbox style for this map." +msgstr "Não foi possível carregar o estilo composto do Mapbox para este mapa." + +#: js/build/jeoMap.js:1 +msgid "Mapbox style composition is unavailable for this one-time map." +msgstr "A composição de estilos do Mapbox está indisponível para este mapa de uso único." + +#: js/build/jeoMap.js:1 +msgid "Mapbox style composition did not return renderable layers for this one-time map." +msgstr "A composição de estilos do Mapbox não retornou camadas renderizáveis para este mapa de uso único." + +#: js/build/jeoMap.js:1 +msgid "Unable to load composed Mapbox style for this one-time map." +msgstr "Não foi possível carregar o estilo composto do Mapbox para este mapa de uso único." + +#: js/build/jeoMap.js:1 +#: js/build/jeoStorymap.js:3 +msgid "Mapbox style layers could not be loaded because the composed style is unavailable." +msgstr "Não foi possível carregar as camadas de estilo do Mapbox porque o estilo composto está indisponível." + +#: js/build/jeoStorymap.js:3 +msgid "Mapbox style composition is unavailable for this storymap." +msgstr "A composição de estilos do Mapbox está indisponível para este mapa narrativo." + +#: js/build/jeoStorymap.js:3 +msgid "Mapbox style composition did not return renderable layers for this storymap." +msgstr "A composição de estilos do Mapbox não retornou camadas renderizáveis para este mapa narrativo." + +#: js/build/jeoStorymap.js:3 +msgid "Unable to load composed Mapbox style for this storymap." +msgstr "Não foi possível carregar o estilo composto do Mapbox para este mapa narrativo." + +#. translators: %d: number of maps whose composed style cache was refreshed. +#: js/build/layersSidebar.js:8 #, js-format msgid "%d map" msgid_plural "%d maps" @@ -2170,3 +4922,478 @@ msgstr "Atualizando cache de estilos compostos" #: js/build/layersSidebar.js:11 msgid "Refresh composed style cache" msgstr "Atualizar cache de estilos compostos" + +#. translators: %s: linked URL +#: includes/ai/class-context-handler.php:703 +#, php-format +msgid "Link to %s removed: not listed in references." +msgstr "Link para %s removido: não listado nas referências." + +#: includes/ai/class-context-handler.php:720 +msgid "Verification notes:" +msgstr "Notas de verificação:" + +#: js/build/mapBlocks.js:1 +msgid "Save the post before generating the map." +msgstr "Salve o post antes de gerar o mapa." + +#: js/build/mapBlocks.js:1 +msgid "Could not start map generation." +msgstr "Não foi possível iniciar a geração do mapa." + +#: js/build/mapBlocks.js:1 +msgid "Could not send message." +msgstr "Não foi possível enviar a mensagem." + +#: js/build/mapBlocks.js:1 +msgid "Could not generate from prompt." +msgstr "Não foi possível gerar a partir do prompt." + +#: jeo.php:36 +msgid "JEO Plugin Error: Your PHP version is too old. JEO AI features require PHP 8.2 or higher. The plugin will remain deactivated." +msgstr "Erro no Plugin JEO: Sua versão do PHP é muito antiga. Os recursos de IA do JEO exigem PHP 8.2 ou superior. O plugin permanecerá desativado." + +#: js/build/storiesNearYou.js:1 +msgid "You denied location access. Please enable location permissions in your browser settings." +msgstr "Você negou o acesso à localização. Habilite a permissão de localização nas configurações do seu navegador." + +#: js/build/storiesNearYou.js:1 +msgid "Location request timed out. Please try again or use the configured location." +msgstr "A solicitação de localização expirou. Tente novamente ou use a localização configurada." + +#: js/build/storiesNearYou.js:1 +msgid "This browser does not support location services." +msgstr "Este navegador não oferece suporte a serviços de localização." + +#: js/build/storiesNearYou.js:1 +msgid "Unable to load stories. Please check your connection and try again." +msgstr "Não foi possível carregar as histórias. Verifique sua conexão e tente novamente." + +#: js/build/storiesNearYou.js:1 +msgid "Your browser could not determine your location. This may be due to strict privacy settings or because GPS is unavailable on your device." +msgstr "Seu navegador não conseguiu determinar sua localização. Isso pode ocorrer por configurações rigorosas de privacidade ou porque o GPS está indisponível no seu dispositivo." + +#. translators: %d: number of preserved layers +#: includes/minimap/class-minimap.php:1388 +#, php-format +msgid "%d manually-added layer(s) were preserved during AI refinement." +msgstr "%d camada(s) adicionada(s) manualmente preservada(s) durante o refinamento da IA." + +#: includes/class-jeo.php:2062 +msgid "A Mapbox style ID and access token are required." +msgstr "Um ID de estilo do Mapbox e um token de acesso são obrigatórios." + +#: includes/class-jeo.php:2073 +#: includes/class-jeo.php:2111 +msgid "Unexpected Mapbox style payload." +msgstr "Resposta de estilo do Mapbox inesperada." + +#: includes/minimap/class-minimap.php:230 +msgid "Initial generation from post content" +msgstr "Geração inicial a partir do conteúdo do post" + +#. translators: %d: version number. +#: includes/minimap/class-minimap.php:933 +#, php-format +msgid "Version %d was not found in the stored history. The configuration reconstructed from the conversation context was applied instead." +msgstr "A versão %d não foi encontrada no histórico armazenado. A configuração reconstruída a partir do contexto da conversa foi aplicada em seu lugar." + +#. translators: %d: version number. +#: includes/minimap/class-minimap.php:946 +#, php-format +msgid "Restored map version %d." +msgstr "Versão %d do mapa restaurada." + +#. translators: %d: HTTP status code. +#: includes/ai/class-funai-place-polygon-adapter.php:152 +#, php-format +msgid "FUNAI WFS returned HTTP %d." +msgstr "O WFS da FUNAI retornou HTTP %d." + +#: includes/ai/class-funai-place-polygon-adapter.php:162 +msgid "Could not parse FUNAI WFS response." +msgstr "Não foi possível analisar a resposta do WFS da FUNAI." + +#: includes/ai/class-funai-place-polygon-adapter.php:84 +msgid "Could not compute bounding box from FUNAI geometry." +msgstr "Não foi possível calcular a caixa delimitadora a partir da geometria da FUNAI." + +#: includes/ai/class-generate-boundary-layer-tool.php:83 +msgid "You do not have permission to generate layers." +msgstr "Você não tem permissão para gerar camadas." + +#: includes/ai/class-ibge-place-polygon-adapter.php:78 +msgid "Could not compute bounding box from IBGE geometry." +msgstr "Não foi possível calcular a caixa delimitadora a partir da geometria do IBGE." + +#. translators: %d: HTTP status code. +#: includes/ai/class-ibge-place-polygon-adapter.php:289 +#, php-format +msgid "IBGE API returned HTTP %d." +msgstr "A API do IBGE retornou HTTP %d." + +#: includes/ai/class-ibge-place-polygon-adapter.php:299 +msgid "Could not parse IBGE geometry response." +msgstr "Não foi possível analisar a resposta de geometria do IBGE." + +#. translators: %d: HTTP status code. +#: includes/ai/class-mapbox-style-builder.php:74 +#, php-format +msgid "Mapbox Styles API returned HTTP %d." +msgstr "A API de Estilos do Mapbox retornou HTTP %d." + +#: includes/ai/class-mapbox-style-builder.php:103 +msgid "Mapbox returned an unexpected style payload." +msgstr "O Mapbox retornou uma resposta de estilo inesperada." + +#: includes/ai/class-mapbox-style-builder.php:136 +msgid "Only \"mapbox\" layer specs can be built into a composed style." +msgstr "Somente especificações de camada \"mapbox\" podem ser construídas em um estilo composto." + +#: includes/ai/class-mapbox-style-builder.php:144 +msgid "Composed style spec is missing style_json." +msgstr "A especificação de estilo composto não tem style_json." + +#. translators: %s: missing style key. +#: includes/ai/class-mapbox-style-builder.php:155 +#, php-format +msgid "Composed style is missing required key: %s." +msgstr "O estilo composto não tem a chave obrigatória: %s." + +#: includes/ai/class-mapbox-style-builder.php:187 +msgid "Mapbox API key is not configured." +msgstr "A chave de API do Mapbox não está configurada." + +#: includes/ai/class-mapbox-style-builder.php:195 +msgid "Mapbox API key format is not recognized." +msgstr "O formato da chave de API do Mapbox não é reconhecido." + +#: includes/ai/class-mapbox-style-builder.php:205 +msgid "Could not decode Mapbox API key." +msgstr "Não foi possível decodificar a chave de API do Mapbox." + +#: includes/ai/class-mapbox-style-builder.php:213 +msgid "Could not extract Mapbox username from API key." +msgstr "Não foi possível extrair o nome de usuário do Mapbox da chave de API." + +#: includes/ai/class-minilayer-classifier.php:57 +msgid "The classifier returned an unexpected response." +msgstr "O classificador retornou uma resposta inesperada." + +#: includes/ai/class-minilayer-handler.php:95 +msgid "Could not classify the layer request. Please try again." +msgstr "Não foi possível classificar a solicitação de camada. Tente novamente." + +#: includes/ai/class-minilayer-handler.php:96 +msgid "The classifier returned an unexpected response. Please try again." +msgstr "O classificador retornou uma resposta inesperada. Tente novamente." + +#: includes/ai/class-minilayer-handler.php:97 +#: includes/ai/class-minilayer-service.php:55 +msgid "The request cannot be approximated with available map data." +msgstr "A solicitação não pode ser aproximada com os dados de mapa disponíveis." + +#: includes/ai/class-minilayer-handler.php:98 +msgid "The AI returned an unsupported layer type." +msgstr "A IA retornou um tipo de camada não suportado." + +#: includes/ai/class-minilayer-handler.php:99 +msgid "The AI response is missing a required tileset field." +msgstr "A resposta da IA não tem um campo tileset obrigatório." + +#: includes/ai/class-minilayer-handler.php:100 +msgid "The AI returned an invalid geometry type." +msgstr "A IA retornou um tipo de geometria inválido." + +#: includes/ai/class-minilayer-handler.php:101 +msgid "The AI response is missing composed style JSON." +msgstr "A resposta da IA não tem o JSON de estilo composto." + +#. translators: %s: layer type. +#: includes/ai/class-minilayer-service.php:71 +#, php-format +msgid "Unsupported layer type: %s." +msgstr "Tipo de camada não suportado: %s." + +#. translators: %s: missing field name. +#: includes/ai/class-minilayer-service.php:92 +#, php-format +msgid "Missing required tileset field: %s." +msgstr "Campo tileset obrigatório ausente: %s." + +#: includes/ai/class-minilayer-service.php:103 +msgid "Invalid layer geometry type for tileset-vector." +msgstr "Tipo de geometria de camada inválido para tileset-vector." + +#: includes/ai/class-minilayer-service.php:221 +msgid "External sources" +msgstr "Fontes externas" + +#: includes/ai/class-minilayer-service.php:222 +msgid "Mapbox composed style" +msgstr "Estilo composto do Mapbox" + +#: includes/ai/class-minilayer-service.php:298 +msgid "Minilayer" +msgstr "Minilayer" + +#: includes/ai/class-osm-place-polygon-adapter.php:99 +msgid "Could not compute bounding box from OSM geometry." +msgstr "Não foi possível calcular a caixa delimitadora a partir da geometria do OSM." + +#: includes/ai/class-osm-place-polygon-adapter.php:364 +msgid "Could not parse Overpass response." +msgstr "Não foi possível analisar a resposta do Overpass." + +#: includes/ai/class-osm-place-polygon-adapter.php:385 +msgid "Could not assemble closed rings from OSM relation." +msgstr "Não foi possível montar anéis fechados a partir da relação do OSM." + +#: includes/ai/class-osm-place-polygon-adapter.php:424 +msgid "No Overpass mirrors configured." +msgstr "Nenhum espelho do Overpass configurado." + +#. translators: %d: HTTP status code. +#: includes/ai/class-osm-place-polygon-adapter.php:460 +#, php-format +msgid "Overpass returned HTTP %d." +msgstr "O Overpass retornou HTTP %d." + +#: includes/ai/class-osm-place-polygon-adapter.php:472 +msgid "All Overpass mirrors failed." +msgstr "Todos os espelhos do Overpass falharam." + +#. translators: %s: place name. +#: includes/ai/class-place-polygon-service.php:96 +#, php-format +msgid "Could not find a boundary polygon for \"%s\"." +msgstr "Não foi possível encontrar um polígono de limite para \"%s\"." + +#. translators: %s: boundary display name. +#: includes/ai/class-place-polygon-service.php:126 +#, php-format +msgid "Boundary: %s" +msgstr "Limite: %s" + +#: includes/ai/class-place-polygon-service.php:238 +msgid "Could not write boundary GeoJSON file." +msgstr "Não foi possível gravar o arquivo GeoJSON de limite." + +#: includes/ai/class-place-polygon-service.php:256 +msgid "Could not create boundary GeoJSON attachment." +msgstr "Não foi possível criar o anexo GeoJSON de limite." + +#: includes/cli/class-ai-cli.php:58 +msgid "Classifying prompt..." +msgstr "Classificando prompt..." + +#. translators: 1: layer title, 2: post ID. +#: includes/cli/class-ai-cli.php:68 +#, php-format +msgid "Layer created: %1$s (ID %2$d)" +msgstr "Camada criada: %1$s (ID %2$d)" + +#: includes/cli/class-ai-cli.php:107 +msgid "A place name is required." +msgstr "Um nome de lugar é obrigatório." + +#: includes/cli/class-ai-cli.php:115 +msgid "Resolving boundary polygon..." +msgstr "Resolvendo polígono de limite..." + +#. translators: 1: layer title, 2: post ID. +#: includes/cli/class-ai-cli.php:126 +#, php-format +msgid "Boundary layer created: %1$s (ID %2$d)" +msgstr "Camada de limite criada: %1$s (ID %2$d)" + +#: includes/cli/class-ai-cli.php:161 +msgid "Classifying prompt (no layer will be created)..." +msgstr "Classificando prompt (nenhuma camada será criada)..." + +#: includes/ai/class-mapbox-style-builder.php:79 +msgid "Mapbox denied the style publish request (HTTP 403): the configured Mapbox token likely lacks the styles:write scope. Use a secret token (sk.…) or add the styles:write scope to the token at mapbox.com/account/access-tokens, then update the key in JEO Settings." +msgstr "O Mapbox recusou a publicação do estilo (HTTP 403): o token do Mapbox configurado provavelmente não tem o escopo styles:write. Use um token secreto (sk.…) ou adicione o escopo styles:write ao token em mapbox.com/account/access-tokens e atualize a chave nas Configurações do JEO." + +#: includes/ai/class-mapbox-style-builder.php:84 +msgid "Mapbox rejected the access token (HTTP 401): the Mapbox API key configured in JEO Settings is invalid or has expired." +msgstr "O Mapbox recusou o token de acesso (HTTP 401): a chave do Mapbox configurada nas Configurações do JEO é inválida ou expirou." + +#: includes/settings/settings-page.php:144 +msgid "Public access token. Used for MapboxGL rendering, Mapbox layers, and AI layer generation. This token is exposed to site visitors (it is required to display maps), so keep its scopes read-only." +msgstr "Token de acesso público. Usado para renderização com MapboxGL, camadas Mapbox e geração de camadas com IA. Este token é exposto a visitantes do site (é necessário para exibir mapas), então mantenha seus escopos somente leitura." + +#: includes/settings/settings-page.php:148 +msgid "Mapbox Secret Key" +msgstr "Chave Secreta do Mapbox" + +#: includes/settings/settings-page.php:150 +msgid "Ex. sk.eyJ3..." +msgstr "Ex.: sk.eyJ3..." + +#: includes/settings/settings-page.php:151 +msgid "Used to create AI-assisted layers (requires the styles:write scope). For security, use a secret token — the public token is visible to site visitors. If empty, publishing falls back to the public token." +msgstr "Chave usada para a criação de camadas com auxílio de IA (requer escopo styles:write). Por segurança, use um token secreto — o token público é visível para visitantes do site. Se vazio, a publicação tenta o token público." + +#: includes/layers/class-layers.php:254 +msgid "AI-suggested default style (filter/paint/layout) applied when a map layer instance sets style.use_default" +msgstr "Estilo padrão sugerido por IA (filter/paint/layout) aplicado quando uma instância de camada do mapa define style.use_default" + +#: includes/maps/class-maps.php:190 +msgid "Per-instance paint and layout overrides for vector and GeoJSON layers" +msgstr "Substituições de paint e layout por instância para camadas vetoriais e GeoJSON" + +#: includes/maps/class-maps.php:194 +msgid "Whether to use the layer default_style meta instead of the instance paint" +msgstr "Se deve usar o meta default_style da camada em vez do paint da instância" + +#: includes/maps/class-maps.php:206 +msgid "MapLibre filter expression" +msgstr "Expressão de filtro do MapLibre" + +#: includes/layer-types/geojson.js:188 +msgid "GeoJSON" +msgstr "GeoJSON" + +#: includes/layer-types/geojson.js:229 +msgid "GeoJSON URL" +msgstr "URL do GeoJSON" + +#: includes/layer-types/geojson.js:230 +msgid "Public URL of a GeoJSON file (usually a FeatureCollection). Used when no inline GeoJSON is provided." +msgstr "URL pública de um arquivo GeoJSON (geralmente uma FeatureCollection). Usada quando nenhum GeoJSON embutido é fornecido." + +#: includes/layer-types/geojson.js:238 +msgid "Inline GeoJSON" +msgstr "GeoJSON embutido" + +#: includes/layer-types/geojson.js:239 +msgid "Optional. Paste a raw GeoJSON object or FeatureCollection. Takes precedence over the URL — useful for small geometries." +msgstr "Opcional. Cole um objeto GeoJSON ou FeatureCollection bruto. Tem precedência sobre a URL — útil para geometrias pequenas." + +#: includes/layer-types/geojson.js:246 +msgid "Layer style" +msgstr "Estilo da camada" + +#: includes/layer-types/geojson.js:247 +msgid "How the GeoJSON features are drawn." +msgstr "Como as feições do GeoJSON são desenhadas." + +#: includes/class-jeo.php:2169 +msgid "A valid http(s) style URL is required." +msgstr "Uma URL de estilo http(s) válida é obrigatória." + +#: includes/class-jeo.php:2184 +#: includes/class-jeo.php:2217 +msgid "Unexpected style JSON payload." +msgstr "Conteúdo inesperado no style JSON." + +#: includes/layer-types/style-json.js:5 +msgid "Style JSON" +msgstr "Style JSON" + +#: includes/layer-types/style-json.js:30 +msgid "Style URL" +msgstr "URL do estilo" + +#: includes/layer-types/style-json.js:31 +msgid "Public URL of a MapLibre GL style JSON file. Used when no inline style is provided. Works with keyless providers such as OpenFreeMap (e.g. https://tiles.openfreemap.org/styles/dark)." +msgstr "URL pública de um arquivo de estilo MapLibre GL (style JSON). Usada quando nenhum estilo inline é fornecido. Funciona com provedores sem chave, como o OpenFreeMap (ex.: https://tiles.openfreemap.org/styles/dark)." + +#: includes/layer-types/style-json.js:39 +msgid "Inline style JSON" +msgstr "Style JSON inline" + +#: includes/layer-types/style-json.js:40 +msgid "Optional. Paste a raw MapLibre GL style object. Takes precedence over the URL — useful for small, self-contained styles." +msgstr "Opcional. Cole um objeto de estilo MapLibre GL bruto. Tem precedência sobre a URL — útil para estilos pequenos e autocontidos." + +#: includes/ai/class-context-handler.php:496 +#: includes/minimap/class-minimap.php:321 +#: includes/minimap/class-minimap.php:442 +msgid "The previous request could not be processed. Please try again." +msgstr "A solicitação anterior não pôde ser processada. Tente novamente." + +#: includes/ai/class-funai-place-polygon-adapter.php:91 +msgid "FUNAI" +msgstr "FUNAI" + +#: includes/ai/class-ibge-place-polygon-adapter.php:85 +msgid "IBGE" +msgstr "IBGE" + +#. translators: %s: tileset ID. +#: includes/ai/class-minilayer-service.php:149 +#, php-format +msgid "Mapbox tileset %s" +msgstr "Tileset %s do Mapbox" + +#: includes/ai/class-osm-place-polygon-adapter.php:106 +msgid "OpenStreetMap contributors" +msgstr "Colaboradores do OpenStreetMap" + +#. translators: %s: municipality name with state abbreviation. +#: includes/ai/class-place-polygon-service.php:326 +#, php-format +msgid "Layer showing the area of the municipality of %s" +msgstr "Camada com área do município de %s" + +#. translators: %s: state name. +#: includes/ai/class-place-polygon-service.php:332 +#, php-format +msgid "Layer showing the area of the state of %s" +msgstr "Camada com área do estado de %s" + +#. translators: %s: indigenous land name. +#: includes/ai/class-place-polygon-service.php:338 +#, php-format +msgid "Layer showing the indigenous land %s" +msgstr "Camada com área da terra indígena %s" + +#. translators: %s: place display name. +#: includes/ai/class-place-polygon-service.php:344 +#, php-format +msgid "Layer showing the area of %s" +msgstr "Camada com área de %s" + +#: js/build/mapBlocks.js:1 +msgid "Understand chosen layers" +msgstr "Entender camadas escolhidas" + +#: includes/ai/class-retrieve-knowledge-tool.php:112 +msgid "Provide either \"query\" (string) or \"queries\" (array of strings)." +msgstr "Informe \"query\" (string) ou \"queries\" (array de strings)." + +#: js/build/contextSidebar.js:1 +msgid "Ask for changes or new paragraphs... (Shift+Enter for a line break)" +msgstr "Peça alterações ou novos parágrafos... (Shift+Enter para nova linha)" + +#: js/build/mapBlocks.js:1 +msgid "Ask the AI to refine the map… (Shift+Enter for a line break)" +msgstr "Peça à IA para refinar o mapa… (Shift+Enter para nova linha)" + +#: js/build/mapBlocks.js:1 +msgid "Describe the map you want… (Shift+Enter for a line break)" +msgstr "Descreva o mapa que você quer… (Shift+Enter para nova linha)" + +#. translators: %d: number of converted style values. +#: includes/maps/class-map-style-composer.php:1387 +#, php-format +msgid "Converted %d legacy Mapbox stop-function value(s) to style-spec expressions for MapLibre compatibility." +msgstr "Convertidos %d valor(es) de função de stops legada do Mapbox em expressões style-spec para compatibilidade com o MapLibre." + +#. translators: %d: number of converted style values. +#: includes/maps/class-map-style-composer.php:1408 +#, php-format +msgid "Converted %d legacy Mapbox token string(s) to style-spec expressions for MapLibre compatibility." +msgstr "Convertidas %d string(s) de token legada(s) do Mapbox em expressões style-spec para compatibilidade com o MapLibre." + +#: includes/maps/class-map-style-composer.php:3137 +msgid "Could not encode the composed style artifact as JSON." +msgstr "Não foi possível codificar o artefato do estilo composto como JSON." + +#. translators: %s: artifact file name. +#: includes/maps/class-map-style-composer.php:3148 +#, php-format +msgid "Could not write the composed style artifact %s. Check that the uploads directory is writable." +msgstr "Não foi possível gravar o artefato do estilo composto %s. Verifique se o diretório de uploads permite escrita." diff --git a/src/languages/jeowp.pot b/src/languages/jeowp.pot index a0e994d90..8fb4e3349 100755 --- a/src/languages/jeowp.pot +++ b/src/languages/jeowp.pot @@ -2,14 +2,14 @@ # This file is distributed under the GPL-3.0-only. msgid "" msgstr "" -"Project-Id-Version: JEO Maps 3.1.2\n" +"Project-Id-Version: JEO Maps 4.0.0-beta.1\n" "Report-Msgid-Bugs-To: https://wordpress.org/support/plugin/jeowp\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" -"POT-Creation-Date: 2026-07-08T18:07:00+00:00\n" +"POT-Creation-Date: 2026-10-07T21:30:49+00:00\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "X-Generator: WP-CLI 2.12.0\n" "X-Domain: jeowp\n" @@ -36,1834 +36,4931 @@ msgstr "" msgid "InfoAmazonia" msgstr "" -#: includes/class-jeo.php:382 -msgid "Ordered list of map-layer IDs to return." +#: includes/admin/dashboard-page.php:306 +msgid "Toggle panel" msgstr "" -#: includes/class-jeo.php:387 -msgid "Request context." +#: includes/admin/dashboard-page.php:308 +msgid "JEO Platform" msgstr "" -#: includes/class-jeo.php:583 -msgid "Mapbox was selected as the rendering library, but no Mapbox API key is configured. Falling back to MapLibre." +#: includes/admin/dashboard-page.php:309 +msgid "Loading geodata..." msgstr "" -#: includes/class-jeo.php:584 -msgid "Mapbox was selected as the rendering library, but the external Mapbox SDK could not be loaded. Falling back to MapLibre." +#: includes/admin/dashboard-page.php:310 +msgid "Minimize Dashboard" msgstr "" -#: includes/class-jeo.php:1107 -msgid "Read more" +#: includes/admin/dashboard-page.php:317 +msgid "Search terms" msgstr "" -#: includes/class-jeo.php:1332 -msgid "This JEO embed is not available." +#: includes/admin/dashboard-page.php:318 +msgid "Title or content..." msgstr "" -#: includes/class-jeo.php:1333 -msgid "Embed not available" +#: includes/admin/dashboard-page.php:322 +msgid "Post Type" msgstr "" -#: includes/geocode/class-geocode-handler.php:130 -msgid "Geocoding service provided by OpenStreetMap at https://nominatim.openstreetmap.org/." +#: includes/admin/dashboard-page.php:324 +msgid "All Types" msgstr "" -#: includes/geocode/class-geocode-handler.php:231 -msgid "Metadata storing the locations related to the post. Each location is an object with latitude, longitude, and geocode attributes." +#: includes/admin/dashboard-page.php:329 +msgid "Taxonomy" msgstr "" -#: includes/geocode/class-geocode-handler.php:247 -#: js/build/mapBlocks.js:3 -#: js/build/mapsSidebar.js:1 -msgid "Latitude" +#: includes/admin/dashboard-page.php:334 +msgid "Term" msgstr "" -#: includes/geocode/class-geocode-handler.php:261 -#: js/build/mapBlocks.js:3 -#: js/build/mapsSidebar.js:1 -msgid "Longitude" +#: includes/admin/dashboard-page.php:339 +msgid "Apply Filters" msgstr "" -#: includes/geocode/class-geocode-handler.php:275 -msgid "Country" +#: includes/admin/dashboard-page.php:344 +msgid "Timeline Range" msgstr "" -#: includes/geocode/class-geocode-handler.php:289 -msgid "Country code" +#: includes/admin/dashboard-page.php:361 +msgid "Gathering geodata and initializing JEO Engine..." msgstr "" -#: includes/geocode/class-geocode-handler.php:303 -msgid "Region level 1" +#: includes/admin/dashboard-page.php:524 +#: includes/settings/class-settings.php:654 +msgid "Select Taxonomy..." msgstr "" -#: includes/geocode/class-geocode-handler.php:317 -msgid "Region level 2" +#: includes/admin/dashboard-page.php:542 +#: includes/settings/class-settings.php:655 +msgid "Select Term..." msgstr "" -#: includes/geocode/class-geocode-handler.php:331 -msgid "Region level 3" +#: includes/admin/dashboard-page.php:605 +#: includes/settings/class-settings.php:656 +msgid " locations found." msgstr "" -#: includes/geocode/class-geocode-handler.php:345 -msgid "City" +#: includes/admin/dashboard-page.php:617 +#: includes/admin/dashboard-page.php:704 +msgid "Untitled" msgstr "" -#: includes/geocode/class-geocode-handler.php:359 -msgid "City sub-level 1" +#: includes/admin/dashboard-page.php:707 +#: includes/settings/class-settings.php:652 +msgid "View Post" msgstr "" -#: includes/layers/class-layers.php:49 -#: includes/layers/class-layers.php:60 -#: js/build/jeoMap.js:2 -#: js/build/mapBlocks.js:1 -msgid "Layers" +#: includes/admin/dashboard-page.php:707 +#: includes/settings/class-settings.php:653 +msgid "Edit Post" msgstr "" -#: includes/layers/class-layers.php:50 -msgid "Layer" +#: includes/admin/uninstall-handler.php:30 +#: includes/admin/uninstall-handler.php:48 +#: includes/admin/uninstall-handler.php:49 +msgid "Uninstall JEO" msgstr "" -#: includes/layers/class-layers.php:51 -#: includes/layers/class-layers.php:52 -msgid "Add new layer" +#: includes/admin/uninstall-handler.php:31 +msgid "Uninstall" msgstr "" -#: includes/layers/class-layers.php:53 -msgid "Edit layer" +#: includes/admin/uninstall-handler.php:63 +#: includes/admin/uninstall-page.php:23 +msgid "You do not have permission to uninstall plugins." msgstr "" -#: includes/layers/class-layers.php:54 -msgid "New layer" +#: includes/admin/uninstall-page.php:50 +msgid "Warning: This action cannot be undone." msgstr "" -#: includes/layers/class-layers.php:55 -msgid "View layer" +#: includes/admin/uninstall-page.php:53 +msgid "Uninstalling JEO will permanently delete the following data from your site:" msgstr "" -#: includes/layers/class-layers.php:56 -msgid "View layers" +#: includes/admin/uninstall-page.php:56 +msgid "All plugin settings stored in the database (map defaults, API keys, appearance settings, etc.)." msgstr "" -#: includes/layers/class-layers.php:57 -msgid "Search layers" +#: includes/admin/uninstall-page.php:57 +msgid "All AI usage logs (private posts of type \"jeo-ai-log\")." msgstr "" -#: includes/layers/class-layers.php:58 -msgid "No layer found" +#: includes/admin/uninstall-page.php:58 +msgid "All geolocation metadata attached to posts (_related_point, _geocode_* index fields)." msgstr "" -#: includes/layers/class-layers.php:59 -msgid "No layer found in the trash" +#: includes/admin/uninstall-page.php:59 +msgid "Cached geocoding results (Nominatim transients)." msgstr "" -#: includes/layers/class-layers.php:61 -msgid "Layer published." +#: includes/admin/uninstall-page.php:60 +msgid "Bulk AI cron logs and embedding token counters." msgstr "" -#: includes/layers/class-layers.php:62 -msgid "Layer published privately." +#: includes/admin/uninstall-page.php:61 +msgid "RAG vector store files from the uploads directory (wp-content/uploads/jeo-ai-store/)." msgstr "" -#: includes/layers/class-layers.php:63 -msgid "Layer reverted to draft." +#: includes/admin/uninstall-page.php:62 +msgid "Scheduled cron jobs created by the plugin." msgstr "" -#: includes/layers/class-layers.php:64 -msgid "Layer scheduled." +#: includes/admin/uninstall-page.php:65 +msgid "Your regular posts, pages, maps, layers, and story maps will remain, but any geolocation data associated with them will be lost." msgstr "" -#: includes/layers/class-layers.php:65 -msgid "Layer updated." +#: includes/admin/uninstall-page.php:72 +msgid "I understand that all data listed above will be permanently deleted." msgstr "" -#: includes/layers/class-layers.php:71 -msgid "JEO Layers" +#: includes/admin/uninstall-page.php:75 +msgid "Confirm and Uninstall JEO" msgstr "" -#: includes/layers/class-layers.php:108 -msgid "The layer type" +#: includes/admin/uninstall-page.php:75 +msgid "Are you absolutely sure? This cannot be undone." msgstr "" -#: includes/layers/class-layers.php:120 -msgid "Layer attribution as text or HTML with a link" +#: includes/admin/uninstall-page.php:76 +#: includes/ai/class-bulk-processor.php:671 +#: js/build/discovery.js:2 +#: js/build/mapBlocks.js:1 +#: js/build/postsSidebar.js:1 +msgid "Cancel" msgstr "" -#: includes/layers/class-layers.php:137 -msgid "Layer type-specific options" +#: includes/ai/class-ai-logger.php:39 +msgctxt "Post Type General Name" +msgid "AI Usage Logs" msgstr "" -#: includes/layers/class-layers.php:149 -msgid "The URL to download the source data of the layer" +#: includes/ai/class-ai-logger.php:40 +msgctxt "Post Type Singular Name" +msgid "AI Usage Log" msgstr "" -#: includes/layers/class-layers.php:161 -msgid "Label for the attribution URL link" +#: includes/ai/class-ai-logger.php:41 +msgid "AI Logs (Costs)" msgstr "" -#: includes/layers/class-layers.php:173 -msgid "The legend type" +#: includes/ai/class-ai-logger.php:42 +msgid "All AI Logs" msgstr "" -#: includes/layers/class-layers.php:190 -msgid "Legend type-specific options" +#: includes/ai/class-ai-logger.php:43 +msgid "View Log" msgstr "" -#: includes/layers/class-layers.php:202 -#: js/build/layersSidebar.js:7 -msgid "Use legend" +#: includes/ai/class-ai-logger.php:47 +msgid "AI Usage Log" msgstr "" -#: includes/layers/class-layers.php:214 -#: js/build/layersSidebar.js:7 -msgid "Legend title" +#: includes/ai/class-ai-logger.php:48 +msgid "Internal logs for AI token usage and costs." msgstr "" -#: includes/layers/class-layers.php:265 -msgid "Layer type not registered" +#: includes/ai/class-ai-settings.php:46 +#: includes/ai/settings/tab-provider.php:12 +msgid "AI Provider" msgstr "" -#. translators: 1: Mapbox terms URL, 2: Mapbox privacy URL, 3: Nominatim usage policy URL, 4: OSMF privacy URL, 5: OSM tile policy URL. -#: includes/loaders.php:351 -#, php-format -msgid "

    JEO can connect to third-party services depending on your configuration.

    • Mapbox: only used when Mapbox is selected as the rendering library or when a map uses Mapbox-hosted resources. In that case the site loads JavaScript/CSS from Mapbox and sends the configured access token plus the visitor’s IP address, browser details and requested map resources to Mapbox. Terms: Mapbox Terms of Service. Privacy Policy: Mapbox Privacy Policy.
    • Nominatim (OpenStreetMap): used when an editor explicitly runs an address search or reverse geocoding request in the post geolocation UI. The typed address or selected coordinates, the site URL in the user agent string and the server IP address are sent to the Nominatim service. Usage Policy: Nominatim Usage Policy. Privacy Policy: OpenStreetMap Foundation Privacy Policy.
    • OpenStreetMap raster tiles: the default MapLibre preview style requests map tiles from the OpenStreetMap tile service, which receives the visitor’s IP address, browser details and requested tile URLs. Tile Policy: OpenStreetMap Tile Usage Policy. Privacy Policy: OpenStreetMap Foundation Privacy Policy.
    • Optional external asset URLs configured by the site administrator: if you configure an external typography stylesheet or footer logo URL, visitors’ browsers will request that asset directly from the selected host. That host may receive the visitor’s IP address, browser details and referrer according to its own terms and privacy policy.
    " +#: includes/ai/class-ai-settings.php:47 +msgid "Knowledge Base" msgstr "" -#. translators: Explore is the name of JEO's discovery page feature. -#: includes/loaders.php:535 -#: templates/embed-discovery.php:18 -#: js/build/discovery.js:6 -msgid "Explore" +#: includes/ai/class-ai-settings.php:48 +msgid "Bulk Geolocation" msgstr "" -#: includes/maps/class-map-style-composer.php:511 -#: includes/maps/class-map-style-composer.php:564 -msgid "Mapbox composed styles are disabled." +#: includes/ai/class-ai-settings.php:49 +msgid "Context Assistant" msgstr "" -#: includes/maps/class-map-style-composer.php:536 -#: includes/maps/class-map-style-composer.php:588 -msgid "Could not create the Mapbox composed style cache directory." +#: includes/ai/class-ai-settings.php:50 +#: includes/settings/class-settings.php:685 +#: includes/settings/class-settings.php:686 +#: js/build/layersSidebar.js:11 +msgid "Settings" msgstr "" -#: includes/maps/class-map-style-composer.php:684 -msgid "Map not found." +#: includes/ai/class-bulk-processor.php:170 +msgid "Bulk clearing started (Background)." msgstr "" -#: includes/maps/class-map-style-composer.php:694 -#: includes/maps/class-map-style-composer.php:757 -msgid "A Mapbox access token is required to compose Mapbox styles." +#: includes/ai/class-bulk-processor.php:174 +msgid "Bulk clearing started in background. Posts will be reset in batches." msgstr "" -#: includes/maps/class-map-style-composer.php:734 -msgid "Public payload composition is only available for one-time maps." +#: includes/ai/class-bulk-processor.php:194 +#: includes/ai/class-rag-worker.php:101 +#: includes/settings/class-settings.php:623 +msgid "Error" msgstr "" -#: includes/maps/class-map-style-composer.php:740 -msgid "The map preview payload is too large." +#: includes/ai/class-bulk-processor.php:194 +#: includes/ai/class-rag-worker.php:101 +msgid "Success" msgstr "" -#: includes/maps/class-map-style-composer.php:752 -msgid "The map preview has too many layers to compose." +#: includes/ai/class-bulk-processor.php:223 +#: includes/ai/class-rag-worker.php:68 +#: includes/ai/settings/tab-bulk.php:49 +#: includes/ai/settings/tab-knowledge.php:155 +msgid "Every Minute" msgstr "" -#: includes/maps/class-map-style-composer.php:763 -msgid "The map preview does not include any renderable layers." +#: includes/ai/class-bulk-processor.php:229 +#: includes/ai/class-rag-worker.php:74 +#: includes/ai/settings/tab-bulk.php:50 +#: includes/ai/settings/tab-knowledge.php:156 +msgid "Every 5 Minutes" msgstr "" -#: includes/maps/class-map-style-composer.php:785 -msgid "Map preview" +#: includes/ai/class-bulk-processor.php:235 +#: includes/ai/class-rag-worker.php:80 +#: includes/ai/settings/tab-bulk.php:51 +#: includes/ai/settings/tab-knowledge.php:157 +msgid "Every 15 Minutes" msgstr "" -#: includes/maps/class-map-style-composer.php:1026 -msgid "Invalid composed style hash." +#: includes/ai/class-bulk-processor.php:344 +msgid "No post types selected for bulk processing." msgstr "" -#: includes/maps/class-map-style-composer.php:1114 -msgid "One or more Mapbox styles could not be fetched." +#: includes/ai/class-bulk-processor.php:364 +msgid "No more posts to process. Deactivating worker." msgstr "" -#: includes/maps/class-map-style-composer.php:1515 -msgid "The PHP GD extension is required to merge Mapbox sprites." +#: includes/ai/class-bulk-processor.php:381 +msgid "CRITICAL: No active AI adapter found for bulk processing." msgstr "" -#: includes/maps/class-map-style-composer.php:1634 -msgid "Could not decode a Mapbox sprite image." -msgstr "" +#. translators: %d: number of posts processed. +#: includes/ai/class-bulk-processor.php:407 +#, php-format +msgid "Processed batch of %d post." +msgid_plural "Processed batch of %d posts." +msgstr[0] "" +msgstr[1] "" -#: includes/maps/class-map-style-composer.php:2351 -msgid "Composed style artifact was not found." +#: includes/ai/class-bulk-processor.php:434 +msgid "No more posts to clear." msgstr "" -#: includes/maps/class-maps.php:54 -#: includes/maps/class-maps.php:65 -msgid "Maps" +#. translators: %d: number of posts cleared. +#: includes/ai/class-bulk-processor.php:452 +#, php-format +msgid "Cleared batch of %d post." +msgid_plural "Cleared batch of %d posts." +msgstr[0] "" +msgstr[1] "" + +#: includes/ai/class-bulk-processor.php:602 +msgid "No posts selected." msgstr "" -#: includes/maps/class-maps.php:55 -#: includes/settings/settings-page.php:34 -msgid "Map" +#: includes/ai/class-bulk-processor.php:664 +msgid "Review Bulk AI Approval" msgstr "" -#: includes/maps/class-maps.php:56 -#: includes/maps/class-maps.php:57 -msgid "Add new map" +#: includes/ai/class-bulk-processor.php:667 +#: includes/ai/class-bulk-processor.php:703 +msgid "Analyzing selected posts..." msgstr "" -#: includes/maps/class-maps.php:58 -msgid "Edit map" +#: includes/ai/class-bulk-processor.php:672 +msgid "Confirm & Apply" msgstr "" -#: includes/maps/class-maps.php:59 -msgid "New map" +#: includes/ai/class-bulk-processor.php:698 +msgid "Please select at least one post." msgstr "" -#: includes/maps/class-maps.php:60 -msgid "View map" +#: includes/ai/class-bulk-processor.php:772 +msgid "JEO AI Status" msgstr "" -#: includes/maps/class-maps.php:61 -msgid "View maps" +#: includes/ai/class-bulk-processor.php:793 +msgctxt "post geolocation status" +msgid "Geolocated" msgstr "" -#: includes/maps/class-maps.php:62 -msgid "Search maps" +#: includes/ai/class-bulk-processor.php:795 +msgctxt "post geolocation status" +msgid "Pending Approval" msgstr "" -#: includes/maps/class-maps.php:63 -msgid "No map found" +#: includes/ai/class-bulk-processor.php:797 +msgid "No Locations Found" msgstr "" -#: includes/maps/class-maps.php:64 -msgid "No map found in the trash" +#: includes/ai/class-bulk-processor.php:799 +msgid "AI Error" msgstr "" -#: includes/maps/class-maps.php:66 -msgid "Map published." +#: includes/ai/class-bulk-processor.php:801 +msgctxt "post geolocation status" +msgid "Not Processed" msgstr "" -#: includes/maps/class-maps.php:67 -msgid "Map published privately." +#: includes/ai/class-bulk-processor.php:841 +msgid "Approve AI" msgstr "" -#: includes/maps/class-maps.php:68 -msgid "Map reverted to draft." +#: includes/ai/class-bulk-processor.php:852 +msgid "Approve JEO AI Geolocations" msgstr "" -#: includes/maps/class-maps.php:69 -msgid "Map scheduled." +#: includes/ai/class-context-agent.php:282 +msgid "No AI provider is configured. Set one in JEO AI Settings." msgstr "" -#: includes/maps/class-maps.php:70 -msgid "Map updated." +#: includes/ai/class-context-agent.php:320 +msgid "The AI returned an empty prompt engineering response." msgstr "" -#: includes/maps/class-maps.php:112 -msgid "The map initial zoom level" +#: includes/ai/class-context-handler.php:231 +#: includes/ai/class-context-handler.php:350 +#: includes/ai/class-context-handler.php:819 +#: includes/ai/class-context-handler.php:895 +#: includes/ai/class-rag-worker.php:223 +#: includes/minimap/class-minimap.php:162 +#: includes/minimap/class-minimap.php:273 +#: includes/minimap/class-minimap.php:374 +msgid "Post not found." msgstr "" -#: includes/maps/class-maps.php:125 -msgid "The map initial latitude" +#: includes/ai/class-context-handler.php:242 +#: includes/ai/class-context-handler.php:371 +#: includes/ai/class-minilayer-classifier.php:35 +#: includes/ai/class-minilayer-handler.php:91 +#: includes/ai/class-minilayer-service.php:41 +#: includes/minimap/class-minimap.php:172 +#: includes/minimap/class-minimap.php:283 +#: includes/minimap/class-minimap.php:394 +msgid "No AI provider configured. Set one in JEO AI Settings." msgstr "" -#: includes/maps/class-maps.php:138 -msgid "The map initial longitude" +#: includes/ai/class-context-handler.php:254 +msgid "The article seems to have very little content. Please write a bit more about the topic, or tell me what you would like suggestions about (e.g. territory, entities, angles)." msgstr "" -#: includes/maps/class-maps.php:153 -#: includes/maps/class-maps.php:171 -msgid "The Layer ID" +#: includes/ai/class-context-handler.php:255 +#: includes/ai/class-context-handler.php:293 +msgid "Generate editorial suggestions for this post based on its content." msgstr "" -#: includes/maps/class-maps.php:157 -msgid "How this layer is used in this map. Can be fixed, switchable or swappable" +#: includes/ai/class-context-handler.php:263 +msgid "Waiting for more content or directions." msgstr "" -#: includes/maps/class-maps.php:161 -msgid "If the layer is loaded into the mapbox as a style (setStyle) or layer (addLayer)" +#: includes/ai/class-context-handler.php:287 +#: js/build/contextSidebar.js:1 +msgid "Initial suggestions" msgstr "" -#: includes/maps/class-maps.php:165 -msgid "Mapbox style layers" +#: includes/ai/class-context-handler.php:299 +msgid "Suggestions generated." msgstr "" -#: includes/maps/class-maps.php:175 -msgid "If the style should render the layer" +#: includes/ai/class-context-handler.php:330 +#: includes/minimap/class-minimap.php:354 +msgid "conversation_id is required." msgstr "" -#: includes/maps/class-maps.php:182 -msgid "Indicates whether this layer is visible by default" +#: includes/ai/class-context-handler.php:340 +#: includes/minimap/class-minimap.php:262 +#: includes/minimap/class-minimap.php:364 +msgid "A post ID is required." msgstr "" -#: includes/maps/class-maps.php:186 -msgid "Indicates if the legend of this layer should be displayed or not" +#: includes/ai/class-context-handler.php:360 +#: includes/minimap/class-minimap.php:384 +msgid "A message is required." msgstr "" -#: includes/maps/class-maps.php:198 -msgid "The map Layers" +#: includes/ai/class-context-handler.php:410 +msgid "Suggestions updated." msgstr "" -#: includes/maps/class-maps.php:211 -#: js/build/mapBlocks.js:1 -msgid "East pan limit" +#: includes/ai/class-context-handler.php:496 +#: includes/minimap/class-minimap.php:321 +#: includes/minimap/class-minimap.php:442 +msgid "The previous request could not be processed. Please try again." msgstr "" -#: includes/maps/class-maps.php:215 -#: js/build/mapBlocks.js:1 -msgid "North pan limit" +#: includes/ai/class-context-handler.php:502 +msgid "The AI did not respond after multiple attempts. Please try again or rephrase your request." msgstr "" -#: includes/maps/class-maps.php:219 -#: js/build/mapBlocks.js:1 -msgid "South pan limit" +#. translators: %s: linked URL +#: includes/ai/class-context-handler.php:703 +#, php-format +msgid "Link to %s removed: not listed in references." msgstr "" -#: includes/maps/class-maps.php:223 -#: js/build/mapBlocks.js:1 -msgid "West pan limit" +#: includes/ai/class-context-handler.php:720 +msgid "Verification notes:" msgstr "" -#: includes/maps/class-maps.php:233 -msgid "Map pan limits" +#: includes/ai/class-context-handler.php:916 +msgid "Conversation cleared." msgstr "" -#: includes/maps/class-maps.php:246 -msgid "List of category IDs related posts must have" +#: includes/ai/class-context-handler.php:935 +#: includes/class-ai-handler.php:1160 +msgid "Prompt is required." msgstr "" -#: includes/maps/class-maps.php:253 -msgid "List of tag IDs related posts must have" +#: includes/ai/class-funai-place-polygon-adapter.php:84 +msgid "Could not compute bounding box from FUNAI geometry." msgstr "" -#: includes/maps/class-maps.php:260 -msgid "Maximum date for related posts" +#: includes/ai/class-funai-place-polygon-adapter.php:91 +msgid "FUNAI" msgstr "" -#: includes/maps/class-maps.php:264 -msgid "Minimum date for related posts" +#. translators: %d: HTTP status code. +#: includes/ai/class-funai-place-polygon-adapter.php:152 +#, php-format +msgid "FUNAI WFS returned HTTP %d." msgstr "" -#: includes/maps/class-maps.php:268 -msgid "List of meta values related posts must have" +#: includes/ai/class-funai-place-polygon-adapter.php:162 +msgid "Could not parse FUNAI WFS response." msgstr "" -#: includes/maps/class-maps.php:292 -msgid "The map criteria to get related posts" +#: includes/ai/class-generate-boundary-layer-tool.php:83 +msgid "You do not have permission to generate layers." msgstr "" -#: includes/maps/class-maps.php:305 -msgid "Disable scroll zoom" +#: includes/ai/class-ibge-place-polygon-adapter.php:78 +msgid "Could not compute bounding box from IBGE geometry." msgstr "" -#: includes/maps/class-maps.php:318 -msgid "Disable drag pan" +#: includes/ai/class-ibge-place-polygon-adapter.php:85 +msgid "IBGE" msgstr "" -#: includes/maps/class-maps.php:331 -msgid "Disable drag rotation" +#. translators: %d: HTTP status code. +#: includes/ai/class-ibge-place-polygon-adapter.php:289 +#, php-format +msgid "IBGE API returned HTTP %d." msgstr "" -#: includes/maps/class-maps.php:344 -msgid "Enable fullscreen button" +#: includes/ai/class-ibge-place-polygon-adapter.php:299 +msgid "Could not parse IBGE geometry response." msgstr "" -#: includes/maps/class-maps.php:357 -msgid "Show all posts on map" +#. translators: %d: HTTP status code. +#: includes/ai/class-mapbox-style-builder.php:74 +#, php-format +msgid "Mapbox Styles API returned HTTP %d." msgstr "" -#: includes/maps/class-maps.php:370 -msgid "Minimum Zoom level" +#: includes/ai/class-mapbox-style-builder.php:79 +msgid "Mapbox denied the style publish request (HTTP 403): the configured Mapbox token likely lacks the styles:write scope. Use a secret token (sk.…) or add the styles:write scope to the token at mapbox.com/account/access-tokens, then update the key in JEO Settings." msgstr "" -#: includes/maps/class-maps.php:383 -msgid "Maximum zoom level" +#: includes/ai/class-mapbox-style-builder.php:84 +msgid "Mapbox rejected the access token (HTTP 401): the Mapbox API key configured in JEO Settings is invalid or has expired." msgstr "" -#: includes/maps/class-maps.php:396 -#: js/build/mapBlocks.js:3 -#: js/build/mapsSidebar.js:1 -msgid "Hide in discovery" +#: includes/ai/class-mapbox-style-builder.php:103 +msgid "Mapbox returned an unexpected style payload." msgstr "" -#: includes/maps/class-maps.php:409 -msgid "Disable embed" +#: includes/ai/class-mapbox-style-builder.php:136 +msgid "Only \"mapbox\" layer specs can be built into a composed style." msgstr "" -#: includes/maps/class-maps.php:565 -msgid "Map zoom must be between 1 and 14" +#: includes/ai/class-mapbox-style-builder.php:144 +msgid "Composed style spec is missing style_json." msgstr "" -#: includes/menu/class-menu.php:37 -#: includes/menu/class-menu.php:38 -msgid "JEO" +#. translators: %s: missing style key. +#: includes/ai/class-mapbox-style-builder.php:155 +#, php-format +msgid "Composed style is missing required key: %s." msgstr "" -#: includes/settings/class-settings.php:179 -msgid "Mapbox requires a valid API key. The rendering library setting was not changed." +#: includes/ai/class-mapbox-style-builder.php:187 +msgid "Mapbox API key is not configured." msgstr "" -#: includes/settings/class-settings.php:317 -msgid "JEO could not save this asset URL. Use a valid http(s) or root-relative URL." +#: includes/ai/class-mapbox-style-builder.php:195 +msgid "Mapbox API key format is not recognized." msgstr "" -#: includes/settings/class-settings.php:347 -#: includes/settings/class-settings.php:348 -#: js/build/layersSidebar.js:11 -msgid "Settings" +#: includes/ai/class-mapbox-style-builder.php:205 +msgid "Could not decode Mapbox API key." msgstr "" -#: includes/settings/settings-page.php:16 -msgid "JEO Settings" +#: includes/ai/class-mapbox-style-builder.php:213 +msgid "Could not extract Mapbox username from API key." msgstr "" -#: includes/settings/settings-page.php:20 -msgid "General" +#: includes/ai/class-minilayer-classifier.php:57 +msgid "The classifier returned an unexpected response." msgstr "" -#: includes/settings/settings-page.php:23 -msgid "Geocoders" +#: includes/ai/class-minilayer-handler.php:72 +#: includes/cli/class-ai-cli.php:52 +#: includes/cli/class-ai-cli.php:157 +#: includes/minimap/class-minimap.php:252 +msgid "A prompt is required." msgstr "" -#: includes/settings/settings-page.php:26 -msgid "Customize" +#: includes/ai/class-minilayer-handler.php:90 +#: includes/ai/class-minilayer-service.php:33 +msgid "Mapbox API key is not configured. Set it in JEO Settings." msgstr "" -#: includes/settings/settings-page.php:40 -msgid "Rendering library" +#: includes/ai/class-minilayer-handler.php:92 +msgid "Could not generate the map style. Please try again." msgstr "" -#: includes/settings/settings-page.php:50 -msgid "Default map latitute" +#: includes/ai/class-minilayer-handler.php:93 +msgid "The AI returned an unexpected response. Please try again." msgstr "" -#: includes/settings/settings-page.php:57 -msgid "Default map longitude" +#: includes/ai/class-minilayer-handler.php:94 +msgid "The AI did not create a valid style. Please try again." msgstr "" -#: includes/settings/settings-page.php:64 -msgid "Default map zoom" +#: includes/ai/class-minilayer-handler.php:95 +msgid "Could not classify the layer request. Please try again." msgstr "" -#: includes/settings/settings-page.php:71 -msgid "Post types" +#: includes/ai/class-minilayer-handler.php:96 +msgid "The classifier returned an unexpected response. Please try again." msgstr "" -#: includes/settings/settings-page.php:77 -msgid "Show story maps on post archives pages" +#: includes/ai/class-minilayer-handler.php:97 +#: includes/ai/class-minilayer-service.php:55 +msgid "The request cannot be approximated with available map data." msgstr "" -#: includes/settings/settings-page.php:84 -msgid "Enabled Post Types. Default: post,storymap" +#: includes/ai/class-minilayer-handler.php:98 +msgid "The AI returned an unsupported layer type." msgstr "" -#: includes/settings/settings-page.php:86 -msgid "Post types separated by comma, Ex: map,post,page" +#: includes/ai/class-minilayer-handler.php:99 +msgid "The AI response is missing a required tileset field." msgstr "" -#: includes/settings/settings-page.php:91 -msgid "Mapbox" +#: includes/ai/class-minilayer-handler.php:100 +msgid "The AI returned an invalid geometry type." msgstr "" -#: includes/settings/settings-page.php:94 -msgid "When selected, Mapbox loads the official Mapbox GL JS assets from api.mapbox.com and uses Mapbox terms and privacy practices." +#: includes/ai/class-minilayer-handler.php:101 +msgid "The AI response is missing composed style JSON." msgstr "" -#: includes/settings/settings-page.php:97 -msgid "Visitors load Mapbox assets in the browser, and Mapbox receives the configured access token plus the visitor IP address and requested map resources." +#. translators: %s: layer type. +#: includes/ai/class-minilayer-service.php:71 +#, php-format +msgid "Unsupported layer type: %s." msgstr "" -#: includes/settings/settings-page.php:103 -msgid "API Key" +#. translators: %s: missing field name. +#: includes/ai/class-minilayer-service.php:92 +#, php-format +msgid "Missing required tileset field: %s." msgstr "" -#: includes/settings/settings-page.php:105 -msgid "Ex. pk.eyJ3..." +#: includes/ai/class-minilayer-service.php:103 +msgid "Invalid layer geometry type for tileset-vector." msgstr "" -#: includes/settings/settings-page.php:107 -msgid "This key is required only when the rendering library is set to Mapbox." +#. translators: %s: tileset ID. +#: includes/ai/class-minilayer-service.php:149 +#, php-format +msgid "Mapbox tileset %s" msgstr "" -#: includes/settings/settings-page.php:121 -msgid "Active Geocoder" +#: includes/ai/class-minilayer-service.php:221 +msgid "External sources" msgstr "" -#: includes/settings/settings-page.php:134 -msgid "The built-in Nominatim geocoder sends the address you submit, or the coordinates selected on the map, to nominatim.openstreetmap.org only when you explicitly run a search or reverse-geocode a point." +#: includes/ai/class-minilayer-service.php:222 +msgid "Mapbox composed style" msgstr "" -#. translators: %s is the geocoder name. Example: Nominatim options. -#: includes/settings/settings-page.php:154 -#, php-format -msgctxt "geocoder_options" -msgid "%s options" +#: includes/ai/class-minilayer-service.php:298 +msgid "Minilayer" msgstr "" -#: includes/settings/settings-page.php:198 -msgid "Typography" +#: includes/ai/class-neuron-factory.php:216 +msgid "No Embedding Model configured. Please select one in the JEO AI settings." msgstr "" -#: includes/settings/settings-page.php:201 -msgid "Typography URL" +#: includes/ai/class-osm-place-polygon-adapter.php:99 +msgid "Could not compute bounding box from OSM geometry." msgstr "" -#: includes/settings/settings-page.php:203 -msgid "Ex. https://example.com/wp-content/uploads/fonts/jeo-font.css" +#: includes/ai/class-osm-place-polygon-adapter.php:106 +msgid "OpenStreetMap contributors" msgstr "" -#: includes/settings/settings-page.php:205 -#: includes/settings/settings-page.php:220 -msgid "You may use a local or external stylesheet URL. If you choose an external host, your visitors will request that asset directly from that provider." +#: includes/ai/class-osm-place-polygon-adapter.php:364 +msgid "Could not parse Overpass response." msgstr "" -#: includes/settings/settings-page.php:210 -msgid "Typography name" +#: includes/ai/class-osm-place-polygon-adapter.php:385 +msgid "Could not assemble closed rings from OSM relation." msgstr "" -#: includes/settings/settings-page.php:212 -msgid "Ex. Open Sans" +#: includes/ai/class-osm-place-polygon-adapter.php:424 +msgid "No Overpass mirrors configured." msgstr "" -#: includes/settings/settings-page.php:216 -msgid "Secondary Typography URL" +#. translators: %d: HTTP status code. +#: includes/ai/class-osm-place-polygon-adapter.php:460 +#, php-format +msgid "Overpass returned HTTP %d." msgstr "" -#: includes/settings/settings-page.php:218 -msgid "Ex. https://example.com/wp-content/uploads/fonts/jeo-story-font.css" +#: includes/ai/class-osm-place-polygon-adapter.php:472 +msgid "All Overpass mirrors failed." msgstr "" -#: includes/settings/settings-page.php:225 -msgid "Secondary Typography name" +#. translators: %s: place name. +#: includes/ai/class-place-polygon-service.php:96 +#, php-format +msgid "Could not find a boundary polygon for \"%s\"." msgstr "" -#: includes/settings/settings-page.php:227 -msgid "Ex. Libre Baskerville" +#. translators: %s: boundary display name. +#: includes/ai/class-place-polygon-service.php:126 +#, php-format +msgid "Boundary: %s" msgstr "" -#: includes/settings/settings-page.php:231 -msgid "Info button font-size (rem)" +#: includes/ai/class-place-polygon-service.php:238 +msgid "Could not write boundary GeoJSON file." msgstr "" -#: includes/settings/settings-page.php:238 -msgid "Colors" +#: includes/ai/class-place-polygon-service.php:256 +msgid "Could not create boundary GeoJSON attachment." msgstr "" -#: includes/settings/settings-page.php:242 -msgid "Primary color" +#. translators: %s: municipality name with state abbreviation. +#: includes/ai/class-place-polygon-service.php:326 +#, php-format +msgid "Layer showing the area of the municipality of %s" msgstr "" -#: includes/settings/settings-page.php:244 -#: includes/settings/settings-page.php:258 -#: includes/settings/settings-page.php:264 -#: includes/settings/settings-page.php:276 -msgid "Ex. #ffffff" +#. translators: %s: state name. +#: includes/ai/class-place-polygon-service.php:332 +#, php-format +msgid "Layer showing the area of the state of %s" msgstr "" -#: includes/settings/settings-page.php:249 -msgid "Text over primary color" +#. translators: %s: indigenous land name. +#: includes/ai/class-place-polygon-service.php:338 +#, php-format +msgid "Layer showing the indigenous land %s" msgstr "" -#: includes/settings/settings-page.php:256 -msgid "Info button background color" +#. translators: %s: place display name. +#: includes/ai/class-place-polygon-service.php:344 +#, php-format +msgid "Layer showing the area of %s" msgstr "" -#: includes/settings/settings-page.php:262 -msgid "Info button color" +#. translators: %s: directory path +#: includes/ai/class-rag-agent.php:64 +#, php-format +msgid "The RAG Vector Store directory (%s) could not be created. Please check your uploads folder permissions." msgstr "" -#: includes/settings/settings-page.php:268 -msgid "Close button background color" +#. translators: %s: directory path +#: includes/ai/class-rag-agent.php:70 +#, php-format +msgid "The RAG Vector Store directory (%s) is not writable. Local Vectorization is disabled." msgstr "" -#: includes/settings/settings-page.php:274 -msgid "Close button color" +#: includes/ai/class-rag-agent.php:76 +msgid "No AI Provider is configured. Please set up a provider (Gemini, OpenAI, etc.) before using RAG." msgstr "" -#: includes/settings/settings-page.php:281 -msgid "Embed" +#: includes/ai/class-rag-agent.php:89 +msgid "No Embedding Model selected. Please choose a model in the Knowledge Base tab." msgstr "" -#: includes/settings/settings-page.php:285 -msgid "Company logo" +#. translators: %s: provider name +#: includes/ai/class-rag-agent.php:108 +#, php-format +msgid "The selected embedding provider (%s) does not support native Embeddings in JEO yet. Please use OpenAI, Gemini or Ollama for Vector Store features." msgstr "" -#: includes/settings/settings-page.php:289 -msgid "You may use a local or external image URL. Large logos will be scaled down automatically in the embed footer." +#. translators: %s: provider name +#: includes/ai/class-rag-agent.php:119 +#, php-format +msgid "The API Key for the Embedding provider (%s) is missing. RAG requires a valid connection to generate embeddings." msgstr "" -#: includes/settings/settings-page.php:291 -msgid "Insert Image" +#: includes/ai/class-rag-backup.php:41 +msgid "The PHP ZipArchive extension is required to create backups." msgstr "" -#: includes/settings/settings-page.php:301 -msgid "Save Changes" +#: includes/ai/class-rag-backup.php:92 +msgid "No vector store found to backup." msgstr "" -#. translators: Story Map is the name of JEO's storytelling map feature. -#: includes/storymap/class-storymap.php:69 -#: includes/storymap/class-storymap.php:70 -#: js/build/mapBlocks.js:3 -msgid "Story Map" +#: includes/ai/class-rag-backup.php:99 +msgid "Failed to create the ZIP archive." msgstr "" -#: includes/storymap/class-storymap.php:71 -#: includes/storymap/class-storymap.php:72 -msgid "Add new Story Map" +#: includes/ai/class-rag-worker.php:209 +msgid "Either post_id or query is required." msgstr "" -#: includes/storymap/class-storymap.php:73 -msgid "Edit Story Map" +#. translators: %s: pipeline name +#: includes/ai/class-rag-worker.php:347 +#, php-format +msgid "Unknown pipeline: %s" msgstr "" -#: includes/storymap/class-storymap.php:74 -msgid "New Story Map" +#. translators: 1: locked model name, 2: current model name +#: includes/ai/class-rag-worker.php:377 +#, php-format +msgid "Vector Store mismatch for %1$s! Expected %2$s, found %3$s." msgstr "" -#: includes/storymap/class-storymap.php:75 -msgid "View Story Map" +#. translators: %s: pipeline name +#: includes/ai/class-rag-worker.php:403 +#, php-format +msgid "No more %s to vectorize." msgstr "" -#: includes/storymap/class-storymap.php:76 -msgid "View Story Maps" +#. translators: %d: number of items vectorized +#: includes/ai/class-rag-worker.php:428 +#, php-format +msgid "Successfully vectorized %1$d %2$s." msgstr "" -#: includes/storymap/class-storymap.php:77 -msgid "Search Story Maps" +#. translators: %s: pipeline name +#: includes/ai/class-rag-worker.php:437 +#, php-format +msgid "Batch skipped for %s (no content found)." msgstr "" -#: includes/storymap/class-storymap.php:78 -msgid "No Story Map found" +#: includes/ai/class-retrieve-knowledge-tool.php:112 +msgid "Provide either \"query\" (string) or \"queries\" (array of strings)." msgstr "" -#: includes/storymap/class-storymap.php:79 -msgid "No Story Map found in the trash" +#: includes/ai/settings/main-page.php:10 +msgid "JEO AI Configuration" msgstr "" -#: includes/storymap/class-storymap.php:80 -msgid "Story Maps" +#: includes/ai/settings/main-page.php:99 +msgid "Save AI Settings" msgstr "" -#: includes/storymap/class-storymap.php:81 -msgid "Story Map published." +#: includes/ai/settings/main-page.php:107 +#: includes/ai/settings/tab-general.php:22 +msgid "JEO AI API Debugger" msgstr "" -#: includes/storymap/class-storymap.php:82 -msgid "Story Map published privately." +#: includes/ai/settings/main-page.php:109 +msgid "Clear logs" msgstr "" -#: includes/storymap/class-storymap.php:83 -msgid "Story Map reverted to draft." +#: includes/ai/settings/main-page.php:115 +msgid "[System] Console initialized. Awaiting API activity..." msgstr "" -#: includes/storymap/class-storymap.php:84 -msgid "Story Map scheduled." +#: includes/ai/settings/tab-bulk.php:12 +msgid "Background Processing" msgstr "" -#: includes/storymap/class-storymap.php:85 -msgid "Story Map updated." +#: includes/ai/settings/tab-bulk.php:15 +msgid "Enable background AI geolocalization" msgstr "" -#: includes/storymap/class-storymap.php:91 -msgid "JEO Story Map" +#: includes/ai/settings/tab-bulk.php:16 +msgid "When active, JEO will scan legacy posts and suggest locations based on their content." msgstr "" -#: templates/discovery.php:19 -msgid "Sorry!" +#: includes/ai/settings/tab-bulk.php:20 +#: includes/ai/settings/tab-knowledge.php:149 +msgid "Batch Size" msgstr "" -#: templates/discovery.php:22 -msgid "This page can't be viewed on mobile devices." +#: includes/ai/settings/tab-bulk.php:23 +msgid "Number of posts to process per batch." msgstr "" -#: templates/discovery.php:26 -msgid "Back to homepage" +#: includes/ai/settings/tab-bulk.php:27 +msgid "Target Post Types" msgstr "" -#: templates/map-content-layers-list.php:18 -#: templates/map-content-layers-list.php:20 -#: js/build/jeoMap.js:1 -msgid "Attribution:" +#: includes/ai/settings/tab-bulk.php:46 +#: includes/ai/settings/tab-knowledge.php:153 +msgid "Cron Interval" msgstr "" -#: templates/map-content-layers-list.php:25 -#: js/build/jeoMap.js:1 -msgid "Download from source" +#: includes/ai/settings/tab-bulk.php:52 +#: includes/ai/settings/tab-knowledge.php:158 +msgid "Hourly" msgstr "" -#: includes/layer-types/mapbox-tileset-raster.js:5 -#: js/build/layersSidebar.js:11 -#: js/build/mapBlocks.js:1 -#: js/build/mapBlocks.js:3 -#: js/build/mapsSidebar.js:1 -msgid "Raster Mapbox Tiled Source" +#: includes/ai/settings/tab-bulk.php:53 +msgid "Twice Daily" msgstr "" -#: includes/layer-types/mapbox-tileset-raster.js:68 -#: includes/layer-types/mapbox-tileset-vector.js:70 -#: js/build/layersSidebar.js:11 -#: js/build/mapBlocks.js:1 -msgid "Tileset ID" +#: includes/ai/settings/tab-bulk.php:59 +msgid "Execution Logs" msgstr "" -#: includes/layer-types/mapbox-tileset-raster.js:69 -#: includes/layer-types/mapbox-tileset-vector.js:71 -#: js/build/layersSidebar.js:11 -#: js/build/mapBlocks.js:1 -msgid "Example: username.tilesetid" +#: includes/ai/settings/tab-bulk.php:62 +msgid "Recent Background Activity" msgstr "" -#: includes/layer-types/mapbox-tileset-raster.js:72 -#: includes/layer-types/mapbox-tileset-vector.js:99 -#: includes/layer-types/mvt.js:101 -#: js/build/layersSidebar.js:11 -#: js/build/mapBlocks.js:1 -msgid "Style Source Type" +#: includes/ai/settings/tab-bulk.php:68 +msgid "No recent activity recorded." msgstr "" -#: includes/layer-types/mapbox-tileset-raster.js:73 -#: includes/layer-types/mvt.js:102 -#: js/build/layersSidebar.js:11 -#: js/build/mapBlocks.js:1 -msgid "Which data the map should display" +#: includes/ai/settings/tab-bulk.php:87 +msgid "Manual Control" msgstr "" -#: includes/layer-types/mapbox-tileset-raster.js:79 -#: includes/layer-types/mapbox-tileset-vector.js:79 -#: js/build/layersSidebar.js:11 -#: js/build/mapBlocks.js:1 -msgid "Layer Type" +#: includes/ai/settings/tab-bulk.php:91 +#: includes/settings/class-settings.php:636 +msgid "Process 1 Batch Now" msgstr "" -#: includes/layer-types/mapbox-tileset-raster.js:81 -#: includes/layer-types/mapbox-tileset-vector.js:81 -#: js/build/layersSidebar.js:11 -#: js/build/mapBlocks.js:1 -msgid "Layers take the data that they get from a source, optionally filter features, and then define how those features are styled." +#: includes/ai/settings/tab-bulk.php:94 +#: includes/settings/class-settings.php:637 +msgid "Clear 1 Batch" msgstr "" -#: includes/layer-types/mapbox-tileset-vector.js:5 -#: js/build/layersSidebar.js:11 -#: js/build/mapBlocks.js:1 -#: js/build/mapBlocks.js:3 -#: js/build/mapsSidebar.js:1 -msgid "Vector Mapbox Tiled Source" +#: includes/ai/settings/tab-bulk.php:97 +msgid "Reset All Posts" msgstr "" -#: includes/layer-types/mapbox-tileset-vector.js:75 -#: includes/layer-types/mvt.js:83 -#: js/build/layersSidebar.js:11 -#: js/build/mapBlocks.js:1 -msgid "Source layer" +#: includes/ai/settings/tab-bulk.php:100 +msgid "Manual Batch: Processes legacy posts immediately. Clear Batch: Resets the \"processed\" flag for a small group. Reset All: Schedules a background task to clear all posts (Double confirmation required)." msgstr "" -#: includes/layer-types/mapbox-tileset-vector.js:76 -#: js/build/layersSidebar.js:11 -#: js/build/mapBlocks.js:1 -msgid "Which data the map should display." +#: includes/ai/settings/tab-bulk.php:105 +msgid "Processing Status" msgstr "" -#: includes/layer-types/mapbox-tileset-vector.js:100 -#: js/build/layersSidebar.js:11 -#: js/build/mapBlocks.js:1 -msgid "The layer source type style" +#. translators: %1$d: processed posts, %2$d: total posts, %3$d: percentage +#: includes/ai/settings/tab-bulk.php:139 +#, php-format +msgid "Geolocalization Progress: %1$d of %2$d posts marked as processed (%3$d%%)." msgstr "" -#: includes/layer-types/mapbox.js:5 -#: js/build/layersSidebar.js:11 -#: js/build/mapBlocks.js:1 -#: js/build/mapBlocks.js:3 -#: js/build/mapsSidebar.js:1 -msgid "Mapbox Style" +#: includes/ai/settings/tab-bulk.php:146 +msgid "Auto-Approval Threshold" msgstr "" -#: includes/layer-types/mapbox.js:117 -#: js/build/layersSidebar.js:11 -#: js/build/mapBlocks.js:1 -msgid "Style ID" +#: includes/ai/settings/tab-bulk.php:149 +msgid "Minimum average confidence required to auto-approve locations when using bulk approval actions in the post list." msgstr "" -#: includes/layer-types/mapbox.js:119 -#: js/build/layersSidebar.js:11 -#: js/build/mapBlocks.js:1 -msgid "The Mapbox Style ID includes the user name and id. Example: username/id or mapbox://styles/username/id" +#: includes/ai/settings/tab-bulk.php:154 +msgid "Internal Debug Log" msgstr "" -#: includes/layer-types/mapbox.js:126 -#: js/build/layersSidebar.js:11 -#: js/build/mapBlocks.js:1 -msgid "Access token" +#: includes/ai/settings/tab-bulk.php:157 +msgid "Enable detailed file-based logging (jeo-bulk-ai.log) for technical troubleshooting." msgstr "" -#: includes/layer-types/mapbox.js:128 -#: js/build/layersSidebar.js:11 -#: js/build/mapBlocks.js:1 -msgid "Optional. If this layer needs a different access token from the one set in Settings, inform it here." +#: includes/ai/settings/tab-bulk.php:161 +msgid "Open Log File" msgstr "" -#: includes/layer-types/mvt.js:5 -#: js/build/layersSidebar.js:11 -#: js/build/mapBlocks.js:1 -#: js/build/mapBlocks.js:3 -#: js/build/mapsSidebar.js:1 -msgid "Mapbox Vector Tiles (MVT)" +#: includes/ai/settings/tab-bulk.php:162 +msgid "Delete Log File" msgstr "" -#: includes/layer-types/mvt.js:79 -#: includes/layer-types/tilelayer.js:57 -#: includes/legend-types/circles.js:25 -#: includes/legend-types/icons.js:25 -#: js/build/layersSidebar.js:11 -#: js/build/mapBlocks.js:1 -msgid "URL" +#: includes/ai/settings/tab-context.php:18 +msgid "Context Assistant System Prompt" msgstr "" -#: includes/layer-types/mvt.js:84 -#: js/build/layersSidebar.js:11 -#: js/build/mapBlocks.js:1 -msgid "Layer to use from a vector tile source." +#: includes/ai/settings/tab-context.php:31 +msgid "Check this to use a custom system prompt. Uncheck to use the built-in default prompt." msgstr "" -#: includes/layer-types/tilelayer.js:5 -#: js/build/layersSidebar.js:11 -#: js/build/mapBlocks.js:1 -#: js/build/mapBlocks.js:3 -#: js/build/mapsSidebar.js:1 -msgid "Raster Tiled Source" +#: includes/ai/settings/tab-context.php:37 +msgid "Custom System Prompt" msgstr "" -#: includes/layer-types/tilelayer.js:61 -#: js/build/layersSidebar.js:11 -#: js/build/mapBlocks.js:1 -msgid "Scheme" +#: includes/ai/settings/tab-context.php:49 +msgid "Custom system prompt used by the AI Context Assistant. Use the assistant below to generate or refine it. This value is stored as structured output JSON internally, but shown here as plain text for editing." msgstr "" -#: includes/layer-types/tilelayer.js:62 -#: js/build/layersSidebar.js:11 -#: js/build/mapBlocks.js:1 -msgid "Influences the Y direction of the tile coordinates." +#: includes/ai/settings/tab-context.php:55 +msgid "Default System Prompt (reference)" msgstr "" -#: includes/layer-types/tilelayer.js:65 -#: js/build/layersSidebar.js:11 -#: js/build/mapBlocks.js:1 -msgid "Slippy Map tilenames (XYZ)" +#: includes/ai/settings/tab-context.php:65 +msgid "This is the built-in default prompt. Enable the custom prompt checkbox above to override it." msgstr "" -#: includes/layer-types/tilelayer.js:66 -#: js/build/layersSidebar.js:11 -#: js/build/mapBlocks.js:1 -msgid "OSGeo spec (TMS)" +#: includes/ai/settings/tab-context.php:74 +msgid "AI Context Prompt Engineer Assistant" msgstr "" -#: includes/legend-types/barscale.js:5 -#: js/build/layersSidebar.js:7 -msgid "Color scale" +#: includes/ai/settings/tab-context.php:76 +msgid "Describe how you want the Context Assistant to behave (e.g., \"Always link place names to the original article\" or \"Ground every claim with a verbatim quote\"). The active LLM will generate a highly optimized system prompt for you, strictly adhering to JEO formatting rules." msgstr "" -#: includes/legend-types/barscale.js:16 -msgid "The label shown at the left end of the bar" +#: includes/ai/settings/tab-context.php:113 +msgid "Ex: I want the assistant to always include inline contextual links and cite the exact sentence from the article that supports each location." msgstr "" -#: includes/legend-types/barscale.js:20 -msgid "The label shown at the right end of the bar" +#: includes/ai/settings/tab-context.php:115 +msgid "Your description is saved automatically in this browser." msgstr "" -#: includes/legend-types/barscale.js:24 -msgid "An array of hexadecimal color values" +#: includes/ai/settings/tab-context.php:121 +msgid "Generate Custom Prompt" msgstr "" -#: includes/legend-types/circles.js:5 -#: js/build/layersSidebar.js:7 -msgid "Circle sizes" +#: includes/ai/settings/tab-context.php:124 +msgid "Suggest initial prompt" msgstr "" -#: includes/legend-types/circles.js:16 -msgid "An array of labels and radius values" +#: includes/ai/settings/tab-context.php:182 +msgid "Initial prompt loaded. Edit and save the settings." msgstr "" -#: includes/legend-types/icons.js:5 -#: js/build/layersSidebar.js:7 -msgid "Icon categories" +#: includes/ai/settings/tab-context.php:192 +msgid "Please describe how you want the assistant to behave first." msgstr "" -#: includes/legend-types/icons.js:16 -msgid "An array of labels and icons" +#: includes/ai/settings/tab-context.php:199 +#: includes/settings/class-settings.php:590 +msgid "Generating..." msgstr "" -#: includes/legend-types/JeoLegend.js:177 -#: includes/legend-types/JeoLegend.js:181 -#: includes/legend-types/JeoLegend.js:186 -#: js/build/JeoLegend.js:1 -#: js/build/layersSidebar.js:7 -msgid "Default label" +#: includes/ai/settings/tab-context.php:217 +msgid "Custom prompt generated. Remember to save the settings." msgstr "" -#: includes/legend-types/simple-color.js:5 -#: js/build/layersSidebar.js:7 -msgid "Color categories" +#: includes/ai/settings/tab-context.php:220 +#: includes/ai/settings/tab-context.php:224 +msgid "Generation failed." msgstr "" -#: includes/legend-types/simple-color.js:16 -msgid "An array of labels and colors" +#: includes/ai/settings/tab-embedded.php:10 +msgid "Embedded Brazilian Territories" msgstr "" -#: includes/settings/settings-page.js:37 -#: includes/settings/settings-page.js:39 -msgid "Choose Image" +#: includes/ai/settings/tab-embedded.php:11 +msgid "The following geographic dictionaries are available locally to improve AI precision:" msgstr "" -#: js/build/discovery.js:1 -#: js/build/discovery.js:2 -msgid "January" +#: includes/ai/settings/tab-embedded.php:18 +#: includes/settings/class-settings.php:660 +msgid "Brazilian Biomes" msgstr "" -#: js/build/discovery.js:1 -#: js/build/discovery.js:2 -msgid "February" +#: includes/ai/settings/tab-embedded.php:19 +#: includes/settings/class-settings.php:661 +msgid "Indigenous Territories" msgstr "" -#: js/build/discovery.js:1 -#: js/build/discovery.js:2 -msgid "March" +#: includes/ai/settings/tab-embedded.php:20 +#: includes/settings/class-settings.php:662 +msgid "Quilombola Territories" msgstr "" -#: js/build/discovery.js:1 -#: js/build/discovery.js:2 -msgid "April" +#: includes/ai/settings/tab-embedded.php:21 +#: includes/settings/class-settings.php:663 +msgid "Extractive Reserves (Resex)" msgstr "" -#: js/build/discovery.js:1 -#: js/build/discovery.js:2 -msgid "May" +#: includes/ai/settings/tab-embedded.php:22 +#: includes/settings/class-settings.php:664 +msgid "Conservation Units" msgstr "" -#: js/build/discovery.js:1 -#: js/build/discovery.js:2 -msgid "June" +#: includes/ai/settings/tab-embedded.php:23 +#: includes/settings/class-settings.php:665 +msgid "Riverside Communities" msgstr "" -#: js/build/discovery.js:1 -#: js/build/discovery.js:2 -msgid "July" +#: includes/ai/settings/tab-embedded.php:24 +#: includes/settings/class-settings.php:666 +msgid "Agrarian Reform Settlements" msgstr "" -#: js/build/discovery.js:1 -#: js/build/discovery.js:2 -msgid "August" +#: includes/ai/settings/tab-embedded.php:25 +#: includes/settings/class-settings.php:667 +msgid "Indigenous Peoples (Ethnicities)" msgstr "" -#: js/build/discovery.js:1 -#: js/build/discovery.js:2 -msgid "September" +#: includes/ai/settings/tab-embedded.php:26 +#: includes/settings/class-settings.php:668 +msgid "Legal Amazon and Boundaries" msgstr "" -#: js/build/discovery.js:1 -#: js/build/discovery.js:2 -msgid "October" +#: includes/ai/settings/tab-embedded.php:27 +#: includes/settings/class-settings.php:669 +msgid "Hydrographic Basins" msgstr "" -#: js/build/discovery.js:1 -#: js/build/discovery.js:2 -msgid "November" +#: includes/ai/settings/tab-embedded.php:46 +msgid "File:" msgstr "" -#: js/build/discovery.js:1 -#: js/build/discovery.js:2 -msgid "December" +#: includes/ai/settings/tab-embedded.php:47 +msgid "Entries found:" msgstr "" -#: js/build/discovery.js:1 -msgid "Previous month" +#: includes/ai/settings/tab-embedded.php:54 +#: js/build/layersSidebar.js:7 +#: js/build/mapBlocks.js:1 +msgid "Preview" msgstr "" -#: js/build/discovery.js:1 -msgid "Month" +#: includes/ai/settings/tab-embedded.php:57 +#: includes/settings/class-settings.php:603 +msgid "Download" msgstr "" -#: js/build/discovery.js:1 -msgid "Year" +#: includes/ai/settings/tab-embedded.php:65 +#: includes/ai/settings/tab-knowledge.php:289 +#: includes/settings/ai-logs-page.php:250 +msgid "Close" msgstr "" -#: js/build/discovery.js:1 -msgid "Next month" +#: includes/ai/settings/tab-embedded.php:69 +msgid "Item List" msgstr "" -#: js/build/discovery.js:1 -#: js/build/discovery.js:6 -#: js/build/mapBlocks.js:3 -#: js/build/mapsSidebar.js:1 -msgid "Clear" +#: includes/ai/settings/tab-embedded.php:73 +msgid "Name" msgstr "" -#: js/build/discovery.js:1 -#: js/build/discovery.js:2 -#: js/build/discovery.js:6 -msgid "Apply" +#: includes/ai/settings/tab-embedded.php:74 +#: includes/geocode/class-geocode-handler.php:265 +#: js/build/mapBlocks.js:3 +#: js/build/mapsSidebar.js:1 +msgid "Latitude" msgstr "" -#: js/build/discovery.js:1 +#: includes/ai/settings/tab-embedded.php:75 +#: includes/geocode/class-geocode-handler.php:282 #: js/build/mapBlocks.js:3 #: js/build/mapsSidebar.js:1 -msgid "Tags" +msgid "Longitude" msgstr "" -#: js/build/discovery.js:1 -msgid "Filter stories by tag" +#: includes/ai/settings/tab-embedded.php:89 +msgid "JSON Raw" msgstr "" -#. translators: %s: selected tag name. -#: js/build/discovery.js:2 -#, js-format -msgid "Remove tag %s" +#: includes/ai/settings/tab-general.php:14 +msgid "Debug Mode" msgstr "" -#: js/build/discovery.js:2 -msgid "MM/DD/YYYY" +#: includes/ai/settings/tab-general.php:17 +msgid "Enable internal logging of all AI calls (Inputs/Outputs) for 24 hours. Useful for debugging prompt regressions." msgstr "" -#: js/build/discovery.js:2 -msgid " - " +#: includes/ai/settings/tab-general.php:25 +msgid "Show the floating API debugger console on AI settings pages. Displays real-time request/response logs." msgstr "" -#: js/build/discovery.js:2 -#: js/build/mapBlocks.js:1 -#: js/build/postsSidebar.js:1 -msgid "Cancel" +#: includes/ai/settings/tab-general.php:30 +msgid "NeuronAI Structured Output" msgstr "" -#: js/build/discovery.js:2 -msgid "From" +#: includes/ai/settings/tab-general.php:33 +msgid "Use native schema enforcement instead of prompt-based JSON extraction. Falls back to text parsing if structured output fails." msgstr "" -#: js/build/discovery.js:2 -msgid "To" +#: includes/ai/settings/tab-general.php:36 +msgid "Limitations:" msgstr "" -#: js/build/discovery.js:2 -msgid "Custom" +#: includes/ai/settings/tab-general.php:38 +msgid "Token usage tracking is unavailable when structured output is active (shows 0 in logs)." msgstr "" -#: js/build/discovery.js:2 -msgid "Su" +#: includes/ai/settings/tab-general.php:39 +msgid "If the provider does not support native schema enforcement, the system will automatically fall back to text parsing." msgstr "" -#: js/build/discovery.js:2 -msgid "Mo" +#: includes/ai/settings/tab-general.php:40 +msgid "Some internal tools (e.g. prompt generator test connection) automatically bypass structured output." msgstr "" -#: js/build/discovery.js:2 -msgid "Tu" +#: includes/ai/settings/tab-general.php:45 +msgid "Recommendations by provider:" msgstr "" -#: js/build/discovery.js:2 -msgid "We" +#: includes/ai/settings/tab-general.php:47 +msgid "Full support. Recommended to keep this option enabled." msgstr "" -#: js/build/discovery.js:2 -msgid "Th" +#: includes/ai/settings/tab-general.php:48 +msgid "Supported via NeuronAI. Test with your model to confirm." msgstr "" -#: js/build/discovery.js:2 -msgid "Fr" +#: includes/ai/settings/tab-general.php:49 +msgid "Variable support depending on model and local configuration. Disable if you encounter errors." msgstr "" -#: js/build/discovery.js:2 -msgid "Sa" +#: includes/ai/settings/tab-general.php:56 +msgid "Embedded Data" msgstr "" -#. translators: %d is the number of stories currently displayed. -#: js/build/discovery.js:3 -#, js-format -msgid "Displayed stories: %d" +#: includes/ai/settings/tab-general.php:59 +msgid "View locally embedded geographic dictionaries used to improve AI precision. This feature is experimental and not yet fully integrated." msgstr "" -#. translators: %s is the active story search query. -#: js/build/discovery.js:4 -#, js-format -msgid "Search story: %s" +#: includes/ai/settings/tab-general.php:63 +msgid "View Embedded Data" msgstr "" -#. translators: %s is the active story date range. -#: js/build/discovery.js:5 -#, js-format -msgid "Date range: %s" +#: includes/ai/settings/tab-knowledge.php:17 +msgid "AI Provider Required" msgstr "" -#. translators: %s is the list of active story tags. -#: js/build/discovery.js:6 -#, js-format -msgid "Tags: %s" +#: includes/ai/settings/tab-knowledge.php:19 +msgid "You need to configure an AI Provider before you can use the Knowledge Base. Set up your API key and choose a model in the Provider tab first." msgstr "" -#: js/build/discovery.js:6 -msgid "Loading…" +#: includes/ai/settings/tab-knowledge.php:22 +msgid "Configure AI Provider" msgstr "" -#: js/build/discovery.js:6 -msgid "No stories found for the current filters." +#: includes/ai/settings/tab-knowledge.php:35 +msgid "RAG Knowledge Base (Vector Store)" msgstr "" -#: js/build/discovery.js:6 -msgid "No stories available." +#: includes/ai/settings/tab-knowledge.php:36 +msgid "Vectorize your WordPress posts to allow the JEO AI to contextually answer questions and cross-reference territorial data." msgstr "" -#: js/build/discovery.js:6 -msgid "Search story" +#: includes/ai/settings/tab-knowledge.php:41 +msgid "Embedding Model" msgstr "" -#: js/build/discovery.js:6 -msgid "Hide filters" +#: includes/ai/settings/tab-knowledge.php:57 +msgid "Model selection is locked." msgstr "" -#: js/build/discovery.js:6 -msgid "Show filters" +#. translators: %s: locked embedding model name +#: includes/ai/settings/tab-knowledge.php:62 +#, php-format +msgid "This Vector Store was initialized with %s. To use a different model, you must clear the current store first." msgstr "" -#: js/build/discovery.js:6 -msgid "Date range" +#: includes/ai/settings/tab-knowledge.php:70 +msgid "Select an embedding model..." msgstr "" -#: js/build/discovery.js:6 -msgid "Updating stories…" +#: includes/ai/settings/tab-knowledge.php:98 +msgid "Select the specific embedding model. You can mix providers (e.g. use Gemini for chat and OpenAI for embeddings)." msgstr "" -#: js/build/discovery.js:6 -msgid "Active filters" +#: includes/ai/settings/tab-knowledge.php:110 +msgid "RAG Knowledge Base is not available" msgstr "" -#: js/build/discovery.js:6 -msgid "View in map" +#: includes/ai/settings/tab-knowledge.php:115 +msgid "How to fix this:" msgstr "" -#: js/build/discovery.js:6 -msgid "layers" +#: includes/ai/settings/tab-knowledge.php:118 +msgid "Please select an Embedding Model from the dropdown above and click \"Save AI Settings\"." msgstr "" -#: js/build/discovery.js:6 -msgid "Unable to load map layers right now." +#: includes/ai/settings/tab-knowledge.php:120 +msgid "Ensure you have an active AI Provider (Gemini, OpenAI, or Ollama) configured in the \"AI Provider\" tab." msgstr "" -#: js/build/discovery.js:6 -msgid "Mapbox style composition is unavailable." +#: includes/ai/settings/tab-knowledge.php:121 +msgid "Check if the \"wp-content/uploads\" directory exists and is writable." msgstr "" -#: js/build/discovery.js:6 -#: js/build/jeoMap.js:1 -msgid "Mapbox style composition is unavailable for this map." +#: includes/ai/settings/tab-knowledge.php:126 +msgid "Go to AI Provider Settings" msgstr "" -#: js/build/discovery.js:6 -msgid "Search map" +#: includes/ai/settings/tab-knowledge.php:134 +msgid "Background Indexing" msgstr "" -#: js/build/discovery.js:6 -msgid "Changes applied" +#: includes/ai/settings/tab-knowledge.php:135 +msgid "Automatically vectorize your posts in small batches using WP-Cron." msgstr "" -#: js/build/discovery.js:6 -msgid "Not applied" +#: includes/ai/settings/tab-knowledge.php:140 +msgid "Enable Auto-indexing" msgstr "" -#: js/build/discovery.js:6 -#: js/build/mapBlocks.js:3 -#: js/build/mapsSidebar.js:1 -msgid "Selected layers" +#: includes/ai/settings/tab-knowledge.php:143 +msgid "Note: WP-Cron only runs when your site receives visitor traffic. If you are on a local development server with no traffic, indexing will pause until pages are loaded." msgstr "" -#: js/build/discovery.js:6 -msgid "No layers selected" +#: includes/ai/settings/tab-knowledge.php:162 +msgid "Search Results (topK)" msgstr "" -#: js/build/discovery.js:6 -msgid "Apply changes" +#: includes/ai/settings/tab-knowledge.php:164 +msgid "Max semantic matches returned per search." msgstr "" -#: js/build/discovery.js:6 -msgid "Stories" +#: includes/ai/settings/tab-knowledge.php:169 +msgid "Recent Background Logs" msgstr "" -#: js/build/discovery.js:6 -#: js/build/mapBlocks.js:3 -#: js/build/mapsSidebar.js:1 -msgid "Map layers" +#: includes/ai/settings/tab-knowledge.php:175 +msgid "No indexing jobs have run recently." msgstr "" -#: js/build/discovery.js:6 -msgid "Collapse discovery panel" +#: includes/ai/settings/tab-knowledge.php:192 +msgid "Manual Indexing" msgstr "" -#: js/build/discovery.js:6 -msgid "Expand discovery panel" +#: includes/ai/settings/tab-knowledge.php:193 +msgid "Trigger a single batch vectorization immediately." msgstr "" -#: js/build/discovery.js:6 -msgid "Unable to load map information right now." +#: includes/ai/settings/tab-knowledge.php:196 +#: includes/ai/settings/tab-knowledge.php:319 +msgid "Vectorize 1 Batch Now" msgstr "" -#: js/build/discovery.js:6 -msgid "Map information" +#: includes/ai/settings/tab-knowledge.php:228 +msgid "Indexing Progress" msgstr "" -#: js/build/discovery.js:6 -msgctxt "discovery share action" -msgid "Embed" +#. translators: %1$d: vectorized count, %2$d: total posts count +#: includes/ai/settings/tab-knowledge.php:237 +#, php-format +msgid "%1$d of %2$d posts indexed." msgstr "" -#: js/build/discovery.js:6 -#: js/build/jeoMap.js:1 -#: js/build/layersSidebar.js:11 -msgid "Legend" +#: includes/ai/settings/tab-knowledge.php:246 +#: includes/ai/settings/tab-knowledge.php:403 +msgid "Status:" msgstr "" -#: js/build/discovery.js:6 -#: js/build/jeoMap.js:1 -msgid "Info" +#: includes/ai/settings/tab-knowledge.php:248 +msgid "Active (Local File Store)" msgstr "" -#: js/build/discovery.js:6 -#: js/build/jeoMap.js:1 -msgid "Close popup" +#: includes/ai/settings/tab-knowledge.php:252 +msgid "Alternative: Use WP-CLI for large databases:" msgstr "" -#: js/build/jeoMap.js:1 -msgid "Mapbox style composition did not return renderable layers for this map." +#: includes/ai/settings/tab-knowledge.php:257 +msgid "Backup Store" msgstr "" -#: js/build/jeoMap.js:1 -msgid "Unable to load composed Mapbox style for this map." +#: includes/ai/settings/tab-knowledge.php:260 +msgid "Clear & Reset Model" msgstr "" -#: js/build/jeoMap.js:1 -msgid "Mapbox style composition is unavailable for this one-time map." +#: includes/ai/settings/tab-knowledge.php:266 +msgid "Available Backups (Last 3)" msgstr "" -#: js/build/jeoMap.js:1 -msgid "Mapbox style composition did not return renderable layers for this one-time map." +#: includes/ai/settings/tab-knowledge.php:268 +#: includes/settings/class-settings.php:605 +msgid "No backups found." msgstr "" -#: js/build/jeoMap.js:1 -msgid "Unable to load composed Mapbox style for this one-time map." +#: includes/ai/settings/tab-knowledge.php:275 +msgid "Test Embeddings & RAG Retrieval" msgstr "" -#: js/build/jeoMap.js:1 -#: js/build/jeoStorymap.js:3 -msgid "Mapbox style layers could not be loaded because the composed style is unavailable." +#: includes/ai/settings/tab-knowledge.php:276 +msgid "Test your configured Embedding Model on a random post to ensure it works correctly before indexing your entire database. This uses a temporary testing store." msgstr "" -#: js/build/jeoMap.js:1 -msgid "This map doesn't have layers" +#: includes/ai/settings/tab-knowledge.php:279 +#: includes/settings/class-settings.php:616 +msgid "Run Test on Random Post" msgstr "" -#: js/build/jeoMap.js:1 -msgid "Show previous post" +#: includes/ai/settings/tab-knowledge.php:280 +msgid "Test Vector Retrieval" msgstr "" -#: js/build/jeoMap.js:1 -msgid "Show next post" +#: includes/ai/settings/tab-knowledge.php:288 +msgid "Test Local Vector Retrieval" msgstr "" -#. translators: %d: number of markers in the current map area. -#: js/build/jeoMap.js:2 -#, js-format -msgid "%d marker in this area" -msgid_plural "%d markers in this area" -msgstr[0] "" -msgstr[1] "" +#: includes/ai/settings/tab-knowledge.php:295 +msgid "Search Production Store" +msgstr "" -#. translators: %s is the authors list -#: js/build/jeoStorymap.js:3 -#, js-format -msgid "By %s" +#: includes/ai/settings/tab-knowledge.php:296 +msgid "Search Test Store" msgstr "" -#: js/build/jeoStorymap.js:3 -msgid "Mapbox style composition is unavailable for this storymap." +#: includes/ai/settings/tab-knowledge.php:298 +msgid "Search the Vector Store for semantic meaning (e.g., \"Indigenous lands in Amazon\")" msgstr "" -#: js/build/jeoStorymap.js:3 -msgid "Mapbox style composition did not return renderable layers for this storymap." +#: includes/ai/settings/tab-knowledge.php:299 +#: includes/settings/class-settings.php:642 +#: js/build/mapBlocks.js:1 +#: js/build/postsSidebar.js:1 +msgid "Search" msgstr "" -#: js/build/jeoStorymap.js:3 -msgid "Unable to load composed Mapbox style for this storymap." +#: includes/ai/settings/tab-knowledge.php:302 +msgid "Enter a semantic search query to retrieve similar vectors from your local store." msgstr "" -#: js/build/jeoStorymap.js:3 -msgid "at" +#: includes/ai/settings/tab-knowledge.php:310 +msgid "Layer Store (Vectorized Map Layers)" msgstr "" -#: js/build/jeoStorymap.js:3 -msgid "START" +#: includes/ai/settings/tab-knowledge.php:311 +msgid "Vectorize your map layers to enable AI-powered semantic search and layer suggestions based on post content." msgstr "" -#: js/build/jeoStorymap.js:3 -msgid "skip intro" +#: includes/ai/settings/tab-knowledge.php:315 +msgid "Manual Layer Indexing" msgstr "" -#: js/build/jeoStorymap.js:3 -msgid "Back to top" +#: includes/ai/settings/tab-knowledge.php:316 +msgid "Trigger a single batch vectorization for map layers. Layers are also auto-indexed on the background cron schedule and when saved." msgstr "" -#: js/build/jeoStorymap.js:3 -msgid "NAVIGATE THE MAP" +#: includes/ai/settings/tab-knowledge.php:344 +msgid "Layer Indexing Progress" msgstr "" -#: js/build/layersSidebar.js:7 -msgid "Select an option" +#. translators: %1$d: vectorized count, %2$d: total layers count +#: includes/ai/settings/tab-knowledge.php:353 +#, php-format +msgid "%1$d of %2$d layers indexed." msgstr "" -#: js/build/layersSidebar.js:7 -msgid "Source data download link" +#: includes/ai/settings/tab-knowledge.php:359 +msgid "Recent Layer Logs" msgstr "" -#: js/build/layersSidebar.js:7 -msgid "Data attribution link" +#: includes/ai/settings/tab-knowledge.php:365 +msgid "No layer indexing jobs have run recently." msgstr "" -#: js/build/layersSidebar.js:7 -msgid "Data attribution name" +#: includes/ai/settings/tab-knowledge.php:382 +msgid "Suggest Layers" msgstr "" -#: js/build/layersSidebar.js:7 -msgid "Circles color" +#: includes/ai/settings/tab-knowledge.php:383 +msgid "Test semantic layer matching. Enter a post ID or text to find layers that match the content." msgstr "" -#: js/build/layersSidebar.js:7 -msgid "Add new label" +#: includes/ai/settings/tab-knowledge.php:387 +msgid "Post ID" msgstr "" -#: js/build/layersSidebar.js:7 -msgid "Label" +#: includes/ai/settings/tab-knowledge.php:388 +msgid "e.g. 123" msgstr "" -#: js/build/layersSidebar.js:7 -msgid "Radius" +#: includes/ai/settings/tab-knowledge.php:391 +msgid "Or search text" msgstr "" -#: js/build/layersSidebar.js:7 -#: js/build/mapBlocks.js:1 -#: js/build/mapBlocks.js:3 -#: js/build/mapsSidebar.js:1 -msgid "Remove" +#: includes/ai/settings/tab-knowledge.php:392 +msgid "e.g., deforestation in Amazon rainforest" msgstr "" -#: js/build/layersSidebar.js:7 -msgid "See preview" +#: includes/ai/settings/tab-knowledge.php:394 +msgid "Find Layers" msgstr "" -#: js/build/layersSidebar.js:7 -#: js/build/mapBlocks.js:1 -msgid "Preview" +#: includes/ai/settings/tab-knowledge.php:405 +msgid "Active (Shared embedding model with Posts Store)" msgstr "" -#: js/build/layersSidebar.js:7 -msgid "Left Label" +#: includes/ai/settings/tab-knowledge.php:409 +msgid "WP-CLI:" msgstr "" -#: js/build/layersSidebar.js:7 -msgid "Right Label" +#: includes/ai/settings/tab-knowledge.php:414 +msgid "Clear Layer Store" msgstr "" -#: js/build/layersSidebar.js:7 -msgid "Minimum size: 60x60 pixels" +#: includes/ai/settings/tab-provider.php:27 +#: includes/layer-types/mvt.js:111 +#: includes/layer-types/tilelayer.js:61 +#: includes/legend-types/circles.js:25 +#: includes/legend-types/icons.js:25 +#: js/build/layersSidebar.js:11 +#: js/build/mapBlocks.js:1 +msgid "URL" msgstr "" -#: js/build/layersSidebar.js:7 -msgid "Icon legend" +#: includes/ai/settings/tab-provider.php:27 +msgid "API Key" msgstr "" -#: js/build/layersSidebar.js:7 -msgid "The uploaded icon is too small. The minimum size required is 60x60 pixels." +#: includes/ai/settings/tab-provider.php:53 +msgid "Paste your API key here..." msgstr "" -#: js/build/layersSidebar.js:7 -msgid "Ok" +#: includes/ai/settings/tab-provider.php:58 +msgid "Set New Key" msgstr "" -#: js/build/layersSidebar.js:7 -#: js/build/mapBlocks.js:3 -#: js/build/mapsSidebar.js:1 -msgid "Type" +#: includes/ai/settings/tab-provider.php:63 +msgid "Status: Unknown" msgstr "" -#: js/build/layersSidebar.js:7 -msgid "No" +#: includes/ai/settings/tab-provider.php:66 +msgid "Test Connection" msgstr "" -#: js/build/layersSidebar.js:7 -msgid "On click" +#: includes/ai/settings/tab-provider.php:73 +msgid "Model" msgstr "" -#: js/build/layersSidebar.js:7 -msgid "On hover" +#: includes/ai/settings/tab-provider.php:79 +#: includes/settings/class-settings.php:581 +msgid "Change Model" msgstr "" -#: js/build/layersSidebar.js:7 -msgid "Show popup?" +#: includes/ai/settings/tab-provider.php:87 +msgid "System Prompt Configuration" msgstr "" -#: js/build/layersSidebar.js:7 -#: js/build/mapBlocks.js:1 -msgid "Title" +#: includes/ai/settings/tab-provider.php:91 +msgid "Check this to override the default behavior. Uncheck to temporarily disable and return to the optimized default prompt." msgstr "" -#: js/build/layersSidebar.js:7 -msgid "Fields" +#: includes/ai/settings/tab-provider.php:95 +msgid "Include post categories and tags in the AI georeferencing request to improve location accuracy." msgstr "" -#: js/build/layersSidebar.js:7 -msgid "Add field" +#: includes/ai/settings/tab-provider.php:100 +#: includes/ai/settings/tab-provider.php:280 +#: includes/settings/class-settings.php:631 +msgid "Back" msgstr "" -#: js/build/layersSidebar.js:7 -#: js/build/mapBlocks.js:1 -#: js/build/mapBlocks.js:3 -#: js/build/mapsSidebar.js:1 -msgid "Add" +#: includes/ai/settings/tab-provider.php:103 +#: includes/ai/settings/tab-provider.php:282 +#: includes/settings/class-settings.php:629 +#: js/build/contextSidebar.js:1 +msgid "Expand" msgstr "" -#: js/build/layersSidebar.js:7 -msgid "Interactions" +#: includes/ai/settings/tab-provider.php:104 +msgid "Validate Custom Prompt" msgstr "" -#: js/build/layersSidebar.js:7 -#: js/build/mapBlocks.js:3 -#: js/build/mapsSidebar.js:1 -msgid "Done" +#: includes/ai/settings/tab-provider.php:105 +msgid "Clear Text" msgstr "" -#. translators: %d: number of maps whose composed style cache was refreshed. -#: js/build/layersSidebar.js:8 -#, js-format -msgid "%d map" -msgid_plural "%d maps" -msgstr[0] "" -msgstr[1] "" +#: includes/ai/settings/tab-provider.php:116 +msgid "AI Prompt Engineer Assistant" +msgstr "" -#. translators: %d: number of map refresh failures. -#: js/build/layersSidebar.js:9 -#, js-format -msgid "%d failure" -msgid_plural "%d failures" -msgstr[0] "" -msgstr[1] "" +#: includes/ai/settings/tab-provider.php:117 +msgid "Describe how you want the AI to behave (e.g., \"Only map cities in Brazil\" or \"Ignore street names\"). The active LLM will generate a highly optimized System Prompt for you, strictly adhering to JEO formatting rules." +msgstr "" -#. translators: 1: refreshed maps count label, 2: failed refresh count label. -#: js/build/layersSidebar.js:10 -#, js-format -msgid "Composed style cache refreshed for %1$s, with %2$s." +#: includes/ai/settings/tab-provider.php:279 +msgid "Ex: I want to map only locations inside Europe. Press Shift+Enter for new line, or Enter to generate." msgstr "" -#. translators: %s: refreshed maps count label. -#: js/build/layersSidebar.js:11 -#, js-format -msgid "Composed style cache refreshed for %s." +#: includes/ai/settings/tab-provider.php:283 +msgid "Shift+Enter for new line" msgstr "" -#: js/build/layersSidebar.js:11 -msgid "Unable to refresh composed style cache." +#: includes/ai/settings/tab-provider.php:288 +msgid "Georeferencing Calibration" msgstr "" -#: js/build/layersSidebar.js:11 -msgid "Refreshing composed style cache" +#: includes/ai/settings/tab-provider.php:292 +#: includes/ai/settings/tab-provider.php:304 +#: includes/ai/settings/tab-provider.php:313 +#: includes/ai/settings/tab-provider.php:322 +#: includes/ai/settings/tab-provider.php:331 +#: includes/ai/settings/tab-provider.php:340 +#: includes/ai/settings/tab-provider.php:349 +#: includes/ai/settings/tab-provider.php:358 +msgid "Use" msgstr "" -#: js/build/layersSidebar.js:11 -msgid "Refresh composed style cache" +#: includes/ai/settings/tab-provider.php:294 +msgid "Location Granularity" msgstr "" -#: js/build/layersSidebar.js:11 -msgid "Edit interactions" +#: includes/ai/settings/tab-provider.php:296 +msgid "Broad (countries, regions, cities)" msgstr "" -#: js/build/layersSidebar.js:11 -msgid "Please fill all required fields, you will not be able to publish or update until that." +#: includes/ai/settings/tab-provider.php:297 +msgid "Balanced (cities + neighborhoods)" msgstr "" -#: js/build/layersSidebar.js:11 -msgid "Your Mapbox access token may be invalid. You will not be able to publish or update. Please check your settings." +#: includes/ai/settings/tab-provider.php:298 +msgid "Fine (streets, landmarks, POIs)" msgstr "" -#: js/build/layersSidebar.js:11 +#: includes/ai/settings/tab-provider.php:306 +msgid "Minimum Confidence" +msgstr "" + +#: includes/ai/settings/tab-provider.php:315 +msgid "Primary Threshold" +msgstr "" + +#: includes/ai/settings/tab-provider.php:324 +msgid "Primary Points" +msgstr "" + +#: includes/ai/settings/tab-provider.php:333 +msgid "Secondary Threshold" +msgstr "" + +#: includes/ai/settings/tab-provider.php:342 +msgid "Secondary Points" +msgstr "" + +#: includes/ai/settings/tab-provider.php:351 +msgid "Prioritize Title Mentions" +msgstr "" + +#: includes/ai/settings/tab-provider.php:360 +msgid "Prompt Token Budget" +msgstr "" + +#: includes/ai/settings/tab-provider.php:366 +msgid "Higher budgets allow the AI to process longer articles but increase cost and latency." +msgstr "" + +#: includes/ai/settings/tab-provider.php:373 +msgid "Balanced" +msgstr "" + +#: includes/ai/settings/tab-provider.php:377 +msgid "No token limit set. The LLM will use as many tokens as it deems necessary to produce the result." +msgstr "" + +#: includes/ai/settings/tab-provider.php:384 +msgid "Output Language" +msgstr "" + +#: includes/ai/settings/tab-provider.php:386 +msgid "English (Optimized)" +msgstr "" + +#. translators: %s: site language +#: includes/ai/settings/tab-provider.php:387 +#, php-format +msgid "Site Language (%s)" +msgstr "" + +#: includes/ai/settings/tab-provider.php:390 +msgid "LLMs usually follow English instructions with higher precision and lower latency. We recommend English for complex rules, even if your posts are in other languages." +msgstr "" + +#: includes/ai/settings/tab-provider.php:395 +#: includes/settings/class-settings.php:593 +msgid "Generate Prompt" +msgstr "" + +#: includes/class-ai-adapter.php:165 +msgid "Empty response from AI." +msgstr "" + +#: includes/class-ai-handler.php:48 +msgid "Warning: Deactivating JEO will permanently remove all API Keys and AI settings for security reasons. Do you want to proceed?" +msgstr "" + +#: includes/class-ai-handler.php:67 +#: includes/settings/ai-logs-page.php:16 +#: includes/settings/class-settings.php:657 +msgid "Unauthorized access." +msgstr "" + +#: includes/class-ai-handler.php:74 +#: includes/settings/class-settings.php:658 +msgid "Invalid file." +msgstr "" + +#: includes/class-ai-handler.php:79 +#: includes/settings/class-settings.php:659 +msgid "File not found." +msgstr "" + +#: includes/class-ai-handler.php:106 +msgid "Google Gemini" +msgstr "" + +#: includes/class-ai-handler.php:107 +msgid "OpenAI" +msgstr "" + +#: includes/class-ai-handler.php:108 +msgid "DeepSeek" +msgstr "" + +#: includes/class-ai-handler.php:109 +msgid "Anthropic Claude" +msgstr "" + +#: includes/class-ai-handler.php:110 +msgid "Ollama (Local/Custom)" +msgstr "" + +#: includes/class-ai-handler.php:111 +msgid "Mistral AI" +msgstr "" + +#: includes/class-ai-handler.php:112 +msgid "Zhipu AI (GLM)" +msgstr "" + +#: includes/class-ai-handler.php:113 +msgid "HuggingFace Inference" +msgstr "" + +#: includes/class-ai-handler.php:114 +msgid "Grok (xAI)" +msgstr "" + +#: includes/class-ai-handler.php:115 +msgid "Cohere" +msgstr "" + +#. translators: %s: AI provider name +#: includes/class-ai-handler.php:169 +#, php-format +msgid "No API key configured for %s. Please add it in JEO → AI Settings." +msgstr "" + +#: includes/class-ai-handler.php:422 +msgid "Invalid store type." +msgstr "" + +#. translators: %s: Store type name. +#: includes/class-ai-handler.php:448 +#, php-format +msgid "%s store cleared successfully." +msgstr "" + +#: includes/class-ai-handler.php:491 +msgid "Layer store cleared successfully." +msgstr "" + +#: includes/class-ai-handler.php:518 +msgid "Query is required." +msgstr "" + +#: includes/class-ai-handler.php:539 +msgid "No matches found in the selected Vector Store. Try vectorizing some posts first." +msgstr "" + +#: includes/class-ai-handler.php:593 +msgid "No posts available to test." +msgstr "" + +#: includes/class-ai-handler.php:601 +msgid "The selected random post has no usable text content." +msgstr "" + +#: includes/class-ai-handler.php:617 +msgid "The embedding provider returned an empty vector." +msgstr "" + +#: includes/class-ai-handler.php:632 +msgid "Embedding created and saved to Test Store!" +msgstr "" + +#: includes/class-ai-handler.php:673 +msgid "API Key or URL is required to list models." +msgstr "" + +#: includes/class-ai-handler.php:706 +msgid "Model fetching not officially supported dynamically for Zhipu AI." +msgstr "" + +#: includes/class-ai-handler.php:862 +msgid "Model fetching is not supported for this provider yet. Please enter the model ID manually." +msgstr "" + +#: includes/class-ai-handler.php:870 +msgid "No models found. Check your API key or permissions." +msgstr "" + +#: includes/class-ai-handler.php:906 +msgid "No API Key provided or found in settings." +msgstr "" + +#: includes/class-ai-handler.php:916 +msgid "Invalid provider." +msgstr "" + +#: includes/class-ai-handler.php:939 +msgid "API Key is valid and active!" +msgstr "" + +#: includes/class-ai-handler.php:949 +msgid "The AI provider returned an unexpected response format." +msgstr "" + +#: includes/class-ai-handler.php:994 +msgid "Context is required." +msgstr "" + +#: includes/class-ai-handler.php:1026 +#: includes/class-ai-handler.php:1186 +#: includes/class-ai-handler.php:1274 +#: includes/class-ai-handler.php:1323 +msgid "No active AI adapter found." +msgstr "" + +#: includes/class-ai-handler.php:1142 +msgid "AI failed to generate prompt." +msgstr "" + +#: includes/class-ai-handler.php:1208 +msgid "Prompt successfully validated! The AI understood your instructions and returned a valid JSON array." +msgstr "" + +#: includes/class-ai-handler.php:1212 +msgid "Validation failed: The AI did not return a valid JSON array." +msgstr "" + +#: includes/class-ai-handler.php:1217 +msgid "Validation failed: The AI missed mandatory keys (name, lat, lon, quote) in its JSON objects." +msgstr "" + +#: includes/class-ai-handler.php:1222 +msgid "Validation failed: The AI returned an invalid is_primary value (must be boolean)." +msgstr "" + +#: includes/class-ai-handler.php:1228 +msgid "Prompt successfully validated! The AI returned an empty array, which means your filtering rules worked perfectly for the test text." +msgstr "" + +#: includes/class-ai-handler.php:1287 +msgid "An unexpected error occurred while processing the AI request." +msgstr "" + +#: includes/class-ai-handler.php:1356 +msgid "An unexpected error occurred while processing the AI chat request." +msgstr "" + +#: includes/class-ai-handler.php:1485 +msgid "Backup created successfully!" +msgstr "" + +#: includes/class-ai-handler.php:1506 +msgid "Missing filename" +msgstr "" + +#: includes/class-ai-handler.php:1517 +msgid "File not found" +msgstr "" + +#: includes/class-jeo.php:672 +msgid "Ordered list of map-layer IDs to return." +msgstr "" + +#: includes/class-jeo.php:677 +msgid "Request context." +msgstr "" + +#: includes/class-jeo.php:931 +msgid "Mapbox was selected as the rendering library, but no Mapbox API key is configured. Falling back to MapLibre." +msgstr "" + +#: includes/class-jeo.php:932 +msgid "Mapbox was selected as the rendering library, but the external Mapbox SDK could not be loaded. Falling back to MapLibre." +msgstr "" + +#: includes/class-jeo.php:1470 +#: js/build/mapBlocks.js:3 +msgid "Read more" +msgstr "" + +#: includes/class-jeo.php:1711 +msgid "This JEO embed is not available." +msgstr "" + +#: includes/class-jeo.php:1712 +msgid "Embed not available" +msgstr "" + +#: includes/class-jeo.php:2062 +msgid "A Mapbox style ID and access token are required." +msgstr "" + +#: includes/class-jeo.php:2073 +#: includes/class-jeo.php:2111 +msgid "Unexpected Mapbox style payload." +msgstr "" + +#: includes/class-jeo.php:2169 +msgid "A valid http(s) style URL is required." +msgstr "" + +#: includes/class-jeo.php:2184 +#: includes/class-jeo.php:2217 +msgid "Unexpected style JSON payload." +msgstr "" + +#: includes/cli/class-ai-cli.php:58 +msgid "Classifying prompt..." +msgstr "" + +#. translators: 1: layer title, 2: post ID. +#: includes/cli/class-ai-cli.php:68 +#, php-format +msgid "Layer created: %1$s (ID %2$d)" +msgstr "" + +#: includes/cli/class-ai-cli.php:107 +msgid "A place name is required." +msgstr "" + +#: includes/cli/class-ai-cli.php:115 +msgid "Resolving boundary polygon..." +msgstr "" + +#. translators: 1: layer title, 2: post ID. +#: includes/cli/class-ai-cli.php:126 +#, php-format +msgid "Boundary layer created: %1$s (ID %2$d)" +msgstr "" + +#: includes/cli/class-ai-cli.php:161 +msgid "Classifying prompt (no layer will be created)..." +msgstr "" + +#: includes/geocode/class-geocode-handler.php:123 +msgid "Geocoding service provided by OpenStreetMap at https://nominatim.openstreetmap.org/." +msgstr "" + +#: includes/geocode/class-geocode-handler.php:132 +msgid "Service provided by Mapbox (requires API Key)." +msgstr "" + +#: includes/geocode/class-geocode-handler.php:249 +msgid "Metadata storing the locations related to the post. Each location is an object with latitude, longitude, and geocode attributes." +msgstr "" + +#: includes/geocode/class-geocode-handler.php:296 +msgid "Country" +msgstr "" + +#: includes/geocode/class-geocode-handler.php:310 +msgid "Country code" +msgstr "" + +#: includes/geocode/class-geocode-handler.php:324 +msgid "Region level 1" +msgstr "" + +#: includes/geocode/class-geocode-handler.php:338 +msgid "Region level 2" +msgstr "" + +#: includes/geocode/class-geocode-handler.php:352 +msgid "Region level 3" +msgstr "" + +#: includes/geocode/class-geocode-handler.php:366 +msgid "City" +msgstr "" + +#: includes/geocode/class-geocode-handler.php:380 +msgid "City sub-level 1" +msgstr "" + +#: includes/layers/class-layers.php:49 +#: includes/layers/class-layers.php:60 +#: js/build/jeoMap.js:2 +#: js/build/mapBlocks.js:1 +msgid "Layers" +msgstr "" + +#: includes/layers/class-layers.php:50 +msgid "Layer" +msgstr "" + +#: includes/layers/class-layers.php:51 +#: includes/layers/class-layers.php:52 +msgid "Add new layer" +msgstr "" + +#: includes/layers/class-layers.php:53 +msgid "Edit layer" +msgstr "" + +#: includes/layers/class-layers.php:54 +msgid "New layer" +msgstr "" + +#: includes/layers/class-layers.php:55 +msgid "View layer" +msgstr "" + +#: includes/layers/class-layers.php:56 +msgid "View layers" +msgstr "" + +#: includes/layers/class-layers.php:57 +msgid "Search layers" +msgstr "" + +#: includes/layers/class-layers.php:58 +msgid "No layer found" +msgstr "" + +#: includes/layers/class-layers.php:59 +msgid "No layer found in the trash" +msgstr "" + +#: includes/layers/class-layers.php:61 +msgid "Layer published." +msgstr "" + +#: includes/layers/class-layers.php:62 +msgid "Layer published privately." +msgstr "" + +#: includes/layers/class-layers.php:63 +msgid "Layer reverted to draft." +msgstr "" + +#: includes/layers/class-layers.php:64 +msgid "Layer scheduled." +msgstr "" + +#: includes/layers/class-layers.php:65 +msgid "Layer updated." +msgstr "" + +#: includes/layers/class-layers.php:71 +msgid "JEO Layers" +msgstr "" + +#: includes/layers/class-layers.php:101 +msgid "Layer Themes" +msgstr "" + +#: includes/layers/class-layers.php:102 +msgid "Layer Theme" +msgstr "" + +#: includes/layers/class-layers.php:103 +msgid "Add New Layer Theme" +msgstr "" + +#: includes/layers/class-layers.php:104 +msgid "Edit Layer Theme" +msgstr "" + +#: includes/layers/class-layers.php:131 +msgid "The layer type" +msgstr "" + +#: includes/layers/class-layers.php:143 +msgid "Layer attribution as text or HTML with a link" +msgstr "" + +#: includes/layers/class-layers.php:160 +msgid "Layer type-specific options" +msgstr "" + +#: includes/layers/class-layers.php:172 +msgid "The URL to download the source data of the layer" +msgstr "" + +#: includes/layers/class-layers.php:184 +msgid "Label for the attribution URL link" +msgstr "" + +#: includes/layers/class-layers.php:196 +msgid "The legend type" +msgstr "" + +#: includes/layers/class-layers.php:213 +msgid "Legend type-specific options" +msgstr "" + +#: includes/layers/class-layers.php:225 +#: js/build/layersSidebar.js:7 +msgid "Use legend" +msgstr "" + +#: includes/layers/class-layers.php:237 +#: js/build/layersSidebar.js:7 +msgid "Legend title" +msgstr "" + +#: includes/layers/class-layers.php:254 +msgid "AI-suggested default style (filter/paint/layout) applied when a map layer instance sets style.use_default" +msgstr "" + +#: includes/layers/class-layers.php:266 +msgid "Deforestation" +msgstr "" + +#: includes/layers/class-layers.php:267 +msgid "Hydrography" +msgstr "" + +#: includes/layers/class-layers.php:268 +msgid "Indigenous Lands" +msgstr "" + +#: includes/layers/class-layers.php:269 +msgid "Protected Areas" +msgstr "" + +#: includes/layers/class-layers.php:270 +msgid "Mining" +msgstr "" + +#: includes/layers/class-layers.php:271 +msgid "Oil and Gas" +msgstr "" + +#: includes/layers/class-layers.php:272 +msgid "Land Use" +msgstr "" + +#: includes/layers/class-layers.php:273 +msgid "Agriculture" +msgstr "" + +#: includes/layers/class-layers.php:274 +msgid "Infrastructure" +msgstr "" + +#: includes/layers/class-layers.php:275 +msgid "Administrative Boundaries" +msgstr "" + +#: includes/layers/class-layers.php:276 +msgid "Socioeconomic" +msgstr "" + +#: includes/layers/class-layers.php:277 +msgid "Biodiversity" +msgstr "" + +#: includes/layers/class-layers.php:278 +msgid "Fire" +msgstr "" + +#: includes/layers/class-layers.php:279 +msgid "Climate" +msgstr "" + +#: includes/layers/class-layers.php:335 +msgid "Layer type not registered" +msgstr "" + +#. translators: Explore is the name of JEO's discovery page feature. +#: includes/loaders.php:591 +#: templates/embed-discovery.php:18 +#: js/build/discovery.js:6 +msgid "Explore" +msgstr "" + +#: includes/maps/class-map-style-composer.php:631 +#: includes/maps/class-map-style-composer.php:700 +msgid "Mapbox composed styles are disabled." +msgstr "" + +#: includes/maps/class-map-style-composer.php:656 +#: includes/maps/class-map-style-composer.php:724 +msgid "Could not create the Mapbox composed style cache directory." +msgstr "" + +#: includes/maps/class-map-style-composer.php:827 +msgid "Map not found." +msgstr "" + +#: includes/maps/class-map-style-composer.php:837 +msgid "A Mapbox access token is required to compose Mapbox styles." +msgstr "" + +#: includes/maps/class-map-style-composer.php:877 +msgid "Public payload composition is only available for one-time maps." +msgstr "" + +#: includes/maps/class-map-style-composer.php:883 +msgid "The map preview payload is too large." +msgstr "" + +#: includes/maps/class-map-style-composer.php:895 +msgid "The map preview has too many layers to compose." +msgstr "" + +#: includes/maps/class-map-style-composer.php:902 +msgid "The map preview does not include any renderable layers." +msgstr "" + +#: includes/maps/class-map-style-composer.php:924 +msgid "Map preview" +msgstr "" + +#: includes/maps/class-map-style-composer.php:1300 +msgid "Invalid composed style hash." +msgstr "" + +#. translators: %d: number of converted style values. +#: includes/maps/class-map-style-composer.php:1387 +#, php-format +msgid "Converted %d legacy Mapbox stop-function value(s) to style-spec expressions for MapLibre compatibility." +msgstr "" + +#. translators: %d: number of converted style values. +#: includes/maps/class-map-style-composer.php:1408 +#, php-format +msgid "Converted %d legacy Mapbox token string(s) to style-spec expressions for MapLibre compatibility." +msgstr "" + +#: includes/maps/class-map-style-composer.php:1456 +msgid "One or more Mapbox styles could not be fetched." +msgstr "" + +#: includes/maps/class-map-style-composer.php:1970 +msgid "The PHP GD extension is required to merge Mapbox sprites." +msgstr "" + +#: includes/maps/class-map-style-composer.php:2095 +msgid "Could not decode a Mapbox sprite image." +msgstr "" + +#: includes/maps/class-map-style-composer.php:3111 +msgid "Composed style artifact was not found." +msgstr "" + +#: includes/maps/class-map-style-composer.php:3137 +msgid "Could not encode the composed style artifact as JSON." +msgstr "" + +#. translators: %s: artifact file name. +#: includes/maps/class-map-style-composer.php:3148 +#, php-format +msgid "Could not write the composed style artifact %s. Check that the uploads directory is writable." +msgstr "" + +#: includes/maps/class-maps.php:54 +#: includes/maps/class-maps.php:65 +msgid "Maps" +msgstr "" + +#: includes/maps/class-maps.php:55 +msgid "Map" +msgstr "" + +#: includes/maps/class-maps.php:56 +#: includes/maps/class-maps.php:57 +msgid "Add new map" +msgstr "" + +#: includes/maps/class-maps.php:58 +msgid "Edit map" +msgstr "" + +#: includes/maps/class-maps.php:59 +msgid "New map" +msgstr "" + +#: includes/maps/class-maps.php:60 +msgid "View map" +msgstr "" + +#: includes/maps/class-maps.php:61 +msgid "View maps" +msgstr "" + +#: includes/maps/class-maps.php:62 +msgid "Search maps" +msgstr "" + +#: includes/maps/class-maps.php:63 +msgid "No map found" +msgstr "" + +#: includes/maps/class-maps.php:64 +msgid "No map found in the trash" +msgstr "" + +#: includes/maps/class-maps.php:66 +msgid "Map published." +msgstr "" + +#: includes/maps/class-maps.php:67 +msgid "Map published privately." +msgstr "" + +#: includes/maps/class-maps.php:68 +msgid "Map reverted to draft." +msgstr "" + +#: includes/maps/class-maps.php:69 +msgid "Map scheduled." +msgstr "" + +#: includes/maps/class-maps.php:70 +#: js/build/mapBlocks.js:1 +msgid "Map updated." +msgstr "" + +#: includes/maps/class-maps.php:112 +msgid "The map initial zoom level" +msgstr "" + +#: includes/maps/class-maps.php:125 +msgid "The map initial latitude" +msgstr "" + +#: includes/maps/class-maps.php:138 +msgid "The map initial longitude" +msgstr "" + +#: includes/maps/class-maps.php:153 +#: includes/maps/class-maps.php:171 +msgid "The Layer ID" +msgstr "" + +#: includes/maps/class-maps.php:157 +msgid "How this layer is used in this map. Can be fixed, switchable or swappable" +msgstr "" + +#: includes/maps/class-maps.php:161 +msgid "If the layer is loaded into the mapbox as a style (setStyle) or layer (addLayer)" +msgstr "" + +#: includes/maps/class-maps.php:165 +msgid "Mapbox style layers" +msgstr "" + +#: includes/maps/class-maps.php:175 +msgid "If the style should render the layer" +msgstr "" + +#: includes/maps/class-maps.php:182 +msgid "Indicates whether this layer is visible by default" +msgstr "" + +#: includes/maps/class-maps.php:186 +msgid "Indicates if the legend of this layer should be displayed or not" +msgstr "" + +#: includes/maps/class-maps.php:190 +msgid "Per-instance paint and layout overrides for vector and GeoJSON layers" +msgstr "" + +#: includes/maps/class-maps.php:194 +msgid "Whether to use the layer default_style meta instead of the instance paint" +msgstr "" + +#: includes/maps/class-maps.php:198 +msgid "Map paint properties" +msgstr "" + +#: includes/maps/class-maps.php:202 +msgid "Map layout properties" +msgstr "" + +#: includes/maps/class-maps.php:206 +msgid "MapLibre filter expression" +msgstr "" + +#: includes/maps/class-maps.php:220 +msgid "The map Layers" +msgstr "" + +#: includes/maps/class-maps.php:233 +#: js/build/mapBlocks.js:1 +msgid "East pan limit" +msgstr "" + +#: includes/maps/class-maps.php:237 +#: js/build/mapBlocks.js:1 +msgid "North pan limit" +msgstr "" + +#: includes/maps/class-maps.php:241 +#: js/build/mapBlocks.js:1 +msgid "South pan limit" +msgstr "" + +#: includes/maps/class-maps.php:245 +#: js/build/mapBlocks.js:1 +msgid "West pan limit" +msgstr "" + +#: includes/maps/class-maps.php:255 +msgid "Map pan limits" +msgstr "" + +#: includes/maps/class-maps.php:268 +msgid "List of category IDs related posts must have" +msgstr "" + +#: includes/maps/class-maps.php:275 +msgid "List of tag IDs related posts must have" +msgstr "" + +#: includes/maps/class-maps.php:282 +msgid "Maximum date for related posts" +msgstr "" + +#: includes/maps/class-maps.php:286 +msgid "Minimum date for related posts" +msgstr "" + +#: includes/maps/class-maps.php:290 +msgid "List of meta values related posts must have" +msgstr "" + +#: includes/maps/class-maps.php:314 +msgid "The map criteria to get related posts" +msgstr "" + +#: includes/maps/class-maps.php:327 +msgid "Disable scroll zoom" +msgstr "" + +#: includes/maps/class-maps.php:340 +msgid "Disable drag pan" +msgstr "" + +#: includes/maps/class-maps.php:353 +msgid "Disable drag rotation" +msgstr "" + +#: includes/maps/class-maps.php:366 +msgid "Enable fullscreen button" +msgstr "" + +#: includes/maps/class-maps.php:379 +msgid "Show all posts on map" +msgstr "" + +#: includes/maps/class-maps.php:392 +msgid "Minimum Zoom level" +msgstr "" + +#: includes/maps/class-maps.php:405 +msgid "Maximum zoom level" +msgstr "" + +#: includes/maps/class-maps.php:418 +#: js/build/mapBlocks.js:3 +#: js/build/mapsSidebar.js:1 +msgid "Hide in discovery" +msgstr "" + +#: includes/maps/class-maps.php:431 +msgid "Disable embed" +msgstr "" + +#: includes/maps/class-maps.php:587 +msgid "Map zoom must be between 1 and 14" +msgstr "" + +#: includes/menu/class-menu.php:35 +#: includes/menu/class-menu.php:36 +msgid "JEO" +msgstr "" + +#: includes/menu/class-menu.php:48 +#: includes/menu/class-menu.php:49 +msgid "Welcome" +msgstr "" + +#: includes/menu/class-menu.php:58 +#: includes/menu/class-menu.php:59 +msgid "Dashboard" +msgstr "" + +#: includes/menu/class-menu.php:68 +#: includes/menu/class-menu.php:69 +msgid "AI (new)" +msgstr "" + +#: includes/minimap/class-minimap.php:201 +msgid "No matching layers found. Add layers manually or run the RAG indexer in JEO Settings." +msgstr "" + +#: includes/minimap/class-minimap.php:229 +msgid "Generate a map for this post based on its content." +msgstr "" + +#: includes/minimap/class-minimap.php:230 +msgid "Initial generation from post content" +msgstr "" + +#: includes/minimap/class-minimap.php:486 +msgid "The AI returned an empty response. Please try again." +msgstr "" + +#. translators: %d: number of removed layers +#: includes/minimap/class-minimap.php:529 +#, php-format +msgid "%d layer(s) returned by the AI were discarded because they are not published map layers." +msgstr "" + +#. translators: %d: version number. +#: includes/minimap/class-minimap.php:933 +#, php-format +msgid "Version %d was not found in the stored history. The configuration reconstructed from the conversation context was applied instead." +msgstr "" + +#. translators: %d: version number. +#: includes/minimap/class-minimap.php:946 +#, php-format +msgid "Restored map version %d." +msgstr "" + +#: includes/minimap/class-minimap.php:1144 +msgid "The agent tried to remove several existing layers without an explicit request. They were kept to preserve the current map." +msgstr "" + +#. translators: %d: number of preserved layers +#: includes/minimap/class-minimap.php:1388 +#, php-format +msgid "%d manually-added layer(s) were preserved during AI refinement." +msgstr "" + +#: includes/minimap/class-minimap.php:1876 +msgid "Dark Base (Mapbox)" +msgstr "" + +#: includes/minimap/class-minimap.php:1883 +msgid "Terrain Base (Mapbox)" +msgstr "" + +#: includes/minimap/class-minimap.php:1890 +msgid "Satellite Base (Mapbox)" +msgstr "" + +#: includes/minimap/class-minimap.php:1901 +msgid "Dark Base" +msgstr "" + +#: includes/minimap/class-minimap.php:1910 +msgid "Light Base" +msgstr "" + +#: includes/minimap/class-minimap.php:1919 +msgid "Satellite Base" +msgstr "" + +#: includes/privacy.php:20 +msgid "JEO collects and stores geolocation metadata for posts when authors explicitly geotag content. This may include latitude, longitude, and reverse-geocoded address components (city, region, country, postcode). This data is attached to the post and is available to anyone with access to the post." +msgstr "" + +#: includes/privacy.php:21 +msgid "When the \"Stories Near You\" feature is enabled, visitors may opt in to share their browser geolocation. This location is sent to the site server solely to retrieve nearby posts and is not stored." +msgstr "" + +#: includes/privacy.php:22 +msgid "If AI-powered georeferencing is enabled, post titles and content may be sent to external AI providers (e.g., OpenAI, Google Gemini). AI usage logs (provider name, token counts) are stored locally as private posts and are not shared externally." +msgstr "" + +#: includes/privacy.php:23 +msgid "Geocoding requests may be sent to third-party services such as Nominatim (OpenStreetMap) or Mapbox, depending on the active geocoder." +msgstr "" + +#: includes/privacy.php:40 +#: includes/privacy.php:126 +msgid "JEO Geolocation Metadata" +msgstr "" + +#: includes/privacy.php:106 +msgid "JEO Geolocation" +msgstr "" + +#: includes/settings/ai-logs-page.php:10 +msgid "AI Cost Dashboard & Debug Logs" +msgstr "" + +#: includes/settings/ai-logs-page.php:11 +msgid "Here you can view the most recent AI interactions powered by Neuron AI, along with their detailed token usage (Input/Output)." +msgstr "" + +#: includes/settings/ai-logs-page.php:31 +msgid "All AI logs have been permanently deleted." +msgstr "" + +#: includes/settings/ai-logs-page.php:75 +#: includes/settings/class-settings.php:646 +msgid "reqs" +msgstr "" + +#: includes/settings/ai-logs-page.php:79 +#: includes/settings/ai-logs-page.php:104 +#: includes/settings/ai-logs-page.php:116 +#: includes/settings/class-settings.php:645 +msgid "Tokens" +msgstr "" + +#: includes/settings/ai-logs-page.php:82 +#: includes/settings/class-settings.php:647 +msgid "In (Prompt):" +msgstr "" + +#: includes/settings/ai-logs-page.php:83 +#: includes/settings/class-settings.php:648 +msgid "Out (Compl.):" +msgstr "" + +#: includes/settings/ai-logs-page.php:94 +msgid "RAG & Vectorization Estimates" +msgstr "" + +#: includes/settings/ai-logs-page.php:95 +msgid "These numbers represent the estimated tokens consumed to create and query your RAG Knowledge Base. Since embedding models do not natively report their specific usage, the system estimates the token count via mathematical approximation." +msgstr "" + +#: includes/settings/ai-logs-page.php:102 +msgid "Document Vectorization" +msgstr "" + +#: includes/settings/ai-logs-page.php:107 +msgid "Tokens used inserting posts to Vector Store." +msgstr "" + +#: includes/settings/ai-logs-page.php:114 +msgid "Semantic Retrieval" +msgstr "" + +#: includes/settings/ai-logs-page.php:119 +msgid "Tokens used mapping user queries to vectors." +msgstr "" + +#: includes/settings/ai-logs-page.php:147 +msgid "Search logs:" +msgstr "" + +#: includes/settings/ai-logs-page.php:149 +msgid "Search Logs" +msgstr "" + +#: includes/settings/ai-logs-page.php:156 +msgid "Are you sure you want to delete ALL logs?" +msgstr "" + +#: includes/settings/ai-logs-page.php:157 +msgid "Clear All Logs" +msgstr "" + +#. translators: %s: Number of log items. +#: includes/settings/ai-logs-page.php:164 +#, php-format +msgid "%s item" +msgid_plural "%s items" +msgstr[0] "" +msgstr[1] "" + +#: includes/settings/ai-logs-page.php:189 +msgid "Date/Time" +msgstr "" + +#: includes/settings/ai-logs-page.php:190 +msgid "Provider" +msgstr "" + +#: includes/settings/ai-logs-page.php:191 +msgid "Tokens (In / Out)" +msgstr "" + +#: includes/settings/ai-logs-page.php:192 +msgid "Status / Output Preview" +msgstr "" + +#: includes/settings/ai-logs-page.php:193 +#: includes/settings/class-settings.php:602 +msgid "Actions" +msgstr "" + +#: includes/settings/ai-logs-page.php:198 +msgid "No logs found yet. Ensure Debug Mode is enabled and georeference a post." +msgstr "" + +#: includes/settings/ai-logs-page.php:238 +#: includes/settings/class-settings.php:649 +msgid "Total" +msgstr "" + +#: includes/settings/ai-logs-page.php:239 +#: includes/settings/class-settings.php:650 +msgid "Prompt:" +msgstr "" + +#: includes/settings/ai-logs-page.php:239 +#: includes/settings/class-settings.php:651 +msgid "Completion:" +msgstr "" + +#: includes/settings/ai-logs-page.php:244 +msgid "View Details" +msgstr "" + +#: includes/settings/ai-logs-page.php:249 +msgid "AI Interaction Details" +msgstr "" + +#: includes/settings/ai-logs-page.php:254 +msgid "Timestamp" +msgstr "" + +#: includes/settings/ai-logs-page.php:255 +msgid "Input Tokens" +msgstr "" + +#: includes/settings/ai-logs-page.php:256 +msgid "Output Tokens" +msgstr "" + +#: includes/settings/ai-logs-page.php:259 +msgid "Input (Prompt sent)" +msgstr "" + +#: includes/settings/ai-logs-page.php:262 +msgid "Output (Raw Response)" +msgstr "" + +#: includes/settings/class-settings.php:538 +msgid "JEO could not save this asset URL. Use a valid http(s) or root-relative URL." +msgstr "" + +#: includes/settings/class-settings.php:573 +msgid "[System] Console cleared." +msgstr "" + +#: includes/settings/class-settings.php:574 +msgid "Missing Configuration" +msgstr "" + +#: includes/settings/class-settings.php:575 +msgid "Checking..." +msgstr "" + +#: includes/settings/class-settings.php:576 +msgctxt "geocoder status" +msgid "Active" +msgstr "" + +#: includes/settings/class-settings.php:577 +msgctxt "geocoder status" +msgid "Invalid" +msgstr "" + +#: includes/settings/class-settings.php:578 +msgid "Failed" +msgstr "" + +#: includes/settings/class-settings.php:579 +msgid "Request Failed" +msgstr "" + +#: includes/settings/class-settings.php:580 +msgid "Loading..." +msgstr "" + +#: includes/settings/class-settings.php:582 +msgid "Select or type a model..." +msgstr "" + +#: includes/settings/class-settings.php:583 +msgid " (Non-chat)" +msgstr "" + +#: includes/settings/class-settings.php:584 +msgid "Please enter an API Key first." +msgstr "" + +#: includes/settings/class-settings.php:585 +msgid "Failed to fetch models: " +msgstr "" + +#: includes/settings/class-settings.php:586 +msgid "Unknown error" +msgstr "" + +#: includes/settings/class-settings.php:587 +msgid "Error fetching models. Check your key and connection." +msgstr "" + +#: includes/settings/class-settings.php:588 +msgid "Please type a description first." +msgstr "" + +#: includes/settings/class-settings.php:589 +msgid "Asking AI..." +msgstr "" + +#: includes/settings/class-settings.php:591 +msgid "✨ Applied above." +msgstr "" + +#: includes/settings/class-settings.php:592 +msgid "Error generating prompt." +msgstr "" + +#: includes/settings/class-settings.php:594 +msgid "Testing..." +msgstr "" + +#: includes/settings/class-settings.php:595 +msgid "✅ Valid" +msgstr "" + +#: includes/settings/class-settings.php:596 +msgid "❌ Invalid" +msgstr "" + +#: includes/settings/class-settings.php:597 +msgid "Validate Prompt" +msgstr "" + +#: includes/settings/class-settings.php:598 +msgid "Loading backups..." +msgstr "" + +#: includes/settings/class-settings.php:599 +msgid "File" +msgstr "" + +#: includes/settings/class-settings.php:600 +msgid "Date" +msgstr "" + +#: includes/settings/class-settings.php:601 +#: js/build/mapBlocks.js:3 +#: js/build/mapsSidebar.js:1 +msgid "Size" +msgstr "" + +#: includes/settings/class-settings.php:604 +#: js/build/postsSidebar.js:2 +msgid "Delete" +msgstr "" + +#: includes/settings/class-settings.php:606 +msgid "Delete?" +msgstr "" + +#: includes/settings/class-settings.php:607 +msgid "Vectorizing..." +msgstr "" + +#: includes/settings/class-settings.php:608 +msgid "Vectorize Now" +msgstr "" + +#: includes/settings/class-settings.php:609 +msgid "Clear knowledge base?" +msgstr "" + +#: includes/settings/class-settings.php:610 +msgid "Fetching post and generating vector embeddings..." +msgstr "" + +#: includes/settings/class-settings.php:611 +msgid "Success!" +msgstr "" + +#: includes/settings/class-settings.php:612 +msgid "Post Extracted:" +msgstr "" + +#: includes/settings/class-settings.php:613 +msgid "Vector Dimensions:" +msgstr "" + +#: includes/settings/class-settings.php:614 +msgid "Text Snippet:" +msgstr "" + +#: includes/settings/class-settings.php:615 +msgid "Vector Preview:" +msgstr "" + +#: includes/settings/class-settings.php:617 +msgid "Searching..." +msgstr "" + +#: includes/settings/class-settings.php:618 +msgid "Searching the knowledge base " +msgstr "" + +#: includes/settings/class-settings.php:619 +msgid "No documents found. Have you run vectorization?" +msgstr "" + +#: includes/settings/class-settings.php:620 +msgid "Score / Relevance" +msgstr "" + +#: includes/settings/class-settings.php:621 +msgid "Metadata" +msgstr "" + +#: includes/settings/class-settings.php:622 +msgctxt "fallback post title" +msgid "Untitled" +msgstr "" + +#: includes/settings/class-settings.php:624 +msgid "Processing..." +msgstr "" + +#: includes/settings/class-settings.php:625 +msgid "Clearing..." +msgstr "" + +#: includes/settings/class-settings.php:626 +msgid "This will schedule clearing ALL AI-geolocated posts in the background. Continue?" +msgstr "" + +#: includes/settings/class-settings.php:627 +msgid "ARE YOU SURE? This cannot be undone and will require full re-vectorization for these posts." +msgstr "" + +#: includes/settings/class-settings.php:628 +msgid "Bulk clearing started in background." +msgstr "" + +#: includes/settings/class-settings.php:630 +msgid "Collapse" +msgstr "" + +#: includes/settings/class-settings.php:632 +msgid "Low" +msgstr "" + +#: includes/settings/class-settings.php:633 +msgid "Fair" +msgstr "" + +#: includes/settings/class-settings.php:634 +msgid "Optimal" +msgstr "" + +#: includes/settings/class-settings.php:635 +msgid "API call failed" +msgstr "" + +#: includes/settings/class-settings.php:638 +msgid "Delete log file?" +msgstr "" + +#: includes/settings/class-settings.php:639 +msgid "No matching layers found." +msgstr "" + +#: includes/settings/class-settings.php:640 +msgid " matching layers:" +msgstr "" + +#: includes/settings/class-settings.php:641 +#: js/build/postsSidebar.js:2 +msgid "Edit" +msgstr "" + +#: includes/settings/class-settings.php:643 +msgid "Clear layer store?" +msgstr "" + +#: includes/settings/class-settings.php:644 +msgid "Please enter a post ID or search text." +msgstr "" + +#: includes/settings/class-settings.php:694 +#: includes/settings/class-settings.php:695 +msgid "AI Debug Logs" +msgstr "" + +#: includes/settings/class-settings.php:709 +msgid "General" +msgstr "" + +#: includes/settings/class-settings.php:710 +msgid "Geocoders" +msgstr "" + +#: includes/settings/class-settings.php:711 +msgid "Appearance" +msgstr "" + +#: includes/settings/class-settings.php:712 +msgid "Discovery" +msgstr "" + +#: includes/settings/settings-page.php:10 +msgid "Jeo Settings" +msgstr "" + +#: includes/settings/settings-page.php:28 +msgid "Rendering library" +msgstr "" + +#: includes/settings/settings-page.php:38 +msgid "Enabled Post Types" +msgstr "" + +#: includes/settings/settings-page.php:57 +msgid "Default map zoom" +msgstr "" + +#: includes/settings/settings-page.php:64 +msgid "Default map center (latitude)" +msgstr "" + +#: includes/settings/settings-page.php:71 +msgid "Default map center (longitude)" +msgstr "" + +#: includes/settings/settings-page.php:78 +msgid "User location precision" +msgstr "" + +#: includes/settings/settings-page.php:83 +msgid "Lower = less precision, more privacy." +msgstr "" + +#: includes/settings/settings-page.php:84 +msgid "https://en.wikipedia.org/wiki/Decimal_degrees#Precision" +msgstr "" + +#: includes/settings/settings-page.php:84 +msgid "Learn more about decimal degree precision." +msgstr "" + +#: includes/settings/settings-page.php:90 +msgid "Show Story Maps on Post Archives" +msgstr "" + +#: includes/settings/settings-page.php:93 +msgid "Enable this to display relevant story maps at the top of category and tag archive pages." +msgstr "" + +#. translators: %s is the geocoder name. Example: Nominatim options. +#: includes/settings/settings-page.php:111 +#, php-format +msgctxt "geocoder_options" +msgid "%s options" +msgstr "" + +#: includes/settings/settings-page.php:137 +msgid "Mapbox" +msgstr "" + +#: includes/settings/settings-page.php:141 +msgid "Mapbox API Key" +msgstr "" + +#: includes/settings/settings-page.php:143 +msgid "Ex. pk.eyJ3..." +msgstr "" + +#: includes/settings/settings-page.php:144 +msgid "Public access token. Used for MapboxGL rendering, Mapbox layers, and AI layer generation. This token is exposed to site visitors (it is required to display maps), so keep its scopes read-only." +msgstr "" + +#: includes/settings/settings-page.php:148 +msgid "Mapbox Secret Key" +msgstr "" + +#: includes/settings/settings-page.php:150 +msgid "Ex. sk.eyJ3..." +msgstr "" + +#: includes/settings/settings-page.php:151 +msgid "Used to create AI-assisted layers (requires the styles:write scope). For security, use a secret token — the public token is visible to site visitors. If empty, publishing falls back to the public token." +msgstr "" + +#: includes/settings/settings-page.php:160 +msgid "Geocoder service" +msgstr "" + +#: includes/settings/settings-page.php:167 +msgid "Select the service used to translate addresses into coordinates. Mapbox requires an API Key configured in the General tab." +msgstr "" + +#: includes/settings/settings-page.php:173 +msgid "Typography" +msgstr "" + +#: includes/settings/settings-page.php:177 +msgid "Font URL" +msgstr "" + +#: includes/settings/settings-page.php:183 +msgid "Font Name" +msgstr "" + +#: includes/settings/settings-page.php:189 +msgid "Secondary Font URL" +msgstr "" + +#: includes/settings/settings-page.php:195 +msgid "Secondary Font Name" +msgstr "" + +#: includes/settings/settings-page.php:201 +msgid "Info button font size (rem)" +msgstr "" + +#: includes/settings/settings-page.php:210 +#: js/build/mapBlocks.js:3 +#: js/build/mapsSidebar.js:1 +msgid "Colors" +msgstr "" + +#: includes/settings/settings-page.php:214 +msgid "Primary Color" +msgstr "" + +#: includes/settings/settings-page.php:220 +msgid "Info button background color" +msgstr "" + +#: includes/settings/settings-page.php:226 +msgid "Info button text color" +msgstr "" + +#: includes/settings/settings-page.php:232 +msgid "Close button background color" +msgstr "" + +#: includes/settings/settings-page.php:238 +msgid "Close button text color" +msgstr "" + +#: includes/settings/settings-page.php:244 +msgid "Secondary Color" +msgstr "" + +#: includes/settings/settings-page.php:250 +msgid "Map widgets background color" +msgstr "" + +#: includes/settings/settings-page.php:256 +msgid "Map widgets text color" +msgstr "" + +#: includes/settings/settings-page.php:262 +msgid "Map widgets background color (hover)" +msgstr "" + +#: includes/settings/settings-page.php:271 +msgid "Map Marker Icons" +msgstr "" + +#: includes/settings/settings-page.php:275 +msgid "Primary Pin URL" +msgstr "" + +#: includes/settings/settings-page.php:278 +msgid "URL for the primary location marker icon. Leave empty to use the default blue marker." +msgstr "" + +#: includes/settings/settings-page.php:282 +msgid "Secondary Pin URL" +msgstr "" + +#: includes/settings/settings-page.php:285 +msgid "URL for the secondary location marker icon. Leave empty to use the default grey marker." +msgstr "" + +#: includes/settings/settings-page.php:292 +msgid "Embed" +msgstr "" + +#: includes/settings/settings-page.php:296 +msgid "Company logo" +msgstr "" + +#: includes/settings/settings-page.php:300 +msgid "You may use a local or external image URL. Large logos will be scaled down automatically in the embed footer." +msgstr "" + +#: includes/settings/settings-page.php:302 +msgid "Insert Image" +msgstr "" + +#: includes/settings/settings-page.php:311 +msgid "Discovery page" +msgstr "" + +#: includes/settings/settings-page.php:318 +msgid "Select a page" +msgstr "" + +#: includes/settings/settings-page.php:322 +msgid "Select the page where the JEO Discovery block is located." +msgstr "" + +#: includes/settings/settings-page.php:331 +msgid "Save Changes" +msgstr "" + +#: includes/stories-near-you/class-stories-near-you.php:432 +#: js/build/storiesNearYou.js:1 +msgid "Unable to load stories near you." +msgstr "" + +#. translators: %s: search radius in kilometers +#: includes/stories-near-you/class-stories-near-you.php:1123 +#, php-format +msgid "No stories found within %s km of your location." +msgstr "" + +#. translators: Story Map is the name of JEO's storytelling map feature. +#: includes/storymap/class-storymap.php:69 +#: includes/storymap/class-storymap.php:70 +#: js/build/mapBlocks.js:3 +msgid "Story Map" +msgstr "" + +#: includes/storymap/class-storymap.php:71 +#: includes/storymap/class-storymap.php:72 +msgid "Add new Story Map" +msgstr "" + +#: includes/storymap/class-storymap.php:73 +msgid "Edit Story Map" +msgstr "" + +#: includes/storymap/class-storymap.php:74 +msgid "New Story Map" +msgstr "" + +#: includes/storymap/class-storymap.php:75 +msgid "View Story Map" +msgstr "" + +#: includes/storymap/class-storymap.php:76 +msgid "View Story Maps" +msgstr "" + +#: includes/storymap/class-storymap.php:77 +msgid "Search Story Maps" +msgstr "" + +#: includes/storymap/class-storymap.php:78 +msgid "No Story Map found" +msgstr "" + +#: includes/storymap/class-storymap.php:79 +msgid "No Story Map found in the trash" +msgstr "" + +#: includes/storymap/class-storymap.php:80 +msgid "Story Maps" +msgstr "" + +#: includes/storymap/class-storymap.php:81 +msgid "Story Map published." +msgstr "" + +#: includes/storymap/class-storymap.php:82 +msgid "Story Map published privately." +msgstr "" + +#: includes/storymap/class-storymap.php:83 +msgid "Story Map reverted to draft." +msgstr "" + +#: includes/storymap/class-storymap.php:84 +msgid "Story Map scheduled." +msgstr "" + +#: includes/storymap/class-storymap.php:85 +msgid "Story Map updated." +msgstr "" + +#: includes/storymap/class-storymap.php:91 +msgid "JEO Story Map" +msgstr "" + +#: jeo.php:36 +msgid "JEO Plugin Error: Your PHP version is too old. JEO AI features require PHP 8.2 or higher. The plugin will remain deactivated." +msgstr "" + +#: templates/discovery.php:19 +msgid "Sorry!" +msgstr "" + +#: templates/discovery.php:22 +msgid "This page can't be viewed on mobile devices." +msgstr "" + +#: templates/discovery.php:26 +msgid "Back to homepage" +msgstr "" + +#: templates/map-content-layers-list.php:18 +#: templates/map-content-layers-list.php:20 +#: templates/map-content-layers-list.php:22 +#: js/build/jeoMap.js:1 +msgid "Attribution:" +msgstr "" + +#: templates/map-content-layers-list.php:27 +#: js/build/jeoMap.js:1 +msgid "Download from source" +msgstr "" + +#: includes/layer-types/geojson.js:188 +msgid "GeoJSON" +msgstr "" + +#: includes/layer-types/geojson.js:229 +msgid "GeoJSON URL" +msgstr "" + +#: includes/layer-types/geojson.js:230 +msgid "Public URL of a GeoJSON file (usually a FeatureCollection). Used when no inline GeoJSON is provided." +msgstr "" + +#: includes/layer-types/geojson.js:238 +msgid "Inline GeoJSON" +msgstr "" + +#: includes/layer-types/geojson.js:239 +msgid "Optional. Paste a raw GeoJSON object or FeatureCollection. Takes precedence over the URL — useful for small geometries." +msgstr "" + +#: includes/layer-types/geojson.js:246 +msgid "Layer style" +msgstr "" + +#: includes/layer-types/geojson.js:247 +msgid "How the GeoJSON features are drawn." +msgstr "" + +#: includes/layer-types/mapbox-tileset-raster.js:5 +#: js/build/layersSidebar.js:11 +#: js/build/mapBlocks.js:1 +#: js/build/mapBlocks.js:3 +#: js/build/mapsSidebar.js:1 +msgid "Raster Mapbox Tiled Source" +msgstr "" + +#: includes/layer-types/mapbox-tileset-raster.js:72 +#: includes/layer-types/mapbox-tileset-vector.js:109 +#: js/build/layersSidebar.js:11 +#: js/build/mapBlocks.js:1 +msgid "Tileset ID" +msgstr "" + +#: includes/layer-types/mapbox-tileset-raster.js:73 +#: includes/layer-types/mapbox-tileset-vector.js:110 +#: js/build/layersSidebar.js:11 +#: js/build/mapBlocks.js:1 +msgid "Example: username.tilesetid" +msgstr "" + +#: includes/layer-types/mapbox-tileset-raster.js:76 +#: includes/layer-types/mapbox-tileset-vector.js:138 +#: includes/layer-types/mvt.js:133 +#: js/build/layersSidebar.js:11 +#: js/build/mapBlocks.js:1 +msgid "Style Source Type" +msgstr "" + +#: includes/layer-types/mapbox-tileset-raster.js:77 +#: includes/layer-types/mvt.js:134 +#: js/build/layersSidebar.js:11 +#: js/build/mapBlocks.js:1 +msgid "Which data the map should display" +msgstr "" + +#: includes/layer-types/mapbox-tileset-raster.js:83 +#: includes/layer-types/mapbox-tileset-vector.js:118 +#: js/build/layersSidebar.js:11 +#: js/build/mapBlocks.js:1 +msgid "Layer Type" +msgstr "" + +#: includes/layer-types/mapbox-tileset-raster.js:85 +#: includes/layer-types/mapbox-tileset-vector.js:120 +#: js/build/layersSidebar.js:11 +#: js/build/mapBlocks.js:1 +msgid "Layers take the data that they get from a source, optionally filter features, and then define how those features are styled." +msgstr "" + +#: includes/layer-types/mapbox-tileset-raster.js:95 +#: includes/layer-types/mapbox-tileset-vector.js:146 +#: includes/layer-types/mapbox.js:128 +#: includes/layer-types/mvt.js:141 +#: js/build/layersSidebar.js:11 +#: js/build/mapBlocks.js:1 +msgid "Access token" +msgstr "" + +#: includes/layer-types/mapbox-tileset-raster.js:96 +#: includes/layer-types/mapbox-tileset-vector.js:147 +#: includes/layer-types/mapbox.js:130 +#: includes/layer-types/mvt.js:142 +#: js/build/layersSidebar.js:11 +#: js/build/mapBlocks.js:1 +msgid "Optional. If this layer needs a different access token from the one set in Settings, inform it here." +msgstr "" + +#: includes/layer-types/mapbox-tileset-vector.js:5 +#: js/build/layersSidebar.js:11 +#: js/build/mapBlocks.js:1 +#: js/build/mapBlocks.js:3 +#: js/build/mapsSidebar.js:1 +msgid "Vector Mapbox Tiled Source" +msgstr "" + +#: includes/layer-types/mapbox-tileset-vector.js:114 +#: includes/layer-types/mvt.js:115 +#: js/build/layersSidebar.js:11 +#: js/build/mapBlocks.js:1 +msgid "Source layer" +msgstr "" + +#: includes/layer-types/mapbox-tileset-vector.js:115 +#: js/build/layersSidebar.js:11 +#: js/build/mapBlocks.js:1 +msgid "Which data the map should display." +msgstr "" + +#: includes/layer-types/mapbox-tileset-vector.js:139 +#: js/build/layersSidebar.js:11 +#: js/build/mapBlocks.js:1 +msgid "The layer source type style" +msgstr "" + +#: includes/layer-types/mapbox.js:5 +#: js/build/layersSidebar.js:11 +#: js/build/mapBlocks.js:1 +#: js/build/mapBlocks.js:3 +#: js/build/mapsSidebar.js:1 +msgid "Mapbox Style" +msgstr "" + +#: includes/layer-types/mapbox.js:119 +#: js/build/layersSidebar.js:11 +#: js/build/mapBlocks.js:1 +msgid "Style ID" +msgstr "" + +#: includes/layer-types/mapbox.js:121 +#: js/build/layersSidebar.js:11 +#: js/build/mapBlocks.js:1 +msgid "The Mapbox Style ID includes the user name and id. Example: username/id or mapbox://styles/username/id" +msgstr "" + +#: includes/layer-types/mvt.js:5 +#: js/build/layersSidebar.js:11 +#: js/build/mapBlocks.js:1 +#: js/build/mapBlocks.js:3 +#: js/build/mapsSidebar.js:1 +msgid "Mapbox Vector Tiles (MVT)" +msgstr "" + +#: includes/layer-types/mvt.js:116 +#: js/build/layersSidebar.js:11 +#: js/build/mapBlocks.js:1 +msgid "Layer to use from a vector tile source." +msgstr "" + +#: includes/layer-types/style-json.js:5 +msgid "Style JSON" +msgstr "" + +#: includes/layer-types/style-json.js:30 +msgid "Style URL" +msgstr "" + +#: includes/layer-types/style-json.js:31 +msgid "Public URL of a MapLibre GL style JSON file. Used when no inline style is provided. Works with keyless providers such as OpenFreeMap (e.g. https://tiles.openfreemap.org/styles/dark)." +msgstr "" + +#: includes/layer-types/style-json.js:39 +msgid "Inline style JSON" +msgstr "" + +#: includes/layer-types/style-json.js:40 +msgid "Optional. Paste a raw MapLibre GL style object. Takes precedence over the URL — useful for small, self-contained styles." +msgstr "" + +#: includes/layer-types/tilelayer.js:5 +#: js/build/layersSidebar.js:11 +#: js/build/mapBlocks.js:1 +#: js/build/mapBlocks.js:3 +#: js/build/mapsSidebar.js:1 +msgid "Raster Tiled Source" +msgstr "" + +#: includes/layer-types/tilelayer.js:65 +#: js/build/layersSidebar.js:11 +#: js/build/mapBlocks.js:1 +msgid "Scheme" +msgstr "" + +#: includes/layer-types/tilelayer.js:66 +#: js/build/layersSidebar.js:11 +#: js/build/mapBlocks.js:1 +msgid "Influences the Y direction of the tile coordinates." +msgstr "" + +#: includes/layer-types/tilelayer.js:69 +#: js/build/layersSidebar.js:11 +#: js/build/mapBlocks.js:1 +msgid "Slippy Map tilenames (XYZ)" +msgstr "" + +#: includes/layer-types/tilelayer.js:70 +#: js/build/layersSidebar.js:11 +#: js/build/mapBlocks.js:1 +msgid "OSGeo spec (TMS)" +msgstr "" + +#: includes/legend-types/barscale.js:5 +#: js/build/layersSidebar.js:7 +msgid "Color scale" +msgstr "" + +#: includes/legend-types/barscale.js:16 +msgid "The label shown at the left end of the bar" +msgstr "" + +#: includes/legend-types/barscale.js:20 +msgid "The label shown at the right end of the bar" +msgstr "" + +#: includes/legend-types/barscale.js:24 +msgid "An array of hexadecimal color values" +msgstr "" + +#: includes/legend-types/circles.js:5 +#: js/build/layersSidebar.js:7 +msgid "Circle sizes" +msgstr "" + +#: includes/legend-types/circles.js:16 +msgid "An array of labels and radius values" +msgstr "" + +#: includes/legend-types/icons.js:5 +#: js/build/layersSidebar.js:7 +msgid "Icon categories" +msgstr "" + +#: includes/legend-types/icons.js:16 +msgid "An array of labels and icons" +msgstr "" + +#: includes/legend-types/JeoLegend.js:177 +#: includes/legend-types/JeoLegend.js:181 +#: includes/legend-types/JeoLegend.js:186 +#: js/build/JeoLegend.js:1 +#: js/build/layersSidebar.js:1 +#: js/build/layersSidebar.js:7 +msgid "Default label" +msgstr "" + +#: includes/legend-types/simple-color.js:5 +#: js/build/layersSidebar.js:7 +msgid "Color categories" +msgstr "" + +#: includes/legend-types/simple-color.js:16 +msgid "An array of labels and colors" +msgstr "" + +#: includes/settings/settings-page.js:916 +#: includes/settings/settings-page.js:918 +msgid "Choose Image" +msgstr "" + +#: js/build/contextSidebar.js:1 +msgid "Suggested Paragraphs" +msgstr "" + +#: js/build/contextSidebar.js:1 +msgid "Inserted" +msgstr "" + +#: js/build/contextSidebar.js:1 +msgid "Insert into article" +msgstr "" + +#: js/build/contextSidebar.js:1 +msgid "Copied!" +msgstr "" + +#: js/build/contextSidebar.js:1 +msgid "Copy" +msgstr "" + +#: js/build/contextSidebar.js:1 +msgid "References used" +msgstr "" + +#: js/build/contextSidebar.js:1 +msgid "Failed to generate suggestions." +msgstr "" + +#: js/build/contextSidebar.js:1 +msgid "Error contacting AI." +msgstr "" + +#: js/build/contextSidebar.js:1 +msgid "Failed to process message." +msgstr "" + +#: js/build/contextSidebar.js:1 +msgid "Generate new editorial suggestions based on the current post content." +msgstr "" + +#: js/build/contextSidebar.js:1 +msgid "Failed to clear conversation." +msgstr "" + +#: js/build/contextSidebar.js:1 +msgid "Restoring conversation..." +msgstr "" + +#: js/build/contextSidebar.js:1 +msgid "Use the AI Context Assistant to generate editorial suggestions based on your article and the site's knowledge base." +msgstr "" + +#: js/build/contextSidebar.js:1 +msgid "Generate Suggestions" +msgstr "" + +#: js/build/contextSidebar.js:1 +msgid "Previous suggestions" +msgstr "" + +#: js/build/contextSidebar.js:1 +msgid "View this version" +msgstr "" + +#: js/build/contextSidebar.js:1 +msgid "Thinking..." +msgstr "" + +#: js/build/contextSidebar.js:1 +msgid "Ask for changes or new paragraphs... (Shift+Enter for a line break)" +msgstr "" + +#: js/build/contextSidebar.js:1 +msgid "Send" +msgstr "" + +#: js/build/contextSidebar.js:1 +msgid "Retry" +msgstr "" + +#: js/build/contextSidebar.js:1 +#: js/build/discovery.js:1 +#: js/build/discovery.js:6 +#: js/build/mapBlocks.js:3 +#: js/build/mapsSidebar.js:1 +msgid "Clear" +msgstr "" + +#: js/build/contextSidebar.js:1 +msgid "AI Context" +msgstr "" + +#: js/build/contextSidebar.js:1 +msgid "AI Context Assistant" +msgstr "" + +#: js/build/discovery.js:1 +#: js/build/discovery.js:2 +msgid "January" +msgstr "" + +#: js/build/discovery.js:1 +#: js/build/discovery.js:2 +msgid "February" +msgstr "" + +#: js/build/discovery.js:1 +#: js/build/discovery.js:2 +msgid "March" +msgstr "" + +#: js/build/discovery.js:1 +#: js/build/discovery.js:2 +msgid "April" +msgstr "" + +#: js/build/discovery.js:1 +#: js/build/discovery.js:2 +msgid "May" +msgstr "" + +#: js/build/discovery.js:1 +#: js/build/discovery.js:2 +msgid "June" +msgstr "" + +#: js/build/discovery.js:1 +#: js/build/discovery.js:2 +msgid "July" +msgstr "" + +#: js/build/discovery.js:1 +#: js/build/discovery.js:2 +msgid "August" +msgstr "" + +#: js/build/discovery.js:1 +#: js/build/discovery.js:2 +msgid "September" +msgstr "" + +#: js/build/discovery.js:1 +#: js/build/discovery.js:2 +msgid "October" +msgstr "" + +#: js/build/discovery.js:1 +#: js/build/discovery.js:2 +msgid "November" +msgstr "" + +#: js/build/discovery.js:1 +#: js/build/discovery.js:2 +msgid "December" +msgstr "" + +#: js/build/discovery.js:1 +msgid "Previous month" +msgstr "" + +#: js/build/discovery.js:1 +msgid "Month" +msgstr "" + +#: js/build/discovery.js:1 +msgid "Year" +msgstr "" + +#: js/build/discovery.js:1 +msgid "Next month" +msgstr "" + +#: js/build/discovery.js:1 +#: js/build/discovery.js:2 +#: js/build/discovery.js:6 +msgid "Apply" +msgstr "" + +#: js/build/discovery.js:1 +#: js/build/mapBlocks.js:3 +#: js/build/mapsSidebar.js:1 +msgid "Tags" +msgstr "" + +#: js/build/discovery.js:1 +msgid "Filter stories by tag" +msgstr "" + +#. translators: %s: selected tag name. +#: js/build/discovery.js:2 +#, js-format +msgid "Remove tag %s" +msgstr "" + +#: js/build/discovery.js:2 +msgid "MM/DD/YYYY" +msgstr "" + +#: js/build/discovery.js:2 +msgid " - " +msgstr "" + +#: js/build/discovery.js:2 +msgid "From" +msgstr "" + +#: js/build/discovery.js:2 +msgid "To" +msgstr "" + +#: js/build/discovery.js:2 +msgid "Custom" +msgstr "" + +#: js/build/discovery.js:2 +msgid "Su" +msgstr "" + +#: js/build/discovery.js:2 +msgid "Mo" +msgstr "" + +#: js/build/discovery.js:2 +msgid "Tu" +msgstr "" + +#: js/build/discovery.js:2 +msgid "We" +msgstr "" + +#: js/build/discovery.js:2 +msgid "Th" +msgstr "" + +#: js/build/discovery.js:2 +msgid "Fr" +msgstr "" + +#: js/build/discovery.js:2 +msgid "Sa" +msgstr "" + +#. translators: %d: number of displayed stories. +#: js/build/discovery.js:3 +#, js-format +msgid "Displayed story: %d" +msgid_plural "Displayed stories: %d" +msgstr[0] "" +msgstr[1] "" + +#. translators: %s is the active story search query. +#: js/build/discovery.js:4 +#, js-format +msgid "Search story: %s" +msgstr "" + +#. translators: %s is the active story date range. +#: js/build/discovery.js:5 +#, js-format +msgid "Date range: %s" +msgstr "" + +#. translators: %s is the list of active story tags. +#: js/build/discovery.js:6 +#, js-format +msgid "Tags: %s" +msgstr "" + +#: js/build/discovery.js:6 +msgid "Loading…" +msgstr "" + +#: js/build/discovery.js:6 +msgid "No stories found for the current filters." +msgstr "" + +#: js/build/discovery.js:6 +msgid "No stories available." +msgstr "" + +#: js/build/discovery.js:6 +msgid "Search story" +msgstr "" + +#: js/build/discovery.js:6 +msgid "Hide filters" +msgstr "" + +#: js/build/discovery.js:6 +msgid "Show filters" +msgstr "" + +#: js/build/discovery.js:6 +msgid "Date range" +msgstr "" + +#: js/build/discovery.js:6 +msgid "Updating stories…" +msgstr "" + +#: js/build/discovery.js:6 +msgid "Active filters" +msgstr "" + +#: js/build/discovery.js:6 +msgid "View in map" +msgstr "" + +#: js/build/discovery.js:6 +msgid "layers" +msgstr "" + +#: js/build/discovery.js:6 +msgid "Unable to load map layers right now." +msgstr "" + +#: js/build/discovery.js:6 +msgid "Mapbox style composition is unavailable." +msgstr "" + +#: js/build/discovery.js:6 +#: js/build/jeoMap.js:1 +msgid "Mapbox style composition is unavailable for this map." +msgstr "" + +#: js/build/discovery.js:6 +msgid "Search map" +msgstr "" + +#: js/build/discovery.js:6 +msgid "Changes applied" +msgstr "" + +#: js/build/discovery.js:6 +msgid "Not applied" +msgstr "" + +#: js/build/discovery.js:6 +#: js/build/mapBlocks.js:3 +#: js/build/mapsSidebar.js:1 +msgid "Selected layers" +msgstr "" + +#: js/build/discovery.js:6 +msgid "No layers selected" +msgstr "" + +#: js/build/discovery.js:6 +msgid "Apply changes" +msgstr "" + +#: js/build/discovery.js:6 +msgid "Stories" +msgstr "" + +#: js/build/discovery.js:6 +#: js/build/mapBlocks.js:3 +#: js/build/mapsSidebar.js:1 +msgid "Map layers" +msgstr "" + +#: js/build/discovery.js:6 +msgid "Collapse discovery panel" +msgstr "" + +#: js/build/discovery.js:6 +msgid "Expand discovery panel" +msgstr "" + +#: js/build/discovery.js:6 +msgid "Unable to load map information right now." +msgstr "" + +#: js/build/discovery.js:6 +msgid "Map information" +msgstr "" + +#: js/build/discovery.js:6 +msgctxt "discovery share action" +msgid "Embed" +msgstr "" + +#: js/build/discovery.js:6 +#: js/build/jeoMap.js:1 +#: js/build/layersSidebar.js:11 +msgid "Legend" +msgstr "" + +#: js/build/discovery.js:6 +#: js/build/jeoMap.js:1 +msgid "Info" +msgstr "" + +#: js/build/discovery.js:6 +#: js/build/jeoMap.js:1 +msgid "Close popup" +msgstr "" + +#: js/build/jeoMap.js:1 +msgid "Mapbox style composition did not return renderable layers for this map." +msgstr "" + +#: js/build/jeoMap.js:1 +msgid "Unable to load composed Mapbox style for this map." +msgstr "" + +#: js/build/jeoMap.js:1 +msgid "Mapbox style composition is unavailable for this one-time map." +msgstr "" + +#: js/build/jeoMap.js:1 +msgid "Mapbox style composition did not return renderable layers for this one-time map." +msgstr "" + +#: js/build/jeoMap.js:1 +msgid "Unable to load composed Mapbox style for this one-time map." +msgstr "" + +#: js/build/jeoMap.js:1 +#: js/build/jeoStorymap.js:3 +msgid "Mapbox style layers could not be loaded because the composed style is unavailable." +msgstr "" + +#: js/build/jeoMap.js:1 +msgid "This map doesn't have layers" +msgstr "" + +#: js/build/jeoMap.js:1 +msgid "Show previous post" +msgstr "" + +#: js/build/jeoMap.js:1 +msgid "Show next post" +msgstr "" + +#. translators: %d: number of markers in the current map area. +#: js/build/jeoMap.js:2 +#, js-format +msgid "%d marker in this area" +msgid_plural "%d markers in this area" +msgstr[0] "" +msgstr[1] "" + +#. translators: %s is the authors list +#: js/build/jeoStorymap.js:3 +#, js-format +msgid "By %s" +msgstr "" + +#: js/build/jeoStorymap.js:3 +msgid "Mapbox style composition is unavailable for this storymap." +msgstr "" + +#: js/build/jeoStorymap.js:3 +msgid "Mapbox style composition did not return renderable layers for this storymap." +msgstr "" + +#: js/build/jeoStorymap.js:3 +msgid "Unable to load composed Mapbox style for this storymap." +msgstr "" + +#: js/build/jeoStorymap.js:3 +msgid "at" +msgstr "" + +#: js/build/jeoStorymap.js:3 +msgid "START" +msgstr "" + +#: js/build/jeoStorymap.js:3 +msgid "skip intro" +msgstr "" + +#: js/build/jeoStorymap.js:3 +msgid "Back to top" +msgstr "" + +#: js/build/jeoStorymap.js:3 +msgid "NAVIGATE THE MAP" +msgstr "" + +#: js/build/layersSidebar.js:7 +msgid "Select an option" +msgstr "" + +#: js/build/layersSidebar.js:7 +msgid "Source data download link" +msgstr "" + +#: js/build/layersSidebar.js:7 +msgid "Data attribution link" +msgstr "" + +#: js/build/layersSidebar.js:7 +msgid "Data attribution name" +msgstr "" + +#: js/build/layersSidebar.js:7 +msgid "Circles color" +msgstr "" + +#: js/build/layersSidebar.js:7 +msgid "Add new label" +msgstr "" + +#: js/build/layersSidebar.js:7 +msgid "Label" +msgstr "" + +#: js/build/layersSidebar.js:7 +msgid "Radius" +msgstr "" + +#: js/build/layersSidebar.js:7 +#: js/build/mapBlocks.js:1 +#: js/build/mapBlocks.js:3 +#: js/build/mapsSidebar.js:1 +msgid "Remove" +msgstr "" + +#: js/build/layersSidebar.js:7 +msgid "See preview" +msgstr "" + +#: js/build/layersSidebar.js:7 +msgid "Left Label" +msgstr "" + +#: js/build/layersSidebar.js:7 +msgid "Right Label" +msgstr "" + +#: js/build/layersSidebar.js:7 +msgid "Minimum size: 60x60 pixels" +msgstr "" + +#: js/build/layersSidebar.js:7 +msgid "Icon legend" +msgstr "" + +#: js/build/layersSidebar.js:7 +msgid "The uploaded icon is too small. The minimum size required is 60x60 pixels." +msgstr "" + +#: js/build/layersSidebar.js:7 +msgid "Ok" +msgstr "" + +#: js/build/layersSidebar.js:7 +#: js/build/mapBlocks.js:3 +#: js/build/mapsSidebar.js:1 +msgid "Type" +msgstr "" + +#: js/build/layersSidebar.js:7 +#: js/build/mapBlocks.js:1 +msgid "No" +msgstr "" + +#: js/build/layersSidebar.js:7 +msgid "On click" +msgstr "" + +#: js/build/layersSidebar.js:7 +msgid "On hover" +msgstr "" + +#: js/build/layersSidebar.js:7 +msgid "Show popup?" +msgstr "" + +#: js/build/layersSidebar.js:7 +#: js/build/mapBlocks.js:1 +msgid "Title" +msgstr "" + +#: js/build/layersSidebar.js:7 +msgid "Fields" +msgstr "" + +#: js/build/layersSidebar.js:7 +msgid "Add field" +msgstr "" + +#: js/build/layersSidebar.js:7 +#: js/build/mapBlocks.js:1 +#: js/build/mapBlocks.js:3 +#: js/build/mapsSidebar.js:1 +msgid "Add" +msgstr "" + +#: js/build/layersSidebar.js:7 +msgid "Interactions" +msgstr "" + +#: js/build/layersSidebar.js:7 +#: js/build/mapBlocks.js:3 +#: js/build/mapsSidebar.js:1 +msgid "Done" +msgstr "" + +#. translators: %d: number of maps whose composed style cache was refreshed. +#: js/build/layersSidebar.js:8 +#, js-format +msgid "%d map" +msgid_plural "%d maps" +msgstr[0] "" +msgstr[1] "" + +#. translators: %d: number of map refresh failures. +#: js/build/layersSidebar.js:9 +#, js-format +msgid "%d failure" +msgid_plural "%d failures" +msgstr[0] "" +msgstr[1] "" + +#. translators: 1: refreshed maps count label, 2: failed refresh count label. +#: js/build/layersSidebar.js:10 +#, js-format +msgid "Composed style cache refreshed for %1$s, with %2$s." +msgstr "" + +#. translators: %s: refreshed maps count label. +#: js/build/layersSidebar.js:11 +#, js-format +msgid "Composed style cache refreshed for %s." +msgstr "" + +#: js/build/layersSidebar.js:11 +msgid "Unable to refresh composed style cache." +msgstr "" + +#: js/build/layersSidebar.js:11 +msgid "Refreshing composed style cache" +msgstr "" + +#: js/build/layersSidebar.js:11 +msgid "Refresh composed style cache" +msgstr "" + +#: js/build/layersSidebar.js:11 +msgid "Edit interactions" +msgstr "" + +#: js/build/layersSidebar.js:11 +msgid "Attributions" +msgstr "" + +#: js/build/mapBlocks.js:1 +msgid "Insert a story map from the library" +msgstr "" + +#: js/build/mapBlocks.js:1 +msgid "Type a story name" +msgstr "" + +#: js/build/mapBlocks.js:1 +msgid "Please fill all required fields, you will not be able to publish or update until that." +msgstr "" + +#: js/build/mapBlocks.js:1 +msgid "Your Mapbox access token may be invalid. You will not be able to publish or update. Please check your settings." +msgstr "" + +#: js/build/mapBlocks.js:1 msgid "Your layer was not found. You will not be able to publish or update. Please check your settings." msgstr "" -#: js/build/layersSidebar.js:11 -msgid "Error loading your layer, you will not be able to publish or update. Please check your settings." +#: js/build/mapBlocks.js:1 +msgid "Error loading your layer, you will not be able to publish or update. Please check your settings." +msgstr "" + +#: js/build/mapBlocks.js:1 +msgid "(Select another map)" +msgstr "" + +#: js/build/mapBlocks.js:1 +msgid "Insert a map from the library" +msgstr "" + +#: js/build/mapBlocks.js:1 +msgid "Type a map name" +msgstr "" + +#: js/build/mapBlocks.js:1 +msgid "Preview zoom controls" +msgstr "" + +#: js/build/mapBlocks.js:1 +msgid "Initial Zoom" +msgstr "" + +#: js/build/mapBlocks.js:1 +msgid "Min Zoom" +msgstr "" + +#: js/build/mapBlocks.js:1 +msgid "Max Zoom" +msgstr "" + +#: js/build/mapBlocks.js:1 +msgid "Analyzing content…" +msgstr "" + +#: js/build/mapBlocks.js:1 +msgid "Searching for map layers…" +msgstr "" + +#: js/build/mapBlocks.js:1 +msgid "Generating map configuration…" +msgstr "" + +#: js/build/mapBlocks.js:1 +msgid "Save the post before generating the map." +msgstr "" + +#: js/build/mapBlocks.js:1 +msgid "Map generated from post content." +msgstr "" + +#: js/build/mapBlocks.js:1 +msgid "Failed to suggest layers." +msgstr "" + +#: js/build/mapBlocks.js:1 +msgid "Request failed." +msgstr "" + +#: js/build/mapBlocks.js:1 +msgid "Could not start map generation." +msgstr "" + +#: js/build/mapBlocks.js:1 +msgid "Failed to update map." +msgstr "" + +#: js/build/mapBlocks.js:1 +msgid "Could not send message." +msgstr "" + +#: js/build/mapBlocks.js:1 +msgid "Map generated from prompt." +msgstr "" + +#: js/build/mapBlocks.js:1 +msgid "Failed to generate from prompt." +msgstr "" + +#: js/build/mapBlocks.js:1 +msgid "Could not generate from prompt." +msgstr "" + +#: js/build/mapBlocks.js:1 +msgid "Map regenerated from post content." +msgstr "" + +#: js/build/mapBlocks.js:1 +msgid "AI-Assisted Map" +msgstr "" + +#: js/build/mapBlocks.js:1 +msgid "AI will analyze your content and suggest relevant map layers, center point, and zoom level." +msgstr "" + +#: js/build/mapBlocks.js:1 +msgid "Generate from post content" +msgstr "" + +#: js/build/mapBlocks.js:1 +msgid "Generate from prompt" +msgstr "" + +#: js/build/mapBlocks.js:1 +msgid "Map prompt" +msgstr "" + +#: js/build/mapBlocks.js:1 +msgid "Generate map" +msgstr "" + +#: js/build/mapBlocks.js:1 +msgid "Base Layer" +msgstr "" + +#: js/build/mapBlocks.js:1 +msgid "Base terrain" +msgstr "" + +#: js/build/mapBlocks.js:1 +msgctxt "base terrain" +msgid "Dark" +msgstr "" + +#: js/build/mapBlocks.js:1 +msgctxt "base terrain" +msgid "Light" +msgstr "" + +#: js/build/mapBlocks.js:1 +msgctxt "base terrain" +msgid "Satellite" +msgstr "" + +#: js/build/mapBlocks.js:1 +msgctxt "base terrain" +msgid "None" +msgstr "" + +#: js/build/mapBlocks.js:1 +msgid "Geolocation Pins" +msgstr "" + +#: js/build/mapBlocks.js:1 +msgid "Show pins" +msgstr "" + +#: js/build/mapBlocks.js:1 +msgid "Yes" +msgstr "" + +#: js/build/mapBlocks.js:1 +msgid "AI Assistant" +msgstr "" + +#: js/build/mapBlocks.js:1 +msgid "Assistant" +msgstr "" + +#: js/build/mapBlocks.js:1 +msgid "You" +msgstr "" + +#: js/build/mapBlocks.js:1 +msgid "Typing…" +msgstr "" + +#: js/build/mapBlocks.js:1 +msgid "Ask the AI to refine the map… (Shift+Enter for a line break)" +msgstr "" + +#: js/build/mapBlocks.js:1 +msgid "Send message" +msgstr "" + +#: js/build/mapBlocks.js:1 +msgid "Regenerate" +msgstr "" + +#: js/build/mapBlocks.js:1 +msgid "New prompt" +msgstr "" + +#: js/build/mapBlocks.js:1 +msgid "Describe the map you want… (Shift+Enter for a line break)" +msgstr "" + +#: js/build/mapBlocks.js:1 +msgid "Generate" +msgstr "" + +#: js/build/mapBlocks.js:1 +msgid "Vector style preview" +msgstr "" + +#: js/build/mapBlocks.js:1 +#: js/build/mapBlocks.js:3 +#: js/build/mapsSidebar.js:1 +msgid "Edit layers settings" +msgstr "" + +#: js/build/mapBlocks.js:1 +msgid "Understand chosen layers" msgstr "" -#: js/build/layersSidebar.js:11 -msgid "Your Mapbox API Key was not found in your JEO Settings. You will not be able to publish or update." +#: js/build/mapBlocks.js:1 +msgid "Black" msgstr "" -#: js/build/layersSidebar.js:11 -msgid "Please, check your settings." +#: js/build/mapBlocks.js:1 +msgid "90% Gray" msgstr "" -#: js/build/layersSidebar.js:11 -msgid "Attributions" +#: js/build/mapBlocks.js:1 +msgid "80% Gray" msgstr "" #: js/build/mapBlocks.js:1 -msgid "Insert a story map from the library" +msgid "70% Gray" msgstr "" #: js/build/mapBlocks.js:1 -msgid "Type a story name" +msgid "60% Gray" msgstr "" #: js/build/mapBlocks.js:1 -msgid "(Select another map)" +msgid "50% Gray" msgstr "" #: js/build/mapBlocks.js:1 -msgid "Insert a map from the library" +msgid "40% Gray" msgstr "" #: js/build/mapBlocks.js:1 -msgid "Type a map name" +msgid "30% Gray" msgstr "" #: js/build/mapBlocks.js:1 -msgid "Preview zoom controls" +msgid "20% Gray" msgstr "" #: js/build/mapBlocks.js:1 -msgid "Initial Zoom" +msgid "10% Gray" msgstr "" #: js/build/mapBlocks.js:1 -msgid "Min Zoom" +msgid "7.5% Gray" msgstr "" #: js/build/mapBlocks.js:1 -msgid "Max Zoom" +msgid "5% Gray" msgstr "" #: js/build/mapBlocks.js:1 -#: js/build/mapBlocks.js:3 -#: js/build/mapsSidebar.js:1 -msgid "Edit layers settings" +msgid "2.5% Gray" msgstr "" #: js/build/mapBlocks.js:1 -msgid "Black" +msgid "White" msgstr "" #: js/build/mapBlocks.js:1 -msgid "90% Gray" +msgid "Maroon" msgstr "" #: js/build/mapBlocks.js:1 -msgid "80% Gray" +msgid "Red" msgstr "" #: js/build/mapBlocks.js:1 -msgid "70% Gray" +msgid "Olive" +msgstr "" + +#: js/build/mapBlocks.js:1 +msgid "Yellow" +msgstr "" + +#: js/build/mapBlocks.js:1 +msgid "Green" +msgstr "" + +#: js/build/mapBlocks.js:1 +msgid "Lime" +msgstr "" + +#: js/build/mapBlocks.js:1 +msgid "Teal" +msgstr "" + +#: js/build/mapBlocks.js:1 +msgid "Aqua" +msgstr "" + +#: js/build/mapBlocks.js:1 +msgid "Navy" +msgstr "" + +#: js/build/mapBlocks.js:1 +msgid "Blue" +msgstr "" + +#: js/build/mapBlocks.js:1 +msgid "Purple" +msgstr "" + +#: js/build/mapBlocks.js:1 +msgid "Fuchsia" +msgstr "" + +#: js/build/mapBlocks.js:1 +#: js/build/postsSidebar.js:1 +msgid "Unable to load address suggestions." +msgstr "" + +#: js/build/mapBlocks.js:1 +#: js/build/postsSidebar.js:1 +msgid "Search address" +msgstr "" + +#: js/build/mapBlocks.js:1 +msgid "Story settings" +msgstr "" + +#: js/build/mapBlocks.js:1 +msgid "Brief description" +msgstr "" + +#: js/build/mapBlocks.js:1 +msgid "Show story map introduction" +msgstr "" + +#: js/build/mapBlocks.js:1 +msgid "Use \"Navigate to map\" button" +msgstr "" + +#: js/build/mapBlocks.js:1 +msgid "Slides settings" +msgstr "" + +#: js/build/mapBlocks.js:1 +msgid "Move slide up" +msgstr "" + +#: js/build/mapBlocks.js:1 +msgid "Move slide down" +msgstr "" + +#: js/build/mapBlocks.js:1 +msgid "Slide" +msgstr "" + +#: js/build/mapBlocks.js:1 +msgid "Lock current spot" +msgstr "" + +#: js/build/mapBlocks.js:1 +msgid "Content" +msgstr "" + +#: js/build/mapBlocks.js:1 +msgid "The minimum number of slides is 1." +msgstr "" + +#: js/build/mapBlocks.js:1 +msgid "Do you really want to remove this slide?" +msgstr "" + +#: js/build/mapBlocks.js:1 +msgid "Current slide:" +msgstr "" + +#: js/build/mapBlocks.js:1 +msgid "Map:" +msgstr "" + +#: js/build/mapBlocks.js:1 +msgid "JEO Map" +msgstr "" + +#: js/build/mapBlocks.js:1 +msgid "Display maps with layers and data" +msgstr "" + +#: js/build/mapBlocks.js:1 +msgid "JEO One-time Map" +msgstr "" + +#: js/build/mapBlocks.js:1 +msgid "Display an AI-generated contextual map with layers and geolocation pins" +msgstr "" + +#: js/build/mapBlocks.js:3 +msgid "Display maps with storytelling" +msgstr "" + +#: js/build/mapBlocks.js:3 +msgid "Embedded Story Map" +msgstr "" + +#: js/build/mapBlocks.js:3 +msgid "Stories Near You" +msgstr "" + +#: js/build/mapBlocks.js:3 +msgid "Display geolocated posts sorted by proximity to the reader" +msgstr "" + +#: js/build/mapBlocks.js:3 +msgid "— None —" +msgstr "" + +#: js/build/mapBlocks.js:3 +msgid "All geo-enabled types" +msgstr "" + +#: js/build/mapBlocks.js:3 +msgid "Location Preview" +msgstr "" + +#: js/build/mapBlocks.js:3 +msgid "Post Card" +msgstr "" + +#: js/build/mapBlocks.js:3 +msgid "Layout" +msgstr "" + +#: js/build/mapBlocks.js:3 +msgid "Grid" +msgstr "" + +#: js/build/mapBlocks.js:3 +msgid "List" +msgstr "" + +#: js/build/mapBlocks.js:3 +msgid "Columns" +msgstr "" + +#: js/build/mapBlocks.js:3 +msgid "Media position" +msgstr "" + +#: js/build/mapBlocks.js:3 +msgid "Top" +msgstr "" + +#: js/build/mapBlocks.js:3 +msgid "Left" +msgstr "" + +#: js/build/mapBlocks.js:3 +msgid "Right" +msgstr "" + +#: js/build/mapBlocks.js:3 +msgid "Behind" +msgstr "" + +#: js/build/mapBlocks.js:3 +msgid "Image shape" +msgstr "" + +#: js/build/mapBlocks.js:3 +msgid "Landscape" +msgstr "" + +#: js/build/mapBlocks.js:3 +msgid "Portrait" +msgstr "" + +#: js/build/mapBlocks.js:3 +msgid "Square" +msgstr "" + +#: js/build/mapBlocks.js:3 +msgid "Uncropped" +msgstr "" + +#: js/build/mapBlocks.js:3 +msgid "Image size" +msgstr "" + +#: js/build/mapBlocks.js:3 +msgid "Link featured image" +msgstr "" + +#: js/build/mapBlocks.js:3 +msgid "Query Settings" +msgstr "" + +#: js/build/mapBlocks.js:3 +msgid "Posts per page" +msgstr "" + +#: js/build/mapBlocks.js:3 +msgid "Search radius (km)" +msgstr "" + +#: js/build/mapBlocks.js:3 +msgid "Order by" +msgstr "" + +#: js/build/mapBlocks.js:3 +msgid "Most recent nearby" +msgstr "" + +#: js/build/mapBlocks.js:3 +msgid "Nearest first" +msgstr "" + +#: js/build/mapBlocks.js:3 +msgid "Relevance (distance + date)" msgstr "" -#: js/build/mapBlocks.js:1 -msgid "60% Gray" +#: js/build/mapBlocks.js:3 +msgid "Max age (days, 0 = no limit)" msgstr "" -#: js/build/mapBlocks.js:1 -msgid "50% Gray" +#: js/build/mapBlocks.js:3 +msgid "Distance weight" msgstr "" -#: js/build/mapBlocks.js:1 -msgid "40% Gray" +#: js/build/mapBlocks.js:3 +msgid "Date weight" msgstr "" -#: js/build/mapBlocks.js:1 -msgid "30% Gray" +#: js/build/mapBlocks.js:3 +msgid "Post type" msgstr "" -#: js/build/mapBlocks.js:1 -msgid "20% Gray" +#: js/build/mapBlocks.js:3 +msgid "Filters" msgstr "" -#: js/build/mapBlocks.js:1 -msgid "10% Gray" +#: js/build/mapBlocks.js:3 +#: js/build/mapsSidebar.js:1 +msgid "Categories" msgstr "" -#: js/build/mapBlocks.js:1 -msgid "7.5% Gray" +#: js/build/mapBlocks.js:3 +msgid "Exclude categories" msgstr "" -#: js/build/mapBlocks.js:1 -msgid "5% Gray" +#: js/build/mapBlocks.js:3 +msgid "Exclude tags" msgstr "" -#: js/build/mapBlocks.js:1 -msgid "2.5% Gray" +#: js/build/mapBlocks.js:3 +msgid "Display" msgstr "" -#: js/build/mapBlocks.js:1 -msgid "White" +#: js/build/mapBlocks.js:3 +msgid "Show featured image" msgstr "" -#: js/build/mapBlocks.js:1 -msgid "Maroon" +#: js/build/mapBlocks.js:3 +msgid "Show category" msgstr "" -#: js/build/mapBlocks.js:1 -msgid "Red" +#: js/build/mapBlocks.js:3 +msgid "Show date" msgstr "" -#: js/build/mapBlocks.js:1 -msgid "Olive" +#: js/build/mapBlocks.js:3 +msgid "Show excerpt" msgstr "" -#: js/build/mapBlocks.js:1 -msgid "Yellow" +#: js/build/mapBlocks.js:3 +msgid "Excerpt length" msgstr "" -#: js/build/mapBlocks.js:1 -msgid "Green" +#: js/build/mapBlocks.js:3 +msgid "Show author" msgstr "" -#: js/build/mapBlocks.js:1 -msgid "Lime" +#: js/build/mapBlocks.js:3 +msgid "Show avatar" msgstr "" -#: js/build/mapBlocks.js:1 -msgid "Teal" +#: js/build/mapBlocks.js:3 +msgid "Show read more link" msgstr "" -#: js/build/mapBlocks.js:1 -msgid "Aqua" +#: js/build/mapBlocks.js:3 +msgid "Read more label" msgstr "" -#: js/build/mapBlocks.js:1 -msgid "Navy" +#: js/build/mapBlocks.js:3 +msgid "Typography & Spacing" msgstr "" -#: js/build/mapBlocks.js:1 -msgid "Blue" +#: js/build/mapBlocks.js:3 +msgid "Type scale" msgstr "" -#: js/build/mapBlocks.js:1 -msgid "Purple" +#: js/build/mapBlocks.js:3 +msgid "Image scale" msgstr "" -#: js/build/mapBlocks.js:1 -msgid "Fuchsia" +#: js/build/mapBlocks.js:3 +msgid "Column gap" msgstr "" -#: js/build/mapBlocks.js:1 -#: js/build/postsSidebar.js:1 -msgid "Unable to load address suggestions." +#: js/build/mapBlocks.js:3 +msgid "Min height (vh)" msgstr "" -#: js/build/mapBlocks.js:1 -#: js/build/postsSidebar.js:1 -msgid "Search address" +#: js/build/mapBlocks.js:3 +msgid "Layer Editor Preview" msgstr "" -#: js/build/mapBlocks.js:1 -#: js/build/postsSidebar.js:1 -msgid "Search" +#: js/build/mapBlocks.js:3 +msgid "Interactive layer preview for the Map Layer post type editor." msgstr "" -#: js/build/mapBlocks.js:1 -msgid "Story settings" +#: js/build/mapBlocks.js:3 +msgid "Map Editor Preview" msgstr "" -#: js/build/mapBlocks.js:1 -msgid "Brief description" +#: js/build/mapBlocks.js:3 +msgid "Interactive map preview for the Map post type editor." msgstr "" -#: js/build/mapBlocks.js:1 -msgid "Show story map introduction" +#: js/build/mapBlocks.js:3 +#: js/build/mapsSidebar.js:1 +msgid "Loading layers data..." msgstr "" -#: js/build/mapBlocks.js:1 -msgid "Use \"Navigate to map\" button" +#: js/build/mapBlocks.js:3 +#: js/build/mapsSidebar.js:1 +msgid "Automatic style applied (this layer had no saved style). Edit layer settings to customize it." msgstr "" -#: js/build/mapBlocks.js:1 -msgid "Slides settings" +#: js/build/mapBlocks.js:3 +#: js/build/mapsSidebar.js:1 +msgid "Themes:" msgstr "" -#: js/build/mapBlocks.js:1 -msgid "Move slide up" +#: js/build/mapBlocks.js:3 +#: js/build/mapsSidebar.js:1 +msgid "Source:" msgstr "" -#: js/build/mapBlocks.js:1 -msgid "Move slide down" +#: js/build/mapBlocks.js:3 +#: js/build/mapsSidebar.js:1 +msgctxt "layer usage default label" +msgid "Default" msgstr "" -#: js/build/mapBlocks.js:1 -msgid "Slide" +#: js/build/mapBlocks.js:3 +#: js/build/mapsSidebar.js:1 +msgid "Fill Color" msgstr "" -#: js/build/mapBlocks.js:1 -msgid "Lock current spot" +#: js/build/mapBlocks.js:3 +#: js/build/mapsSidebar.js:1 +msgid "Outline Color" msgstr "" -#: js/build/mapBlocks.js:1 -msgid "Content" +#: js/build/mapBlocks.js:3 +#: js/build/mapsSidebar.js:1 +msgid "Line Color" msgstr "" -#: js/build/mapBlocks.js:1 -msgid "The minimum number of slides is 1." +#: js/build/mapBlocks.js:3 +#: js/build/mapsSidebar.js:1 +msgid "Circle Color" msgstr "" -#: js/build/mapBlocks.js:1 -msgid "Do you really want to remove this slide?" +#: js/build/mapBlocks.js:3 +#: js/build/mapsSidebar.js:1 +msgid "Stroke Color" msgstr "" -#: js/build/mapBlocks.js:1 -msgid "Current slide:" +#: js/build/mapBlocks.js:3 +#: js/build/mapsSidebar.js:1 +msgid "Fill Opacity" msgstr "" -#: js/build/mapBlocks.js:1 -msgid "Map:" +#: js/build/mapBlocks.js:3 +#: js/build/mapsSidebar.js:1 +msgid "Line Opacity" msgstr "" -#: js/build/mapBlocks.js:1 -msgid "JEO Map" +#: js/build/mapBlocks.js:3 +#: js/build/mapsSidebar.js:1 +msgid "Circle Opacity" msgstr "" -#: js/build/mapBlocks.js:1 -msgid "Display maps with layers and data" +#: js/build/mapBlocks.js:3 +#: js/build/mapsSidebar.js:1 +msgid "Line Width" msgstr "" -#: js/build/mapBlocks.js:1 -msgid "JEO One-time Map" +#: js/build/mapBlocks.js:3 +#: js/build/mapsSidebar.js:1 +msgid "Circle Radius" msgstr "" #: js/build/mapBlocks.js:3 -msgid "Display maps with storytelling" +#: js/build/mapsSidebar.js:1 +msgid "Stroke Width" msgstr "" #: js/build/mapBlocks.js:3 -msgid "Embedded Story Map" +#: js/build/mapsSidebar.js:1 +msgid "Layer Style" msgstr "" #: js/build/mapBlocks.js:3 -msgid "Map Editor Preview" +#: js/build/mapsSidebar.js:1 +msgid "Styling is only supported for fill, line, and circle layer types." msgstr "" #: js/build/mapBlocks.js:3 -msgid "Interactive map preview for the Map post type editor." +#: js/build/mapsSidebar.js:1 +msgid "AI Default Style" msgstr "" #: js/build/mapBlocks.js:3 -msgid "Layer Editor Preview" +#: js/build/mapsSidebar.js:1 +msgid "Use AI-suggested default style" msgstr "" #: js/build/mapBlocks.js:3 -msgid "Interactive layer preview for the Map Layer post type editor." +#: js/build/mapsSidebar.js:1 +msgid "Filter:" msgstr "" #: js/build/mapBlocks.js:3 #: js/build/mapsSidebar.js:1 -msgid "Loading layers data..." +msgid "Paint:" msgstr "" #: js/build/mapBlocks.js:3 #: js/build/mapsSidebar.js:1 -msgid "Default" +msgid "Opacity" msgstr "" #: js/build/mapBlocks.js:3 @@ -1891,6 +4988,11 @@ msgstr "" msgid "Show legend" msgstr "" +#: js/build/mapBlocks.js:3 +#: js/build/mapsSidebar.js:1 +msgid "Style layer" +msgstr "" + #: js/build/mapBlocks.js:3 #: js/build/mapsSidebar.js:1 msgid "If the layer is not visible in Mapbox Studio, we will not be able to control its visibility." @@ -1901,6 +5003,11 @@ msgstr "" msgid "Select a layer type" msgstr "" +#: js/build/mapBlocks.js:3 +#: js/build/mapsSidebar.js:1 +msgid "Do you really want to delete this layer?" +msgstr "" + #: js/build/mapBlocks.js:3 #: js/build/mapsSidebar.js:1 msgid "Enter keywords to search layers" @@ -1926,11 +5033,6 @@ msgstr "" msgid "Add to map" msgstr "" -#: js/build/mapBlocks.js:3 -#: js/build/mapsSidebar.js:1 -msgid "Do you really want to delete this layer?" -msgstr "" - #: js/build/mapBlocks.js:3 #: js/build/mapsSidebar.js:1 msgid "Remove from map" @@ -2048,16 +5150,19 @@ msgstr "" #: js/build/mapBlocks.js:3 #: js/build/mapsSidebar.js:1 +msgctxt "layer" msgid "Fixed" msgstr "" #: js/build/mapBlocks.js:3 #: js/build/mapsSidebar.js:1 +msgctxt "layer" msgid "Swappable" msgstr "" #: js/build/mapBlocks.js:3 #: js/build/mapsSidebar.js:1 +msgctxt "layer" msgid "Switchable" msgstr "" @@ -2086,11 +5191,6 @@ msgstr "" msgid "Use related posts" msgstr "" -#: js/build/mapBlocks.js:3 -#: js/build/mapsSidebar.js:1 -msgid "Categories" -msgstr "" - #: js/build/mapBlocks.js:3 #: js/build/mapsSidebar.js:1 msgid "Start date" @@ -2127,10 +5227,12 @@ msgid "Relevance" msgstr "" #: js/build/postsSidebar.js:1 +#: js/build/postsSidebar.js:3 msgid "Primary" msgstr "" #: js/build/postsSidebar.js:1 +#: js/build/postsSidebar.js:3 msgid "Secondary" msgstr "" @@ -2142,30 +5244,155 @@ msgstr "" msgid "Save new point" msgstr "" -#: js/build/postsSidebar.js:1 +#. translators: %d: number of geolocation points. +#: js/build/postsSidebar.js:2 +#, js-format +msgid "Point (%d)" +msgid_plural "Points (%d)" +msgstr[0] "" +msgstr[1] "" + +#: js/build/postsSidebar.js:2 +#: js/build/postsSidebar.js:4 +msgid "Geolocate this post" +msgstr "" + +#: js/build/postsSidebar.js:2 +#: js/build/postsSidebar.js:3 +msgid "Expand panel" +msgstr "" + +#: js/build/postsSidebar.js:2 +#: js/build/postsSidebar.js:3 +msgid "Minimize panel" +msgstr "" + +#: js/build/postsSidebar.js:2 msgid "Add new point" msgstr "" -#: js/build/postsSidebar.js:1 +#: js/build/postsSidebar.js:2 msgid "Current points" msgstr "" -#: js/build/postsSidebar.js:1 +#: js/build/postsSidebar.js:2 msgid "No points" msgstr "" -#: js/build/postsSidebar.js:1 -msgid "Delete" +#. translators: %d: number of selected AI suggestions. +#: js/build/postsSidebar.js:3 +#, js-format +msgid "AI Suggestion (%d selected)" +msgid_plural "AI Suggestions (%d selected)" +msgstr[0] "" +msgstr[1] "" + +#: js/build/postsSidebar.js:3 +#: js/build/postsSidebar.js:4 +msgid "Review AI Suggestions" msgstr "" -#: js/build/postsSidebar.js:1 -msgid "Edit" +#: js/build/postsSidebar.js:3 +msgid "Select locations to add to the map:" msgstr "" -#: js/build/postsSidebar.js:1 -msgid "Geolocate this post" +#: js/build/postsSidebar.js:3 +msgid "Enriched via Geocoder" msgstr "" -#: js/build/postsSidebar.js:1 +#: js/build/postsSidebar.js:3 +msgid "AI flagged this as primary location" +msgstr "" + +#: js/build/postsSidebar.js:3 +msgid "Enrich" +msgstr "" + +#: js/build/postsSidebar.js:3 +msgid "Context:" +msgstr "" + +#: js/build/postsSidebar.js:3 +msgid "Verified:" +msgstr "" + +#: js/build/postsSidebar.js:3 +msgid "Score too low for reliable mapping." +msgstr "" + +#: js/build/postsSidebar.js:3 +msgid "Discard All" +msgstr "" + +#: js/build/postsSidebar.js:3 +msgid "Retrying..." +msgstr "" + +#: js/build/postsSidebar.js:3 +msgid "Retry AI" +msgstr "" + +#: js/build/postsSidebar.js:3 +msgid "Add to Map" +msgstr "" + +#: js/build/postsSidebar.js:3 +msgid "Invalid response from AI." +msgstr "" + +#: js/build/postsSidebar.js:3 +msgid "No locations found by the AI." +msgstr "" + +#: js/build/postsSidebar.js:3 +msgid "Error processing AI georeference." +msgstr "" + +#: js/build/postsSidebar.js:3 +msgctxt "Artifical Intelligence" +msgid "AI" +msgstr "" + +#. translators: %d: number of locations found during bulk AI processing. +#: js/build/postsSidebar.js:4 +#, js-format +msgid "AI found %d location during bulk processing." +msgid_plural "AI found %d locations during bulk processing." +msgstr[0] "" +msgstr[1] "" + +#: js/build/postsSidebar.js:4 +msgid "Review Suggestions" +msgstr "" + +#: js/build/postsSidebar.js:4 +msgid "Processing AI..." +msgstr "" + +#: js/build/postsSidebar.js:4 +msgid "Geolocate with AI" +msgstr "" + +#: js/build/postsSidebar.js:4 msgid "Geolocation" msgstr "" + +#: js/build/storiesNearYou.js:1 +msgid "You denied location access. Please enable location permissions in your browser settings." +msgstr "" + +#: js/build/storiesNearYou.js:1 +msgid "Your browser could not determine your location. This may be due to strict privacy settings or because GPS is unavailable on your device." +msgstr "" + +#: js/build/storiesNearYou.js:1 +msgid "Location request timed out. Please try again or use the configured location." +msgstr "" + +#: js/build/storiesNearYou.js:1 +msgid "This browser does not support location services." +msgstr "" + +#: js/build/storiesNearYou.js:1 +msgid "Unable to load stories. Please check your connection and try again." +msgstr "" diff --git a/src/readme.txt b/src/readme.txt index f5ad43b34..4894bfcb7 100755 --- a/src/readme.txt +++ b/src/readme.txt @@ -1,12 +1,13 @@ === JEO Maps === Contributors: infoamazonia +Tags: maps, geolocation, gutenberg, storymap, map, geocoding, journalism, interactive-maps, blocks, openstreetmap, news, maplibre Tested up to: 7.0 -Stable tag: 3.1.2 -Requires PHP: 8.0 +Stable tag: 4.0.0-beta.1 +Requires PHP: 8.2 Requires at least: 6.6 License: GPL-3.0-only License URI: https://www.gnu.org/licenses/gpl-3.0.html -Version: 3.1.2 +Version: 4.0.0-beta.1 Geojournalism platform for building maps, geolocating posts, and publishing interactive storymaps in WordPress. @@ -50,10 +51,13 @@ JEO includes: JEO can connect to the following third-party services: -* Mapbox, only when you select Mapbox as the rendering library or use Mapbox-hosted map resources. The site loads JavaScript/CSS from `api.mapbox.com`, and Mapbox receives the configured access token, visitor IP address, browser details, and requested map resources. Terms of Service: [https://www.mapbox.com/legal/tos](https://www.mapbox.com/legal/tos). Privacy Policy: [https://www.mapbox.com/legal/privacy](https://www.mapbox.com/legal/privacy). +* Mapbox, only when you select Mapbox as the rendering library, use Mapbox-hosted map resources, or use AI-generated layers that publish composed styles or vector tilesets. The site loads JavaScript/CSS from `api.mapbox.com`, and Mapbox receives the configured access token, visitor IP address, browser details, and requested map resources. Terms of Service: [https://www.mapbox.com/legal/tos](https://www.mapbox.com/legal/tos). Privacy Policy: [https://www.mapbox.com/legal/privacy](https://www.mapbox.com/legal/privacy). * Nominatim (OpenStreetMap), only when an editor explicitly runs an address search or reverse-geocodes a point in the post geolocation UI. The typed address or selected coordinates, the site URL in the request user agent, and the server IP address are sent to `nominatim.openstreetmap.org`. Usage Policy: [https://operations.osmfoundation.org/policies/nominatim/](https://operations.osmfoundation.org/policies/nominatim/). Privacy Policy: [https://osmfoundation.org/wiki/Privacy_Policy](https://osmfoundation.org/wiki/Privacy_Policy). * OpenStreetMap raster tiles, used by the default MapLibre preview style. The tile service receives the visitor IP address, browser details, and requested tile URLs. Tile Usage Policy: [https://operations.osmfoundation.org/policies/tiles/](https://operations.osmfoundation.org/policies/tiles/). Privacy Policy: [https://osmfoundation.org/wiki/Privacy_Policy](https://osmfoundation.org/wiki/Privacy_Policy). -* Optional external typography and footer logo URLs, only when the site administrator explicitly configures those fields with an external host. In that case the visitor browser requests the asset directly from the chosen host, which may receive the visitor IP address, browser details, and referrer according to that provider's own terms and privacy policy. +* Overpass API (OpenStreetMap), only when the AI boundary resolver falls back to OpenStreetMap for international boundaries. The place name is sent to `overpass-api.de` (or another configured Overpass endpoint) and the server IP address is included in the request. Usage Policy: [https://wiki.openstreetmap.org/wiki/Overpass_API](https://wiki.openstreetmap.org/wiki/Overpass_API). Privacy Policy: [https://osmfoundation.org/wiki/Privacy_Policy](https://osmfoundation.org/wiki/Privacy_Policy). +* IBGE (Brazilian Institute of Geography and Statistics), only when resolving Brazilian municipality or state boundaries. The place name is sent to `servicodados.ibge.gov.br` and the server IP address is included in the request. Terms of Use: [https://www.ibge.gov.br/](https://www.ibge.gov.br/). Privacy Policy: [https://www.ibge.gov.br/](https://www.ibge.gov.br/). +* FUNAI (National Indian Foundation), only when resolving Brazilian indigenous land boundaries. The place name is sent to the FUNAI WFS endpoint and the server IP address is included in the request. Privacy Policy: [https://www.gov.br/funai/](https://www.gov.br/funai/). +* AI providers (Gemini, OpenAI, Anthropic, etc.), only when an administrator configures an AI provider in JEO AI Settings and an editor uses an AI-powered feature (georeferencing, context suggestions, minimap layer generation, etc.). The prompt, post content, or layer description is sent to the configured provider's API using the administrator's API key. Data handling is governed by the chosen provider's terms and privacy policy. = Where can I find more tutorials and developer documentation? = @@ -61,7 +65,15 @@ Visit [https://www.jeowp.org/](https://www.jeowp.org/) for tutorials, user guide == Third Party Services == -See "Which third-party services can JEO connect to?" in the FAQ above for the required service disclosures. +JEO connects to the following third-party services only when the corresponding feature is enabled or explicitly used: + +* **Mapbox** — Map rendering library, hosted styles, vector/raster tilesets, and AI-generated style publishing. Requires a configured access token. [Terms](https://www.mapbox.com/legal/tos) · [Privacy](https://www.mapbox.com/legal/privacy). +* **Nominatim / OpenStreetMap** — Address search and reverse geocoding in the post geolocation UI. [Usage Policy](https://operations.osmfoundation.org/policies/nominatim/) · [Privacy](https://osmfoundation.org/wiki/Privacy_Policy). +* **OpenStreetMap tiles** — Default raster tile background in MapLibre previews. [Tile Policy](https://operations.osmfoundation.org/policies/tiles/) · [Privacy](https://osmfoundation.org/wiki/Privacy_Policy). +* **Overpass API** — Boundary geometry fallback for international places. [Wiki](https://wiki.openstreetmap.org/wiki/Overpass_API) · [Privacy](https://osmfoundation.org/wiki/Privacy_Policy). +* **IBGE** — Boundary geometry for Brazilian municipalities and states. [Terms](https://www.ibge.gov.br/). +* **FUNAI** — Boundary geometry for Brazilian indigenous lands. [Site](https://www.gov.br/funai/). +* **AI providers** (Gemini, OpenAI, Anthropic, etc.) — Used by JEO AI features when configured by an administrator. Governed by the selected provider's terms and privacy policy. == Screenshots == 1. Embed interactive maps in posts with reader-facing layer and legend controls. diff --git a/src/templates/map-content-layers-list.php b/src/templates/map-content-layers-list.php index 2c88b0b0a..1ca66d5f5 100644 --- a/src/templates/map-content-layers-list.php +++ b/src/templates/map-content-layers-list.php @@ -14,7 +14,9 @@ 0 ) : ?> - 0 ) : ?> + + + 0 ) : ?> diff --git a/src/templates/single-map.php b/src/templates/single-map.php index 9d199dc35..ffeb60955 100644 --- a/src/templates/single-map.php +++ b/src/templates/single-map.php @@ -49,13 +49,17 @@ class="jeomap map_id_ID ); ?>"
    - -
    - data-preview-map="" - - >
    +
    + ', '

' ); ?> + +
+
+ data-preview-map="" + + >
+
diff --git a/src/uninstall.php b/src/uninstall.php new file mode 100644 index 000000000..631ba443c --- /dev/null +++ b/src/uninstall.php @@ -0,0 +1,124 @@ + 'jeo-ai-log', + 'posts_per_page' => -1, + 'post_status' => 'any', + 'fields' => 'ids', + ) +); +foreach ( $ai_logs as $log_id ) { + wp_delete_post( $log_id, true ); +} + +// ------------------------------------------------------------------ +// 4. POST METADATA +// ------------------------------------------------------------------ +// Remove _related_point and all derived _geocode_* index meta from posts. +$meta_keys = array( + '_related_point', + '_geocode_lat', + '_geocode_lon', + '_geocode_lat_p', + '_geocode_lon_p', + '_geocode_lat_s', + '_geocode_lon_s', + '_geocode_city_level_1', + '_geocode_city', + '_geocode_region_level_3', + '_geocode_region_level_2', + '_geocode_region_level_1', + '_geocode_country_code', + '_geocode_country', + '_geocode_address', + '_geocode_address_number', + '_geocode_postcode', + '_geocode_full_address', + '_jeo_ai_context_conversation_id', + '_jeo_ai_context_last_response', + '_jeo_ai_context_chat_messages', + '_jeo_ai_context_suggestion_history', +); + +global $wpdb; +foreach ( $meta_keys as $key ) { + $wpdb->delete( $wpdb->postmeta, array( 'meta_key' => $key ), array( '%s' ) ); +} + +// ------------------------------------------------------------------ +// 5. TRANSIENTS +// ------------------------------------------------------------------ +// Nominatim geocoding cache entries. +$wpdb->query( "DELETE FROM {$wpdb->options} WHERE option_name LIKE '_transient_jeo_nominatim_%' OR option_name LIKE '_transient_timeout_jeo_nominatim_%'" ); + +// ------------------------------------------------------------------ +// 6. RAG VECTOR STORE +// ------------------------------------------------------------------ +// Remove the local filesystem vector store directory. +$upload_dir = wp_upload_dir(); +$store_dir = $upload_dir['basedir'] . '/jeo-ai-store'; +if ( is_dir( $store_dir ) ) { + jeo_recursive_rmdir_uninstall( $store_dir ); +} + +/** + * Recursively remove a directory and its contents. + * + * @param string $dir Directory path. + * @return void + */ +function jeo_recursive_rmdir_uninstall( $dir ) { + $files = array_diff( scandir( $dir ), array( '.', '..' ) ); + foreach ( $files as $file ) { + $path = $dir . DIRECTORY_SEPARATOR . $file; + is_dir( $path ) ? jeo_recursive_rmdir_uninstall( $path ) : wp_delete_file( $path ); + } + rmdir( $dir ); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_rmdir +} diff --git a/webpack.config.js b/webpack.config.js index e793c593e..4cbc1b85b 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -1,8 +1,26 @@ const path = require( 'path' ); +const webpack = require( 'webpack' ); const defaultConfig = require( './node_modules/@wordpress/scripts/config/webpack.config' ); module.exports = { ...defaultConfig, + plugins: [ + ...( defaultConfig.plugins || [] ), + new webpack.NormalModuleReplacementPlugin( + /^node:(.*)$/, + ( resource ) => { + resource.request = resource.request.replace( /^node:/, '' ); + } + ), + ], + resolve: { + ...defaultConfig.resolve, + fallback: { + ...( defaultConfig.resolve?.fallback ), + fs: false, + path: false, + }, + }, entry: { mapglLoader: './src/js/src/lib/mapgl-loader.js', mapglReact: { @@ -13,6 +31,7 @@ module.exports = { JeoLayer: './src/includes/layer-types/JeoLayer.js', JeoLegend: './src/includes/legend-types/JeoLegend.js', postsSidebar: './src/js/src/posts-sidebar/index.js', + contextSidebar: './src/js/src/context-sidebar/index.js', jeoMap: { import: './src/js/src/jeo-map/index.js', @@ -39,6 +58,7 @@ module.exports = { import: './src/js/src/maps-sidebar/index.js', dependOn: ['mapglReact'], }, + storiesNearYou: './src/js/src/stories-near-you/stories-near-you-frontend.js', }, output: { path: path.resolve( __dirname, './src/js/build/' ),