diff --git a/crumb.php b/crumb.php index 2224e63..6996f35 100644 --- a/crumb.php +++ b/crumb.php @@ -3,7 +3,7 @@ * Plugin Name: Crumb * Plugin URI: https://wordpress.org/plugins/crumb/ * Description: Embeds the Crumb meeting finder widget on any page or post using a shortcode. - * Version: 1.4.0 + * Version: 1.5.0 * Author: bmltenabled * Author URI: https://bmlt.app * License: GPL v2 or later @@ -15,19 +15,23 @@ exit; } -define( 'CRUMB_VERSION', '1.4.0' ); +define( 'CRUMB_VERSION', '1.5.0' ); class Crumb { private static ?self $instance = null; private static ?bool $shortcode_geolocation = null; private static ?int $shortcode_geolocation_radius = null; + private static ?string $shortcode_language = null; private static ?array $crouton_options = null; private static array $compat_tags = []; const DEFAULT_CDN_URL = 'https://cdn.aws.bmlt.app/crumb-widget.js'; const REWRITE_VERSION = '1'; + /** Languages the widget supports (kept in sync with src/stores/localization.ts). */ + const SUPPORTED_LANGUAGES = [ 'en', 'es', 'fr', 'de', 'pt', 'it', 'sv', 'da', 'el', 'fa', 'pl', 'ru', 'ja' ]; + /** * Tag → forced view for crouton-named shortcodes. * Map-flavored tags become "both" (map + list) so users don't lose the list view @@ -254,6 +258,7 @@ public static function setup_shortcode( array $atts ): string { 'geolocation_radius' => null, 'update_url' => null, 'columns' => null, + 'language' => null, ], $atts, 'crumb' @@ -271,6 +276,15 @@ public static function setup_shortcode( array $atts ): string { } } + // Language must be one of the codes the widget bundles; otherwise we drop it + // and let the widget auto-detect from navigator.language. + if ( null !== $atts['language'] ) { + $lang = strtolower( trim( (string) $atts['language'] ) ); + if ( in_array( $lang, self::SUPPORTED_LANGUAGES, true ) ) { + self::$shortcode_language = $lang; + } + } + // Shortcode attribute takes precedence; fall back to saved option only when not provided. // Crouton settings (bmlt_tabs_options) are used as a fallback when the crumb option is empty. $server = esc_url( trim( $atts['server'] ?? self::get_option_or_crouton( 'crumb_server', 'https://latest.aws.bmlt.app/main_server/' ) ) ); @@ -433,6 +447,17 @@ public static function localize_config(): void { $config['geolocationRadius'] = self::$shortcode_geolocation_radius; } + // Shortcode language overrides; otherwise fall back to the saved option. + // Empty string in either path means "let the widget auto-detect". + if ( null !== self::$shortcode_language ) { + $config['language'] = self::$shortcode_language; + } elseif ( ! isset( $config['language'] ) ) { + $saved_language = get_option( 'crumb_language', '' ); + if ( '' !== $saved_language && in_array( $saved_language, self::SUPPORTED_LANGUAGES, true ) ) { + $config['language'] = $saved_language; + } + } + if ( ! empty( $config ) ) { wp_add_inline_script( 'crumb', 'var CrumbWidgetConfig = ' . wp_json_encode( $config ) . ';', 'before' ); } @@ -477,6 +502,11 @@ public static function sanitize_geolocation_radius( string $input ): string { return ( 0 !== $val ) ? (string) $val : ''; } + public static function sanitize_language( string $input ): string { + $lang = strtolower( trim( $input ) ); + return in_array( $lang, self::SUPPORTED_LANGUAGES, true ) ? $lang : ''; + } + public static function sanitize_config( string $input ): string { $input = trim( $input ); @@ -528,6 +558,14 @@ public static function register_settings(): void { register_setting( $group, 'crumb_css_template', 'sanitize_text_field' ); register_setting( $group, 'crumb_view', 'sanitize_text_field' ); register_setting( $group, 'crumb_update_url', 'sanitize_text_field' ); + register_setting( + $group, + 'crumb_language', + [ + 'type' => 'string', + 'sanitize_callback' => [ static::class, 'sanitize_language' ], + ] + ); register_setting( $group, 'crumb_geolocation_radius', @@ -658,6 +696,38 @@ class="regular-text" placeholder="meetings" />
Optional. Sets the default view when the widget loads. Can be overridden at runtime via the ?view= query parameter, or per-page via the shortcode view attribute.
Optional. Forces the widget UI language. Default behavior is to detect from the visitor's browser (navigator.language). Can be overridden per-page via the shortcode language attribute.
[crumb]
- [crumb server="https://your-server/main_server" service_body="42" format_ids="17,54" view="map" geolocation="true" geolocation_radius="-50"]
+ [crumb server="https://your-server/main_server" service_body="42" format_ids="17,54" view="map" geolocation="true" geolocation_radius="-50" language="es"]
diff --git a/readme.txt b/readme.txt
index d4014c9..eb6f0ba 100644
--- a/readme.txt
+++ b/readme.txt
@@ -5,7 +5,7 @@ Tags: narcotics anonymous, na, meetings, bmlt, meeting finder
Requires at least: 6.0
Tested up to: 7.0
Requires PHP: 8.1
-Stable tag: 1.4.0
+Stable tag: 1.5.0
License: GPLv2 or later
License URI: http://www.gnu.org/licenses/gpl-2.0.html
@@ -42,6 +42,7 @@ Shortcode attributes:
* `geolocation_radius` — Geolocation search radius. Positive integer = fixed radius in miles (or km per server settings). Negative integer = BMLT auto-radius: the server expands the search until it finds roughly that many meetings (e.g. `-50` finds ~50 nearby meetings). Overrides the Geolocation Radius setting and Widget Configuration.
* `update_url` — URL template for the **Update Meeting Info** link shown at the bottom of the meeting detail panel. Supports tokens `{meeting_id}`, `{meeting_name}`, `{server_url}`, `{return_url}` (URL-encoded on substitution). Works with bmlt-workflow, hosted forms, or `mailto:` URLs.
* `columns` — Comma-separated list of columns to show in list view (e.g. `time,name,location,address,service_body`). Omit a name to hide that column. Leave unset to use the widget default.
+* `language` — Force the widget UI language for this page (e.g. `en`, `es`, `fr`, `de`, `pt`, `it`, `sv`, `da`, `el`, `fa`, `pl`, `ru`, `ja`). Leave unset to auto-detect from the visitor's browser.
= Switching from Crouton =
@@ -125,6 +126,9 @@ The widget fetches meeting data from a BMLT server whose URL you configure in Se
== Changelog ==
+= 1.5.0 =
+* Added **Language** setting on the global settings page and matching `language` shortcode attribute (e.g. `[crumb language="es"]`). Forces the widget UI language; leave empty to auto-detect from the visitor's browser. Supported codes: `en`, `es`, `fr`, `de`, `pt`, `it`, `sv`, `da`, `el`, `fa`, `pl`, `ru`, `ja`. Per-shortcode value overrides the saved setting; `widget_config` JSON `language` key still wins over both.
+
= 1.4.0 =
* Added `columns` shortcode attribute — comma-separated list of columns to show in list view (e.g. `[crumb columns="time,name,location,address,service_body"]`). Omit a name to hide that column. Pairs with the new `data-columns` reader in the Crumb widget; leave unset to use the widget default.
* Crouton compatibility: `has_areas` and `has_regions` on `[bmlt_tabs]` / `[bmlt_map]` / `[crouton_tabs]` / `[crouton_map]` now add the `service_body` column to the widget output, preserving crouton's behavior of surfacing the originating service body in the listing.
diff --git a/tests/test-crumb.php b/tests/test-crumb.php
index 40a9410..10950ca 100644
--- a/tests/test-crumb.php
+++ b/tests/test-crumb.php
@@ -11,7 +11,7 @@ public function setUp(): void {
$GLOBALS['wp_scripts'] = new WP_Scripts();
// Reset private statics on Crumb.
$ref = new ReflectionClass( Crumb::class );
- foreach ( [ 'shortcode_geolocation', 'shortcode_geolocation_radius', 'crouton_options' ] as $prop ) {
+ foreach ( [ 'shortcode_geolocation', 'shortcode_geolocation_radius', 'shortcode_language', 'crouton_options' ] as $prop ) {
$p = $ref->getProperty( $prop );
$p->setAccessible( true );
$p->setValue( null, null );
@@ -414,6 +414,92 @@ public function test_shortcode_geolocation_radius_negative_is_accepted() {
$this->assertSame( -50, $config['geolocationRadius'] );
}
+ // -------------------------------------------------------------------------
+ // language shortcode attribute + option
+ // -------------------------------------------------------------------------
+
+ public function test_shortcode_language_sets_config() {
+ $this->enqueue_crumb();
+ do_shortcode( '[crumb language="es"]' );
+
+ Crumb::localize_config();
+
+ $config = $this->get_inline_config();
+ $this->assertNotNull( $config );
+ $this->assertSame( 'es', $config['language'] );
+ }
+
+ public function test_shortcode_language_invalid_is_dropped() {
+ $this->enqueue_crumb();
+ do_shortcode( '[crumb language="banana"]' );
+
+ Crumb::localize_config();
+
+ $raw = wp_scripts()->get_data( 'crumb', 'before' );
+ $this->assertEmpty( $raw );
+ }
+
+ public function test_saved_language_option_sets_config() {
+ $this->enqueue_crumb();
+ update_option( 'crumb_language', 'fr' );
+
+ Crumb::localize_config();
+
+ $config = $this->get_inline_config();
+ $this->assertNotNull( $config );
+ $this->assertSame( 'fr', $config['language'] );
+
+ delete_option( 'crumb_language' );
+ }
+
+ public function test_shortcode_language_overrides_saved_option() {
+ $this->enqueue_crumb();
+ update_option( 'crumb_language', 'fr' );
+ do_shortcode( '[crumb language="de"]' );
+
+ Crumb::localize_config();
+
+ $config = $this->get_inline_config();
+ $this->assertNotNull( $config );
+ $this->assertSame( 'de', $config['language'] );
+
+ delete_option( 'crumb_language' );
+ }
+
+ public function test_widget_config_json_language_takes_precedence_over_option() {
+ // JSON widget_config is loaded first; saved option only fills in when language is unset.
+ $this->enqueue_crumb();
+ update_option( 'crumb_widget_config', '{"language":"pt"}' );
+ update_option( 'crumb_language', 'fr' );
+
+ Crumb::localize_config();
+
+ $config = $this->get_inline_config();
+ $this->assertNotNull( $config );
+ $this->assertSame( 'pt', $config['language'] );
+
+ delete_option( 'crumb_widget_config' );
+ delete_option( 'crumb_language' );
+ }
+
+ public function test_sanitize_language_accepts_supported_code() {
+ $this->assertSame( 'es', Crumb::sanitize_language( 'es' ) );
+ $this->assertSame( 'ja', Crumb::sanitize_language( 'ja' ) );
+ }
+
+ public function test_sanitize_language_rejects_unsupported() {
+ $this->assertSame( '', Crumb::sanitize_language( 'banana' ) );
+ $this->assertSame( '', Crumb::sanitize_language( 'EN-US' ) );
+ }
+
+ public function test_sanitize_language_empty_returns_empty() {
+ $this->assertSame( '', Crumb::sanitize_language( '' ) );
+ }
+
+ public function test_sanitize_language_lowercases_and_trims() {
+ $this->assertSame( 'fr', Crumb::sanitize_language( ' FR ' ) );
+ }
+
// -------------------------------------------------------------------------
// sanitize_geolocation_radius
// -------------------------------------------------------------------------