WP Manifestindependent plugin directory
manifest / i18n / unbabble

Unbabble WP Registry grade D+. High-severity findings · 3 findingsWP RegistryHigh-severity findings · 3 findingsOpen the reportD+

Yet another i18n system for WordPress

★ 4stars
1.1kcomposer installs
0.7.0latest · 2mo ago

Install

Download source zip No release zip yet

This is the repository archive. It installs, but the folder name will carry the branch suffix and updates will not flow.

Install straight from GitHub with WP-CLI:

wp plugin install https://github.com/26b/unbabble/archive/refs/heads/main.zip

On Packagist as 26b/unbabble:

composer require 26b/unbabble
No built-in updater. New versions won't appear under Dashboard → Updates. Install new releases yourself, or let Git Updater track the repository.

[!WARNING] This plugin is under active development which could result in changes that require manual intervention on data as code developed on top of our API. We'll try to mitigate and provide information, as much as possible, but you should be aware of this in case you plan to use it in production.

Our stab at translations in WordPress.

This plugin aims to be several things which, hopefully, we'll be able to keep:

  • Simple concepts of translation
  • Aligned with how we found translations to be used in WordPress (it's our take after all)
  • Developer first APIs and configuration
  • No magic stuff, just default to something obvious, even if wrong or causes an error.

No more than what's needed

Many of the existing solutions to translate content in WordPress offer too many features. They are not bad or wrong, but in our use cases we found that they are more trouble when there is a lot of custom code. In Unbabble we remove everything that is not translation management related, pushing these things to add-ons and third-party plugins that then enhance the existing solution.

Here's what Unbabble does:

  • Manages languages that exist for variations of your content
  • Allows for content to be created in different languages
  • Allows for different languages to be connected as translations of the same content

Here's what it doesn't do:

  • Copy content when creating a new translation There are many plugins that do cloning and copying well, we don't need this. We support the use of Yoast's Duplicate Post to enable this as a feature, through an internal extension.
  • Translate your content for you.

Architecture

A lot of decisions went into this. We try to provide information on all of them to allow for new contributions to be as easy as possible, in case our solution aligns with the needs of others. As well as, keep as much documentation as possible for everyone.

Extra table for translations

We add two new tables with the following schemas that contain all the translation references.

{$wpdb->prefix}_ubb_post_translations

Columns name Type
post_id Integer
locale VARCHAR(5)

{$wpdb->prefix}_ubb_term_translations

Columns name Type
post_id Integer
locale VARCHAR(5)

Options

Options have a specific table, but there is no interface to connect them and create translations groups. For Options we make an automatic translation management. When you view an option on a given language, or access it in a given context (admin or code), we filter every option access to a new value my_option_[language-code]. All access is filtered through the language in a given context.

Note: When looking directly at the database structure and data, you should be aware of this, so that you search appropriately for the option's key.

Translations Links

To know which entities are translations of any other entity, we add a meta to all entities that contains the original ID ubb_source. The original is the first post that was created, and can be in any language. Although the site might have a given default language that does not make it the language of all original posts/terms.

Enforced restrictions

  • Only one translation for a given ubb_source with the same locale (avoids multiple options for the same entity). This does not break the site, but is shown as an issue. By default, when many posts share the same language, the first is selected as the content's language.

Develop

We use a standard WordPress plugin approach with OOP style and composer to manage dependencies. There are also some screens that are made with JavaScript, using ReactJS, and we try to match all the minimum requirements from WordPress.

Development requires:

  • NodeJS v20
  • PHP >=8.3
  • Composer v2
  • WordPress >=6.5
  • Docker (optional)

To get up and running follow the steps below.

  • Clone and install dependencies
# Clone the repository.
git clone git@github.com:26B/unbabble.git

# Switch to the plugin folder and install dependencies
cd unbabble
composer install
npm install
  • If you want to use docker, you can use @wordpress/env to set it up
# Local wp-env setup
npm run wp-env start

# If you have a global wp-env install just use that
wp-env start
  • Build development assets and watch for changes
npm run start

Contributions

We welcome all contributors that find our approach reasonable and useful. You may contribute by filing a bug report, suggesting some feature or submiting your own code. We'll revew new features and code submitted through our goals for the project, stated above.

All contributions must include a CHANGELOG entry adhering to the specification from: https://keepachangelog.com/en/1.1.0/

Releases

Newest 25 of 53 recorded releases.

Tag
Published
0.7.0 latest
Jul 28, 2026 2mo ago
Apr 16, 2026 6mo ago
Mar 12, 2026 7mo ago
Mar 2, 2026 7mo ago
Sep 29, 2025 1 year ago
Jul 16, 2025 1 year ago
Jul 2, 2025 1 year ago
Jul 2, 2025 1 year ago
Jun 23, 2025 1 year ago
May 12, 2025 1 year ago
Mar 31, 2025 1 year ago
Feb 26, 2025 1 year ago
Feb 21, 2025 1 year ago
Feb 19, 2025 1 year ago
Feb 13, 2025 1 year ago
Dec 19, 2024 1 year ago
Nov 11, 2024 1 year ago
Nov 11, 2024 1 year ago
Nov 11, 2024 1 year ago
Sep 11, 2024 2 years ago
Sep 9, 2024 2 years ago
Sep 3, 2024 2 years ago
Aug 27, 2024 2 years ago
Jul 30, 2024 2 years ago
Jul 25, 2024 2 years ago
All releases on GitHub →

These releases are tags only. The author does not attach a packaged zip, so there are no download counts to report.

D+ grade

Security

Unbabble 0.7.0 · audited by WP Registry

High-severity findings.

1 high 2 medium
Audited release
0.7.0
Findings
3
Worst severity
high
Content hash
40bc715564c8be2240d90864…

Findings

  • high Authenticated administrator SQL injection in HiddenContent REST endpoints

    sql_injection

    GET /wp-json/unbabble/v1/default_on_hidden/posts and .../terms are state-changing REST routes gated only by manage_options. The post-types and taxonomies query params are whitelist-filtered only when they are strings. When passed as arrays (post-types[]=...), the values are implode()'d and interpolated into SELECT ... post_type IN ('{$post_types}') / taxonomy IN ('{$taxonomies}') with no $wpdb->prepare or esc_sql. Allowed locale codes are likewise implode-interpolated into locale IN ('{$allowed_languages}'). WP REST unslashes query params, so magic quotes do not apply. An administrator (or stolen admin application password) can time-based dump or modify the database; the same handler then bulk-calls LangInterface::set_post_language / set_term_language on the result set.

    lib/API/Actions/HiddenContent.php:52-198

    Recommendation

    Require POST, add a JSON schema with sanitize_callback, and build the query exclusively with $wpdb->prepare plus a strict in_array whitelist applied to arrays as well as strings. Escape or parameterize locale lists (e.g. array_map('esc_sql', get_languages()) or a %s placeholder per locale). Validate language codes to a locale pattern before they are stored in ubb_options.

  • medium Translation-link REST and save_post write ubb_source on posts the user cannot edit

    missing_capability

    All Gutenberg post routes share one permission_callback that only checks current_user_can(edit_post, $request['id']). PATCH /unbabble/v1/edit/post/{id}/translation/{translation_id}/link then calls LinkTranslation::link_translations($post_id, $translation_id) with no cap check on translation_id. If the target has no ubb_source, set_post_source(..., true) writes that meta on the victim post. The same missing object-level check exists on save_post via $_POST['ubb_link_translation'] (classic/Gutenberg form save). A Contributor/Author who can edit one translatable post can attach it to any other post of that type, pollute another author's translation group, and later unlink a two-member group which also deletes the remaining post's source. Frontend LangInterface::translate_current_url will then treat the victim as a language alternate.

    lib/API/Gutenberg/Post.php:93-212

    Recommendation

    In permission_callback (or inside link_translations) also require current_user_can(edit_post, $translation_id) and refuse to force-write ubb_source on a post the user cannot edit. Apply the same check in Posts\LinkTranslation::save_link_translations.

  • medium Authors can enumerate other users' draft, private, and trash post titles via translation-link search

    information_disclosure

    GET /unbabble/v1/edit/post/{id}/translation/link is allowed for anyone who can edit {id}. get_possible_links runs an unscoped SQL query for every post of that type with post_status NOT IN ('revision','auto-draft') and returns titles, IDs, and locales grouped by ubb_source. WordPress Authors/Contributors cannot list other users' drafts in wp-admin, but this endpoint returns them (including private and trash). Separately, Admin::enqueue_scripts copies get_post($_GET['ubb_source'])->post_title into the UBB localized script on post-new.php with no capability check, so ?ubb_source={id} leaks another post's title to any user who can load the editor.

    lib/Posts/LinkTranslation.php:148-220

    Recommendation

    Restrict candidate posts with the same caps WP_Query uses for the user (author=current user unless they have edit_others_posts; exclude private unless read_private_posts). Strip other authors' non-public titles from the REST payload. In Admin::enqueue_scripts, only expose source_title after current_user_can('edit_post', $source_id) or 'read_post'.

WP Registry hashes the installable build and reports on that exact bytes-for-bytes copy. Embargoed findings are withheld until they are disclosed, so a clean verdict means nothing public is outstanding. WP Manifest does not audit code itself.