Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
76 changes: 73 additions & 3 deletions crumb.php
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -254,6 +258,7 @@ public static function setup_shortcode( array $atts ): string {
'geolocation_radius' => null,
'update_url' => null,
'columns' => null,
'language' => null,
],
$atts,
'crumb'
Expand All @@ -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/' ) ) );
Expand Down Expand Up @@ -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' );
}
Expand Down Expand Up @@ -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 );

Expand Down Expand Up @@ -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',
Expand Down Expand Up @@ -658,6 +696,38 @@ class="regular-text" placeholder="meetings" />
<p class="description">Optional. Sets the default view when the widget loads. Can be overridden at runtime via the <code>?view=</code> query parameter, or per-page via the shortcode <code>view</code> attribute.</p>
</td>
</tr>
<tr>
<th scope="row"><label for="crumb_language">Language</label></th>
<td>
<?php
$current_language = get_option( 'crumb_language', '' );
$language_names = [
'en' => 'English',
'es' => 'Español',
'fr' => 'Français',
'de' => 'Deutsch',
'pt' => 'Português',
'it' => 'Italiano',
'sv' => 'Svenska',
'da' => 'Dansk',
'el' => 'Ελληνικά',
'fa' => 'فارسی',
'pl' => 'Polski',
'ru' => 'Русский',
'ja' => '日本語',
];
?>
<select id="crumb_language" name="crumb_language">
<option value="" <?php selected( $current_language, '' ); ?>><?php esc_html_e( '— Auto-detect from browser —', 'crumb' ); ?></option>
<?php foreach ( self::SUPPORTED_LANGUAGES as $code ) : ?>
<option value="<?php echo esc_attr( $code ); ?>" <?php selected( $current_language, $code ); ?>>
<?php echo esc_html( $language_names[ $code ] ?? $code ); ?> (<?php echo esc_html( $code ); ?>)
</option>
<?php endforeach; ?>
</select>
<p class="description">Optional. Forces the widget UI language. Default behavior is to detect from the visitor's browser (<code>navigator.language</code>). Can be overridden per-page via the shortcode <code>language</code> attribute.</p>
</td>
</tr>
<tr>
<th scope="row"><label for="crumb_geolocation_radius">Geolocation Radius</label></th>
<td>
Expand Down Expand Up @@ -719,7 +789,7 @@ class="large-text" placeholder="https://example.org/meeting-update-form/?meeting
<p><?php esc_html_e( 'Place this shortcode on any page or post:', 'crumb' ); ?></p>
<code>[crumb]</code>
<p><?php esc_html_e( 'Override settings per page:', 'crumb' ); ?></p>
<code>[crumb server="https://your-server/main_server" service_body="42" format_ids="17,54" view="map" geolocation="true" geolocation_radius="-50"]</code>
<code>[crumb server="https://your-server/main_server" service_body="42" format_ids="17,54" view="map" geolocation="true" geolocation_radius="-50" language="es"]</code>

<?php submit_button(); ?>
</form>
Expand Down
6 changes: 5 additions & 1 deletion readme.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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 =

Expand Down Expand Up @@ -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.
Expand Down
88 changes: 87 additions & 1 deletion tests/test-crumb.php
Original file line number Diff line number Diff line change
Expand Up @@ -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 );
Expand Down Expand Up @@ -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
// -------------------------------------------------------------------------
Expand Down