JavaScript Internationalization


Over the last few releases, WordPress has made huge steps in prioritizing internationalization and localization throughout around core. To name a few highlights: language packs for core, language packs for plugins and themes, a language chooser, just-in-time loading for translations, and a user-specific language setting. But core isn’t the only place improvements have been made. is the platform for making WordPress available in as many languages as possible. Did you know that WordPress 4.6 is currently translated into 72 – s e v e n t y t w o – languages? Recently the number of unique translators passed the 10k mark. 🌎🌍🌏

The platform is powered by GlotPress, a collaborative, web-based software translation tool.

Internationalization is not a feature, it fixes a usability issue for many users whose primary language is not English. And that’s about 47% of our users.

The next big thing

As of today, WordPress doesn’t support a proper internationalization API in JavaScript. There are two, 4-year old tickets, #22229 and #20491, which request such an API. (The former one was closed as a duplicate of #20491 since an internationalization API in JavaScript should provide support for plurals by default.)

And that’s what this post is about: JavaScript Internationalization.

@swissspidy and I (@ocean90) have been thinking about and working on this topic, and now is the time to gather feedback on the direction and get help from everyone interested.

As you may have noticed in the prequel, internationalization spans more than WordPress core. We’ve split the task into four areas: Core, GlotPress, wp-i18n-tools, and language packs.

Jed and wp.i18n (Core)

Since we didn’t want to reinvent any wheels, our proposal is to use Jed as the base library. Jed provides “Gettext Style i18n for Modern JavaScript Apps”.

wp.i18n will be a wrapper for Jed and should be used by WordPress and plugins/themes. One of the biggest benefits of leveraging Jed is that the actual implementation is very similar to the one we already have for PHP code. The latest patch currently introduces these API functions:

  • wp.i18n.getLocale()
  • wp.i18n.loadLocaleData( data )
  • wp.i18n.addTranslations( domain, data )
  • wp.i18n.__( text, domain )
  • wp.i18n._x( text, context, domain )
  • wp.i18n._n( single, plural, number, domain )
  • wp.i18n._nx( single, plural, number, context, domain )
  • wp.i18n.esc_attr__( text, domain )
  • wp.i18n.esc_html__( text, domain )
  • wp.i18n.esc_attr_x( text, context, domain )
  • wp.i18n.esc_html_x( text, context, domain )
  • wp.i18n.numberFormat( number, decimals )
To do
  • Rethink WP_Scripts::load_translation_file() and get_js_i18n_data(), add tests.
  • Get feedback on the proposed API. (That’s this post!)
  • Apply the new API in our JS files.
    • There’s a POC patch that can be worked upon.
    • Do not forget: Underscore templates.
  • Include date of json files in update requests for language packs.
    • See wp_get_installed_translations().
    • See wp_get_json_file_data(), maybe json_decode() might be enough for this case.
    • What’s the format for meta data? See this GitHub issue for discussion.
  • sprintf() in JavaScript – useful for core?

GlotPress (

Jed supports JSON for translation files. GlotPress currently doesn’t support exporting translations in a JSON format. But there is a pull request which adds the required functionality to GlotPress. The format is going to support “simple” JSON (key -> value) and a Jed 1.x compatible format. The latter will be used by wp.i18n.

To do
  • Get the PR merged. Done. 🎉
  • Find a solution for the missing meta data, see the GitHub issue mentioned before.
  • To enable exports like “get all translations for file common.js”, the current filter system in GlotPress needs to be improved to support search only in file references and search for multiple file references.

wp-i18n-tools (Meta)

A script named MakePOT parses PHP files to extract the original strings and their file references. This script needs to be extended to also parse JavaScript files. A POC has been created.

To do
  • Convert the POC into nice code with tests.
  • Fix #20881 because it’s important for the GlotPress export that all file references remain.

Language Packs (Meta)

Currently a language pack includes a .po and a .mo file. Support needs to be added so that a .json file gets bundled if a project has translations for JavaScript files.

To do
  • Export Jed files when JS translations exist.
  • Include JSON file in language packs.
  • Update API to support version checks for JSON files.


Thanks for reading, now it’s your turn. 🙂 Do you have any feedback? Are there things we should do differently? Do you have resources to help us? Please let us know in the comments!

There are currently no meetings scheduled but we’re happy to discuss any topics in #core-i18n. Just say “Hi”!

#i18n, #javascript

Proposal: Customizer Transactions

Note: Note: This post has been superseded by Customize Changesets (formerly Transactions) Merge Proposal. See also Customize Changesets Technical Design Decisions.

#customize, #feature-selective-refresh, #javascript, #json-api, #partial-refresh, #rest-api

You may be familiar with transactions in a database context. The idea is simple: once you start a transaction, any change made to the database in the current session will not be applied to other database sessions until a transaction COMMIT is done. Changes performed when the transaction is open will be reflected in any SQL SELECT queries made, and if you decide that you do not want to persist the changes in the transaction in the database, you can simply do ROLLBACK and the changes will be discarded. And actually WordPress already uses MySQL transactions, but only in unit tests: a rollback is done after each test is torn down to restore the database state for the next test.

The parallels between database transactions and the WordPress Customizer are clear. When you open the Customizer you are starting a “settings transaction”. Any changes made to the settings in the Customizer get reflected in the preview only, and they get written (committed) to the database only when the user hits “Save & Publish”.

As good as the Customizer currently is, the way it has been implemented means that there are limitations on what we can do with it.

Current Limitations

The existence of modified settings in the Customizer is restricted to the life of a browser window. When a user changes a control in the Customizer and a setting is modified (with transport=refresh), an Ajax request is made with the changed settings data POSTed to the previewed URL. The Customizer then boots up and adds the setting preview filters based on what it sees in $_POST['customized'] so that the changes are reflected when WordPress builds the page. When this Ajax response is received, the Customizer JS code then writes the response to the iframe via document.write().

There are a few downsides to this current approach:

One problem is that if the user navigates away from the Customizer, they lose their drafted settings. To get around this, an AYS dialog was added in #25439, but this still doesn’t account for browser crashes or system failures. It would be ideal if the settings could persist in the same way as when drafting a post.

Another downside is that whenever the preview needs to refresh it has to re-send all the modified settings so that the Customizer preview will have them available to add to the filters, since the Customized settings data is not persisted in WordPress in any way. There’s a performance hit to continually send all data with each request, which was partially improved with #28580.

Additional problems stem from the Ajax POST + document.write() approach to refreshing the preview. Since the Customizer iframe starts out at about:blank and the HTML is written to from the document at customize.php, much of the context for the document in the iframe gets inherited from the parent window. This means that window.location in the preview window is the same as in the parent window: /wp-admin/customize.php. Needless to say, this means that JavaScript code running in the Preview will not run as expected (e.g. #23225).

The Customizer preview intercepts all click events and sends the intended URL to the parent window so that the Customizer can initiate the request to refresh the preview. The only way to change the current page in the preview is by clicking a standard links with a URL; any form submissions in the preview are completely disabled, resulting in the search results page not being reachable from within the preview (#20714). Any navigation system that uses JavaScript to change the window’s location also will fail, for instance using a dropdown.

The current unique method for refreshing the preview worked fine when the Customizer was limited to a handful of settings. But now as more and more of WordPress is being added to the Customizer, and now that themes are increasingly leveraging JavaScript, we need a more robust approach to implementing the Customizer which will solve the above challenges and provide new opportunities.

Customizer Transactions

The proposal is that we introduce persisted Customizer settings, in other words “Customizer transactions”. Here’s how it may work:

When opening the Customizer for the first time, a transaction UUID is generated. Whenever a setting changes, an Ajax request sends the updated setting to WordPress to be persisted in a wp_transaction post which has a post_name corresponding to that UUID (or such a transaction post is created on the fly if not existing already). Any changes made in the Customizer then get amended to the same wp_transaction post, which has a key/value JSON blob as its post_content.

When a user hits the Save & Publish button, the underlying wp_transaction post gets a post status change to publish. When transitioning into this status, each of the settings in the transaction at that point get saved to the database—they get committed.

Instead of using an Ajax POST to send the customized settings to the preview, we then only have to reference the transaction UUID when loading URLs into the Customizer preview. What this means is that we no longer have to use a blank iframe but can load the window with the natural URL for what is being previewed (#30028), but just with the transaction UUID query parameter tacked on.

When this transaction UUID query parameter is present, filters get added to amend all URLs generated in the preview to also include this UUID, so the transaction context is persisted as the user navigates around the site in the preview window. Forms also get this transaction UUID added as another input element, so any form submissions will also keep the preview inside the transaction. Additionally, WP Ajax requests are intercepted to tack on the transaction UUID so that now even Ajax requests can be previewed in the Customizer without any extra work.

Now that the document in the preview window is actually at the URL being previewed (as opposed to about:blank), refreshing the preview is greatly simplified: instead of capturing scroll position, doing Ajax POST, writing the response with document.write(), and restoring the scroll position—now the preview window just has to do a simple location.reload(). JavaScript now runs in the expected context, and full JS applications can be previewed in the Customizer.

As noted above, each time the Customizer is opened and a setting is updated the first time, a wp_transaction post is created with a draft status, and this post gets updated each time a setting is changed during that Customizer session. You also can open the Customizer as a whole (at customize.php) with this transaction UUID supplied and that settings in that existing transaction draft will be loaded. This means you can draft Customizer settings and return to them later, or make some changes and then send it along to another user to finalize (realtime collaboration would be possible as well with some heartbeat integration, or else a locking mechanism would make sense). The capability to publish wp_transaction posts could be restricted to an administrator role, with other roles being able to save posts with a pending status to submit for review.

Also as noted above, the point at which the settings in a transaction get saved (committed) to the database is when the wp_transaction post transitions to a publish status. This being the case it naturally allows for transaction posts to be scheduled to apply in the future. If you want to make a bunch of changes to your site appear at midnight on Saturday, you could go in on Friday and add/remove widgets, change background images, and do anything else the Customizer allows and then have this transaction be scheduled for the desired time. When it publishes, all of the settings would go live on the site. This resolves #28721.

With each Customizer session resulting in a new transaction post being created, then there is automatically a Customizer revision history (see #31089). Every transaction that has a publish post status is a change that went live on the site.

Another side benefit to reworking the Customizer preview to load via a natural URL with the transaction UUID supplied is that there aren’t any intrinsic capabilities needed to preview a transaction on the site. A setting change gets authorized at the time of the change, and the sanitized setting is then persisted in the transaction post. The preview then just applies the pre-authorized and pre-sanitized settings. The interesting side-effect of this is that it means Customizer previews (frontend URLs with the transaction UUID amended) can be shared with anonymous users to review. You can pop open the URL in the preview iframe into a new window and share it with any user for review, and they don’t need the capability to customize.

Lastly, something else that motivated my investigation into Customizer transactions is thinking about how the Customizer will relate to the upcoming REST API. How can the REST API be improved with the Customizer? Where do they intersect? If the REST API provides a transactions endpoint for doing CRUD operations on Customizer settings, and if the REST API also has global recognition for a customize_transaction_uuid query parameter in all requests, then it becomes possible for the Customizer to be used to preview changes in applications that merely interact with the JSON REST API, as long as they include the transaction UUID in the requests.

Partial Refresh

There’s one drawback I’ve encountered when implementing a patch for what I’ve described above. As noted above, when a setting has a refresh transport, the preview window now does a regular location.reload(). When this happens, there is a momentary “flash of unloaded content” (white screen) which currently doesn’t happen when document.write() is employed to refresh the preview window. I’m not sure why this is, other than maybe document.write() initiates a synchronous DOM operation, whereas doing location.reload() initiates an asynchronous one. I’ve tried doing output buffering as well, to try to make sure the response gets sent all at once. But I haven’t had success. This is the current refresh behavior:

If no solution can be found for the white-screen-flash-during-reload issue, there is an alternative (besides the postMessage transport) which would provide an even better experience than even now with the “seamless” full page refresh: partial refresh (#27355).

When a setting change can’t be previewed purely with JavaScript (via postMessage), or it doesn’t make sense to re-implement all of the PHP logic in JS (which is not DRY), the Customizer currently necessitates a full refresh of the entire page. With the proposed partial refresh transport, however, only the container element(s) in which the setting appears in the preview would get fetched from the server via Ajax and inserted into the DOM. This is much faster than having to refresh the entire page, and it retains the overall document state (e.g. whether the sidebar is expanded or not).

There are challenges for implementing partial refresh in a way that it can be enabled by default, however. When implementing partial refresh support for widgets in the Widget Customizer feature-as-plugin for 3.9, I found that themes had to explicitly opt-in to partial-refreshed widgets because a widget could be inside a sidebar that has a dynamic layout (e.g. jQuery Masonry) or the widget may have JS-driven functionality that has to be re-initialized when updated partial is injected. So partial refresh for widgets was removed from being included in WordPress 3.9, but the functionality has recently been resurrected in the Customize Partial Refresh plugin. More research is needed into how much partial refresh we can have enabled by default, and where we need explicit opt-in.

Call for Feedback

So there’s a lot of exciting possibilities introduced with Customizer transactions. I’d love to hear what you think. I have an working patch written and it exists in a pull request on GitHub. I welcome comments there on the PR. Naturally, the changes would need to be split up into smaller patches for committing to SVN.

Related tickets:

  • #30937: Add Customizer transactions (main ticket)
  • #30028: Load Customizer preview iframe with natural URL
  • #30936: Dynamically create WP_Customize_Settings for settings created on JS client
  • #27355: Customizer: Add framework for partial preview refreshes
  • #20714: Theme customizer: Impossible to preview a search results page
  • #23225: Customizer is Incompatible with jQuery UI Tabs.
  • #28721: Scheduled changes for the customizer
  • #31089: Customizer revisions
  • #31517: Customizer: show a notice after attempting to navigate to external links in live previews

Appendix: Why not just use MySQL transactions?

Something interesting to investigate for the future would if we could take this another (lower) level and actually use MySQL transactions for the Customizer. This would make the Customizer much easier to extend beyond options and theme mods, as the Customizer could just start a MySQL transaction and when a setting is changed, just keep a log of any INSERT/UPDATE/DELETE statement performed during a MySQL transaction. They can then be re-played whenever the preview reloads, and then followed by a COMMIT when the Customizer is saved. These SQL statements can be saved in the wp_transaction post, as opposed to the JSON blob containing the key/value settings data. Or the use of MySQL transactions could go deeper and the SAVEPOINT could be utilized to store the transaction natively in MySQL.

But there are some concerns about using MySQL transactions: first, there’s the question of compatibility and whether MySQL transactions would be available on the wide array of hosting environments where WordPress runs, and what MySQL storage engines are used. Then there’s the question of how conflicts would be resolved when the auto-incremented IDs in the transaction diverge from those outside. And also there’s the concern of storing SQL statements the wp_transaction post’s post_content, and how this might introduce vulnerabilities. Lastly, if we use MySQL transactions as opposed to storing the staged settings in a wp_transaction post, then we’d miss out on being able to inspect the contents of a transaction to see what changes are contained within it.

In any case, using MySQL transactions would be an interesting alternative to the current WP_Customize_Setting abstraction.

#customize, #feature-selective-refresh, #javascript, #json-api, #partial-refresh, #rest-api

Finding and Fixing JavaScript errors with JSHint

The JavaScript Coding Standards have been updated, so it’s time to move on to tackling our JSHint errors!

JSHint is a tool to check for errors in JavaScript code. As was discussed last week, we’re kicking off a small effort to work through our core JavaScript files. To get through the errors revealed by JSHint as quickly as possible, we’re following the model established by the Inline Docs team and posting a list of files with issues so that people can “claim” the files they’d like to fix!

At the bottom is a list of every file in core that is displaying JSHint errors. Files with a checkmark have been patched and should now be passing lint. Files marked with (@username #xxxxx) are already claimed, and being worked on.

Please read and understand the process we’ll be following to address these issues! Many thanks to @azaozz, @nacin and @jorbin for helping identify the safest way to approach fixing these errors, and to @rzen for posting the Inline Docs article on which we based this guide.

How to contribute:

  1. Leave a comment on this post with the file* you’re about to edit (check the list first to make sure it hasn’t already been claimed).
  2. Update your local WordPress SVN to the latest version of WordPress trunk (currently 3.8-alpha).
  3. Create a new ticket on Trac for the file.
    JSHint-related trac ticket settings
    • Format the title as “jshint shouldn’t throw errors – path/to/file.js”.
    • Assign the ticket to the “Build Tools” component.
    • Make sure your email is stored in Trac’s preferences

    If you are logged in, you can click this link to automatically open a ticket with the right settings.

  4. Edit the file, and make a patch. Please make sure you create the patch from the root directory of your WordPress SVN checkout. If you are working on a large file, consider making multiple patches for each type of change.
  5. Upload your patch to the Trac ticket you created, and add the keyword “has-patch”.

*Note: We strongly encourage you to work on one file at a time. These shouldn’t take very long, but if you call a bunch at once and get tied up, we won’t be able to get through these as quickly as possible. To quote @rzen from the inline docs effort, “your edits should be made and patched swiftly so that they aren’t invalidated by (or don’t invalidate) another patch.”

Keeping Discussions Focused:

Any discussion about the specifics of a patch itself should happen on Trac. Discussion about the overall effort should take place during our standing weekly meeting, on Wednesdays at 1900 UTC in #wordpress-dev*.

Files needing patches:

Checked files are now passing JSHint

    <li class='o2-task-item o2-task-sortable o2-task-completed' data-item-hash='78d42d5fc72bf4984e6c0c7f926093df' data-hash-instance='0' data-item-text='wp-admin/js/common.js (@jorbin, #25912)' >wp-admin/js/common.js (@jorbin, #25912) <li class='o2-task-item o2-task-sortable o2-task-completed' data-item-hash='c9d4c3c83dac8aec0f9e5db5e6787d6f' data-hash-instance='0' data-item-text='wp-admin/js/custom-background.js (@jorbin, #25915)' >wp-admin/js/custom-background.js (@jorbin, #25915) <li class='o2-task-item o2-task-sortable o2-task-completed' data-item-hash='1fcc57fc992db1d0ef11a4aaa914bfd5' data-hash-instance='0' data-item-text='wp-admin/js/custom-header.js (@jorbin, #25916)' >wp-admin/js/custom-header.js (@jorbin, #25916) <li class='o2-task-item o2-task-sortable o2-task-completed' data-item-hash='50f0a69a6da03ec3f54b5e05d84722a4' data-hash-instance='0' data-item-text='wp-admin/js/customize-controls.js (@jorbin, #25917)' >wp-admin/js/customize-controls.js (@jorbin, #25917) <li class='o2-task-item o2-task-sortable o2-task-completed' data-item-hash='bed98ec6f364f76685628b5f4a15e215' data-hash-instance='0' data-item-text='wp-admin/js/dashboard.js (@tommcfarlin, #25943) (@nacin)' >wp-admin/js/dashboard.js (@tommcfarlin, #25943) (@nacin) <li class='o2-task-item o2-task-sortable o2-task-completed' data-item-hash='8d918df924681550bb2b418ec39c29fa' data-hash-instance='0' data-item-text='wp-admin/js/edit-comments.js (@adamsilverstein, #25979)' >wp-admin/js/edit-comments.js (@adamsilverstein, #25979) <li class='o2-task-item o2-task-sortable o2-task-completed' data-item-hash='c751fd27cfa449d6e8086d91e3fe8448' data-hash-instance='0' data-item-text='wp-admin/js/editor.js (@dougwollison, #25947) (@nacin)' >wp-admin/js/editor.js (@dougwollison, #25947) (@nacin) <li class='o2-task-item o2-task-sortable o2-task-completed' data-item-hash='a7597f586237251475ab242d0690e19a' data-hash-instance='0' data-item-text='wp-admin/js/gallery.js (@dougwollison, #25999) (@jorbin)' >wp-admin/js/gallery.js (@dougwollison, #25999) (@jorbin) <li class='o2-task-item o2-task-sortable ' data-item-hash='55d344b7e5e1d61c1cb5f4fadf1f8e45' data-hash-instance='0' data-item-text='wp-admin/js/image-edit.js ( #26000)' >wp-admin/js/image-edit.js ( #26000) <li class='o2-task-item o2-task-sortable o2-task-completed' data-item-hash='8b6addd2e14967662b8b7d7f736c7743' data-hash-instance='0' data-item-text='wp-admin/js/inline-edit-post.js ( #26001) (@jorbin)' >wp-admin/js/inline-edit-post.js ( #26001) (@jorbin) <li class='o2-task-item o2-task-sortable o2-task-completed' data-item-hash='65f6c474ceaee8842c1feb9188413194' data-hash-instance='0' data-item-text='wp-admin/js/inline-edit-tax.js ( #26002) (@jorbin)' >wp-admin/js/inline-edit-tax.js ( #26002) (@jorbin) <li class='o2-task-item o2-task-sortable o2-task-completed' data-item-hash='33c8e5b81159c73d623adc47ab1f5d68' data-hash-instance='0' data-item-text='wp-admin/js/link.js ( #26034)' >wp-admin/js/link.js ( #26034) <li class='o2-task-item o2-task-sortable o2-task-completed' data-item-hash='8f6c42146a30e0874fcd530e78d6c2cc' data-hash-instance='0' data-item-text='wp-admin/js/media-gallery.js (@tommcfarlin, #25942) (@nacin)' >wp-admin/js/media-gallery.js (@tommcfarlin, #25942) (@nacin) <li class='o2-task-item o2-task-sortable o2-task-completed' data-item-hash='e40ff12d05f80445bb3316c913090e1e' data-hash-instance='0' data-item-text='wp-admin/js/media-upload.js ( #26023)' >wp-admin/js/media-upload.js ( #26023) <li class='o2-task-item o2-task-sortable o2-task-completed' data-item-hash='42a43b1a21e9fed1798d8ce51a521d86' data-hash-instance='0' data-item-text='wp-admin/js/media.js ( #26020)' >wp-admin/js/media.js ( #26020)
  • wp-admin/js/nav-menu.js (51 errors)
  • <li class='o2-task-item o2-task-sortable o2-task-completed' data-item-hash='57e64fb3d39cdc06a167a4e3f0ac9b94' data-hash-instance='0' data-item-text='wp-admin/js/password-strength-meter.js ( #25990)' >wp-admin/js/password-strength-meter.js ( #25990) <li class='o2-task-item o2-task-sortable o2-task-completed' data-item-hash='fc2c50b5f75ac04e477b046bcb52c265' data-hash-instance='0' data-item-text='wp-admin/js/plugin-install.js ( #25993)' >wp-admin/js/plugin-install.js ( #25993) <li class='o2-task-item o2-task-sortable o2-task-completed' data-item-hash='a90190874fea65e144e06c99fde60611' data-hash-instance='0' data-item-text='wp-admin/js/post.js ( #25994)' >wp-admin/js/post.js ( #25994)
  • wp-admin/js/postbox.js (10 errors)
  • <li class='o2-task-item o2-task-sortable o2-task-completed' data-item-hash='b8aac71263098666d93ed4cd902c515d' data-hash-instance='0' data-item-text='wp-admin/js/revisions.js ( #25864)' >wp-admin/js/revisions.js ( #25864) <li class='o2-task-item o2-task-sortable o2-task-completed' data-item-hash='082b1787823872b0a833a8a05b7eab38' data-hash-instance='0' data-item-text='wp-admin/js/set-post-thumbnail.js ( #26008)' >wp-admin/js/set-post-thumbnail.js ( #26008) <li class='o2-task-item o2-task-sortable o2-task-completed' data-item-hash='f3e05608176ced508911ed622545388a' data-hash-instance='0' data-item-text='wp-admin/js/tags.js ( #26009)' >wp-admin/js/tags.js ( #26009) <li class='o2-task-item o2-task-sortable o2-task-completed' data-item-hash='3d4287dc6660d9bb22426e69a326bd9b' data-hash-instance='0' data-item-text='wp-admin/js/theme-install.js (@kovshenin, #26045) (@nacin)' >wp-admin/js/theme-install.js (@kovshenin, #26045) (@nacin) <li class='o2-task-item o2-task-sortable o2-task-completed' data-item-hash='7ce42a9e73f7c66dacc6586be97f41e3' data-hash-instance='0' data-item-text='wp-admin/js/theme-preview.js (@tommcfarlin, #25944) (@nacin)' >wp-admin/js/theme-preview.js (@tommcfarlin, #25944) (@nacin)
  • wp-admin/js/theme.js (22 errors) (@nacin)
  • <li class='o2-task-item o2-task-sortable o2-task-completed' data-item-hash='d63e08b76e1dc51e972be935880fe577' data-hash-instance='0' data-item-text='wp-admin/js/user-profile.js ( #26016) (@jorbin)' >wp-admin/js/user-profile.js ( #26016) (@jorbin) <li class='o2-task-item o2-task-sortable o2-task-completed' data-item-hash='880088a39c35dd354cf8ae17aa5a8562' data-hash-instance='0' data-item-text='wp-admin/js/user-suggest.js ( #26017) (@jorbin)' >wp-admin/js/user-suggest.js ( #26017) (@jorbin) <li class='o2-task-item o2-task-sortable o2-task-completed' data-item-hash='cebb7dcca1b70319912e9fced45bb6c4' data-hash-instance='0' data-item-text='wp-admin/js/word-count.js ( #26018) (@nacin)' >wp-admin/js/word-count.js ( #26018) (@nacin) <li class='o2-task-item o2-task-sortable ' data-item-hash='b5a724edcd8a47ee3f16b5c34d7c4447' data-hash-instance='0' data-item-text='wp-admin/js/wp-fullscreen.js ( #26029)' >wp-admin/js/wp-fullscreen.js ( #26029) <li class='o2-task-item o2-task-sortable o2-task-completed' data-item-hash='5d9c0fee439ccebd614a26fe6be993b1' data-hash-instance='0' data-item-text='wp-admin/js/xfn.js ( #25997)' >wp-admin/js/xfn.js ( #25997)
    <li class='o2-task-item o2-task-sortable o2-task-completed' data-item-hash='42c98f62970a56987f1897345c660359' data-hash-instance='0' data-item-text='wp-content/themes/twentyfourteen/js/functions.js ( #26031) (@jorbin)' >wp-content/themes/twentyfourteen/js/functions.js ( #26031) (@jorbin) <li class='o2-task-item o2-task-sortable o2-task-completed' data-item-hash='090c20909a7758ca05ebf68e0865abdb' data-hash-instance='0' data-item-text='wp-content/themes/twentyfourteen/js/slider.js ( #26032) (@jorbin)' >wp-content/themes/twentyfourteen/js/slider.js ( #26032) (@jorbin)
    <li class='o2-task-item o2-task-sortable o2-task-completed' data-item-hash='e41b6925eb5d8ce584375ad941f6d588' data-hash-instance='0' data-item-text='wp-includes/js/admin-bar.js (@kadamwhite, #25970)' >wp-includes/js/admin-bar.js (@kadamwhite, #25970) <li class='o2-task-item o2-task-sortable o2-task-completed' data-item-hash='fbb5dd8611ee04166f75586d56dbec0c' data-hash-instance='0' data-item-text='wp-includes/js/autosave.js ( #26035)' >wp-includes/js/autosave.js ( #26035) <li class='o2-task-item o2-task-sortable o2-task-completed' data-item-hash='9d5e966f3ce9cb429cc3eb81cec2f103' data-hash-instance='0' data-item-text='wp-includes/js/comment-reply.js ( #26038)' >wp-includes/js/comment-reply.js ( #26038) <li class='o2-task-item o2-task-sortable o2-task-completed' data-item-hash='416fda5531cffc8706e5dab609582b85' data-hash-instance='0' data-item-text='wp-includes/js/customize-base.js ( #26039)' >wp-includes/js/customize-base.js ( #26039) <li class='o2-task-item o2-task-sortable o2-task-completed' data-item-hash='4a77cab2c6476999695e6353e425e553' data-hash-instance='0' data-item-text='wp-includes/js/customize-loader.js ( #26040)' >wp-includes/js/customize-loader.js ( #26040) <li class='o2-task-item o2-task-sortable o2-task-completed' data-item-hash='4cef57ac227e350c3e478450144aea62' data-hash-instance='0' data-item-text='wp-includes/js/customize-preview.js ( #26019)' >wp-includes/js/customize-preview.js ( #26019) <li class='o2-task-item o2-task-sortable o2-task-completed' data-item-hash='f84982d37ee68f0a9fa6b839036d2440' data-hash-instance='0' data-item-text='wp-includes/js/heartbeat.js ( #25986)' >wp-includes/js/heartbeat.js ( #25986) <li class='o2-task-item o2-task-sortable o2-task-completed' data-item-hash='8ed259af302e4f3eb936eb0f79f4f03a' data-hash-instance='0' data-item-text='wp-includes/js/jquery/jquery.table-hotkeys.js ( #26015)' >wp-includes/js/jquery/jquery.table-hotkeys.js ( #26015) <li class='o2-task-item o2-task-sortable o2-task-completed' data-item-hash='c65b3bbac9ecd32ef87c9cd935d4ac02' data-hash-instance='0' data-item-text='wp-includes/js/media-editor.js ( #26022)' >wp-includes/js/media-editor.js ( #26022) <li class='o2-task-item o2-task-sortable o2-task-completed' data-item-hash='2dd718e1cc4d8c48e693d4f80e0f1675' data-hash-instance='0' data-item-text='wp-includes/js/media-models.js (@kadamwhite, #26132)' >wp-includes/js/media-models.js (@kadamwhite, #26132) <li class='o2-task-item o2-task-sortable o2-task-completed' data-item-hash='99960bbc65f799f17580f1f83732400c' data-hash-instance='0' data-item-text='wp-includes/js/media-views.js (@kadamwhite, #25974)' >wp-includes/js/media-views.js (@kadamwhite, #25974)
  • wp-includes/js/mediaelement/wp-mediaelement.js (3 errors)
  • <li class='o2-task-item o2-task-sortable o2-task-completed' data-item-hash='c02a83b3c04ebae9844913965245e9aa' data-hash-instance='0' data-item-text='wp-includes/js/plupload/handlers.js ( #26041)' >wp-includes/js/plupload/handlers.js ( #26041) <li class='o2-task-item o2-task-sortable o2-task-completed' data-item-hash='1dcd1f17c6de947e7510e53739108c5c' data-hash-instance='0' data-item-text='wp-includes/js/plupload/wp-plupload.js (@atimmer, #26044)' >wp-includes/js/plupload/wp-plupload.js (@atimmer, #26044) <li class='o2-task-item o2-task-sortable o2-task-completed' data-item-hash='2c2e5882723a448d614efd844c6db7e1' data-hash-instance='0' data-item-text='wp-includes/js/quicktags.js (@kovshenin, #26046)' >wp-includes/js/quicktags.js (@kovshenin, #26046) <li class='o2-task-item o2-task-sortable o2-task-completed' data-item-hash='9569372d9ad85b2175ce8a6318a15ffb' data-hash-instance='0' data-item-text='wp-includes/js/shortcode.js (@tommcfarlin, #25945) (@nacin)' >wp-includes/js/shortcode.js (@tommcfarlin, #25945) (@nacin) <li class='o2-task-item o2-task-sortable o2-task-completed' data-item-hash='efb76405dd9cf3e6b76ba059df1f13fb' data-hash-instance='0' data-item-text='wp-includes/js/tinymce/mark_loaded_src.js ( #26014)' >wp-includes/js/tinymce/mark_loaded_src.js ( #26014) <li class='o2-task-item o2-task-sortable o2-task-completed' data-item-hash='f5c363893195bd2ece3afb597b3cb9ae' data-hash-instance='0' data-item-text='wp-includes/js/tinymce/plugins/wordpress/editor_plugin_src.js ( #26048)' >wp-includes/js/tinymce/plugins/wordpress/editor_plugin_src.js ( #26048) <li class='o2-task-item o2-task-sortable o2-task-completed' data-item-hash='77c9104774aeadd16894051aad6f9251' data-hash-instance='0' data-item-text='wp-includes/js/tinymce/plugins/wpdialogs/editor_plugin_src.js ( #26048)' >wp-includes/js/tinymce/plugins/wpdialogs/editor_plugin_src.js ( #26048) <li class='o2-task-item o2-task-sortable o2-task-completed' data-item-hash='5be55e14cd92112ff876b62291dd48e5' data-hash-instance='0' data-item-text='wp-includes/js/tinymce/plugins/wpeditimage/editor_plugin_src.js ( #26048)' >wp-includes/js/tinymce/plugins/wpeditimage/editor_plugin_src.js ( #26048) <li class='o2-task-item o2-task-sortable o2-task-completed' data-item-hash='cd60db26fc351e0057855d8db8bafff2' data-hash-instance='0' data-item-text='wp-includes/js/tinymce/plugins/wpfullscreen/editor_plugin_src.js ( #26048)' >wp-includes/js/tinymce/plugins/wpfullscreen/editor_plugin_src.js ( #26048) <li class='o2-task-item o2-task-sortable o2-task-completed' data-item-hash='3ca186662905b3670bb1f3d0d2eaecf7' data-hash-instance='0' data-item-text='wp-includes/js/tinymce/plugins/wpgallery/editor_plugin_src.js ( #26048)' >wp-includes/js/tinymce/plugins/wpgallery/editor_plugin_src.js ( #26048) <li class='o2-task-item o2-task-sortable o2-task-completed' data-item-hash='b21f3c2ca09e74692e2e02338ef7336f' data-hash-instance='0' data-item-text='wp-includes/js/tinymce/plugins/wplink/editor_plugin_src.js ( #26024)' >wp-includes/js/tinymce/plugins/wplink/editor_plugin_src.js ( #26024) <li class='o2-task-item o2-task-sortable o2-task-completed' data-item-hash='1d6d5a7a6b7d57deab5b523f41e52a71' data-hash-instance='0' data-item-text='wp-includes/js/tinymce/plugins/wpview/editor_plugin_src.js ( #26027)' >wp-includes/js/tinymce/plugins/wpview/editor_plugin_src.js ( #26027) <li class='o2-task-item o2-task-sortable o2-task-completed' data-item-hash='39a425200b91833ac669a64ec19fa1b9' data-hash-instance='0' data-item-text='wp-includes/js/utils.js (@adamsilverstein, #25957)' >wp-includes/js/utils.js (@adamsilverstein, #25957) <li class='o2-task-item o2-task-sortable o2-task-completed' data-item-hash='13c089b8f69e5325125fcda3e91b1cf1' data-hash-instance='0' data-item-text='wp-includes/js/wp-ajax-response.js (@originalexe, #25954)' >wp-includes/js/wp-ajax-response.js (@originalexe, #25954) <li class='o2-task-item o2-task-sortable o2-task-completed' data-item-hash='e56ff6ba0c3627713c3a8d7cfd5d11a5' data-hash-instance='0' data-item-text='wp-includes/js/wp-auth-check.js ( #26009)' >wp-includes/js/wp-auth-check.js ( #26009) <li class='o2-task-item o2-task-sortable o2-task-completed' data-item-hash='9da7644cd5bcbb3449d2e6cdbcad169b' data-hash-instance='0' data-item-text='wp-includes/js/wp-list-revisions.js (#25864)' >wp-includes/js/wp-list-revisions.js (#25864) <li class='o2-task-item o2-task-sortable o2-task-completed' data-item-hash='c464f7d9d8a87290d098281181dc01c7' data-hash-instance='0' data-item-text='wp-includes/js/wp-lists.js ( #26012)' >wp-includes/js/wp-lists.js ( #26012) <li class='o2-task-item o2-task-sortable o2-task-completed' data-item-hash='e13fd595169c46f75139c67108f8cd23' data-hash-instance='0' data-item-text='wp-includes/js/wp-pointer.js ( #26012)' >wp-includes/js/wp-pointer.js ( #26012) <li class='o2-task-item o2-task-sortable o2-task-completed' data-item-hash='5b9c10af82c4e4d1418557769aa007f5' data-hash-instance='0' data-item-text='wp-includes/js/wp-util.js ( #25957)' >wp-includes/js/wp-util.js ( #25957) <li class='o2-task-item o2-task-sortable o2-task-completed' data-item-hash='b1d715a3b430baf4bec8f3128854ffb1' data-hash-instance='0' data-item-text='wp-includes/js/wplink.js (@jorbin, #25914)' >wp-includes/js/wplink.js (@jorbin, #25914)

See all open tickets in the Build Tools component

For tips on dealing with global variables, inlined third-party code within first-party scripts, etc, see the JSHint tips in the JavaScript Coding Standards

For the curious, this list was created with a jazzy little command @nacin came up with to pipe Grunt output through ack:

grunt jshint --force | ack '^Linting src/' | ack -o 'wp-.*.js' | sort | uniq -c | sort

What we’re NOT doing

The two JSHint options called out in the earlier post, “curly” and “eqeqeq,” would ordinarily make up the vast majority of the errors JSHint reports in our files. We’ve currently set Grunt and JSHint to ignore these two types of errors when JSHint is run against core. While these are best practices, we’ll come back to them once we address the more significant code smell issues like undefined variables.

Also note that we’re not tackling whitespace or non-JSHint-related refactoring during this effort. We’ll get there, but we have to mitigate the risk to core as much as possible so we don’t interrupt the 3.8 cycle. Keep your changes focused on passing JSHint this go-around.

#build-tools, #javascript, #jshint

JavaScript Coding Standards

The PHP files in WordPress core become cleaner and easier to read with every release, thanks in part to our standards for PHP code style. Our JavaScript, on the other hand, hasn’t gotten nearly enough love. This post is intended to open up the recent discussion around JavaScript style to the greater community so we can make up for lost time.

Don’t we already have a style guide for JavaScript?

Back in March, @tommcfarlin added a set of coding standards for JavaScript to the developer handbook. These WordPress JS coding standards are a great work-in-progress, but weren’t fully comprehensive (leading to some comment threads clarifying various areas). More importantly, without any clear implementation plan the style guide failed to gain traction.

At WordCamp Boston’s core contributor day I revisited this style guide with @mattwiebe and Corey Frang (@gnarf37). It is important to identify *and implement* conventions for JS style ASAP because syntax issues in the JS within WordPress may hide latent bugs, and inconsistent code discourages contribution. Focusing on implementation lead us to look for an existing, proven JS style guide with a .jshintrc file (a set of configuration options for the JSHint code quality tool) which we could adopt largely as-is: Getting JSHint in place lets us see the biggest issues in our JS, so we can begin tackling them incrementally (perhaps in the same manner as the inline docs effort).

After looking at Idiomatic.js and several other widely-adopted JS style guides, we feel the jQuery Foundation’s jQuery Core JavaScript Style Guide guide is the closest match for what we need in WordPress.

Adopting the jQuery Core JavaScript Style Guide

jQuery’s guide shared WordPress core’s love of white space—the same “when in doubt, space it out” mantra from the existing JS style page. Moreover, jQuery’s code conventions have been referenced in trac tickets as an example of how we should be writing our code. Adopting their guide wholesale capitalizes on our stylistic similarities, and will let us adopt their .jshintrc and any future code quality tools they write with minimal changes.
Continue reading

#coding-style, #javascript

JavaScript Unit Tests for Core

Recently WordPress added QUnit as a JavaScript unit testing framework and added its first JavaScript unit tests. I thought I would walk through how to run the tests and how to write tests so that we can increase our JavaScript test coverage. All of these are based upon using the checkout which is where the JS unit tests live.
Continue reading

#javascript, #unit-tests

JavaScript meeting on Monday @koop @azaozz @carldanley @adamsilverstein…

JavaScript meeting on Monday. @koop @azaozz @carldanley @adamsilverstein @kadamwhite will be meeting on Monday at 9 p.m. EDT (Tuesday, August 20 at 01:00 GMT) in the #wordpress-dev IRC channel. I’ll let them build out an agenda in the comments, but here’s some potential topics I know they’ve been kicking around:

  • JS style, structure, and design patterns, and what new code should look like
  • JS unit testing, including candidate frameworks and where to start
  • APIs, such as WP-style hooks (actions/filters), what form they should take if they should exist
  • Which areas of our JS need refactoring and overhauls, and what that might look like
  • Which JS-heavy features need eventual iteration (often “version 2”)
  • What action items could be done for WP 3.7 and 3.8

#3-7, #javascript

External Libraries in 3.5

We work hard to keep our external libraries as up-to-date as possible with each major release. Here’s what you can find today in trunk, all of which are the latest version:

Edit: jQuery 1.8.1 (due out this week) and jQuery UI 1.9 (due out next week) are now both in trunk.

And, Backbone.js 0.9.2 and Underscore.js 1.3.3 were both added to core last week.

If you’re a plugin developer using a bundled library, we strongly suggest you follow their development as well, to ensure code you write will be compatible in the future. The projects we have adopted have very similar backwards compatibility policies as ours (which would be strive to always be compatible), which make this pretty easy to do.

Additionally, Prototype and have been removed from core. They are still registered; enqueueing them will pull from This is only being done for backwards compatibility and doesn’t indicate a change in policy. It was just a lot of bloat to keep shipping when core hasn’t used either for four years. (Both were updated to the latest versions in the process, something we can/may continue to do.)

See also the conversion to .min.js from .dev.js.

#3-5, #javascript

Minified versus development scripts and .min.js

For some time (since r10291), WordPress has shipped minified scripts and styles as .js and .css, with the non-minified, “development” versions at .dev.js and .dev.css.

These weren’t great for discoverability and it has become clear that these are non-standard. So, we’ve moved to using .min.js and .min.css for minified files. You can now find the “development” versions at .js and .css. This also works nicely with tools like ack, which are coded to ignore .min.js.

This was implemented in #21633. Now if only we can get TinyMCE to move away from _src.js. 🙂

A note, for some external libraries, we don’t include the un-minified versions. In this case, you can find them on their respective websites and also in the sources repository. (This is linked from, which in turn is referenced at the bottom of our license file.) @scribu and I were talking about writing a developer plugin to use un-minified versions of these libraries, which would be cool.

More on external libraries in 3.5 here.

#3-5, #javascript

Image header cropping now uses Jcrop.

Image header cropping now uses Jcrop.

#images, #javascript