基础代码
This commit is contained in:
Vendored
+3
@@ -0,0 +1,3 @@
|
||||
# Configuration file for nektos/act.
|
||||
# See https://github.com/nektos/act#configuration
|
||||
-P ubuntu-latest=shivammathur/node:latest
|
||||
+18
@@ -0,0 +1,18 @@
|
||||
.DS_Store
|
||||
.git
|
||||
.gitignore
|
||||
.gitlab-ci.yml
|
||||
.editorconfig
|
||||
.travis.yml
|
||||
behat.yml
|
||||
circle.yml
|
||||
phpcs.xml.dist
|
||||
phpunit.xml.dist
|
||||
bin/
|
||||
features/
|
||||
utils/
|
||||
*.zip
|
||||
*.tar.gz
|
||||
*.swp
|
||||
*.txt
|
||||
*.log
|
||||
+25
@@ -0,0 +1,25 @@
|
||||
# This file is for unifying the coding style for different editors and IDEs
|
||||
# editorconfig.org
|
||||
|
||||
# WordPress Coding Standards
|
||||
# https://make.wordpress.org/core/handbook/coding-standards/
|
||||
|
||||
root = true
|
||||
|
||||
[*]
|
||||
charset = utf-8
|
||||
end_of_line = lf
|
||||
insert_final_newline = true
|
||||
trim_trailing_whitespace = true
|
||||
indent_style = tab
|
||||
|
||||
[{.jshintrc,*.json,*.yml,*.feature}]
|
||||
indent_style = space
|
||||
indent_size = 2
|
||||
|
||||
[{*.txt,wp-config-sample.php}]
|
||||
end_of_line = crlf
|
||||
|
||||
[composer.json]
|
||||
indent_style = space
|
||||
indent_size = 4
|
||||
@@ -0,0 +1 @@
|
||||
* @wp-cli/committers
|
||||
@@ -0,0 +1,11 @@
|
||||
<!--
|
||||
|
||||
Thanks for creating a new issue!
|
||||
|
||||
Found a bug or want to suggest an enhancement? Before completing your issue, please review our best practices: https://make.wordpress.org/cli/handbook/bug-reports/
|
||||
|
||||
Need help with something? GitHub issues aren't for general support questions, but there are other venues you can try: https://wp-cli.org/#support
|
||||
|
||||
You can safely delete this comment.
|
||||
|
||||
-->
|
||||
@@ -0,0 +1,16 @@
|
||||
<!--
|
||||
|
||||
Thanks for submitting a pull request!
|
||||
|
||||
Please review our contributing guidelines if you haven't recently: https://make.wordpress.org/cli/handbook/contributing/#creating-a-pull-request
|
||||
|
||||
Here's an overview to our process:
|
||||
|
||||
1. One of the project committers will soon provide a code review: https://make.wordpress.org/cli/handbook/code-review/
|
||||
2. You are expected to address the code review comments in a timely manner (if we don't hear from you in two weeks, we'll consider your pull request abandoned).
|
||||
3. Please make sure to include functional tests for your changes.
|
||||
4. The reviewing committer will merge your pull request as soon as it passes code review (and provided it fits within the scope of the project).
|
||||
|
||||
You can safely delete this comment.
|
||||
|
||||
-->
|
||||
@@ -0,0 +1,9 @@
|
||||
version: 2
|
||||
updates:
|
||||
- package-ecosystem: composer
|
||||
directory: "/"
|
||||
schedule:
|
||||
interval: daily
|
||||
open-pull-requests-limit: 10
|
||||
labels:
|
||||
- scope:distribution
|
||||
+19
@@ -0,0 +1,19 @@
|
||||
# Configuration for move-issues - https://github.com/dessant/move-issues
|
||||
|
||||
# Repository to extend settings from
|
||||
_extends: wp-cli/wp-cli
|
||||
|
||||
# Delete the command comment when it contains no other content
|
||||
# deleteCommand: true
|
||||
|
||||
# Close the source issue after moving
|
||||
# closeSourceIssue: true
|
||||
|
||||
# Lock the source issue after moving
|
||||
# lockSourceIssue: false
|
||||
|
||||
# Mention issue and comment authors
|
||||
# mentionAuthors: true
|
||||
|
||||
# Preserve mentions in the issue content
|
||||
# keepContentMentions: true
|
||||
+28
@@ -0,0 +1,28 @@
|
||||
# Used by Probot Settings: https://probot.github.io/apps/settings/
|
||||
repository:
|
||||
description: Provides internationalization tools for WordPress projects.
|
||||
labels:
|
||||
- name: bug
|
||||
color: fc2929
|
||||
- name: scope:documentation
|
||||
color: 0e8a16
|
||||
- name: scope:testing
|
||||
color: 5319e7
|
||||
- name: good-first-issue
|
||||
color: eb6420
|
||||
- name: help-wanted
|
||||
color: 159818
|
||||
- name: maybelater
|
||||
color: c2e0c6
|
||||
- name: state:unconfirmed
|
||||
color: bfe5bf
|
||||
- name: state:unsupported
|
||||
color: bfe5bf
|
||||
- name: wontfix
|
||||
color: c2e0c6
|
||||
- name: command:i18n
|
||||
color: c5def5
|
||||
- name: command:i18n-make-pot
|
||||
color: c5def5
|
||||
- name: command:i18n-make-json
|
||||
color: c5def5
|
||||
@@ -0,0 +1,102 @@
|
||||
name: Code Quality Checks
|
||||
|
||||
on: pull_request
|
||||
|
||||
jobs:
|
||||
|
||||
lint: #-----------------------------------------------------------------------
|
||||
name: Lint PHP files
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Check out source code
|
||||
uses: actions/checkout@v2
|
||||
|
||||
- name: Check existence of composer.json file
|
||||
id: check_composer_file
|
||||
uses: andstor/file-existence-action@v1
|
||||
with:
|
||||
files: "composer.json"
|
||||
|
||||
- name: Set up PHP envirnoment
|
||||
uses: shivammathur/setup-php@v2
|
||||
with:
|
||||
php-version: '7.4'
|
||||
tools: cs2pr
|
||||
|
||||
- name: Get Composer cache Directory
|
||||
if: steps.check_composer_file.outputs.files_exists == 'true'
|
||||
id: composer-cache
|
||||
run: |
|
||||
echo "::set-output name=dir::$(composer config cache-files-dir)"
|
||||
|
||||
- name: Use Composer cache
|
||||
if: steps.check_composer_file.outputs.files_exists == 'true'
|
||||
uses: actions/cache@v1
|
||||
with:
|
||||
path: ${{ steps['composer-cache'].outputs.dir }}
|
||||
key: ${{ runner.os }}-composer-${{ hashFiles('**/composer.lock') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-composer-
|
||||
|
||||
- name: Install dependencies
|
||||
if: steps.check_composer_file.outputs.files_exists == 'true'
|
||||
run: COMPOSER_ROOT_VERSION=dev-master composer install --prefer-dist --no-progress --no-suggest
|
||||
|
||||
- name: Check existence of vendor/bin/parallel-lint file
|
||||
id: check_linter_file
|
||||
uses: andstor/file-existence-action@v1
|
||||
with:
|
||||
files: "vendor/bin/parallel-lint"
|
||||
|
||||
- name: Run Linter
|
||||
if: steps.check_linter_file.outputs.files_exists == 'true'
|
||||
run: vendor/bin/parallel-lint -j 10 . --exclude vendor --checkstyle | cs2pr
|
||||
|
||||
phpcs: #----------------------------------------------------------------------
|
||||
name: PHPCS
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Check out source code
|
||||
uses: actions/checkout@v2
|
||||
|
||||
- name: Check existence of composer.json & phpcs.xml.dist files
|
||||
id: check_files
|
||||
uses: andstor/file-existence-action@v1
|
||||
with:
|
||||
files: "composer.json, phpcs.xml.dist"
|
||||
|
||||
- name: Set up PHP envirnoment
|
||||
uses: shivammathur/setup-php@v2
|
||||
with:
|
||||
php-version: '7.4'
|
||||
tools: cs2pr
|
||||
|
||||
- name: Get Composer cache Directory
|
||||
if: steps.check_files.outputs.files_exists == 'true'
|
||||
id: composer-cache
|
||||
run: |
|
||||
echo "::set-output name=dir::$(composer config cache-files-dir)"
|
||||
|
||||
- name: Use Composer cache
|
||||
if: steps.check_files.outputs.files_exists == 'true'
|
||||
uses: actions/cache@v1
|
||||
with:
|
||||
path: ${{ steps.composer-cache.outputs.dir }}
|
||||
key: ${{ runner.os }}-composer-${{ hashFiles('**/composer.lock') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-composer-
|
||||
|
||||
- name: Install dependencies
|
||||
if: steps.check_files.outputs.files_exists == 'true'
|
||||
run: COMPOSER_ROOT_VERSION=dev-master composer install --prefer-dist --no-progress --no-suggest
|
||||
|
||||
- name: Check existence of vendor/bin/phpcs file
|
||||
id: check_phpcs_binary_file
|
||||
uses: andstor/file-existence-action@v1
|
||||
with:
|
||||
files: "vendor/bin/phpcs"
|
||||
|
||||
- name: Run PHPCS
|
||||
if: steps.check_phpcs_binary_file.outputs.files_exists == 'true'
|
||||
run: vendor/bin/phpcs -q --report=checkstyle | cs2pr
|
||||
@@ -0,0 +1,146 @@
|
||||
name: Testing
|
||||
|
||||
on: pull_request
|
||||
|
||||
jobs:
|
||||
|
||||
unit: #-----------------------------------------------------------------------
|
||||
name: Unit test / PHP ${{ matrix.php }}
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
php: ['5.6', '7.0', '7.1', '7.2', '7.3', '7.4', '8.0']
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Check out source code
|
||||
uses: actions/checkout@v2
|
||||
|
||||
- name: Check existence of composer.json file
|
||||
id: check_files
|
||||
uses: andstor/file-existence-action@v1
|
||||
with:
|
||||
files: "composer.json, phpunit.xml.dist"
|
||||
|
||||
- name: Set up PHP environment
|
||||
if: steps.check_files.outputs.files_exists == 'true'
|
||||
uses: shivammathur/setup-php@v2
|
||||
with:
|
||||
php-version: '${{ matrix.php }}'
|
||||
coverage: none
|
||||
tools: composer,cs2pr
|
||||
|
||||
- name: Get Composer cache Directory
|
||||
if: steps.check_files.outputs.files_exists == 'true'
|
||||
id: composer-cache
|
||||
run: |
|
||||
echo "::set-output name=dir::$(composer config cache-files-dir)"
|
||||
|
||||
- name: Use Composer cache
|
||||
if: steps.check_files.outputs.files_exists == 'true'
|
||||
uses: actions/cache@master
|
||||
with:
|
||||
path: ${{ steps['composer-cache'].outputs.dir }}
|
||||
key: ${{ runner.os }}-composer-${{ hashFiles('**/composer.lock') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-composer-
|
||||
|
||||
- name: Install dependencies
|
||||
if: steps.check_files.outputs.files_exists == 'true'
|
||||
run: COMPOSER_ROOT_VERSION=dev-master composer install --prefer-dist --no-progress --no-suggest
|
||||
|
||||
- name: Setup problem matcher to provide annotations for PHPUnit
|
||||
if: steps.check_files.outputs.files_exists == 'true'
|
||||
run: echo "::add-matcher::${{ runner.tool_cache }}/phpunit.json"
|
||||
|
||||
- name: Run PHPUnit
|
||||
if: steps.check_files.outputs.files_exists == 'true'
|
||||
run: composer phpunit
|
||||
|
||||
functional: #----------------------------------------------------------------------
|
||||
name: Functional - WP ${{ matrix.wp }} on PHP ${{ matrix.php }}
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
php: ['5.6', '7.0', '7.1', '7.2', '7.3', '7.4']
|
||||
wp: ['latest']
|
||||
test: ["composer behat || composer behat-rerun"]
|
||||
include:
|
||||
- php: '5.6'
|
||||
wp: 'trunk'
|
||||
test: "composer behat || composer behat-rerun"
|
||||
- php: '7.4'
|
||||
wp: 'trunk'
|
||||
test: "composer behat || composer behat-rerun"
|
||||
- php: '5.6'
|
||||
wp: '3.7'
|
||||
test: "composer behat || composer behat-rerun || true"
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
services:
|
||||
mysql:
|
||||
image: mysql:5.7
|
||||
env:
|
||||
MYSQL_DATABASE: wp_cli_test
|
||||
MYSQL_USER: root
|
||||
MYSQL_ROOT_PASSWORD: root
|
||||
ports:
|
||||
- 3306
|
||||
options: --health-cmd="mysqladmin ping" --health-interval=10s --health-timeout=5s --health-retries=3
|
||||
|
||||
steps:
|
||||
- name: Check out source code
|
||||
uses: actions/checkout@v2
|
||||
|
||||
- name: Check existence of composer.json & behat.yml files
|
||||
id: check_files
|
||||
uses: andstor/file-existence-action@v1
|
||||
with:
|
||||
files: "composer.json, behat.yml"
|
||||
|
||||
- name: Set up PHP envirnoment
|
||||
uses: shivammathur/setup-php@v2
|
||||
with:
|
||||
php-version: '${{ matrix.php }}'
|
||||
extensions: mysql, zip
|
||||
coverage: none
|
||||
tools: composer
|
||||
|
||||
- name: Get Composer cache Directory
|
||||
if: steps.check_files.outputs.files_exists == 'true'
|
||||
id: composer-cache
|
||||
run: |
|
||||
echo "::set-output name=dir::$(composer config cache-files-dir)"
|
||||
|
||||
- name: Use Composer cache
|
||||
if: steps.check_files.outputs.files_exists == 'true'
|
||||
uses: actions/cache@master
|
||||
with:
|
||||
path: ${{ steps['composer-cache'].outputs.dir }}
|
||||
key: ${{ runner.os }}-composer-${{ hashFiles('**/composer.lock') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-composer-
|
||||
|
||||
- name: Install dependencies
|
||||
if: steps.check_files.outputs.files_exists == 'true'
|
||||
run: COMPOSER_ROOT_VERSION=dev-master composer install --prefer-dist --no-progress --no-suggest
|
||||
|
||||
- name: Start MySQL server
|
||||
if: steps.check_files.outputs.files_exists == 'true'
|
||||
run: sudo service mysql start
|
||||
|
||||
- name: Prepare test database
|
||||
if: steps.check_files.outputs.files_exists == 'true'
|
||||
run: |
|
||||
export MYQSL_HOST=127.0.0.1
|
||||
export MYSQL_TCP_PORT=${{ job.services.mysql.ports['3306'] }}
|
||||
mysql -e 'CREATE DATABASE IF NOT EXISTS wp_cli_test;' -uroot -proot
|
||||
mysql -e 'GRANT ALL PRIVILEGES ON wp_cli_test.* TO "wp_cli_test"@"127.0.0.1" IDENTIFIED BY "password1"' -uroot -proot
|
||||
mysql -e 'GRANT ALL PRIVILEGES ON wp_cli_test_scaffold.* TO "wp_cli_test"@"127.0.0.1" IDENTIFIED BY "password1"' -uroot -proot
|
||||
|
||||
- name: Run Behat
|
||||
if: steps.check_files.outputs.files_exists == 'true'
|
||||
env:
|
||||
WP_VERSION: '${{ matrix.wp }}'
|
||||
run: ${{ matrix.test }}
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
.DS_Store
|
||||
wp-cli.local.yml
|
||||
/node_modules
|
||||
/vendor
|
||||
*.zip
|
||||
*.tar.gz
|
||||
*.swp
|
||||
*.txt
|
||||
*.log
|
||||
composer.lock
|
||||
phpunit.xml
|
||||
phpcs.xml
|
||||
.phpcs.xml
|
||||
+8
@@ -0,0 +1,8 @@
|
||||
Contributing
|
||||
============
|
||||
|
||||
We appreciate you taking the initiative to contribute to this project.
|
||||
|
||||
Contributing isn’t limited to just code. We encourage you to contribute in the way that best fits your abilities, by writing tutorials, giving a demo at your local meetup, helping other users with their support questions, or revising our documentation.
|
||||
|
||||
For a more thorough introduction, [check out WP-CLI's guide to contributing](https://make.wordpress.org/cli/handbook/contributing/). This package follows those policy and guidelines.
|
||||
Vendored
+21
@@ -0,0 +1,21 @@
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (C) 2011-2018 WP-CLI Development Group (https://github.com/wp-cli/i18n-command/contributors)
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in
|
||||
all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
THE SOFTWARE.
|
||||
Vendored
+196
@@ -0,0 +1,196 @@
|
||||
wp-cli/i18n-command
|
||||
===================
|
||||
|
||||
Provides internationalization tools for WordPress projects.
|
||||
|
||||
[](https://travis-ci.org/wp-cli/i18n-command)
|
||||
|
||||
Quick links: [Using](#using) | [Installing](#installing) | [Contributing](#contributing) | [Support](#support)
|
||||
|
||||
## Using
|
||||
|
||||
This package implements the following commands:
|
||||
|
||||
### wp i18n
|
||||
|
||||
Provides internationalization tools for WordPress projects.
|
||||
|
||||
~~~
|
||||
wp i18n
|
||||
~~~
|
||||
|
||||
**EXAMPLES**
|
||||
|
||||
# Create a POT file for the WordPress plugin/theme in the current directory
|
||||
$ wp i18n make-pot . languages/my-plugin.pot
|
||||
|
||||
|
||||
|
||||
### wp i18n make-pot
|
||||
|
||||
Create a POT file for a WordPress project.
|
||||
|
||||
~~~
|
||||
wp i18n make-pot <source> [<destination>] [--slug=<slug>] [--domain=<domain>] [--ignore-domain] [--merge[=<paths>]] [--subtract=<paths>] [--include=<paths>] [--exclude=<paths>] [--headers=<headers>] [--skip-js] [--skip-audit] [--file-comment=<file-comment>] [--package-name=<name>]
|
||||
~~~
|
||||
|
||||
Scans PHP and JavaScript files for translatable strings, as well as theme stylesheets and plugin files
|
||||
if the source directory is detected as either a plugin or theme.
|
||||
|
||||
**OPTIONS**
|
||||
|
||||
<source>
|
||||
Directory to scan for string extraction.
|
||||
|
||||
[<destination>]
|
||||
Name of the resulting POT file.
|
||||
|
||||
[--slug=<slug>]
|
||||
Plugin or theme slug. Defaults to the source directory's basename.
|
||||
|
||||
[--domain=<domain>]
|
||||
Text domain to look for in the source code, unless the `--ignore-domain` option is used.
|
||||
By default, the "Text Domain" header of the plugin or theme is used.
|
||||
If none is provided, it falls back to the project slug.
|
||||
|
||||
[--ignore-domain]
|
||||
Ignore the text domain completely and extract strings with any text domain.
|
||||
|
||||
[--merge[=<paths>]]
|
||||
Comma-separated list of POT files whose contents should be merged with the extracted strings.
|
||||
If left empty, defaults to the destination POT file. POT file headers will be ignored.
|
||||
|
||||
[--subtract=<paths>]
|
||||
Comma-separated list of POT files whose contents should act as some sort of blacklist for string extraction.
|
||||
Any string which is found on that blacklist will not be extracted.
|
||||
This can be useful when you want to create multiple POT files from the same source directory with slightly
|
||||
different content and no duplicate strings between them.
|
||||
|
||||
[--include=<paths>]
|
||||
Comma-separated list of files and paths that should be used for string extraction.
|
||||
If provided, only these files and folders will be taken into account for string extraction.
|
||||
For example, `--include="src,my-file.php` will ignore anything besides `my-file.php` and files in the `src`
|
||||
directory. Simple glob patterns can be used, i.e. `--include=foo-*.php` includes any PHP file with the `foo-`
|
||||
prefix. Leading and trailing slashes are ignored, i.e. `/my/directory/` is the same as `my/directory`.
|
||||
|
||||
[--exclude=<paths>]
|
||||
Comma-separated list of files and paths that should be skipped for string extraction.
|
||||
For example, `--exclude=".github,myfile.php` would ignore any strings found within `myfile.php` or the `.github`
|
||||
folder. Simple glob patterns can be used, i.e. `--exclude=foo-*.php` excludes any PHP file with the `foo-`
|
||||
prefix. Leading and trailing slashes are ignored, i.e. `/my/directory/` is the same as `my/directory`. The
|
||||
following files and folders are always excluded: node_modules, .git, .svn, .CVS, .hg, vendor, *.min.js.
|
||||
|
||||
[--headers=<headers>]
|
||||
Array in JSON format of custom headers which will be added to the POT file. Defaults to empty array.
|
||||
|
||||
[--skip-js]
|
||||
Skips JavaScript string extraction. Useful when this is done in another build step, e.g. through Babel.
|
||||
|
||||
[--skip-audit]
|
||||
Skips string audit where it tries to find possible mistakes in translatable strings. Useful when running in an
|
||||
automated environment.
|
||||
|
||||
[--file-comment=<file-comment>]
|
||||
String that should be added as a comment to the top of the resulting POT file.
|
||||
By default, a copyright comment is added for WordPress plugins and themes in the following manner:
|
||||
|
||||
```
|
||||
Copyright (C) 2018 Example Plugin Author
|
||||
This file is distributed under the same license as the Example Plugin package.
|
||||
```
|
||||
|
||||
If a plugin or theme specifies a license in their main plugin file or stylesheet, the comment looks like
|
||||
this:
|
||||
|
||||
```
|
||||
Copyright (C) 2018 Example Plugin Author
|
||||
This file is distributed under the GPLv2.
|
||||
```
|
||||
|
||||
[--package-name=<name>]
|
||||
Name to use for package name in the resulting POT file's `Project-Id-Version` header.
|
||||
Overrides plugin or theme name, if applicable.
|
||||
|
||||
**EXAMPLES**
|
||||
|
||||
# Create a POT file for the WordPress plugin/theme in the current directory
|
||||
$ wp i18n make-pot . languages/my-plugin.pot
|
||||
|
||||
# Create a POT file for the continents and cities list in WordPress core.
|
||||
$ wp i18n make-pot . continents-and-cities.pot --include="wp-admin/includes/continents-cities.php"
|
||||
--ignore-domain
|
||||
|
||||
|
||||
|
||||
### wp i18n make-json
|
||||
|
||||
Extract JavaScript strings from PO files and add them to individual JSON files.
|
||||
|
||||
~~~
|
||||
wp i18n make-json <source> [<destination>] [--purge] [--pretty-print]
|
||||
~~~
|
||||
|
||||
For JavaScript internationalization purposes, WordPress requires translations to be split up into
|
||||
one Jed-formatted JSON file per JavaScript source file.
|
||||
|
||||
See https://make.wordpress.org/core/2018/11/09/new-javascript-i18n-support-in-wordpress/ to learn more
|
||||
about WordPress JavaScript internationalization.
|
||||
|
||||
**OPTIONS**
|
||||
|
||||
<source>
|
||||
Path to an existing PO file or a directory containing multiple PO files.
|
||||
|
||||
[<destination>]
|
||||
Path to the destination directory for the resulting JSON files. Defaults to the source directory.
|
||||
|
||||
[--purge]
|
||||
Whether to purge the strings that were extracted from the original source file. Defaults to true, use `--no-purge` to skip the removal.
|
||||
|
||||
[--pretty-print]
|
||||
Pretty-print resulting JSON files.
|
||||
|
||||
**EXAMPLES**
|
||||
|
||||
# Create JSON files for all PO files in the languages directory
|
||||
$ wp i18n make-json languages
|
||||
|
||||
# Create JSON files for my-plugin-de_DE.po and leave the PO file untouched.
|
||||
$ wp i18n make-json my-plugin-de_DE.po /tmp --no-purge
|
||||
|
||||
## Installing
|
||||
|
||||
This package is included with WP-CLI itself, no additional installation necessary.
|
||||
|
||||
To install the latest version of this package over what's included in WP-CLI, run:
|
||||
|
||||
wp package install git@github.com:wp-cli/i18n-command.git
|
||||
|
||||
## Contributing
|
||||
|
||||
We appreciate you taking the initiative to contribute to this project.
|
||||
|
||||
Contributing isn’t limited to just code. We encourage you to contribute in the way that best fits your abilities, by writing tutorials, giving a demo at your local meetup, helping other users with their support questions, or revising our documentation.
|
||||
|
||||
For a more thorough introduction, [check out WP-CLI's guide to contributing](https://make.wordpress.org/cli/handbook/contributing/). This package follows those policy and guidelines.
|
||||
|
||||
### Reporting a bug
|
||||
|
||||
Think you’ve found a bug? We’d love for you to help us get it fixed.
|
||||
|
||||
Before you create a new issue, you should [search existing issues](https://github.com/wp-cli/i18n-command/issues?q=label%3Abug%20) to see if there’s an existing resolution to it, or if it’s already been fixed in a newer version.
|
||||
|
||||
Once you’ve done a bit of searching and discovered there isn’t an open or fixed issue for your bug, please [create a new issue](https://github.com/wp-cli/i18n-command/issues/new). Include as much detail as you can, and clear steps to reproduce if possible. For more guidance, [review our bug report documentation](https://make.wordpress.org/cli/handbook/bug-reports/).
|
||||
|
||||
### Creating a pull request
|
||||
|
||||
Want to contribute a new feature? Please first [open a new issue](https://github.com/wp-cli/i18n-command/issues/new) to discuss whether the feature is a good fit for the project.
|
||||
|
||||
Once you've decided to commit the time to seeing your pull request through, [please follow our guidelines for creating a pull request](https://make.wordpress.org/cli/handbook/pull-requests/) to make sure it's a pleasant experience. See "[Setting up](https://make.wordpress.org/cli/handbook/pull-requests/#setting-up)" for details specific to working on this package locally.
|
||||
|
||||
## Support
|
||||
|
||||
Github issues aren't for general support questions, but there are other venues you can try: https://wp-cli.org/#support
|
||||
|
||||
|
||||
*This README.md is generated dynamically from the project's codebase using `wp scaffold package-readme` ([doc](https://github.com/wp-cli/scaffold-package-command#wp-scaffold-package-readme)). To suggest changes, please submit a pull request against the corresponding part of the codebase.*
|
||||
+64
@@ -0,0 +1,64 @@
|
||||
{
|
||||
"name": "wp-cli/i18n-command",
|
||||
"type": "wp-cli-package",
|
||||
"description": "Provides internationalization tools for WordPress projects.",
|
||||
"homepage": "https://github.com/wp-cli/i18n-command",
|
||||
"license": "MIT",
|
||||
"authors": [
|
||||
{
|
||||
"name": "Pascal Birchler",
|
||||
"homepage": "https://pascalbirchler.com/"
|
||||
}
|
||||
],
|
||||
"require": {
|
||||
"gettext/gettext": "^4.8",
|
||||
"mck89/peast": "^1.8",
|
||||
"wp-cli/wp-cli": "^2"
|
||||
},
|
||||
"require-dev": {
|
||||
"wp-cli/scaffold-command": "^1.2 || ^2",
|
||||
"wp-cli/wp-cli-tests": "^2.1.3"
|
||||
},
|
||||
"config": {
|
||||
"process-timeout": 7200,
|
||||
"sort-packages": true
|
||||
},
|
||||
"extra": {
|
||||
"branch-alias": {
|
||||
"dev-master": "2.x-dev"
|
||||
},
|
||||
"bundled": true,
|
||||
"commands": [
|
||||
"i18n",
|
||||
"i18n make-pot",
|
||||
"i18n make-json"
|
||||
]
|
||||
},
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"WP_CLI\\I18n\\": "src/"
|
||||
},
|
||||
"files": [
|
||||
"i18n-command.php"
|
||||
]
|
||||
},
|
||||
"minimum-stability": "dev",
|
||||
"prefer-stable": true,
|
||||
"scripts": {
|
||||
"behat": "run-behat-tests",
|
||||
"behat-rerun": "rerun-behat-tests",
|
||||
"lint": "run-linter-tests",
|
||||
"phpcs": "run-phpcs-tests",
|
||||
"phpunit": "run-php-unit-tests",
|
||||
"prepare-tests": "install-package-tests",
|
||||
"test": [
|
||||
"@lint",
|
||||
"@phpcs",
|
||||
"@phpunit",
|
||||
"@behat"
|
||||
]
|
||||
},
|
||||
"support": {
|
||||
"issues": "https://github.com/wp-cli/i18n-command/issues"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,566 @@
|
||||
Feature: Split PO files into JSON files.
|
||||
|
||||
Background:
|
||||
Given a WP install
|
||||
|
||||
Scenario: Bail for invalid source file or directory
|
||||
When I try `wp i18n make-json foo`
|
||||
Then STDERR should contain:
|
||||
"""
|
||||
Error: Source file or directory does not exist!
|
||||
"""
|
||||
And the return code should be 1
|
||||
|
||||
Scenario: Uses source folder as destination by default
|
||||
Given an empty foo-plugin directory
|
||||
And a foo-plugin/foo-plugin-de_DE.po file:
|
||||
"""
|
||||
# Copyright (C) 2018 Foo Plugin
|
||||
# This file is distributed under the same license as the Foo Plugin package.
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: Foo Plugin\n"
|
||||
"Report-Msgid-Bugs-To: https://wordpress.org/support/plugin/foo-plugin\n"
|
||||
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
|
||||
"Language-Team: LANGUAGE <LL@li.org>\n"
|
||||
"Language: de_DE\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
"POT-Creation-Date: 2018-05-02T22:06:24+00:00\n"
|
||||
"PO-Revision-Date: 2018-05-02T22:06:24+00:00\n"
|
||||
"X-Domain: foo-plugin\n"
|
||||
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
|
||||
|
||||
#: foo-plugin.js:15
|
||||
msgid "Foo Plugin"
|
||||
msgstr "Foo Plugin"
|
||||
"""
|
||||
|
||||
When I run `wp i18n make-json foo-plugin`
|
||||
Then STDOUT should contain:
|
||||
"""
|
||||
Success: Created 1 file.
|
||||
"""
|
||||
And the return code should be 0
|
||||
And the foo-plugin/foo-plugin-de_DE-56746e49c6485323d16a717754b7447e.json file should exist
|
||||
|
||||
Scenario: Allows setting custom destination directory
|
||||
Given an empty foo-plugin directory
|
||||
And a foo-plugin/foo-plugin-de_DE.po file:
|
||||
"""
|
||||
# Copyright (C) 2018 Foo Plugin
|
||||
# This file is distributed under the same license as the Foo Plugin package.
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: Foo Plugin\n"
|
||||
"Report-Msgid-Bugs-To: https://wordpress.org/support/plugin/foo-plugin\n"
|
||||
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
|
||||
"Language-Team: LANGUAGE <LL@li.org>\n"
|
||||
"Language: de_DE\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
"POT-Creation-Date: 2018-05-02T22:06:24+00:00\n"
|
||||
"PO-Revision-Date: 2018-05-02T22:06:24+00:00\n"
|
||||
"X-Domain: foo-plugin\n"
|
||||
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
|
||||
|
||||
#: foo-plugin.js:15
|
||||
msgid "Foo Plugin"
|
||||
msgstr "Foo Plugin"
|
||||
"""
|
||||
|
||||
When I run `wp i18n make-json foo-plugin result`
|
||||
Then STDOUT should contain:
|
||||
"""
|
||||
Success: Created 1 file.
|
||||
"""
|
||||
And the return code should be 0
|
||||
And the result/foo-plugin-de_DE-56746e49c6485323d16a717754b7447e.json file should exist
|
||||
|
||||
Scenario: Sets some meta data
|
||||
Given an empty foo-plugin directory
|
||||
And a foo-plugin/foo-plugin-de_DE.po file:
|
||||
"""
|
||||
# Copyright (C) 2018 Foo Plugin
|
||||
# This file is distributed under the same license as the Foo Plugin package.
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: Foo Plugin\n"
|
||||
"Report-Msgid-Bugs-To: https://wordpress.org/support/plugin/foo-plugin\n"
|
||||
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
|
||||
"Language-Team: LANGUAGE <LL@li.org>\n"
|
||||
"Language: de_DE\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
"POT-Creation-Date: 2018-05-02T22:06:24+00:00\n"
|
||||
"PO-Revision-Date: 2018-05-02T22:06:24+00:00\n"
|
||||
"X-Domain: foo-plugin\n"
|
||||
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
|
||||
|
||||
#: foo-plugin.js:15
|
||||
msgid "Foo Plugin"
|
||||
msgstr "Foo Plugin"
|
||||
"""
|
||||
|
||||
When I run `wp i18n make-json foo-plugin`
|
||||
Then STDOUT should contain:
|
||||
"""
|
||||
Success: Created 1 file.
|
||||
"""
|
||||
And the return code should be 0
|
||||
And the foo-plugin/foo-plugin-de_DE-56746e49c6485323d16a717754b7447e.json file should contain:
|
||||
"""
|
||||
"translation-revision-date":
|
||||
"""
|
||||
And the foo-plugin/foo-plugin-de_DE-56746e49c6485323d16a717754b7447e.json file should contain:
|
||||
"""
|
||||
"generator":"WP-CLI
|
||||
"""
|
||||
And the foo-plugin/foo-plugin-de_DE-56746e49c6485323d16a717754b7447e.json file should contain:
|
||||
"""
|
||||
"source":"foo-plugin.js"
|
||||
"""
|
||||
|
||||
Scenario: Always uses messages as text domain
|
||||
Given an empty foo-plugin directory
|
||||
And a foo-plugin/foo-plugin-de_DE.po file:
|
||||
"""
|
||||
# Copyright (C) 2018 Foo Plugin
|
||||
# This file is distributed under the same license as the Foo Plugin package.
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: Foo Plugin\n"
|
||||
"Report-Msgid-Bugs-To: https://wordpress.org/support/plugin/foo-plugin\n"
|
||||
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
|
||||
"Language-Team: LANGUAGE <LL@li.org>\n"
|
||||
"Language: de_DE\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
"POT-Creation-Date: 2018-05-02T22:06:24+00:00\n"
|
||||
"PO-Revision-Date: 2018-05-02T22:06:24+00:00\n"
|
||||
"X-Domain: foo-plugin\n"
|
||||
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
|
||||
|
||||
#: foo-plugin.js:15
|
||||
msgid "Foo Plugin"
|
||||
msgstr "Foo Plugin"
|
||||
"""
|
||||
|
||||
When I run `wp i18n make-json foo-plugin`
|
||||
Then STDOUT should contain:
|
||||
"""
|
||||
Success: Created 1 file.
|
||||
"""
|
||||
And the return code should be 0
|
||||
And the foo-plugin/foo-plugin-de_DE-56746e49c6485323d16a717754b7447e.json file should contain:
|
||||
"""
|
||||
"domain":"messages"
|
||||
"""
|
||||
And the foo-plugin/foo-plugin-de_DE-56746e49c6485323d16a717754b7447e.json file should contain:
|
||||
"""
|
||||
"messages":{
|
||||
"""
|
||||
|
||||
Scenario: Sets correct plural form
|
||||
Given an empty foo-plugin directory
|
||||
And a foo-plugin/foo-plugin-de_DE.po file:
|
||||
"""
|
||||
# Copyright (C) 2018 Foo Plugin
|
||||
# This file is distributed under the same license as the Foo Plugin package.
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: Foo Plugin\n"
|
||||
"Report-Msgid-Bugs-To: https://wordpress.org/support/plugin/foo-plugin\n"
|
||||
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
|
||||
"Language-Team: LANGUAGE <LL@li.org>\n"
|
||||
"Language: de_DE\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
"POT-Creation-Date: 2018-05-02T22:06:24+00:00\n"
|
||||
"PO-Revision-Date: 2018-05-02T22:06:24+00:00\n"
|
||||
"X-Domain: foo-plugin\n"
|
||||
"Plural-Forms: nplurals=3; plural=(n != 2);\n"
|
||||
|
||||
#: foo-plugin.js:15
|
||||
msgid "Foo Plugin"
|
||||
msgstr "Foo Plugin"
|
||||
"""
|
||||
|
||||
When I run `wp i18n make-json foo-plugin`
|
||||
Then STDOUT should contain:
|
||||
"""
|
||||
Success: Created 1 file.
|
||||
"""
|
||||
And the return code should be 0
|
||||
And the foo-plugin/foo-plugin-de_DE-56746e49c6485323d16a717754b7447e.json file should contain:
|
||||
"""
|
||||
"plural-forms":"nplurals=3; plural=(n != 2);"
|
||||
"""
|
||||
|
||||
Scenario: Sets default plural form if missing
|
||||
Given an empty foo-plugin directory
|
||||
And a foo-plugin/foo-plugin-de_DE.po file:
|
||||
"""
|
||||
# Copyright (C) 2018 Foo Plugin
|
||||
# This file is distributed under the same license as the Foo Plugin package.
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: Foo Plugin\n"
|
||||
"Report-Msgid-Bugs-To: https://wordpress.org/support/plugin/foo-plugin\n"
|
||||
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
|
||||
"Language-Team: LANGUAGE <LL@li.org>\n"
|
||||
"Language: de_DE\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
"POT-Creation-Date: 2018-05-02T22:06:24+00:00\n"
|
||||
"PO-Revision-Date: 2018-05-02T22:06:24+00:00\n"
|
||||
"X-Domain: foo-plugin\n"
|
||||
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
|
||||
|
||||
#: foo-plugin.js:15
|
||||
msgid "Foo Plugin"
|
||||
msgstr "Foo Plugin"
|
||||
"""
|
||||
|
||||
When I run `wp i18n make-json foo-plugin`
|
||||
Then STDOUT should contain:
|
||||
"""
|
||||
Success: Created 1 file.
|
||||
"""
|
||||
And the return code should be 0
|
||||
And the foo-plugin/foo-plugin-de_DE-56746e49c6485323d16a717754b7447e.json file should contain:
|
||||
"""
|
||||
"plural-forms":"nplurals=2; plural=(n != 1);"
|
||||
"""
|
||||
|
||||
Scenario: Splits PO file into multiple JSON files
|
||||
Given an empty foo-plugin directory
|
||||
And a foo-plugin/foo-plugin-de_DE.po file:
|
||||
"""
|
||||
# Copyright (C) 2018 Foo Plugin
|
||||
# This file is distributed under the same license as the Foo Plugin package.
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: Foo Plugin\n"
|
||||
"Report-Msgid-Bugs-To: https://wordpress.org/support/plugin/foo-plugin\n"
|
||||
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
|
||||
"Language-Team: LANGUAGE <LL@li.org>\n"
|
||||
"Language: de_DE\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
"POT-Creation-Date: 2018-05-02T22:06:24+00:00\n"
|
||||
"PO-Revision-Date: 2018-05-02T22:06:24+00:00\n"
|
||||
"X-Domain: foo-plugin\n"
|
||||
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
|
||||
|
||||
#: a.js:10
|
||||
msgid "A"
|
||||
msgstr "A"
|
||||
|
||||
#: b.js:10
|
||||
msgid "B"
|
||||
msgstr "B"
|
||||
|
||||
#: a.js:10
|
||||
#: b.js:10
|
||||
msgid "C"
|
||||
msgstr "C"
|
||||
|
||||
#: foo-plugin.php:10
|
||||
msgid "D"
|
||||
msgstr "D"
|
||||
"""
|
||||
|
||||
When I run `wp i18n make-json foo-plugin`
|
||||
Then STDOUT should contain:
|
||||
"""
|
||||
Success: Created 2 files.
|
||||
"""
|
||||
And the return code should be 0
|
||||
|
||||
And the foo-plugin/foo-plugin-de_DE-95f0a310f289230d56c3a4949c17963e.json file should exist
|
||||
And the foo-plugin/foo-plugin-de_DE-95f0a310f289230d56c3a4949c17963e.json file should contain:
|
||||
"""
|
||||
"A"
|
||||
"""
|
||||
And the foo-plugin/foo-plugin-de_DE-95f0a310f289230d56c3a4949c17963e.json file should contain:
|
||||
"""
|
||||
"C"
|
||||
"""
|
||||
And the foo-plugin/foo-plugin-de_DE-95f0a310f289230d56c3a4949c17963e.json file should not contain:
|
||||
"""
|
||||
"B"
|
||||
"""
|
||||
And the foo-plugin/foo-plugin-de_DE-95f0a310f289230d56c3a4949c17963e.json file should not contain:
|
||||
"""
|
||||
"D"
|
||||
"""
|
||||
|
||||
And the foo-plugin/foo-plugin-de_DE-656ad21ad877025a82411b49aa0f8b88.json file should exist
|
||||
And the foo-plugin/foo-plugin-de_DE-656ad21ad877025a82411b49aa0f8b88.json file should contain:
|
||||
"""
|
||||
"B"
|
||||
"""
|
||||
And the foo-plugin/foo-plugin-de_DE-656ad21ad877025a82411b49aa0f8b88.json file should contain:
|
||||
"""
|
||||
"C"
|
||||
"""
|
||||
And the foo-plugin/foo-plugin-de_DE-656ad21ad877025a82411b49aa0f8b88.json file should not contain:
|
||||
"""
|
||||
"A"
|
||||
"""
|
||||
And the foo-plugin/foo-plugin-de_DE-656ad21ad877025a82411b49aa0f8b88.json file should not contain:
|
||||
"""
|
||||
"D"
|
||||
"""
|
||||
|
||||
And the foo-plugin/foo-plugin-de_DE.po file should contain:
|
||||
"""
|
||||
"D"
|
||||
"""
|
||||
And the foo-plugin/foo-plugin-de_DE.po file should not contain:
|
||||
"""
|
||||
"A"
|
||||
"""
|
||||
And the foo-plugin/foo-plugin-de_DE.po file should not contain:
|
||||
"""
|
||||
"B"
|
||||
"""
|
||||
And the foo-plugin/foo-plugin-de_DE.po file should not contain:
|
||||
"""
|
||||
"C"
|
||||
"""
|
||||
|
||||
Scenario: Does not remove strings from original PO file
|
||||
Given an empty foo-plugin directory
|
||||
And a foo-plugin/foo-plugin-de_DE.po file:
|
||||
"""
|
||||
# Copyright (C) 2018 Foo Plugin
|
||||
# This file is distributed under the same license as the Foo Plugin package.
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: Foo Plugin\n"
|
||||
"Report-Msgid-Bugs-To: https://wordpress.org/support/plugin/foo-plugin\n"
|
||||
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
|
||||
"Language-Team: LANGUAGE <LL@li.org>\n"
|
||||
"Language: de_DE\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
"POT-Creation-Date: 2018-05-02T22:06:24+00:00\n"
|
||||
"PO-Revision-Date: 2018-05-02T22:06:24+00:00\n"
|
||||
"X-Domain: foo-plugin\n"
|
||||
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
|
||||
|
||||
#: a.js:10
|
||||
msgid "A"
|
||||
msgstr "A"
|
||||
|
||||
#: b.js:10
|
||||
msgid "B"
|
||||
msgstr "B"
|
||||
|
||||
#: a.js:10
|
||||
#: b.js:10
|
||||
msgid "C"
|
||||
msgstr "C"
|
||||
|
||||
#: foo-plugin.php:10
|
||||
msgid "D"
|
||||
msgstr "D"
|
||||
"""
|
||||
|
||||
When I run `wp i18n make-json foo-plugin --no-purge`
|
||||
Then STDOUT should contain:
|
||||
"""
|
||||
Success: Created 2 files.
|
||||
"""
|
||||
And the return code should be 0
|
||||
|
||||
And the foo-plugin/foo-plugin-de_DE-95f0a310f289230d56c3a4949c17963e.json file should exist
|
||||
And the foo-plugin/foo-plugin-de_DE-95f0a310f289230d56c3a4949c17963e.json file should contain:
|
||||
"""
|
||||
"A"
|
||||
"""
|
||||
And the foo-plugin/foo-plugin-de_DE-95f0a310f289230d56c3a4949c17963e.json file should contain:
|
||||
"""
|
||||
"C"
|
||||
"""
|
||||
And the foo-plugin/foo-plugin-de_DE-95f0a310f289230d56c3a4949c17963e.json file should not contain:
|
||||
"""
|
||||
"B"
|
||||
"""
|
||||
And the foo-plugin/foo-plugin-de_DE-95f0a310f289230d56c3a4949c17963e.json file should not contain:
|
||||
"""
|
||||
"D"
|
||||
"""
|
||||
|
||||
And the foo-plugin/foo-plugin-de_DE-656ad21ad877025a82411b49aa0f8b88.json file should exist
|
||||
And the foo-plugin/foo-plugin-de_DE-656ad21ad877025a82411b49aa0f8b88.json file should contain:
|
||||
"""
|
||||
"B"
|
||||
"""
|
||||
And the foo-plugin/foo-plugin-de_DE-656ad21ad877025a82411b49aa0f8b88.json file should contain:
|
||||
"""
|
||||
"C"
|
||||
"""
|
||||
And the foo-plugin/foo-plugin-de_DE-656ad21ad877025a82411b49aa0f8b88.json file should not contain:
|
||||
"""
|
||||
"A"
|
||||
"""
|
||||
And the foo-plugin/foo-plugin-de_DE-656ad21ad877025a82411b49aa0f8b88.json file should not contain:
|
||||
"""
|
||||
"D"
|
||||
"""
|
||||
|
||||
And the foo-plugin/foo-plugin-de_DE.po file should contain:
|
||||
"""
|
||||
"D"
|
||||
"""
|
||||
And the foo-plugin/foo-plugin-de_DE.po file should contain:
|
||||
"""
|
||||
"A"
|
||||
"""
|
||||
And the foo-plugin/foo-plugin-de_DE.po file should contain:
|
||||
"""
|
||||
"B"
|
||||
"""
|
||||
And the foo-plugin/foo-plugin-de_DE.po file should contain:
|
||||
"""
|
||||
"C"
|
||||
"""
|
||||
|
||||
Scenario: Correctly saves strings with context
|
||||
Given an empty foo-plugin directory
|
||||
And a foo-plugin/foo-plugin-de_DE.po file:
|
||||
"""
|
||||
# Copyright (C) 2018 Foo Plugin
|
||||
# This file is distributed under the same license as the Foo Plugin package.
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: Foo Plugin\n"
|
||||
"Report-Msgid-Bugs-To: https://wordpress.org/support/plugin/foo-plugin\n"
|
||||
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
|
||||
"Language-Team: LANGUAGE <LL@li.org>\n"
|
||||
"Language: de_DE\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
"POT-Creation-Date: 2018-05-02T22:06:24+00:00\n"
|
||||
"PO-Revision-Date: 2018-05-02T22:06:24+00:00\n"
|
||||
"X-Domain: foo-plugin\n"
|
||||
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
|
||||
|
||||
#: foo-plugin.js:15
|
||||
msgctxt "Plugin Name"
|
||||
msgid "Foo Plugin (EN)"
|
||||
msgstr "Foo Plugin (DE)"
|
||||
"""
|
||||
|
||||
When I run `wp i18n make-json foo-plugin`
|
||||
Then STDOUT should contain:
|
||||
"""
|
||||
Success: Created 1 file.
|
||||
"""
|
||||
And the return code should be 0
|
||||
And the foo-plugin/foo-plugin-de_DE-56746e49c6485323d16a717754b7447e.json file should contain:
|
||||
"""
|
||||
"domain":"messages"
|
||||
"""
|
||||
And the foo-plugin/foo-plugin-de_DE-56746e49c6485323d16a717754b7447e.json file should contain:
|
||||
"""
|
||||
"Plugin Name\u0004Foo Plugin (EN)":["Foo Plugin (DE)"]
|
||||
"""
|
||||
|
||||
Scenario: Should create pretty-printed JSON files
|
||||
Given an empty foo-plugin directory
|
||||
And a foo-plugin/foo-plugin-de_DE.po file:
|
||||
"""
|
||||
# Copyright (C) 2018 Foo Plugin
|
||||
# This file is distributed under the same license as the Foo Plugin package.
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: Foo Plugin\n"
|
||||
"Report-Msgid-Bugs-To: https://wordpress.org/support/plugin/foo-plugin\n"
|
||||
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
|
||||
"Language-Team: LANGUAGE <LL@li.org>\n"
|
||||
"Language: de_DE\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
"POT-Creation-Date: 2018-05-02T22:06:24+00:00\n"
|
||||
"PO-Revision-Date: 2018-05-02T22:06:24+00:00\n"
|
||||
"X-Domain: foo-plugin\n"
|
||||
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
|
||||
|
||||
#: foo-plugin.js:15
|
||||
msgid "Foo Plugin"
|
||||
msgstr "Foo Plugin"
|
||||
"""
|
||||
|
||||
When I run `wp i18n make-json foo-plugin --pretty-print`
|
||||
Then STDOUT should contain:
|
||||
"""
|
||||
Success: Created 1 file.
|
||||
"""
|
||||
And the return code should be 0
|
||||
And the foo-plugin/foo-plugin-de_DE-56746e49c6485323d16a717754b7447e.json file should contain:
|
||||
"""
|
||||
"domain": "messages",
|
||||
"locale_data": {
|
||||
"messages": {
|
||||
"": {
|
||||
"domain": "messages",
|
||||
"lang": "de_DE",
|
||||
"plural-forms": "nplurals=2; plural=(n != 1);"
|
||||
},
|
||||
"Foo Plugin": [
|
||||
"Foo Plugin"
|
||||
]
|
||||
}
|
||||
}
|
||||
"""
|
||||
|
||||
Scenario: Should not error for invalid languages
|
||||
Given an empty foo-plugin directory
|
||||
And a foo-plugin/foo-plugin-invalid.po file:
|
||||
"""
|
||||
# Copyright (C) 2018 Foo Plugin
|
||||
# This file is distributed under the same license as the Foo Plugin package.
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: Foo Plugin\n"
|
||||
"Report-Msgid-Bugs-To: https://wordpress.org/support/plugin/foo-plugin\n"
|
||||
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
|
||||
"Language-Team: LANGUAGE <LL@li.org>\n"
|
||||
"Language: invalid\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
"POT-Creation-Date: 2018-05-02T22:06:24+00:00\n"
|
||||
"PO-Revision-Date: 2018-05-02T22:06:24+00:00\n"
|
||||
"X-Domain: foo-plugin\n"
|
||||
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
|
||||
|
||||
#: foo-plugin.js:15
|
||||
msgid "Foo Plugin"
|
||||
msgstr "Foo Plugin"
|
||||
"""
|
||||
|
||||
When I run `wp i18n make-json foo-plugin`
|
||||
Then STDOUT should contain:
|
||||
"""
|
||||
Success: Created 1 file.
|
||||
"""
|
||||
And the return code should be 0
|
||||
And the foo-plugin/foo-plugin-invalid-56746e49c6485323d16a717754b7447e.json file should contain:
|
||||
"""
|
||||
"lang":"invalid"
|
||||
"""
|
||||
+158
@@ -0,0 +1,158 @@
|
||||
Feature: Generate MO files from PO files
|
||||
|
||||
Background:
|
||||
Given a WP install
|
||||
|
||||
Scenario: Bail for invalid source directories
|
||||
When I try `wp i18n make-mo foo`
|
||||
Then STDERR should contain:
|
||||
"""
|
||||
Error: Source file or directory does not exist!
|
||||
"""
|
||||
And the return code should be 1
|
||||
|
||||
Scenario: Uses source folder as destination by default
|
||||
Given an empty foo-plugin directory
|
||||
And a foo-plugin/foo-plugin-de_DE.po file:
|
||||
"""
|
||||
# Copyright (C) 2018 Foo Plugin
|
||||
# This file is distributed under the same license as the Foo Plugin package.
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: Foo Plugin\n"
|
||||
"Report-Msgid-Bugs-To: https://wordpress.org/support/plugin/foo-plugin\n"
|
||||
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
|
||||
"Language-Team: LANGUAGE <LL@li.org>\n"
|
||||
"Language: de_DE\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
"POT-Creation-Date: 2018-05-02T22:06:24+00:00\n"
|
||||
"PO-Revision-Date: 2018-05-02T22:06:24+00:00\n"
|
||||
"X-Domain: foo-plugin\n"
|
||||
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
|
||||
|
||||
#: foo-plugin.js:15
|
||||
msgid "Foo Plugin"
|
||||
msgstr "Foo Plugin"
|
||||
"""
|
||||
|
||||
When I run `wp i18n make-mo foo-plugin`
|
||||
Then STDOUT should contain:
|
||||
"""
|
||||
Success: Created 1 file.
|
||||
"""
|
||||
And the return code should be 0
|
||||
And the foo-plugin/foo-plugin-de_DE.mo file should exist
|
||||
|
||||
Scenario: Allows setting custom destination directory
|
||||
Given an empty foo-plugin directory
|
||||
And a foo-plugin/foo-plugin-de_DE.po file:
|
||||
"""
|
||||
# Copyright (C) 2018 Foo Plugin
|
||||
# This file is distributed under the same license as the Foo Plugin package.
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: Foo Plugin\n"
|
||||
"Report-Msgid-Bugs-To: https://wordpress.org/support/plugin/foo-plugin\n"
|
||||
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
|
||||
"Language-Team: LANGUAGE <LL@li.org>\n"
|
||||
"Language: de_DE\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
"POT-Creation-Date: 2018-05-02T22:06:24+00:00\n"
|
||||
"PO-Revision-Date: 2018-05-02T22:06:24+00:00\n"
|
||||
"X-Domain: foo-plugin\n"
|
||||
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
|
||||
|
||||
#: foo-plugin.js:15
|
||||
msgid "Foo Plugin"
|
||||
msgstr "Foo Plugin"
|
||||
"""
|
||||
|
||||
When I run `wp i18n make-mo foo-plugin result`
|
||||
Then STDOUT should contain:
|
||||
"""
|
||||
Success: Created 1 file.
|
||||
"""
|
||||
And the return code should be 0
|
||||
And the result/foo-plugin-de_DE.mo file should exist
|
||||
|
||||
Scenario: Does include headers
|
||||
Given an empty foo-plugin directory
|
||||
And a foo-plugin/foo-plugin-de_DE.po file:
|
||||
"""
|
||||
# Copyright (C) 2018 Foo Plugin
|
||||
# This file is distributed under the same license as the Foo Plugin package.
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: Foo Plugin\n"
|
||||
"Report-Msgid-Bugs-To: https://wordpress.org/support/plugin/foo-plugin\n"
|
||||
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
|
||||
"Language-Team: LANGUAGE <LL@li.org>\n"
|
||||
"Language: de_DE\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
"POT-Creation-Date: 2018-05-02T22:06:24+00:00\n"
|
||||
"PO-Revision-Date: 2018-05-02T22:06:24+00:00\n"
|
||||
"X-Domain: foo-plugin\n"
|
||||
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
|
||||
|
||||
#: foo-plugin.js:15
|
||||
msgid "Foo Plugin"
|
||||
msgstr "Foo Plugin"
|
||||
"""
|
||||
|
||||
When I run `wp i18n make-mo foo-plugin`
|
||||
Then STDOUT should contain:
|
||||
"""
|
||||
Success: Created 1 file.
|
||||
"""
|
||||
And the return code should be 0
|
||||
And the foo-plugin/foo-plugin-de_DE.mo file should contain:
|
||||
"""
|
||||
Language: de_DE
|
||||
"""
|
||||
And the foo-plugin/foo-plugin-de_DE.mo file should contain:
|
||||
"""
|
||||
X-Domain: foo-plugin
|
||||
"""
|
||||
|
||||
Scenario: Does include translations
|
||||
Given an empty foo-plugin directory
|
||||
And a foo-plugin/foo-plugin-de_DE.po file:
|
||||
"""
|
||||
# Copyright (C) 2018 Foo Plugin
|
||||
# This file is distributed under the same license as the Foo Plugin package.
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: Foo Plugin\n"
|
||||
"Report-Msgid-Bugs-To: https://wordpress.org/support/plugin/foo-plugin\n"
|
||||
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
|
||||
"Language-Team: LANGUAGE <LL@li.org>\n"
|
||||
"Language: de_DE\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
"POT-Creation-Date: 2018-05-02T22:06:24+00:00\n"
|
||||
"PO-Revision-Date: 2018-05-02T22:06:24+00:00\n"
|
||||
"X-Domain: foo-plugin\n"
|
||||
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
|
||||
|
||||
#: foo-plugin.js:15
|
||||
msgid "Foo Plugin"
|
||||
msgstr "Bar Plugin"
|
||||
"""
|
||||
|
||||
When I run `wp i18n make-mo foo-plugin`
|
||||
Then STDOUT should contain:
|
||||
"""
|
||||
Success: Created 1 file.
|
||||
"""
|
||||
And the return code should be 0
|
||||
And the foo-plugin/foo-plugin-de_DE.mo file should contain:
|
||||
"""
|
||||
Bar Plugin
|
||||
"""
|
||||
+2773
File diff suppressed because it is too large
Load Diff
+21
@@ -0,0 +1,21 @@
|
||||
<?php
|
||||
|
||||
if ( ! class_exists( 'WP_CLI' ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
$wpcli_i18n_autoloader = __DIR__ . '/vendor/autoload.php';
|
||||
|
||||
if ( file_exists( $wpcli_i18n_autoloader ) ) {
|
||||
require_once $wpcli_i18n_autoloader;
|
||||
}
|
||||
|
||||
if ( class_exists( 'WP_CLI\Dispatcher\CommandNamespace' ) ) {
|
||||
WP_CLI::add_command( 'i18n', '\WP_CLI\I18n\CommandNamespace' );
|
||||
}
|
||||
|
||||
WP_CLI::add_command( 'i18n make-pot', '\WP_CLI\I18n\MakePotCommand' );
|
||||
|
||||
WP_CLI::add_command( 'i18n make-json', '\WP_CLI\I18n\MakeJsonCommand' );
|
||||
|
||||
WP_CLI::add_command( 'i18n make-mo', '\WP_CLI\I18n\MakeMoCommand' );
|
||||
+86
@@ -0,0 +1,86 @@
|
||||
<?xml version="1.0"?>
|
||||
<ruleset name="WP-CLI-i18n">
|
||||
<description>Custom ruleset for WP-CLI I18n-command</description>
|
||||
|
||||
<!--
|
||||
#############################################################################
|
||||
COMMAND LINE ARGUMENTS
|
||||
For help understanding this file: https://github.com/squizlabs/PHP_CodeSniffer/wiki/Annotated-ruleset.xml
|
||||
For help using PHPCS: https://github.com/squizlabs/PHP_CodeSniffer/wiki/Usage
|
||||
#############################################################################
|
||||
-->
|
||||
|
||||
<!-- What to scan. -->
|
||||
<file>.</file>
|
||||
|
||||
<!-- Ignoring select files/folders.
|
||||
https://github.com/squizlabs/PHP_CodeSniffer/wiki/Advanced-Usage#ignoring-files-and-folders -->
|
||||
<exclude-pattern>*/tests/data/*</exclude-pattern>
|
||||
|
||||
<!-- Show progress. -->
|
||||
<arg value="p"/>
|
||||
|
||||
<!-- Strip the filepaths down to the relevant bit. -->
|
||||
<arg name="basepath" value="./"/>
|
||||
|
||||
<!-- Check up to 8 files simultaneously. -->
|
||||
<arg name="parallel" value="8"/>
|
||||
|
||||
<!--
|
||||
#############################################################################
|
||||
USE THE WP_CLI_CS RULESET
|
||||
#############################################################################
|
||||
-->
|
||||
|
||||
<rule ref="WP_CLI_CS"/>
|
||||
|
||||
<!--
|
||||
#############################################################################
|
||||
PROJECT SPECIFIC CONFIGURATION FOR SNIFFS
|
||||
#############################################################################
|
||||
-->
|
||||
|
||||
<!-- For help understanding the `testVersion` configuration setting:
|
||||
https://github.com/PHPCompatibility/PHPCompatibility#sniffing-your-code-for-compatibility-with-specific-php-versions -->
|
||||
<config name="testVersion" value="5.6-"/>
|
||||
|
||||
<!-- Verify that everything in the global namespace is either namespaced or prefixed.
|
||||
See: https://github.com/WordPress-Coding-Standards/WordPress-Coding-Standards/wiki/Customizable-sniff-properties#naming-conventions-prefix-everything-in-the-global-namespace -->
|
||||
<rule ref="WordPress.NamingConventions.PrefixAllGlobals">
|
||||
<properties>
|
||||
<property name="prefixes" type="array">
|
||||
<element value="WP_CLI\I18n"/><!-- Namespaces. -->
|
||||
<element value="wpcli_i18n"/><!-- Global variables and such. -->
|
||||
</property>
|
||||
</properties>
|
||||
</rule>
|
||||
|
||||
<!-- Whitelist method names for classes that extend a PSR-2 package. -->
|
||||
<rule ref="WordPress.NamingConventions.ValidFunctionName.MethodNameInvalid">
|
||||
<exclude-pattern>*/src/IterableCodeExtractor\.php$</exclude-pattern>
|
||||
</rule>
|
||||
|
||||
<!-- Whitelist property name for a a JSON-decoded object. -->
|
||||
<rule ref="WordPress.NamingConventions.ValidVariableName">
|
||||
<properties>
|
||||
<property name="customPropertiesWhitelist" type="array">
|
||||
<element value="sourcesContent"/>
|
||||
<element value="functionsScannerClass"/>
|
||||
</property>
|
||||
</properties>
|
||||
</rule>
|
||||
|
||||
<!-- Allow for JavaScript format examples to be provided in comments.
|
||||
See: https://github.com/squizlabs/PHP_CodeSniffer/wiki/Customisable-Sniff-Properties#squizphpcommentedoutcode -->
|
||||
<rule ref="Squiz.PHP.CommentedOutCode">
|
||||
<properties>
|
||||
<property name="maxPercentage" value="45"/>
|
||||
</properties>
|
||||
</rule>
|
||||
|
||||
<!-- Whitelist method names for classes that extend a PSR-2 package. -->
|
||||
<rule ref="WordPress.DateTime.RestrictedFunctions.date_date">
|
||||
<exclude-pattern>*/src/MakePotCommand\.php$</exclude-pattern>
|
||||
</rule>
|
||||
|
||||
</ruleset>
|
||||
@@ -0,0 +1,72 @@
|
||||
<?php
|
||||
|
||||
namespace WP_CLI\I18n;
|
||||
|
||||
use Gettext\Extractors\Extractor;
|
||||
use Gettext\Extractors\ExtractorInterface;
|
||||
use Gettext\Translations;
|
||||
use WP_CLI;
|
||||
|
||||
final class BlockExtractor extends Extractor implements ExtractorInterface {
|
||||
use IterableCodeExtractor;
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
public static function fromString( $string, Translations $translations, array $options = [] ) {
|
||||
$file = $options['file'];
|
||||
WP_CLI::debug( "Parsing file {$file}", 'make-pot' );
|
||||
|
||||
$file_data = json_decode( $string, true );
|
||||
|
||||
if ( null === $file_data ) {
|
||||
WP_CLI::debug(
|
||||
sprintf(
|
||||
'Could not parse file %1$s: error code %2$s',
|
||||
$file,
|
||||
json_last_error()
|
||||
),
|
||||
'make-pot'
|
||||
);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$domain = isset( $file_data['textdomain'] ) ? $file_data['textdomain'] : null;
|
||||
|
||||
// Allow missing domain, but skip if they don't match.
|
||||
if ( null !== $domain && $domain !== $translations->getDomain() ) {
|
||||
return;
|
||||
}
|
||||
|
||||
foreach ( $file_data as $key => $original ) {
|
||||
switch ( $key ) {
|
||||
case 'title':
|
||||
case 'description':
|
||||
$translation = $translations->insert( sprintf( 'block %s', $key ), $original );
|
||||
$translation->addReference( $file );
|
||||
break;
|
||||
case 'keywords':
|
||||
if ( ! is_array( $original ) ) {
|
||||
continue 2;
|
||||
}
|
||||
|
||||
foreach ( $original as $msg ) {
|
||||
$translation = $translations->insert( 'block keyword', $msg );
|
||||
$translation->addReference( $file );
|
||||
}
|
||||
|
||||
break;
|
||||
case 'styles':
|
||||
if ( ! is_array( $original ) ) {
|
||||
continue 2;
|
||||
}
|
||||
|
||||
foreach ( $original as $msg ) {
|
||||
$translation = $translations->insert( 'block style label', $msg['label'] );
|
||||
$translation->addReference( $file );
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
<?php
|
||||
|
||||
namespace WP_CLI\I18n;
|
||||
|
||||
use WP_CLI\Dispatcher\CommandNamespace as BaseCommandNamespace;
|
||||
|
||||
/**
|
||||
* Provides internationalization tools for WordPress projects.
|
||||
*
|
||||
* ## EXAMPLES
|
||||
*
|
||||
* # Create a POT file for the WordPress plugin/theme in the current directory
|
||||
* $ wp i18n make-pot . languages/my-plugin.pot
|
||||
*
|
||||
* @when before_wp_load
|
||||
*/
|
||||
class CommandNamespace extends BaseCommandNamespace {
|
||||
}
|
||||
@@ -0,0 +1,264 @@
|
||||
<?php
|
||||
|
||||
namespace WP_CLI\I18n;
|
||||
|
||||
use Gettext\Translation;
|
||||
use Gettext\Translations;
|
||||
use RecursiveCallbackFilterIterator;
|
||||
use RecursiveDirectoryIterator;
|
||||
use RecursiveIteratorIterator;
|
||||
use SplFileInfo;
|
||||
use WP_CLI;
|
||||
use WP_CLI\Utils;
|
||||
|
||||
trait IterableCodeExtractor {
|
||||
|
||||
private static $dir = '';
|
||||
|
||||
/**
|
||||
* Extract the translations from a file.
|
||||
*
|
||||
* @param array|string $file_or_files A path of a file or files
|
||||
* @param Translations $translations The translations instance to append the new translations.
|
||||
* @param array $options {
|
||||
* Optional. An array of options passed down to static::fromString()
|
||||
*
|
||||
* @type bool $wpExtractTemplates Extract 'Template Name' headers in theme files. Default 'false'.
|
||||
* @type array $restrictFileNames Skip all files which are not included in this array.
|
||||
* }
|
||||
* @return null
|
||||
*/
|
||||
public static function fromFile( $file_or_files, Translations $translations, array $options = [] ) {
|
||||
foreach ( static::getFiles( $file_or_files ) as $file ) {
|
||||
if ( ! empty( $options['restrictFileNames'] ) ) {
|
||||
$basename = Utils\basename( $file );
|
||||
if ( ! in_array( $basename, $options['restrictFileNames'], true ) ) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
// Make sure a relative file path is added as a comment.
|
||||
$options['file'] = ltrim( str_replace( static::$dir, '', Utils\normalize_path( $file ) ), '/' );
|
||||
|
||||
$string = file_get_contents( $file );
|
||||
|
||||
if ( ! $string ) {
|
||||
WP_CLI::debug(
|
||||
sprintf(
|
||||
'Could not load file %1s',
|
||||
$file
|
||||
),
|
||||
'make-pot'
|
||||
);
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
if ( ! empty( $options['wpExtractTemplates'] ) ) {
|
||||
$headers = MakePotCommand::get_file_data_from_string( $string, [ 'Template Name' => 'Template Name' ] );
|
||||
|
||||
if ( ! empty( $headers['Template Name'] ) ) {
|
||||
$translation = new Translation( '', $headers['Template Name'] );
|
||||
$translation->addExtractedComment( 'Template Name of the theme' );
|
||||
|
||||
$translations[] = $translation;
|
||||
}
|
||||
}
|
||||
|
||||
static::fromString( $string, $translations, $options );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract the translations from a file.
|
||||
*
|
||||
* @param string $dir Root path to start the recursive traversal in.
|
||||
* @param Translations $translations The translations instance to append the new translations.
|
||||
* @param array $options {
|
||||
* Optional. An array of options passed down to static::fromString()
|
||||
*
|
||||
* @type bool $wpExtractTemplates Extract 'Template Name' headers in theme files. Default 'false'.
|
||||
* @type array $exclude A list of path to exclude. Default [].
|
||||
* @type array $extensions A list of extensions to process. Default [].
|
||||
* }
|
||||
* @return null
|
||||
*/
|
||||
public static function fromDirectory( $dir, Translations $translations, array $options = [] ) {
|
||||
$dir = Utils\normalize_path( $dir );
|
||||
|
||||
static::$dir = $dir;
|
||||
|
||||
$include = isset( $options['include'] ) ? $options['include'] : [];
|
||||
$exclude = isset( $options['exclude'] ) ? $options['exclude'] : [];
|
||||
|
||||
$files = static::getFilesFromDirectory( $dir, $include, $exclude, $options['extensions'] );
|
||||
|
||||
if ( ! empty( $files ) ) {
|
||||
static::fromFile( $files, $translations, $options );
|
||||
}
|
||||
|
||||
static::$dir = '';
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines whether a file is valid based on given matchers.
|
||||
*
|
||||
* @param SplFileInfo $file File or directory.
|
||||
* @param array $matchers List of files and directories to match.
|
||||
* @return int How strongly the file is matched.
|
||||
*/
|
||||
protected static function calculateMatchScore( SplFileInfo $file, array $matchers = [] ) {
|
||||
if ( empty( $matchers ) ) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
if ( in_array( $file->getBasename(), $matchers, true ) ) {
|
||||
return 10;
|
||||
}
|
||||
|
||||
// Check for more complex paths, e.g. /some/sub/folder.
|
||||
$root_relative_path = str_replace( static::$dir, '', $file->getPathname() );
|
||||
|
||||
foreach ( $matchers as $path_or_file ) {
|
||||
$pattern = preg_quote( str_replace( '*', '__wildcard__', $path_or_file ), '/' );
|
||||
$pattern = '(^|/)' . str_replace( '__wildcard__', '(.+)', $pattern );
|
||||
|
||||
// Base score is the amount of nested directories, discounting wildcards.
|
||||
$base_score = count(
|
||||
array_filter(
|
||||
explode( '/', $path_or_file ),
|
||||
static function ( $component ) {
|
||||
return '*' !== $component;
|
||||
}
|
||||
)
|
||||
);
|
||||
if ( 0 === $base_score ) {
|
||||
// If the matcher is simply * it gets a score above the implicit score but below 1.
|
||||
$base_score = 0.2;
|
||||
}
|
||||
|
||||
// If the matcher contains no wildcards and matches the end of the path.
|
||||
if (
|
||||
false === strpos( $path_or_file, '*' ) &&
|
||||
false !== mb_ereg( $pattern . '$', $root_relative_path )
|
||||
) {
|
||||
return $base_score * 10;
|
||||
}
|
||||
|
||||
// If the matcher matches the end of the path or a full directory contained.
|
||||
if ( false !== mb_ereg( $pattern . '(/|$)', $root_relative_path ) ) {
|
||||
return $base_score;
|
||||
}
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines whether or not a directory has children that may be matched.
|
||||
*
|
||||
* @param SplFileInfo $dir Directory.
|
||||
* @param array $matchers List of files and directories to match.
|
||||
* @return bool Whether or not there are any matchers for children of this directory.
|
||||
*/
|
||||
protected static function containsMatchingChildren( SplFileInfo $dir, array $matchers = [] ) {
|
||||
if ( empty( $matchers ) ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
/** @var string $root_relative_path */
|
||||
$root_relative_path = str_replace( static::$dir, '', $dir->getPathname() );
|
||||
|
||||
foreach ( $matchers as $path_or_file ) {
|
||||
// If the matcher contains no wildcards and the path matches the start of the matcher.
|
||||
if (
|
||||
'' !== $root_relative_path &&
|
||||
false === strpos( $path_or_file, '*' ) &&
|
||||
0 === strpos( $path_or_file . '/', $root_relative_path )
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
|
||||
$base = current( explode( '*', $path_or_file ) );
|
||||
|
||||
// If start of the path matches the start of the matcher until the first wildcard.
|
||||
// Or the start of the matcher until the first wildcard matches the start of the path.
|
||||
if (
|
||||
( '' !== $root_relative_path && 0 === strpos( $base, $root_relative_path ) ) ||
|
||||
( '' !== $base && 0 === strpos( $root_relative_path, $base ) )
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Recursively gets all PHP files within a directory.
|
||||
*
|
||||
* @param string $dir A path of a directory.
|
||||
* @param array $include List of files and directories to include.
|
||||
* @param array $exclude List of files and directories to skip.
|
||||
* @param array $extensions List of filename extensions to process.
|
||||
*
|
||||
* @return array File list.
|
||||
*/
|
||||
public static function getFilesFromDirectory( $dir, array $include = [], array $exclude = [], $extensions = [] ) {
|
||||
$filtered_files = [];
|
||||
|
||||
$files = new RecursiveIteratorIterator(
|
||||
new RecursiveCallbackFilterIterator(
|
||||
new RecursiveDirectoryIterator( $dir, RecursiveDirectoryIterator::SKIP_DOTS | RecursiveDirectoryIterator::UNIX_PATHS ),
|
||||
static function ( $file, $key, $iterator ) use ( $include, $exclude, $extensions ) {
|
||||
/** @var RecursiveCallbackFilterIterator $iterator */
|
||||
/** @var SplFileInfo $file */
|
||||
|
||||
// Normalize include and exclude paths.
|
||||
$include = array_map( 'static::trim_leading_slash', $include );
|
||||
$exclude = array_map( 'static::trim_leading_slash', $exclude );
|
||||
|
||||
// If no $include is passed everything gets the weakest possible matching score.
|
||||
$inclusion_score = empty( $include ) ? 0.1 : static::calculateMatchScore( $file, $include );
|
||||
$exclusion_score = static::calculateMatchScore( $file, $exclude );
|
||||
|
||||
// Always include directories that aren't excluded.
|
||||
if ( 0 === $exclusion_score && $iterator->hasChildren() ) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if ( ( 0 === $inclusion_score || $exclusion_score > $inclusion_score ) && $iterator->hasChildren() ) {
|
||||
// Always include directories that may have matching children even if they are excluded.
|
||||
return static::containsMatchingChildren( $file, $include );
|
||||
}
|
||||
|
||||
return ( ( $inclusion_score >= $exclusion_score ) && $file->isFile() && in_array( $file->getExtension(), $extensions, true ) );
|
||||
}
|
||||
),
|
||||
RecursiveIteratorIterator::CHILD_FIRST
|
||||
);
|
||||
|
||||
foreach ( $files as $file ) {
|
||||
/** @var SplFileInfo $file */
|
||||
if ( ! $file->isFile() || ! in_array( $file->getExtension(), $extensions, true ) ) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$filtered_files[] = Utils\normalize_path( $file->getPathname() );
|
||||
}
|
||||
|
||||
sort( $filtered_files, SORT_NATURAL | SORT_FLAG_CASE );
|
||||
|
||||
return $filtered_files;
|
||||
}
|
||||
|
||||
/**
|
||||
* Trim leading slash from a path.
|
||||
*
|
||||
* @param string $path Path to trim.
|
||||
* @return string Trimmed path.
|
||||
*/
|
||||
private static function trim_leading_slash( $path ) {
|
||||
return ltrim( $path, '/' );
|
||||
}
|
||||
}
|
||||
+82
@@ -0,0 +1,82 @@
|
||||
<?php
|
||||
|
||||
namespace WP_CLI\I18n;
|
||||
|
||||
use Gettext\Generators\Jed;
|
||||
use Gettext\Translation;
|
||||
use Gettext\Translations;
|
||||
|
||||
/**
|
||||
* Jed file generator.
|
||||
*
|
||||
* Adds some more meta data to JED translation files than the default generator.
|
||||
*/
|
||||
class JedGenerator extends Jed {
|
||||
/**
|
||||
* {@parentDoc}.
|
||||
*/
|
||||
public static function toString( Translations $translations, array $options = [] ) {
|
||||
$options += static::$options;
|
||||
$domain = $translations->getDomain() ?: 'messages';
|
||||
$messages = static::buildMessages( $translations );
|
||||
|
||||
$configuration = [
|
||||
'' => [
|
||||
'domain' => $domain,
|
||||
'lang' => $translations->getLanguage() ?: 'en',
|
||||
'plural-forms' => $translations->getHeader( 'Plural-Forms' ) ?: 'nplurals=2; plural=(n != 1);',
|
||||
],
|
||||
];
|
||||
|
||||
$data = [
|
||||
'translation-revision-date' => $translations->getHeader( 'PO-Revision-Date' ),
|
||||
'generator' => 'WP-CLI/' . WP_CLI_VERSION,
|
||||
'source' => $options['source'],
|
||||
'domain' => $domain,
|
||||
'locale_data' => [
|
||||
$domain => $configuration + $messages,
|
||||
],
|
||||
];
|
||||
|
||||
return json_encode( $data, $options['json'] );
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates an array with all translations.
|
||||
*
|
||||
* @param Translations $translations
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public static function buildMessages( Translations $translations ) {
|
||||
$plural_forms = $translations->getPluralForms();
|
||||
$number_of_plurals = is_array( $plural_forms ) ? ( $plural_forms[0] - 1 ) : null;
|
||||
$messages = [];
|
||||
$context_glue = chr( 4 );
|
||||
|
||||
foreach ( $translations as $translation ) {
|
||||
/** @var Translation $translation */
|
||||
|
||||
if ( $translation->isDisabled() ) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$key = $translation->getOriginal();
|
||||
|
||||
if ( $translation->hasContext() ) {
|
||||
$key = $translation->getContext() . $context_glue . $key;
|
||||
}
|
||||
|
||||
if ( $translation->hasPluralTranslations( true ) ) {
|
||||
$message = $translation->getPluralTranslations( $number_of_plurals );
|
||||
array_unshift( $message, $translation->getTranslation() );
|
||||
} else {
|
||||
$message = [ $translation->getTranslation() ];
|
||||
}
|
||||
|
||||
$messages[ $key ] = $message;
|
||||
}
|
||||
|
||||
return $messages;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
<?php
|
||||
|
||||
namespace WP_CLI\I18n;
|
||||
|
||||
use Exception;
|
||||
use Gettext\Extractors\JsCode;
|
||||
use Gettext\Translations;
|
||||
use Peast\Syntax\Exception as PeastException;
|
||||
use WP_CLI;
|
||||
|
||||
final class JsCodeExtractor extends JsCode {
|
||||
use IterableCodeExtractor;
|
||||
|
||||
public static $options = [
|
||||
'extractComments' => [ 'translators', 'Translators' ],
|
||||
'constants' => [],
|
||||
'functions' => [
|
||||
'__' => 'text_domain',
|
||||
'_x' => 'text_context_domain',
|
||||
'_n' => 'single_plural_number_domain',
|
||||
'_nx' => 'single_plural_number_context_domain',
|
||||
],
|
||||
];
|
||||
|
||||
protected static $functionsScannerClass = 'WP_CLI\I18n\JsFunctionsScanner';
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
public static function fromString( $string, Translations $translations, array $options = [] ) {
|
||||
WP_CLI::debug( "Parsing file {$options['file']}", 'make-pot' );
|
||||
|
||||
try {
|
||||
static::fromStringMultiple( $string, [ $translations ], $options );
|
||||
} catch ( PeastException $exception ) {
|
||||
WP_CLI::debug(
|
||||
sprintf(
|
||||
'Could not parse file %1$s: %2$s (line %3$d, column %4$d)',
|
||||
$options['file'],
|
||||
$exception->getMessage(),
|
||||
$exception->getPosition()->getLine(),
|
||||
$exception->getPosition()->getColumn()
|
||||
),
|
||||
'make-pot'
|
||||
);
|
||||
} catch ( Exception $exception ) {
|
||||
WP_CLI::debug(
|
||||
sprintf(
|
||||
'Could not parse file %1$s: %2$s',
|
||||
$options['file'],
|
||||
$exception->getMessage()
|
||||
),
|
||||
'make-pot'
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public static function fromStringMultiple( $string, array $translations, array $options = [] ) {
|
||||
$options += static::$options;
|
||||
|
||||
/** @var JsFunctionsScanner $functions */
|
||||
$functions = new static::$functionsScannerClass( $string );
|
||||
$functions->enableCommentsExtraction( $options['extractComments'] );
|
||||
$functions->saveGettextFunctions( $translations, $options );
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,279 @@
|
||||
<?php
|
||||
|
||||
namespace WP_CLI\I18n;
|
||||
|
||||
use Gettext\Utils\JsFunctionsScanner as GettextJsFunctionsScanner;
|
||||
use Gettext\Utils\ParsedComment;
|
||||
use Peast\Peast;
|
||||
use Peast\Syntax\Node;
|
||||
use Peast\Traverser;
|
||||
|
||||
final class JsFunctionsScanner extends GettextJsFunctionsScanner {
|
||||
|
||||
/**
|
||||
* If not false, comments will be extracted.
|
||||
*
|
||||
* @var string|false|array
|
||||
*/
|
||||
private $extract_comments = false;
|
||||
|
||||
/**
|
||||
* Enable extracting comments that start with a tag (if $tag is empty all the comments will be extracted).
|
||||
*
|
||||
* @param mixed $tag
|
||||
*/
|
||||
public function enableCommentsExtraction( $tag = '' ) {
|
||||
$this->extract_comments = $tag;
|
||||
}
|
||||
|
||||
/**
|
||||
* Disable comments extraction.
|
||||
*/
|
||||
public function disableCommentsExtraction() {
|
||||
$this->extract_comments = false;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function saveGettextFunctions( $translations, array $options ) {
|
||||
// Ignore multiple translations for now.
|
||||
// @todo Add proper support for multiple translations.
|
||||
if ( is_array( $translations ) ) {
|
||||
$translations = $translations[0];
|
||||
}
|
||||
|
||||
$code = $this->code;
|
||||
// See https://github.com/mck89/peast/issues/7
|
||||
// Temporary workaround to fix dynamic imports. The τ is a greek letter.
|
||||
// This will trick the parser into thinking that it is a normal method call.
|
||||
$code = preg_replace( '/import(\\s*\\()/', 'imporτ$1', $code );
|
||||
|
||||
$peast_options = [
|
||||
'sourceType' => Peast::SOURCE_TYPE_MODULE,
|
||||
'comments' => false !== $this->extract_comments,
|
||||
'jsx' => true,
|
||||
];
|
||||
$ast = Peast::latest( $code, $peast_options )->parse();
|
||||
|
||||
$traverser = new Traverser();
|
||||
|
||||
$all_comments = [];
|
||||
|
||||
/**
|
||||
* Traverse through JS code to find and extract gettext functions.
|
||||
*
|
||||
* Make sure translator comments in front of variable declarations
|
||||
* and inside nested call expressions are available when parsing the function call.
|
||||
*/
|
||||
$traverser->addFunction(
|
||||
function ( $node ) use ( &$translations, $options, &$all_comments ) {
|
||||
$functions = $options['functions'];
|
||||
$file = $options['file'];
|
||||
|
||||
/** @var Node\Node $node */
|
||||
foreach ( $node->getLeadingComments() as $comment ) {
|
||||
$all_comments[] = $comment;
|
||||
}
|
||||
|
||||
/** @var Node\CallExpression $node */
|
||||
if ( 'CallExpression' !== $node->getType() ) {
|
||||
return;
|
||||
}
|
||||
|
||||
$callee = $this->resolveExpressionCallee( $node );
|
||||
|
||||
if ( ! $callee || ! isset( $functions[ $callee['name'] ] ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
/** @var Node\CallExpression $node */
|
||||
foreach ( $node->getArguments() as $argument ) {
|
||||
// Support nested function calls.
|
||||
$argument->setLeadingComments( $argument->getLeadingComments() + $node->getLeadingComments() );
|
||||
}
|
||||
|
||||
foreach ( $callee['comments'] as $comment ) {
|
||||
$all_comments[] = $comment;
|
||||
}
|
||||
|
||||
$domain = null;
|
||||
$original = null;
|
||||
$context = null;
|
||||
$plural = null;
|
||||
$args = [];
|
||||
|
||||
/** @var Node\Node $argument */
|
||||
foreach ( $node->getArguments() as $argument ) {
|
||||
foreach ( $argument->getLeadingComments() as $comment ) {
|
||||
$all_comments[] = $comment;
|
||||
}
|
||||
|
||||
if ( 'Identifier' === $argument->getType() ) {
|
||||
$args[] = ''; // The value doesn't matter as it's unused.
|
||||
}
|
||||
|
||||
if ( 'Literal' === $argument->getType() ) {
|
||||
/** @var Node\Literal $argument */
|
||||
$args[] = $argument->getValue();
|
||||
}
|
||||
}
|
||||
|
||||
switch ( $functions[ $callee['name'] ] ) {
|
||||
case 'text_domain':
|
||||
case 'gettext':
|
||||
list( $original, $domain ) = array_pad( $args, 2, null );
|
||||
break;
|
||||
|
||||
case 'text_context_domain':
|
||||
list( $original, $context, $domain ) = array_pad( $args, 3, null );
|
||||
break;
|
||||
|
||||
case 'single_plural_number_domain':
|
||||
list( $original, $plural, $number, $domain ) = array_pad( $args, 4, null );
|
||||
break;
|
||||
|
||||
case 'single_plural_number_context_domain':
|
||||
list( $original, $plural, $number, $context, $domain ) = array_pad( $args, 5, null );
|
||||
break;
|
||||
}
|
||||
|
||||
if ( '' === (string) $original ) {
|
||||
return;
|
||||
}
|
||||
|
||||
if ( $domain !== $translations->getDomain() && null !== $translations->getDomain() ) {
|
||||
return;
|
||||
}
|
||||
|
||||
$translation = $translations->insert( $context, $original, $plural );
|
||||
$translation->addReference( $file, $node->getLocation()->getStart()->getLine() );
|
||||
|
||||
/** @var Node\Comment $comment */
|
||||
foreach ( $all_comments as $comment ) {
|
||||
// Comments should be before the translation.
|
||||
if ( ! $this->commentPrecedesNode( $comment, $node ) ) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$parsed_comment = ParsedComment::create( $comment->getRawText(), $comment->getLocation()->getStart()->getLine() );
|
||||
$prefixes = array_filter( (array) $this->extract_comments );
|
||||
|
||||
if ( $parsed_comment->checkPrefixes( $prefixes ) ) {
|
||||
$translation->addExtractedComment( $parsed_comment->getComment() );
|
||||
}
|
||||
}
|
||||
|
||||
if ( isset( $parsed_comment ) ) {
|
||||
$all_comments = [];
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
$traverser->traverse( $ast );
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve the callee of a call expression using known formats.
|
||||
*
|
||||
* @param Node\CallExpression $node The call expression whose callee to resolve.
|
||||
*
|
||||
* @return array|bool Array containing the name and comments of the identifier if resolved. False if not.
|
||||
*/
|
||||
private function resolveExpressionCallee( Node\CallExpression $node ) {
|
||||
$callee = $node->getCallee();
|
||||
|
||||
// If the callee is a simple identifier it can simply be returned.
|
||||
// For example: __( "translation" ).
|
||||
if ( 'Identifier' === $callee->getType() ) {
|
||||
return [
|
||||
'name' => $callee->getName(),
|
||||
'comments' => $callee->getLeadingComments(),
|
||||
];
|
||||
}
|
||||
|
||||
// If the callee is a member expression resolve it to the property.
|
||||
// For example: wp.i18n.__( "translation" ) or u.__( "translation" ).
|
||||
if (
|
||||
'MemberExpression' === $callee->getType() &&
|
||||
'Identifier' === $callee->getProperty()->getType()
|
||||
) {
|
||||
// Make sure to unpack wp.i18n which is a nested MemberExpression.
|
||||
$comments = 'MemberExpression' === $callee->getObject()->getType()
|
||||
? $callee->getObject()->getObject()->getLeadingComments()
|
||||
: $callee->getObject()->getLeadingComments();
|
||||
|
||||
return [
|
||||
'name' => $callee->getProperty()->getName(),
|
||||
'comments' => $comments,
|
||||
];
|
||||
}
|
||||
|
||||
// If the callee is a call expression as created by Webpack resolve it.
|
||||
// For example: Object(u.__)( "translation" ).
|
||||
if (
|
||||
'CallExpression' === $callee->getType() &&
|
||||
'Identifier' === $callee->getCallee()->getType() &&
|
||||
'Object' === $callee->getCallee()->getName() &&
|
||||
[] !== $callee->getArguments() &&
|
||||
'MemberExpression' === $callee->getArguments()[0]->getType()
|
||||
) {
|
||||
$property = $callee->getArguments()[0]->getProperty();
|
||||
|
||||
// Matches minified webpack statements: Object(u.__)( "translation" ).
|
||||
if ( 'Identifier' === $property->getType() ) {
|
||||
return [
|
||||
'name' => $property->getName(),
|
||||
'comments' => $callee->getCallee()->getLeadingComments(),
|
||||
];
|
||||
}
|
||||
|
||||
// Matches unminified webpack statements:
|
||||
// Object(_wordpress_i18n__WEBPACK_IMPORTED_MODULE_7__["__"])( "translation" );
|
||||
if ( 'Literal' === $property->getType() ) {
|
||||
$name = $property->getValue();
|
||||
|
||||
// Matches mangled webpack statement:
|
||||
// Object(_wordpress_i18n__WEBPACK_IMPORTED_MODULE_7__[/* __ */ "a"])( "translation" );
|
||||
$leading_property_comments = $property->getLeadingComments();
|
||||
if ( count( $leading_property_comments ) === 1 && $leading_property_comments[0]->getKind() === 'multiline' ) {
|
||||
$name = trim( $leading_property_comments[0]->getText() );
|
||||
}
|
||||
|
||||
return [
|
||||
'name' => $name,
|
||||
'comments' => $callee->getCallee()->getLeadingComments(),
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
// Unknown format.
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns wether or not a comment precedes a node.
|
||||
* The comment must be before the node and on the same line or the one before.
|
||||
*
|
||||
* @param Node\Comment $comment The comment.
|
||||
* @param Node\Node $node The node.
|
||||
*
|
||||
* @return bool Whether or not the comment precedes the node.
|
||||
*/
|
||||
private function commentPrecedesNode( Node\Comment $comment, Node\Node $node ) {
|
||||
// Comments should be on the same or an earlier line than the translation.
|
||||
if ( $node->getLocation()->getStart()->getLine() - $comment->getLocation()->getEnd()->getLine() > 1 ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Comments on the same line should be before the translation.
|
||||
if (
|
||||
$node->getLocation()->getStart()->getLine() === $comment->getLocation()->getEnd()->getLine() &&
|
||||
$node->getLocation()->getStart()->getColumn() < $comment->getLocation()->getStart()->getColumn()
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
+254
@@ -0,0 +1,254 @@
|
||||
<?php
|
||||
|
||||
namespace WP_CLI\I18n;
|
||||
|
||||
use Gettext\Extractors\Po as PoExtractor;
|
||||
use Gettext\Generators\Po as PoGenerator;
|
||||
use Gettext\Translation;
|
||||
use Gettext\Translations;
|
||||
use WP_CLI;
|
||||
use WP_CLI_Command;
|
||||
use WP_CLI\Utils;
|
||||
use DirectoryIterator;
|
||||
use IteratorIterator;
|
||||
use SplFileInfo;
|
||||
|
||||
class MakeJsonCommand extends WP_CLI_Command {
|
||||
/**
|
||||
* Options passed to json_encode().
|
||||
*
|
||||
* @var int JSON options.
|
||||
*/
|
||||
protected $json_options = 0;
|
||||
|
||||
/**
|
||||
* Extract JavaScript strings from PO files and add them to individual JSON files.
|
||||
*
|
||||
* For JavaScript internationalization purposes, WordPress requires translations to be split up into
|
||||
* one Jed-formatted JSON file per JavaScript source file.
|
||||
*
|
||||
* See https://make.wordpress.org/core/2018/11/09/new-javascript-i18n-support-in-wordpress/ to learn more
|
||||
* about WordPress JavaScript internationalization.
|
||||
*
|
||||
* ## OPTIONS
|
||||
*
|
||||
* <source>
|
||||
* : Path to an existing PO file or a directory containing multiple PO files.
|
||||
*
|
||||
* [<destination>]
|
||||
* : Path to the destination directory for the resulting JSON files. Defaults to the source directory.
|
||||
*
|
||||
* [--purge]
|
||||
* : Whether to purge the strings that were extracted from the original source file. Defaults to true, use `--no-purge` to skip the removal.
|
||||
*
|
||||
* [--pretty-print]
|
||||
* : Pretty-print resulting JSON files.
|
||||
*
|
||||
* ## EXAMPLES
|
||||
*
|
||||
* # Create JSON files for all PO files in the languages directory
|
||||
* $ wp i18n make-json languages
|
||||
*
|
||||
* # Create JSON files for my-plugin-de_DE.po and leave the PO file untouched.
|
||||
* $ wp i18n make-json my-plugin-de_DE.po /tmp --no-purge
|
||||
*
|
||||
* @when before_wp_load
|
||||
*
|
||||
* @throws WP_CLI\ExitException
|
||||
*/
|
||||
public function __invoke( $args, $assoc_args ) {
|
||||
$purge = Utils\get_flag_value( $assoc_args, 'purge', true );
|
||||
|
||||
if ( Utils\get_flag_value( $assoc_args, 'pretty-print', false ) ) {
|
||||
$this->json_options |= JSON_PRETTY_PRINT;
|
||||
}
|
||||
|
||||
$source = realpath( $args[0] );
|
||||
|
||||
if ( ! $source || ( ! is_file( $source ) && ! is_dir( $source ) ) ) {
|
||||
WP_CLI::error( 'Source file or directory does not exist!' );
|
||||
}
|
||||
|
||||
$destination = is_file( $source ) ? dirname( $source ) : $source;
|
||||
|
||||
if ( isset( $args[1] ) ) {
|
||||
$destination = $args[1];
|
||||
}
|
||||
|
||||
// Two is_dir() checks in case of a race condition.
|
||||
if ( ! is_dir( $destination )
|
||||
&& ! mkdir( $destination, 0777, true )
|
||||
&& ! is_dir( $destination )
|
||||
) {
|
||||
WP_CLI::error( 'Could not create destination directory!' );
|
||||
}
|
||||
|
||||
$result_count = 0;
|
||||
|
||||
if ( is_file( $source ) ) {
|
||||
$files = [ new SplFileInfo( $source ) ];
|
||||
} else {
|
||||
$files = new IteratorIterator( new DirectoryIterator( $source ) );
|
||||
}
|
||||
|
||||
/** @var DirectoryIterator $file */
|
||||
foreach ( $files as $file ) {
|
||||
if ( $file->isFile() && $file->isReadable() && 'po' === $file->getExtension() ) {
|
||||
$result = $this->make_json( $file->getRealPath(), $destination );
|
||||
$result_count += count( $result );
|
||||
|
||||
if ( $purge ) {
|
||||
$removed = $this->remove_js_strings_from_po_file( $file->getRealPath() );
|
||||
|
||||
if ( ! $removed ) {
|
||||
WP_CLI::warning( sprintf( 'Could not update file %s', basename( $source ) ) );
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
WP_CLI::success( sprintf( 'Created %d %s.', $result_count, Utils\pluralize( 'file', $result_count ) ) );
|
||||
}
|
||||
|
||||
/**
|
||||
* Splits a single PO file into multiple JSON files.
|
||||
*
|
||||
* @param string $source_file Path to the source file.
|
||||
* @param string $destination Path to the destination directory.
|
||||
* @return array List of created JSON files.
|
||||
*/
|
||||
protected function make_json( $source_file, $destination ) {
|
||||
/** @var Translations[] $mapping */
|
||||
$mapping = [];
|
||||
$translations = new Translations();
|
||||
$result = [];
|
||||
|
||||
PoExtractor::fromFile( $source_file, $translations );
|
||||
|
||||
$base_file_name = basename( $source_file, '.po' );
|
||||
|
||||
foreach ( $translations as $index => $translation ) {
|
||||
/** @var Translation $translation */
|
||||
|
||||
// Find all unique sources this translation originates from.
|
||||
$sources = array_map(
|
||||
function ( $reference ) {
|
||||
$file = $reference[0];
|
||||
|
||||
if ( substr( $file, - 7 ) === '.min.js' ) {
|
||||
return substr( $file, 0, - 7 ) . '.js';
|
||||
}
|
||||
|
||||
if ( substr( $file, - 3 ) === '.js' ) {
|
||||
return $file;
|
||||
}
|
||||
|
||||
return null;
|
||||
},
|
||||
$translation->getReferences()
|
||||
);
|
||||
|
||||
$sources = array_unique( array_filter( $sources ) );
|
||||
|
||||
foreach ( $sources as $source ) {
|
||||
if ( ! isset( $mapping[ $source ] ) ) {
|
||||
$mapping[ $source ] = new Translations();
|
||||
|
||||
// phpcs:ignore Squiz.PHP.CommentedOutCode.Found -- Provide code that is meant to be used once the bug is fixed.
|
||||
// See https://core.trac.wordpress.org/ticket/45441
|
||||
// $mapping[ $source ]->setDomain( $translations->getDomain() );
|
||||
|
||||
$mapping[ $source ]->setHeader( 'Language', $translations->getLanguage() );
|
||||
$mapping[ $source ]->setHeader( 'PO-Revision-Date', $translations->getHeader( 'PO-Revision-Date' ) );
|
||||
|
||||
$plural_forms = $translations->getPluralForms();
|
||||
|
||||
if ( $plural_forms ) {
|
||||
list( $count, $rule ) = $plural_forms;
|
||||
$mapping[ $source ]->setPluralForms( $count, $rule );
|
||||
}
|
||||
}
|
||||
|
||||
$mapping[ $source ][] = $translation;
|
||||
}
|
||||
}
|
||||
|
||||
$result += $this->build_json_files( $mapping, $base_file_name, $destination );
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds a mapping of JS file names to translation entries.
|
||||
*
|
||||
* Exports translations for each JS file to a separate translation file.
|
||||
*
|
||||
* @param array $mapping A mapping of files to translation entries.
|
||||
* @param string $base_file_name Base file name for JSON files.
|
||||
* @param string $destination Path to the destination directory.
|
||||
*
|
||||
* @return array List of created JSON files.
|
||||
*/
|
||||
protected function build_json_files( $mapping, $base_file_name, $destination ) {
|
||||
$result = [];
|
||||
|
||||
foreach ( $mapping as $file => $translations ) {
|
||||
/** @var Translations $translations */
|
||||
|
||||
$hash = md5( $file );
|
||||
$destination_file = "${destination}/{$base_file_name}-{$hash}.json";
|
||||
|
||||
$success = JedGenerator::toFile(
|
||||
$translations,
|
||||
$destination_file,
|
||||
[
|
||||
'json' => $this->json_options,
|
||||
'source' => $file,
|
||||
]
|
||||
);
|
||||
|
||||
if ( ! $success ) {
|
||||
WP_CLI::warning( sprintf( 'Could not create file %s', basename( $destination_file, '.json' ) ) );
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
$result[] = $destination_file;
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes strings from PO file that only occur in JavaScript file.
|
||||
*
|
||||
* @param string $source_file Path to the PO file.
|
||||
* @return bool True on success, false otherwise.
|
||||
*/
|
||||
protected function remove_js_strings_from_po_file( $source_file ) {
|
||||
/** @var Translations[] $mapping */
|
||||
$translations = new Translations();
|
||||
|
||||
PoExtractor::fromFile( $source_file, $translations );
|
||||
|
||||
foreach ( $translations->getArrayCopy() as $translation ) {
|
||||
/** @var Translation $translation */
|
||||
|
||||
if ( ! $translation->hasReferences() ) {
|
||||
continue;
|
||||
}
|
||||
|
||||
foreach ( $translation->getReferences() as $reference ) {
|
||||
$file = $reference[0];
|
||||
|
||||
if ( substr( $file, - 3 ) !== '.js' ) {
|
||||
continue 2;
|
||||
}
|
||||
}
|
||||
|
||||
unset( $translations[ $translation->getId() ] );
|
||||
}
|
||||
|
||||
return PoGenerator::toFile( $translations, $source_file );
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
<?php
|
||||
|
||||
namespace WP_CLI\I18n;
|
||||
|
||||
use DirectoryIterator;
|
||||
use Gettext\Translations;
|
||||
use IteratorIterator;
|
||||
use SplFileInfo;
|
||||
use WP_CLI;
|
||||
use WP_CLI\Utils;
|
||||
use WP_CLI_Command;
|
||||
|
||||
class MakeMoCommand extends WP_CLI_Command {
|
||||
/**
|
||||
* Create MO files from PO files.
|
||||
*
|
||||
* ## OPTIONS
|
||||
*
|
||||
* <source>
|
||||
* : Path to an existing PO file or a directory containing multiple PO files.
|
||||
*
|
||||
* [<destination>]
|
||||
* : Path to the destination directory for the resulting MO files. Defaults to the source directory.
|
||||
*
|
||||
* @when before_wp_load
|
||||
*
|
||||
* @throws WP_CLI\ExitException
|
||||
*/
|
||||
public function __invoke( $args, $assoc_args ) {
|
||||
$source = realpath( $args[0] );
|
||||
if ( ! $source || ( ! is_file( $source ) && ! is_dir( $source ) ) ) {
|
||||
WP_CLI::error( 'Source file or directory does not exist!' );
|
||||
}
|
||||
|
||||
$destination = is_file( $source ) ? dirname( $source ) : $source;
|
||||
if ( isset( $args[1] ) ) {
|
||||
$destination = $args[1];
|
||||
}
|
||||
|
||||
// Two is_dir() checks in case of a race condition.
|
||||
if ( ! is_dir( $destination )
|
||||
&& ! mkdir( $destination, 0777, true )
|
||||
&& ! is_dir( $destination )
|
||||
) {
|
||||
WP_CLI::error( 'Could not create destination directory!' );
|
||||
}
|
||||
|
||||
if ( is_file( $source ) ) {
|
||||
$files = [ new SplFileInfo( $source ) ];
|
||||
} else {
|
||||
$files = new IteratorIterator( new DirectoryIterator( $source ) );
|
||||
}
|
||||
|
||||
$result_count = 0;
|
||||
/** @var DirectoryIterator $file */
|
||||
foreach ( $files as $file ) {
|
||||
if ( 'po' !== $file->getExtension() ) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if ( ! $file->isFile() || ! $file->isReadable() ) {
|
||||
WP_CLI::warning( sprintf( 'Could not read file %s', $file->getFilename() ) );
|
||||
continue;
|
||||
}
|
||||
|
||||
$file_basename = basename( $file->getFilename(), '.po' );
|
||||
$destination_file = "{$destination}/{$file_basename}.mo";
|
||||
|
||||
$translations = Translations::fromPoFile( $file->getPathname() );
|
||||
if ( ! $translations->toMoFile( $destination_file ) ) {
|
||||
WP_CLI::warning( sprintf( 'Could not create file %s', $destination_file ) );
|
||||
continue;
|
||||
}
|
||||
|
||||
$result_count++;
|
||||
}
|
||||
|
||||
WP_CLI::success( sprintf( 'Created %d %s.', $result_count, Utils\pluralize( 'file', $result_count ) ) );
|
||||
}
|
||||
}
|
||||
+937
@@ -0,0 +1,937 @@
|
||||
<?php
|
||||
|
||||
namespace WP_CLI\I18n;
|
||||
|
||||
use Gettext\Extractors\Po;
|
||||
use Gettext\Merge;
|
||||
use Gettext\Translation;
|
||||
use Gettext\Translations;
|
||||
use WP_CLI;
|
||||
use WP_CLI_Command;
|
||||
use WP_CLI\Utils;
|
||||
use DirectoryIterator;
|
||||
use IteratorIterator;
|
||||
|
||||
class MakePotCommand extends WP_CLI_Command {
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
protected $source;
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
protected $destination;
|
||||
|
||||
/**
|
||||
* @var array
|
||||
*/
|
||||
protected $merge = [];
|
||||
|
||||
/**
|
||||
* @var Translations
|
||||
*/
|
||||
protected $exceptions;
|
||||
|
||||
/**
|
||||
* @var array
|
||||
*/
|
||||
protected $include = [];
|
||||
|
||||
/**
|
||||
* @var array
|
||||
*/
|
||||
protected $exclude = [ 'node_modules', '.git', '.svn', '.CVS', '.hg', 'vendor', 'Gruntfile.js', 'webpack.config.js', '*.min.js' ];
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
protected $slug;
|
||||
|
||||
/**
|
||||
* @var array
|
||||
*/
|
||||
protected $main_file_data = [];
|
||||
|
||||
/**
|
||||
* @var bool
|
||||
*/
|
||||
protected $skip_js = false;
|
||||
|
||||
/**
|
||||
* @var bool
|
||||
*/
|
||||
protected $skip_php = false;
|
||||
|
||||
/**
|
||||
* @var bool
|
||||
*/
|
||||
protected $skip_block_json = false;
|
||||
|
||||
/**
|
||||
* @var bool
|
||||
*/
|
||||
protected $skip_audit = false;
|
||||
|
||||
/**
|
||||
* @var array
|
||||
*/
|
||||
protected $headers = [];
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
protected $domain;
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
protected $package_name;
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
protected $file_comment;
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
protected $project_type = 'generic';
|
||||
|
||||
/**
|
||||
* These Regexes copied from http://php.net/manual/en/function.sprintf.php#93552
|
||||
* and adjusted for better precision and updated specs.
|
||||
*/
|
||||
const SPRINTF_PLACEHOLDER_REGEX = '/(?:
|
||||
(?<!%) # Don\'t match a literal % (%%).
|
||||
(
|
||||
% # Start of placeholder.
|
||||
(?:[0-9]+\$)? # Optional ordering of the placeholders.
|
||||
[+-]? # Optional sign specifier.
|
||||
(?:
|
||||
(?:0|\'.)? # Optional padding specifier - excluding the space.
|
||||
-? # Optional alignment specifier.
|
||||
[0-9]* # Optional width specifier.
|
||||
(?:\.(?:[ 0]|\'.)?[0-9]+)? # Optional precision specifier with optional padding character.
|
||||
| # Only recognize the space as padding in combination with a width specifier.
|
||||
(?:[ ])? # Optional space padding specifier.
|
||||
-? # Optional alignment specifier.
|
||||
[0-9]+ # Width specifier.
|
||||
(?:\.(?:[ 0]|\'.)?[0-9]+)? # Optional precision specifier with optional padding character.
|
||||
)
|
||||
[bcdeEfFgGosuxX] # Type specifier.
|
||||
)
|
||||
)/x';
|
||||
|
||||
/**
|
||||
* "Unordered" means there's no position specifier: '%s', not '%2$s'.
|
||||
*/
|
||||
const UNORDERED_SPRINTF_PLACEHOLDER_REGEX = '/(?:
|
||||
(?<!%) # Don\'t match a literal % (%%).
|
||||
% # Start of placeholder.
|
||||
[+-]? # Optional sign specifier.
|
||||
(?:
|
||||
(?:0|\'.)? # Optional padding specifier - excluding the space.
|
||||
-? # Optional alignment specifier.
|
||||
[0-9]* # Optional width specifier.
|
||||
(?:\.(?:[ 0]|\'.)?[0-9]+)? # Optional precision specifier with optional padding character.
|
||||
| # Only recognize the space as padding in combination with a width specifier.
|
||||
(?:[ ])? # Optional space padding specifier.
|
||||
-? # Optional alignment specifier.
|
||||
[0-9]+ # Width specifier.
|
||||
(?:\.(?:[ 0]|\'.)?[0-9]+)? # Optional precision specifier with optional padding character.
|
||||
)
|
||||
[bcdeEfFgGosuxX] # Type specifier.
|
||||
)/x';
|
||||
|
||||
/**
|
||||
* Create a POT file for a WordPress project.
|
||||
*
|
||||
* Scans PHP and JavaScript files for translatable strings, as well as theme stylesheets and plugin files
|
||||
* if the source directory is detected as either a plugin or theme.
|
||||
*
|
||||
* ## OPTIONS
|
||||
*
|
||||
* <source>
|
||||
* : Directory to scan for string extraction.
|
||||
*
|
||||
* [<destination>]
|
||||
* : Name of the resulting POT file.
|
||||
*
|
||||
* [--slug=<slug>]
|
||||
* : Plugin or theme slug. Defaults to the source directory's basename.
|
||||
*
|
||||
* [--domain=<domain>]
|
||||
* : Text domain to look for in the source code, unless the `--ignore-domain` option is used.
|
||||
* By default, the "Text Domain" header of the plugin or theme is used.
|
||||
* If none is provided, it falls back to the project slug.
|
||||
*
|
||||
* [--ignore-domain]
|
||||
* : Ignore the text domain completely and extract strings with any text domain.
|
||||
*
|
||||
* [--merge[=<paths>]]
|
||||
* : Comma-separated list of POT files whose contents should be merged with the extracted strings.
|
||||
* If left empty, defaults to the destination POT file. POT file headers will be ignored.
|
||||
*
|
||||
* [--subtract=<paths>]
|
||||
* : Comma-separated list of POT files whose contents should act as some sort of blacklist for string extraction.
|
||||
* Any string which is found on that blacklist will not be extracted.
|
||||
* This can be useful when you want to create multiple POT files from the same source directory with slightly
|
||||
* different content and no duplicate strings between them.
|
||||
*
|
||||
* [--include=<paths>]
|
||||
* : Comma-separated list of files and paths that should be used for string extraction.
|
||||
* If provided, only these files and folders will be taken into account for string extraction.
|
||||
* For example, `--include="src,my-file.php` will ignore anything besides `my-file.php` and files in the `src`
|
||||
* directory. Simple glob patterns can be used, i.e. `--include=foo-*.php` includes any PHP file with the `foo-`
|
||||
* prefix. Leading and trailing slashes are ignored, i.e. `/my/directory/` is the same as `my/directory`.
|
||||
*
|
||||
* [--exclude=<paths>]
|
||||
* : Comma-separated list of files and paths that should be skipped for string extraction.
|
||||
* For example, `--exclude=".github,myfile.php` would ignore any strings found within `myfile.php` or the `.github`
|
||||
* folder. Simple glob patterns can be used, i.e. `--exclude=foo-*.php` excludes any PHP file with the `foo-`
|
||||
* prefix. Leading and trailing slashes are ignored, i.e. `/my/directory/` is the same as `my/directory`. The
|
||||
* following files and folders are always excluded: node_modules, .git, .svn, .CVS, .hg, vendor, *.min.js.
|
||||
*
|
||||
* [--headers=<headers>]
|
||||
* : Array in JSON format of custom headers which will be added to the POT file. Defaults to empty array.
|
||||
*
|
||||
* [--skip-js]
|
||||
* : Skips JavaScript string extraction. Useful when this is done in another build step, e.g. through Babel.
|
||||
*
|
||||
* [--skip-php]
|
||||
* : Skips PHP string extraction.
|
||||
*
|
||||
* [--skip-block-json]
|
||||
* : Skips string extraction from block.json files.
|
||||
*
|
||||
* [--skip-audit]
|
||||
* : Skips string audit where it tries to find possible mistakes in translatable strings. Useful when running in an
|
||||
* automated environment.
|
||||
*
|
||||
* [--file-comment=<file-comment>]
|
||||
* : String that should be added as a comment to the top of the resulting POT file.
|
||||
* By default, a copyright comment is added for WordPress plugins and themes in the following manner:
|
||||
*
|
||||
* ```
|
||||
* Copyright (C) 2018 Example Plugin Author
|
||||
* This file is distributed under the same license as the Example Plugin package.
|
||||
* ```
|
||||
*
|
||||
* If a plugin or theme specifies a license in their main plugin file or stylesheet, the comment looks like
|
||||
* this:
|
||||
*
|
||||
* ```
|
||||
* Copyright (C) 2018 Example Plugin Author
|
||||
* This file is distributed under the GPLv2.
|
||||
* ```
|
||||
*
|
||||
* [--package-name=<name>]
|
||||
* : Name to use for package name in the resulting POT file's `Project-Id-Version` header.
|
||||
* Overrides plugin or theme name, if applicable.
|
||||
*
|
||||
* ## EXAMPLES
|
||||
*
|
||||
* # Create a POT file for the WordPress plugin/theme in the current directory
|
||||
* $ wp i18n make-pot . languages/my-plugin.pot
|
||||
*
|
||||
* # Create a POT file for the continents and cities list in WordPress core.
|
||||
* $ wp i18n make-pot . continents-and-cities.pot --include="wp-admin/includes/continents-cities.php"
|
||||
* --ignore-domain
|
||||
*
|
||||
* @when before_wp_load
|
||||
*
|
||||
* @throws WP_CLI\ExitException
|
||||
*/
|
||||
public function __invoke( $args, $assoc_args ) {
|
||||
$this->handle_arguments( $args, $assoc_args );
|
||||
|
||||
$translations = $this->extract_strings();
|
||||
|
||||
if ( ! $translations ) {
|
||||
WP_CLI::warning( 'No strings found' );
|
||||
}
|
||||
|
||||
$translations_count = count( $translations );
|
||||
|
||||
if ( 1 === $translations_count ) {
|
||||
WP_CLI::debug( sprintf( 'Extracted %d string', $translations_count ), 'make-pot' );
|
||||
} else {
|
||||
WP_CLI::debug( sprintf( 'Extracted %d strings', $translations_count ), 'make-pot' );
|
||||
}
|
||||
|
||||
if ( ! PotGenerator::toFile( $translations, $this->destination ) ) {
|
||||
WP_CLI::error( 'Could not generate a POT file!' );
|
||||
}
|
||||
|
||||
WP_CLI::success( 'POT file successfully generated!' );
|
||||
}
|
||||
|
||||
/**
|
||||
* Process arguments from command-line in a reusable way.
|
||||
*
|
||||
* @throws WP_CLI\ExitException
|
||||
*
|
||||
* @param array $args Command arguments.
|
||||
* @param array $assoc_args Associative arguments.
|
||||
*/
|
||||
public function handle_arguments( $args, $assoc_args ) {
|
||||
$array_arguments = array( 'headers' );
|
||||
$assoc_args = Utils\parse_shell_arrays( $assoc_args, $array_arguments );
|
||||
|
||||
$this->source = realpath( $args[0] );
|
||||
$this->slug = Utils\get_flag_value( $assoc_args, 'slug', Utils\basename( $this->source ) );
|
||||
$this->skip_js = Utils\get_flag_value( $assoc_args, 'skip-js', $this->skip_js );
|
||||
$this->skip_php = Utils\get_flag_value( $assoc_args, 'skip-php', $this->skip_php );
|
||||
$this->skip_block_json = Utils\get_flag_value( $assoc_args, 'skip-block-json', $this->skip_block_json );
|
||||
$this->skip_audit = Utils\get_flag_value( $assoc_args, 'skip-audit', $this->skip_audit );
|
||||
$this->headers = Utils\get_flag_value( $assoc_args, 'headers', $this->headers );
|
||||
$this->file_comment = Utils\get_flag_value( $assoc_args, 'file-comment' );
|
||||
$this->package_name = Utils\get_flag_value( $assoc_args, 'package-name' );
|
||||
|
||||
$ignore_domain = Utils\get_flag_value( $assoc_args, 'ignore-domain', false );
|
||||
|
||||
if ( ! $this->source || ! is_dir( $this->source ) ) {
|
||||
WP_CLI::error( 'Not a valid source directory!' );
|
||||
}
|
||||
|
||||
$this->main_file_data = $this->get_main_file_data();
|
||||
|
||||
if ( $ignore_domain ) {
|
||||
WP_CLI::debug( 'Extracting all strings regardless of text domain', 'make-pot' );
|
||||
}
|
||||
|
||||
if ( ! $ignore_domain ) {
|
||||
$this->domain = $this->slug;
|
||||
|
||||
if ( ! empty( $this->main_file_data['Text Domain'] ) ) {
|
||||
$this->domain = $this->main_file_data['Text Domain'];
|
||||
}
|
||||
|
||||
$this->domain = Utils\get_flag_value( $assoc_args, 'domain', $this->domain );
|
||||
|
||||
WP_CLI::debug( sprintf( 'Extracting all strings with text domain "%s"', $this->domain ), 'make-pot' );
|
||||
}
|
||||
|
||||
// Determine destination.
|
||||
$this->destination = "{$this->source}/{$this->slug}.pot";
|
||||
|
||||
if ( ! empty( $this->main_file_data['Domain Path'] ) ) {
|
||||
// Domain Path inside source folder.
|
||||
$this->destination = sprintf(
|
||||
'%s/%s/%s.pot',
|
||||
$this->source,
|
||||
$this->unslashit( $this->main_file_data['Domain Path'] ),
|
||||
$this->slug
|
||||
);
|
||||
}
|
||||
|
||||
if ( isset( $args[1] ) ) {
|
||||
$this->destination = $args[1];
|
||||
}
|
||||
|
||||
WP_CLI::debug( sprintf( 'Destination: %s', $this->destination ), 'make-pot' );
|
||||
|
||||
// Two is_dir() checks in case of a race condition.
|
||||
if ( ! is_dir( dirname( $this->destination ) )
|
||||
&& ! mkdir( dirname( $this->destination ), 0777, true )
|
||||
&& ! is_dir( dirname( $this->destination ) )
|
||||
) {
|
||||
WP_CLI::error( 'Could not create destination directory!' );
|
||||
}
|
||||
|
||||
if ( isset( $assoc_args['merge'] ) ) {
|
||||
if ( true === $assoc_args['merge'] ) {
|
||||
$this->merge = [ $this->destination ];
|
||||
} elseif ( ! empty( $assoc_args['merge'] ) ) {
|
||||
$this->merge = explode( ',', $assoc_args['merge'] );
|
||||
}
|
||||
|
||||
$this->merge = array_filter(
|
||||
$this->merge,
|
||||
function ( $file ) {
|
||||
if ( ! file_exists( $file ) ) {
|
||||
WP_CLI::warning( sprintf( 'Invalid file provided to --merge: %s', $file ) );
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
);
|
||||
|
||||
if ( ! empty( $this->merge ) ) {
|
||||
WP_CLI::debug(
|
||||
sprintf(
|
||||
'Merging with existing POT %s: %s',
|
||||
WP_CLI\Utils\pluralize( 'file', count( $this->merge ) ),
|
||||
implode( ',', $this->merge )
|
||||
),
|
||||
'make-pot'
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
$this->exceptions = new Translations();
|
||||
|
||||
if ( isset( $assoc_args['subtract'] ) ) {
|
||||
$exceptions = explode( ',', $assoc_args['subtract'] );
|
||||
|
||||
$exceptions = array_filter(
|
||||
$exceptions,
|
||||
function ( $exception ) {
|
||||
if ( ! file_exists( $exception ) ) {
|
||||
WP_CLI::warning( sprintf( 'Invalid file provided to --subtract: %s', $exception ) );
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
$exception_translations = new Translations();
|
||||
|
||||
Po::fromFile( $exception, $exception_translations );
|
||||
$this->exceptions->mergeWith( $exception_translations );
|
||||
|
||||
return true;
|
||||
}
|
||||
);
|
||||
|
||||
if ( ! empty( $exceptions ) ) {
|
||||
WP_CLI::debug( sprintf( 'Ignoring any string already existing in: %s', implode( ',', $exceptions ) ), 'make-pot' );
|
||||
}
|
||||
}
|
||||
|
||||
if ( isset( $assoc_args['include'] ) ) {
|
||||
$this->include = array_filter( explode( ',', $assoc_args['include'] ) );
|
||||
$this->include = array_map( [ $this, 'unslashit' ], $this->include );
|
||||
$this->include = array_unique( $this->include );
|
||||
|
||||
WP_CLI::debug( sprintf( 'Only including the following files: %s', implode( ',', $this->include ) ), 'make-pot' );
|
||||
}
|
||||
|
||||
if ( isset( $assoc_args['exclude'] ) ) {
|
||||
$this->exclude = array_filter( array_merge( $this->exclude, explode( ',', $assoc_args['exclude'] ) ) );
|
||||
$this->exclude = array_map( [ $this, 'unslashit' ], $this->exclude );
|
||||
$this->exclude = array_unique( $this->exclude );
|
||||
}
|
||||
|
||||
WP_CLI::debug( sprintf( 'Excluding the following files: %s', implode( ',', $this->exclude ) ), 'make-pot' );
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes leading and trailing slashes of a string.
|
||||
*
|
||||
* @param string $string What to add and remove slashes from.
|
||||
* @return string String without leading and trailing slashes.
|
||||
*/
|
||||
protected function unslashit( $string ) {
|
||||
return ltrim( rtrim( trim( $string ), '/\\' ), '/\\' );
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the main file data of the plugin or theme.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
protected function get_main_file_data() {
|
||||
$files = new IteratorIterator( new DirectoryIterator( $this->source ) );
|
||||
|
||||
/** @var DirectoryIterator $file */
|
||||
foreach ( $files as $file ) {
|
||||
// wp-content/themes/my-theme/style.css
|
||||
if ( $file->isFile() && 'style' === $file->getBasename( '.css' ) && $file->isReadable() ) {
|
||||
$theme_data = static::get_file_data( $file->getRealPath(), array_combine( $this->get_file_headers( 'theme' ), $this->get_file_headers( 'theme' ) ) );
|
||||
|
||||
// Stop when it contains a valid Theme Name header.
|
||||
if ( ! empty( $theme_data['Theme Name'] ) ) {
|
||||
WP_CLI::log( 'Theme stylesheet detected.' );
|
||||
WP_CLI::debug( sprintf( 'Theme stylesheet: %s', $file->getRealPath() ), 'make-pot' );
|
||||
|
||||
$this->project_type = 'theme';
|
||||
|
||||
return $theme_data;
|
||||
}
|
||||
}
|
||||
|
||||
// wp-content/themes/my-themes/theme-a/style.css
|
||||
if ( $file->isDir() && ! $file->isDot() && is_readable( $file->getRealPath() . '/style.css' ) ) {
|
||||
$theme_data = static::get_file_data( $file->getRealPath() . '/style.css', array_combine( $this->get_file_headers( 'theme' ), $this->get_file_headers( 'theme' ) ) );
|
||||
|
||||
// Stop when it contains a valid Theme Name header.
|
||||
if ( ! empty( $theme_data['Theme Name'] ) ) {
|
||||
WP_CLI::log( 'Theme stylesheet detected.' );
|
||||
WP_CLI::debug( sprintf( 'Theme stylesheet: %s', $file->getRealPath() . '/style.css' ), 'make-pot' );
|
||||
|
||||
$this->project_type = 'theme';
|
||||
|
||||
return $theme_data;
|
||||
}
|
||||
}
|
||||
|
||||
// wp-content/plugins/my-plugin/my-plugin.php
|
||||
if ( $file->isFile() && $file->isReadable() && 'php' === $file->getExtension() ) {
|
||||
$plugin_data = static::get_file_data( $file->getRealPath(), array_combine( $this->get_file_headers( 'plugin' ), $this->get_file_headers( 'plugin' ) ) );
|
||||
|
||||
// Stop when we find a file with a valid Plugin Name header.
|
||||
if ( ! empty( $plugin_data['Plugin Name'] ) ) {
|
||||
WP_CLI::log( 'Plugin file detected.' );
|
||||
WP_CLI::debug( sprintf( 'Plugin file: %s', $file->getRealPath() ), 'make-pot' );
|
||||
|
||||
$this->project_type = 'plugin';
|
||||
|
||||
return $plugin_data;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
WP_CLI::debug( 'No valid theme stylesheet or plugin file found, treating as a regular project.', 'make-pot' );
|
||||
|
||||
return [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the file headers for themes and plugins.
|
||||
*
|
||||
* @param string $type Source type, either theme or plugin.
|
||||
*
|
||||
* @return array List of file headers.
|
||||
*/
|
||||
protected function get_file_headers( $type ) {
|
||||
switch ( $type ) {
|
||||
case 'plugin':
|
||||
return [
|
||||
'Plugin Name',
|
||||
'Plugin URI',
|
||||
'Description',
|
||||
'Author',
|
||||
'Author URI',
|
||||
'Version',
|
||||
'License',
|
||||
'Domain Path',
|
||||
'Text Domain',
|
||||
];
|
||||
case 'theme':
|
||||
return [
|
||||
'Theme Name',
|
||||
'Theme URI',
|
||||
'Description',
|
||||
'Author',
|
||||
'Author URI',
|
||||
'Version',
|
||||
'License',
|
||||
'Domain Path',
|
||||
'Text Domain',
|
||||
];
|
||||
default:
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a POT file and stores it on disk.
|
||||
*
|
||||
* @throws WP_CLI\ExitException
|
||||
*
|
||||
* @return Translations A Translation set.
|
||||
*/
|
||||
protected function extract_strings() {
|
||||
$translations = new Translations();
|
||||
|
||||
// Add existing strings first but don't keep headers.
|
||||
if ( ! empty( $this->merge ) ) {
|
||||
$existing_translations = new Translations();
|
||||
Po::fromFile( $this->merge, $existing_translations );
|
||||
$translations->mergeWith( $existing_translations, Merge::ADD | Merge::REMOVE );
|
||||
}
|
||||
|
||||
PotGenerator::setCommentBeforeHeaders( $this->get_file_comment() );
|
||||
|
||||
$this->set_default_headers( $translations );
|
||||
|
||||
// POT files have no Language header.
|
||||
$translations->deleteHeader( Translations::HEADER_LANGUAGE );
|
||||
|
||||
// Only relevant for PO files, not POT files.
|
||||
$translations->setHeader( 'PO-Revision-Date', 'YEAR-MO-DA HO:MI+ZONE' );
|
||||
|
||||
if ( $this->domain ) {
|
||||
$translations->setDomain( $this->domain );
|
||||
}
|
||||
|
||||
unset( $this->main_file_data['Version'], $this->main_file_data['License'], $this->main_file_data['Domain Path'], $this->main_file_data['Text Domain'] );
|
||||
|
||||
// Set entries from main file data.
|
||||
foreach ( $this->main_file_data as $header => $data ) {
|
||||
if ( empty( $data ) ) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$translation = new Translation( '', $data );
|
||||
|
||||
if ( isset( $this->main_file_data['Theme Name'] ) ) {
|
||||
$translation->addExtractedComment( sprintf( '%s of the theme', $header ) );
|
||||
} else {
|
||||
$translation->addExtractedComment( sprintf( '%s of the plugin', $header ) );
|
||||
}
|
||||
|
||||
$translations[] = $translation;
|
||||
}
|
||||
|
||||
try {
|
||||
if ( ! $this->skip_php ) {
|
||||
$options = [
|
||||
// Extract 'Template Name' headers in theme files.
|
||||
'wpExtractTemplates' => isset( $this->main_file_data['Theme Name'] ),
|
||||
'include' => $this->include,
|
||||
'exclude' => $this->exclude,
|
||||
'extensions' => [ 'php' ],
|
||||
];
|
||||
PhpCodeExtractor::fromDirectory( $this->source, $translations, $options );
|
||||
}
|
||||
|
||||
if ( ! $this->skip_js ) {
|
||||
JsCodeExtractor::fromDirectory(
|
||||
$this->source,
|
||||
$translations,
|
||||
[
|
||||
'include' => $this->include,
|
||||
'exclude' => $this->exclude,
|
||||
'extensions' => [ 'js', 'jsx' ],
|
||||
]
|
||||
);
|
||||
|
||||
MapCodeExtractor::fromDirectory(
|
||||
$this->source,
|
||||
$translations,
|
||||
[
|
||||
'include' => $this->include,
|
||||
'exclude' => $this->exclude,
|
||||
'extensions' => [ 'map' ],
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
if ( ! $this->skip_block_json ) {
|
||||
BlockExtractor::fromDirectory(
|
||||
$this->source,
|
||||
$translations,
|
||||
[
|
||||
// Only look for block.json files, nothing else.
|
||||
'restrictFileNames' => [ 'block.json' ],
|
||||
'include' => $this->include,
|
||||
'exclude' => $this->exclude,
|
||||
'extensions' => [ 'json' ],
|
||||
]
|
||||
);
|
||||
}
|
||||
} catch ( \Exception $e ) {
|
||||
WP_CLI::error( $e->getMessage() );
|
||||
}
|
||||
|
||||
foreach ( $this->exceptions as $translation ) {
|
||||
if ( $translations->find( $translation ) ) {
|
||||
unset( $translations[ $translation->getId() ] );
|
||||
}
|
||||
}
|
||||
|
||||
if ( ! $this->skip_audit ) {
|
||||
$this->audit_strings( $translations );
|
||||
}
|
||||
|
||||
return $translations;
|
||||
}
|
||||
|
||||
/**
|
||||
* Audits strings.
|
||||
*
|
||||
* Goes through all extracted strings to find possible mistakes.
|
||||
*
|
||||
* @param Translations $translations Translations object.
|
||||
*/
|
||||
protected function audit_strings( $translations ) {
|
||||
foreach ( $translations as $translation ) {
|
||||
/** @var Translation $translation */
|
||||
|
||||
$references = $translation->getReferences();
|
||||
|
||||
// File headers don't have any file references.
|
||||
$location = $translation->hasReferences() ? '(' . implode( ':', array_shift( $references ) ) . ')' : '';
|
||||
|
||||
// Check 1: Flag strings with placeholders that should have translator comments.
|
||||
if (
|
||||
! $translation->hasExtractedComments() &&
|
||||
preg_match( self::SPRINTF_PLACEHOLDER_REGEX, $translation->getOriginal(), $placeholders ) >= 1
|
||||
) {
|
||||
$message = sprintf(
|
||||
'The string "%1$s" contains placeholders but has no "translators:" comment to clarify their meaning. %2$s',
|
||||
$translation->getOriginal(),
|
||||
$location
|
||||
);
|
||||
WP_CLI::warning( $message );
|
||||
}
|
||||
|
||||
// Check 2: Flag strings with different translator comments.
|
||||
if ( $translation->hasExtractedComments() ) {
|
||||
$comments = $translation->getExtractedComments();
|
||||
|
||||
// Remove plugin header information from comments.
|
||||
$comments = array_filter(
|
||||
$comments,
|
||||
function ( $comment ) {
|
||||
/** @var string $comment */
|
||||
/** @var string $file_header */
|
||||
foreach ( $this->get_file_headers( $this->project_type ) as $file_header ) {
|
||||
if ( 0 === strpos( $comment, $file_header ) ) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
return $comment;
|
||||
}
|
||||
);
|
||||
|
||||
$comments_count = count( $comments );
|
||||
|
||||
if ( $comments_count > 1 ) {
|
||||
$message = sprintf(
|
||||
'The string "%1$s" has %2$d different translator comments. %3$s',
|
||||
$translation->getOriginal(),
|
||||
$comments_count,
|
||||
$location
|
||||
);
|
||||
WP_CLI::warning( $message );
|
||||
}
|
||||
}
|
||||
|
||||
$non_placeholder_content = trim( preg_replace( '`^([\'"])(.*)\1$`Ds', '$2', $translation->getOriginal() ) );
|
||||
$non_placeholder_content = preg_replace( self::SPRINTF_PLACEHOLDER_REGEX, '', $non_placeholder_content );
|
||||
|
||||
// Check 3: Flag empty strings without any translatable content.
|
||||
if ( '' === $non_placeholder_content ) {
|
||||
$message = sprintf(
|
||||
'Found string without translatable content. %s',
|
||||
$location
|
||||
);
|
||||
WP_CLI::warning( $message );
|
||||
}
|
||||
|
||||
// Check 4: Flag strings with multiple unordered placeholders (%s %s %s vs. %1$s %2$s %3$s).
|
||||
$unordered_matches_count = preg_match_all( self::UNORDERED_SPRINTF_PLACEHOLDER_REGEX, $translation->getOriginal(), $unordered_matches );
|
||||
$unordered_matches = $unordered_matches[0];
|
||||
|
||||
if ( $unordered_matches_count >= 2 ) {
|
||||
$message = sprintf(
|
||||
'Multiple placeholders should be ordered. %s',
|
||||
$location
|
||||
);
|
||||
WP_CLI::warning( $message );
|
||||
}
|
||||
|
||||
if ( $translation->hasPlural() ) {
|
||||
preg_match_all( self::SPRINTF_PLACEHOLDER_REGEX, $translation->getOriginal(), $single_placeholders );
|
||||
$single_placeholders = $single_placeholders[0];
|
||||
|
||||
preg_match_all( self::SPRINTF_PLACEHOLDER_REGEX, $translation->getPlural(), $plural_placeholders );
|
||||
$plural_placeholders = $plural_placeholders[0];
|
||||
|
||||
// see https://developer.wordpress.org/plugins/internationalization/how-to-internationalize-your-plugin/#plurals
|
||||
if ( count( $single_placeholders ) < count( $plural_placeholders ) ) {
|
||||
// Check 5: Flag things like _n( 'One comment', '%s Comments' )
|
||||
$message = sprintf(
|
||||
'Missing singular placeholder, needed for some languages. See https://developer.wordpress.org/plugins/internationalization/how-to-internationalize-your-plugin/#plurals %s',
|
||||
$location
|
||||
);
|
||||
WP_CLI::warning( $message );
|
||||
} else {
|
||||
// Reordering is fine, but mismatched placeholders is probably wrong.
|
||||
sort( $single_placeholders );
|
||||
sort( $plural_placeholders );
|
||||
|
||||
// Check 6: Flag things like _n( '%s Comment (%d)', '%s Comments (%s)' )
|
||||
if ( $single_placeholders !== $plural_placeholders ) {
|
||||
$message = sprintf(
|
||||
'Mismatched placeholders for singular and plural string. %s',
|
||||
$location
|
||||
);
|
||||
WP_CLI::warning( $message );
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the copyright comment for the given package.
|
||||
*
|
||||
* @return string File comment.
|
||||
*/
|
||||
protected function get_file_comment() {
|
||||
if ( '' === $this->file_comment ) {
|
||||
return '';
|
||||
}
|
||||
|
||||
if ( isset( $this->file_comment ) ) {
|
||||
return implode( "\n", explode( '\n', $this->file_comment ) );
|
||||
}
|
||||
|
||||
if ( isset( $this->main_file_data['Theme Name'] ) ) {
|
||||
if ( isset( $this->main_file_data['License'] ) ) {
|
||||
return sprintf(
|
||||
"Copyright (C) %1\$s %2\$s\nThis file is distributed under the %3\$s.",
|
||||
date( 'Y' ), // phpcs:ignore WordPress.DateTime.RestrictedFunctions.date_date
|
||||
$this->main_file_data['Author'],
|
||||
$this->main_file_data['License']
|
||||
);
|
||||
}
|
||||
|
||||
return sprintf(
|
||||
"Copyright (C) %1\$s %2\$s\nThis file is distributed under the same license as the %3\$s theme.",
|
||||
date( 'Y' ), // phpcs:ignore WordPress.DateTime.RestrictedFunctions.date_date
|
||||
$this->main_file_data['Author'],
|
||||
$this->main_file_data['Theme Name']
|
||||
);
|
||||
}
|
||||
|
||||
if ( isset( $this->main_file_data['Plugin Name'] ) ) {
|
||||
if ( isset( $this->main_file_data['License'] ) && ! empty( $this->main_file_data['License'] ) ) {
|
||||
return sprintf(
|
||||
"Copyright (C) %1\$s %2\$s\nThis file is distributed under the %3\$s.",
|
||||
date( 'Y' ), // phpcs:ignore WordPress.DateTime.RestrictedFunctions.date_date
|
||||
$this->main_file_data['Author'],
|
||||
$this->main_file_data['License']
|
||||
);
|
||||
}
|
||||
|
||||
return sprintf(
|
||||
"Copyright (C) %1\$s %2\$s\nThis file is distributed under the same license as the %3\$s plugin.",
|
||||
date( 'Y' ), // phpcs:ignore WordPress.DateTime.RestrictedFunctions.date_date
|
||||
$this->main_file_data['Author'],
|
||||
$this->main_file_data['Plugin Name']
|
||||
);
|
||||
}
|
||||
|
||||
return '';
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets default POT file headers for the project.
|
||||
*
|
||||
* @param Translations $translations Translations object.
|
||||
*/
|
||||
protected function set_default_headers( $translations ) {
|
||||
$name = null;
|
||||
$version = $this->get_wp_version();
|
||||
$bugs_address = null;
|
||||
|
||||
if ( ! $version && isset( $this->main_file_data['Version'] ) ) {
|
||||
$version = $this->main_file_data['Version'];
|
||||
}
|
||||
|
||||
if ( isset( $this->main_file_data['Theme Name'] ) ) {
|
||||
$name = $this->main_file_data['Theme Name'];
|
||||
$bugs_address = sprintf( 'https://wordpress.org/support/theme/%s', $this->slug );
|
||||
} elseif ( isset( $this->main_file_data['Plugin Name'] ) ) {
|
||||
$name = $this->main_file_data['Plugin Name'];
|
||||
$bugs_address = sprintf( 'https://wordpress.org/support/plugin/%s', $this->slug );
|
||||
}
|
||||
|
||||
if ( null !== $this->package_name ) {
|
||||
$name = $this->package_name;
|
||||
}
|
||||
|
||||
if ( null !== $name ) {
|
||||
$translations->setHeader( 'Project-Id-Version', $name . ( $version ? ' ' . $version : '' ) );
|
||||
}
|
||||
|
||||
if ( null !== $bugs_address ) {
|
||||
$translations->setHeader( 'Report-Msgid-Bugs-To', $bugs_address );
|
||||
}
|
||||
|
||||
$translations->setHeader( 'Last-Translator', 'FULL NAME <EMAIL@ADDRESS>' );
|
||||
$translations->setHeader( 'Language-Team', 'LANGUAGE <LL@li.org>' );
|
||||
$translations->setHeader( 'X-Generator', 'WP-CLI ' . WP_CLI_VERSION );
|
||||
|
||||
foreach ( $this->headers as $key => $value ) {
|
||||
$translations->setHeader( $key, $value );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Extracts the WordPress version number from wp-includes/version.php.
|
||||
*
|
||||
* @return string|false Version number on success, false otherwise.
|
||||
*/
|
||||
protected function get_wp_version() {
|
||||
$version_php = $this->source . '/wp-includes/version.php';
|
||||
if ( ! file_exists( $version_php ) || ! is_readable( $version_php ) ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return preg_match( '/\$wp_version\s*=\s*\'(.*?)\';/', file_get_contents( $version_php ), $matches ) ? $matches[1] : false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves metadata from a file.
|
||||
*
|
||||
* Searches for metadata in the first 8kiB of a file, such as a plugin or theme.
|
||||
* Each piece of metadata must be on its own line. Fields can not span multiple
|
||||
* lines, the value will get cut at the end of the first line.
|
||||
*
|
||||
* If the file data is not within that first 8kiB, then the author should correct
|
||||
* their plugin file and move the data headers to the top.
|
||||
*
|
||||
* @see get_file_data()
|
||||
*
|
||||
* @param string $file Path to the file.
|
||||
* @param array $headers List of headers, in the format array('HeaderKey' => 'Header Name').
|
||||
*
|
||||
* @return array Array of file headers in `HeaderKey => Header Value` format.
|
||||
*/
|
||||
protected static function get_file_data( $file, $headers ) {
|
||||
// We don't need to write to the file, so just open for reading.
|
||||
$fp = fopen( $file, 'rb' );
|
||||
|
||||
// Pull only the first 8kiB of the file in.
|
||||
$file_data = fread( $fp, 8192 );
|
||||
|
||||
// PHP will close file handle, but we are good citizens.
|
||||
fclose( $fp );
|
||||
|
||||
// Make sure we catch CR-only line endings.
|
||||
$file_data = str_replace( "\r", "\n", $file_data );
|
||||
|
||||
return static::get_file_data_from_string( $file_data, $headers );
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves metadata from a string.
|
||||
*
|
||||
* @param string $string String to look for metadata in.
|
||||
* @param array $headers List of headers.
|
||||
*
|
||||
* @return array Array of file headers in `HeaderKey => Header Value` format.
|
||||
*/
|
||||
public static function get_file_data_from_string( $string, $headers ) {
|
||||
foreach ( $headers as $field => $regex ) {
|
||||
if ( preg_match( '/^[ \t\/*#@]*' . preg_quote( $regex, '/' ) . ':(.*)$/mi', $string, $match ) && $match[1] ) {
|
||||
$headers[ $field ] = static::_cleanup_header_comment( $match[1] );
|
||||
} else {
|
||||
$headers[ $field ] = '';
|
||||
}
|
||||
}
|
||||
|
||||
return $headers;
|
||||
}
|
||||
|
||||
/**
|
||||
* Strip close comment and close php tags from file headers used by WP.
|
||||
*
|
||||
* @see _cleanup_header_comment()
|
||||
*
|
||||
* @param string $str Header comment to clean up.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
protected static function _cleanup_header_comment( $str ) { // phpcs:ignore PSR2.Methods.MethodDeclaration.Underscore -- Not changing because third-party commands might use/extend.
|
||||
return trim( preg_replace( '/\s*(?:\*\/|\?>).*/', '', $str ) );
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
<?php
|
||||
|
||||
namespace WP_CLI\I18n;
|
||||
|
||||
use Gettext\Extractors\JsCode;
|
||||
use Gettext\Translations;
|
||||
use Peast\Syntax\Exception as PeastException;
|
||||
use WP_CLI;
|
||||
|
||||
final class MapCodeExtractor extends JsCode {
|
||||
use IterableCodeExtractor;
|
||||
|
||||
public static $options = [
|
||||
'extractComments' => [ 'translators', 'Translators' ],
|
||||
'constants' => [],
|
||||
'functions' => [
|
||||
'__' => 'text_domain',
|
||||
'_x' => 'text_context_domain',
|
||||
'_n' => 'single_plural_number_domain',
|
||||
'_nx' => 'single_plural_number_context_domain',
|
||||
],
|
||||
];
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public static function fromString( $string, Translations $translations, array $options = [] ) {
|
||||
if ( ! array_key_exists( 'file', $options ) || substr( $options['file'], -7 ) !== '.js.map' ) {
|
||||
return;
|
||||
}
|
||||
$options['file'] = substr( $options['file'], 0, -7 ) . '.js';
|
||||
|
||||
try {
|
||||
$options += static::$options;
|
||||
|
||||
$map_object = json_decode( $string );
|
||||
|
||||
if ( ! isset( $map_object->sourcesContent ) || ! is_array( $map_object->sourcesContent ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
$string = implode( "\n", $map_object->sourcesContent );
|
||||
|
||||
WP_CLI::debug( "Parsing file {$options['file']}", 'make-pot' );
|
||||
|
||||
$functions = new JsFunctionsScanner( $string );
|
||||
|
||||
$functions->enableCommentsExtraction( $options['extractComments'] );
|
||||
$functions->saveGettextFunctions( $translations, $options );
|
||||
} catch ( PeastException $e ) {
|
||||
WP_CLI::debug(
|
||||
sprintf(
|
||||
'Could not parse file %1$s.map: %2$s (line %3$d, column %4$d in the concatenated sourcesContent)',
|
||||
$options['file'],
|
||||
$e->getMessage(),
|
||||
$e->getPosition()->getLine(),
|
||||
$e->getPosition()->getColumn()
|
||||
),
|
||||
'make-pot'
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
<?php
|
||||
|
||||
namespace WP_CLI\I18n;
|
||||
|
||||
use Exception;
|
||||
use Gettext\Extractors\PhpCode;
|
||||
use Gettext\Translations;
|
||||
use WP_CLI;
|
||||
|
||||
final class PhpCodeExtractor extends PhpCode {
|
||||
use IterableCodeExtractor;
|
||||
|
||||
public static $options = [
|
||||
'extractComments' => [ 'translators', 'Translators' ],
|
||||
'constants' => [],
|
||||
'functions' => [
|
||||
'__' => 'text_domain',
|
||||
'esc_attr__' => 'text_domain',
|
||||
'esc_html__' => 'text_domain',
|
||||
'esc_xml__' => 'text_domain',
|
||||
'_e' => 'text_domain',
|
||||
'esc_attr_e' => 'text_domain',
|
||||
'esc_html_e' => 'text_domain',
|
||||
'esc_xml_e' => 'text_domain',
|
||||
'_x' => 'text_context_domain',
|
||||
'_ex' => 'text_context_domain',
|
||||
'esc_attr_x' => 'text_context_domain',
|
||||
'esc_html_x' => 'text_context_domain',
|
||||
'esc_xml_x' => 'text_context_domain',
|
||||
'_n' => 'single_plural_number_domain',
|
||||
'_nx' => 'single_plural_number_context_domain',
|
||||
'_n_noop' => 'single_plural_domain',
|
||||
'_nx_noop' => 'single_plural_context_domain',
|
||||
|
||||
// Compat.
|
||||
'_' => 'gettext', // Same as 'text_domain'.
|
||||
|
||||
// Deprecated.
|
||||
'_c' => 'text_domain',
|
||||
'_nc' => 'single_plural_number_domain',
|
||||
'__ngettext' => 'single_plural_number_domain',
|
||||
'__ngettext_noop' => 'single_plural_domain',
|
||||
],
|
||||
];
|
||||
|
||||
protected static $functionsScannerClass = 'WP_CLI\I18n\PhpFunctionsScanner';
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public static function fromString( $string, Translations $translations, array $options = [] ) {
|
||||
WP_CLI::debug( "Parsing file {$options['file']}", 'make-pot' );
|
||||
|
||||
try {
|
||||
static::fromStringMultiple( $string, [ $translations ], $options );
|
||||
} catch ( Exception $exception ) {
|
||||
WP_CLI::debug(
|
||||
sprintf(
|
||||
'Could not parse file %1$s: %2$s',
|
||||
$options['file'],
|
||||
$exception->getMessage()
|
||||
),
|
||||
'make-pot'
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,83 @@
|
||||
<?php
|
||||
|
||||
namespace WP_CLI\I18n;
|
||||
|
||||
use Gettext\Utils\PhpFunctionsScanner as GettextPhpFunctionsScanner;
|
||||
|
||||
class PhpFunctionsScanner extends GettextPhpFunctionsScanner {
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function saveGettextFunctions( $translations, array $options ) {
|
||||
// Ignore multiple translations for now.
|
||||
// @todo Add proper support for multiple translations.
|
||||
if ( is_array( $translations ) ) {
|
||||
$translations = $translations[0];
|
||||
}
|
||||
|
||||
$functions = $options['functions'];
|
||||
$file = $options['file'];
|
||||
|
||||
foreach ( $this->getFunctions( $options['constants'] ) as $function ) {
|
||||
list( $name, $line, $args ) = $function;
|
||||
|
||||
if ( ! isset( $functions[ $name ] ) ) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$original = null;
|
||||
$domain = null;
|
||||
$context = null;
|
||||
$plural = null;
|
||||
|
||||
switch ( $functions[ $name ] ) {
|
||||
case 'text_domain':
|
||||
case 'gettext':
|
||||
list( $original, $domain ) = array_pad( $args, 2, null );
|
||||
break;
|
||||
|
||||
case 'text_context_domain':
|
||||
list( $original, $context, $domain ) = array_pad( $args, 3, null );
|
||||
break;
|
||||
|
||||
case 'single_plural_number_domain':
|
||||
list( $original, $plural, $number, $domain ) = array_pad( $args, 4, null );
|
||||
break;
|
||||
|
||||
case 'single_plural_number_context_domain':
|
||||
list( $original, $plural, $number, $context, $domain ) = array_pad( $args, 5, null );
|
||||
break;
|
||||
|
||||
case 'single_plural_domain':
|
||||
list( $original, $plural, $domain ) = array_pad( $args, 3, null );
|
||||
break;
|
||||
|
||||
case 'single_plural_context_domain':
|
||||
list( $original, $plural, $context, $domain ) = array_pad( $args, 4, null );
|
||||
break;
|
||||
|
||||
default:
|
||||
// Should never happen.
|
||||
\WP_CLI::error( sprintf( "Internal error: unknown function map '%s' for '%s'.", $functions[ $name ], $name ) );
|
||||
}
|
||||
|
||||
if ( '' === (string) $original ) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if ( $domain !== $translations->getDomain() && null !== $translations->getDomain() ) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$translation = $translations->insert( $context, $original, $plural );
|
||||
$translation = $translation->addReference( $file, $line );
|
||||
|
||||
if ( isset( $function[3] ) ) {
|
||||
foreach ( $function[3] as $extracted_comment ) {
|
||||
$translation = $translation->addExtractedComment( $extracted_comment );
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+139
@@ -0,0 +1,139 @@
|
||||
<?php
|
||||
|
||||
namespace WP_CLI\I18n;
|
||||
|
||||
use Gettext\Generators\Po as PoGenerator;
|
||||
use Gettext\Translations;
|
||||
|
||||
/**
|
||||
* POT file generator.
|
||||
*
|
||||
* The only difference to the existing PO file generator is that this
|
||||
* adds some comments at the very beginning of the file.
|
||||
*/
|
||||
class PotGenerator extends PoGenerator {
|
||||
protected static $comments_before_headers = [];
|
||||
|
||||
/**
|
||||
* Text to include as a comment before the start of the PO contents
|
||||
*
|
||||
* Doesn't need to include # in the beginning of lines, these are added automatically.
|
||||
*
|
||||
* @param string $comment File comment.
|
||||
*/
|
||||
public static function setCommentBeforeHeaders( $comment ) {
|
||||
$comments = explode( "\n", $comment );
|
||||
|
||||
foreach ( $comments as $line ) {
|
||||
if ( '' !== trim( $line ) ) {
|
||||
static::$comments_before_headers[] = '# ' . $line;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* {@parentDoc}.
|
||||
*/
|
||||
public static function toString( Translations $translations, array $options = [] ) {
|
||||
$lines = static::$comments_before_headers;
|
||||
$lines[] = 'msgid ""';
|
||||
$lines[] = 'msgstr ""';
|
||||
|
||||
$plural_form = $translations->getPluralForms();
|
||||
$plural_size = is_array( $plural_form ) ? ( $plural_form[0] - 1 ) : 1;
|
||||
|
||||
foreach ( $translations->getHeaders() as $name => $value ) {
|
||||
$lines[] = sprintf( '"%s: %s\\n"', $name, $value );
|
||||
}
|
||||
|
||||
$lines[] = '';
|
||||
|
||||
foreach ( $translations as $translation ) {
|
||||
/** @var \Gettext\Translation $translation */
|
||||
if ( $translation->hasComments() ) {
|
||||
foreach ( $translation->getComments() as $comment ) {
|
||||
$lines[] = '# ' . $comment;
|
||||
}
|
||||
}
|
||||
|
||||
if ( $translation->hasExtractedComments() ) {
|
||||
foreach ( $translation->getExtractedComments() as $comment ) {
|
||||
$lines[] = '#. ' . $comment;
|
||||
}
|
||||
}
|
||||
|
||||
foreach ( $translation->getReferences() as $reference ) {
|
||||
$lines[] = '#: ' . $reference[0] . ( null !== $reference[1] ? ':' . $reference[1] : '' );
|
||||
}
|
||||
|
||||
if ( $translation->hasFlags() ) {
|
||||
$lines[] = '#, ' . implode( ',', $translation->getFlags() );
|
||||
}
|
||||
|
||||
$prefix = $translation->isDisabled() ? '#~ ' : '';
|
||||
|
||||
if ( $translation->hasContext() ) {
|
||||
$lines[] = $prefix . 'msgctxt ' . self::convertString( $translation->getContext() );
|
||||
}
|
||||
|
||||
self::addLines( $lines, $prefix . 'msgid', $translation->getOriginal() );
|
||||
|
||||
if ( $translation->hasPlural() ) {
|
||||
self::addLines( $lines, $prefix . 'msgid_plural', $translation->getPlural() );
|
||||
|
||||
for ( $i = 0; $i <= $plural_size; $i ++ ) {
|
||||
self::addLines( $lines, $prefix . 'msgstr[' . $i . ']', '' );
|
||||
}
|
||||
} else {
|
||||
self::addLines( $lines, $prefix . 'msgstr', $translation->getTranslation() );
|
||||
}
|
||||
|
||||
$lines[] = '';
|
||||
}
|
||||
|
||||
return implode( "\n", $lines );
|
||||
}
|
||||
|
||||
/**
|
||||
* Escapes and adds double quotes to a string.
|
||||
*
|
||||
* @param string $string Multiline string.
|
||||
*
|
||||
* @return string[]
|
||||
*/
|
||||
protected static function multilineQuote( $string ) {
|
||||
$lines = explode( "\n", $string );
|
||||
$last = count( $lines ) - 1;
|
||||
|
||||
foreach ( $lines as $k => $line ) {
|
||||
if ( $k === $last ) {
|
||||
$lines[ $k ] = self::convertString( $line );
|
||||
} else {
|
||||
$lines[ $k ] = self::convertString( $line . "\n" );
|
||||
}
|
||||
}
|
||||
|
||||
return $lines;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add one or more lines depending whether the string is multiline or not.
|
||||
*
|
||||
* @param array &$lines Array lines should be added to.
|
||||
* @param string $name Name of the line, e.g. msgstr or msgid_plural.
|
||||
* @param string $value The line to add.
|
||||
*/
|
||||
protected static function addLines( array &$lines, $name, $value ) {
|
||||
$newlines = self::multilineQuote( $value );
|
||||
|
||||
if ( count( $newlines ) === 1 ) {
|
||||
$lines[] = $name . ' ' . $newlines[0];
|
||||
} else {
|
||||
$lines[] = $name . ' ""';
|
||||
|
||||
foreach ( $newlines as $line ) {
|
||||
$lines[] = $line;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,151 @@
|
||||
<?php
|
||||
|
||||
namespace WP_CLI\I18n\Tests;
|
||||
|
||||
use PHPUnit_Framework_TestCase;
|
||||
use WP_CLI\I18n\IterableCodeExtractor;
|
||||
use WP_CLI\Utils;
|
||||
|
||||
class IterableCodeExtractorTest extends PHPUnit_Framework_TestCase {
|
||||
|
||||
/** @var string A path files are located */
|
||||
private static $base;
|
||||
|
||||
public function setUp() {
|
||||
/**
|
||||
* PHP5.4 cannot set property with __DIR__ constant.
|
||||
*/
|
||||
self::$base = Utils\normalize_path( __DIR__ ) . '/data/';
|
||||
|
||||
$property = new \ReflectionProperty( 'WP_CLI\I18n\IterableCodeExtractor', 'dir' );
|
||||
$property->setAccessible( true );
|
||||
$property->setValue( null, self::$base );
|
||||
$property->setAccessible( false );
|
||||
}
|
||||
|
||||
public function test_can_include_files() {
|
||||
$includes = [ 'foo-plugin', 'bar', 'baz/inc*.js' ];
|
||||
$result = IterableCodeExtractor::getFilesFromDirectory( self::$base, $includes, [], [ 'php', 'js' ] );
|
||||
$expected = static::$base . 'foo-plugin/foo-plugin.php';
|
||||
$this->assertContains( $expected, $result );
|
||||
$expected = static::$base . 'baz/includes/should_be_included.js';
|
||||
$this->assertContains( $expected, $result );
|
||||
$expected = 'hoge/should_NOT_be_included.js';
|
||||
$this->assertNotContains( $expected, $result );
|
||||
}
|
||||
|
||||
public function test_can_include_empty_array() {
|
||||
$result = IterableCodeExtractor::getFilesFromDirectory( self::$base, [], [], [ 'php', 'js' ] );
|
||||
$expected_1 = static::$base . 'foo-plugin/foo-plugin.php';
|
||||
$expected_2 = static::$base . 'baz/includes/should_be_included.js';
|
||||
$this->assertContains( $expected_1, $result );
|
||||
$this->assertContains( $expected_2, $result );
|
||||
}
|
||||
|
||||
public function test_can_include_wildcard() {
|
||||
$result = IterableCodeExtractor::getFilesFromDirectory( self::$base, [ '*' ], [], [ 'php', 'js' ] );
|
||||
$expected_1 = static::$base . 'foo-plugin/foo-plugin.php';
|
||||
$expected_2 = static::$base . 'baz/includes/should_be_included.js';
|
||||
$this->assertContains( $expected_1, $result );
|
||||
$this->assertContains( $expected_2, $result );
|
||||
}
|
||||
|
||||
public function test_can_include_subdirectories() {
|
||||
$result = IterableCodeExtractor::getFilesFromDirectory( self::$base, [ 'foo/bar/*' ], [], [ 'php', 'js' ] );
|
||||
$expected_1 = static::$base . 'foo/bar/foo/bar/foo/bar/deep_directory_also_included.php';
|
||||
$expected_2 = static::$base . 'foo/bar/foofoo/included.js';
|
||||
$this->assertContains( $expected_1, $result );
|
||||
$this->assertContains( $expected_2, $result );
|
||||
}
|
||||
|
||||
public function test_can_include_only_php() {
|
||||
$result = IterableCodeExtractor::getFilesFromDirectory( self::$base, [ 'foo/bar/*' ], [], [ 'php' ] );
|
||||
$expected_1 = static::$base . 'foo/bar/foo/bar/foo/bar/deep_directory_also_included.php';
|
||||
$expected_2 = static::$base . 'foo/bar/foofoo/ignored.js';
|
||||
$this->assertContains( $expected_1, $result );
|
||||
$this->assertNotContains( $expected_2, $result );
|
||||
}
|
||||
|
||||
public function test_can_exclude_override_wildcard() {
|
||||
$result = IterableCodeExtractor::getFilesFromDirectory( self::$base, [ 'foo/bar/*' ], [ 'foo/bar/excluded/*' ], [ 'php' ] );
|
||||
$expected_1 = static::$base . 'foo/bar/foo/bar/foo/bar/deep_directory_also_included.php';
|
||||
$expected_2 = static::$base . 'foo/bar/excluded/excluded.js';
|
||||
$this->assertContains( $expected_1, $result );
|
||||
$this->assertNotContains( $expected_2, $result );
|
||||
}
|
||||
|
||||
public function test_can_exclude_override_matching_directory() {
|
||||
$result = IterableCodeExtractor::getFilesFromDirectory( self::$base, [ 'foo/bar/*' ], [ 'foo/bar/excluded/*' ], [ 'php' ] );
|
||||
$expected_1 = static::$base . 'foo/bar/foo/bar/foo/bar/deep_directory_also_included.php';
|
||||
$expected_2 = static::$base . 'foo/bar/excluded/excluded.js';
|
||||
$this->assertContains( $expected_1, $result );
|
||||
$this->assertNotContains( $expected_2, $result );
|
||||
}
|
||||
|
||||
public function test_can_not_exclude_partially_directory() {
|
||||
$result = IterableCodeExtractor::getFilesFromDirectory( self::$base, [ 'foo/bar/*' ], [ 'exc' ], [ 'js' ] );
|
||||
$expected_1 = static::$base . 'foo/bar/foo/bar/foo/bar/deep_directory_also_included.php';
|
||||
$expected_2 = static::$base . 'foo/bar/excluded/ignored.js';
|
||||
$this->assertNotContains( $expected_1, $result );
|
||||
$this->assertContains( $expected_2, $result );
|
||||
}
|
||||
|
||||
public function test_can_exclude_by_wildcard() {
|
||||
$result = IterableCodeExtractor::getFilesFromDirectory( self::$base, [], [ '*' ], [ 'php', 'js' ] );
|
||||
$this->assertEmpty( $result );
|
||||
}
|
||||
|
||||
public function test_can_exclude_files() {
|
||||
$excludes = [ 'hoge' ];
|
||||
$result = IterableCodeExtractor::getFilesFromDirectory( self::$base, [], $excludes, [ 'php', 'js' ] );
|
||||
$expected = static::$base . 'hoge/should_NOT_be_included.js';
|
||||
$this->assertNotContains( $expected, $result );
|
||||
}
|
||||
|
||||
public function test_can_override_exclude_by_include() {
|
||||
// Overrides include option
|
||||
$includes = [ 'excluded/ignored.js' ];
|
||||
$excludes = [ 'excluded/*.js' ];
|
||||
$result = IterableCodeExtractor::getFilesFromDirectory( self::$base, $includes, $excludes, [ 'php', 'js' ] );
|
||||
$expected = static::$base . 'foo/bar/excluded/ignored.js';
|
||||
$this->assertContains( $expected, $result );
|
||||
}
|
||||
|
||||
public function test_can_return_all_directory_files_sorted() {
|
||||
$result = IterableCodeExtractor::getFilesFromDirectory( self::$base, [ '*' ], [], [ 'php', 'js' ] );
|
||||
$expected = array(
|
||||
static::$base . 'baz/includes/should_be_included.js',
|
||||
static::$base . 'foo-plugin/foo-plugin.php',
|
||||
static::$base . 'foo/bar/excluded/ignored.js',
|
||||
static::$base . 'foo/bar/foo/bar/foo/bar/deep_directory_also_included.php',
|
||||
static::$base . 'foo/bar/foofoo/included.js',
|
||||
static::$base . 'hoge/should_NOT_be_included.js',
|
||||
static::$base . 'vendor/vendor-file.php',
|
||||
);
|
||||
$this->assertEquals( $expected, $result );
|
||||
}
|
||||
|
||||
public function test_can_include_file_in_excluded_folder() {
|
||||
$includes = [ 'vendor/vendor-file.php' ];
|
||||
$excludes = [ 'vendor' ];
|
||||
$result = IterableCodeExtractor::getFilesFromDirectory( self::$base, $includes, $excludes, [ 'php', 'js' ] );
|
||||
$expected = static::$base . 'vendor/vendor-file.php';
|
||||
$this->assertContains( $expected, $result );
|
||||
}
|
||||
|
||||
public function test_can_include_file_in_excluded_folder_with_leading_slash() {
|
||||
$includes = [ '/vendor/vendor-file.php' ];
|
||||
$excludes = [ 'vendor' ];
|
||||
$result = IterableCodeExtractor::getFilesFromDirectory( self::$base, $includes, $excludes, [ 'php', 'js' ] );
|
||||
$expected = static::$base . 'vendor/vendor-file.php';
|
||||
$this->assertContains( $expected, $result );
|
||||
}
|
||||
|
||||
public function test_can_include_file_in_excluded_folder_by_wildcard() {
|
||||
$includes = [ 'vendor/**' ];
|
||||
$excludes = [ 'vendor' ];
|
||||
$result = IterableCodeExtractor::getFilesFromDirectory( self::$base, $includes, $excludes, [ 'php', 'js' ] );
|
||||
$expected = static::$base . 'vendor/vendor-file.php';
|
||||
$this->assertContains( $expected, $result );
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
<?php
|
||||
|
||||
namespace WP_CLI\I18n\Tests;
|
||||
|
||||
use Gettext\Translation;
|
||||
use WP_CLI\I18n\PotGenerator;
|
||||
use Gettext\Translations;
|
||||
use PHPUnit_Framework_TestCase;
|
||||
|
||||
class PotGeneratorTest extends PHPUnit_Framework_TestCase {
|
||||
public function test_adds_correct_amount_of_plural_strings() {
|
||||
$translations = new Translations();
|
||||
|
||||
$translation = new Translation( '', '%d cat', '%d cats' );
|
||||
|
||||
$translations[] = $translation;
|
||||
|
||||
$result = PotGenerator::toString( $translations );
|
||||
|
||||
$this->assertContains( 'msgid "%d cat"', $result );
|
||||
$this->assertContains( 'msgid_plural "%d cats"', $result );
|
||||
$this->assertContains( 'msgstr[0] ""', $result );
|
||||
$this->assertContains( 'msgstr[1] ""', $result );
|
||||
}
|
||||
}
|
||||
Vendored
+2
@@ -0,0 +1,2 @@
|
||||
require:
|
||||
- i18n-command.php
|
||||
Reference in New Issue
Block a user