Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a1f7933e90 | ||
|
|
becb4ae3d6 | ||
|
|
7aead0fb97 | ||
|
|
06591a0b24 | ||
|
|
f6f6a4cbb6 | ||
|
|
ae9d1cad4c | ||
|
|
3fa50cb126 | ||
|
|
67c897256f | ||
|
|
b51c35b9da | ||
|
|
424c6eba32 | ||
|
|
ad7f7d498d | ||
|
|
2f2046a809 | ||
|
|
7ad51fb281 | ||
|
|
9a865912c0 | ||
|
|
460a70fc3c | ||
|
|
ea65c7b93b | ||
|
|
156502ba77 | ||
|
|
c253efda07 | ||
|
|
1792d785c0 | ||
|
|
5ceaa907d8 | ||
|
|
96c49503ee | ||
|
|
e526dd95fd | ||
|
|
1703da84b7 | ||
|
|
f4e0a30ef1 | ||
|
|
dcf96cd108 | ||
|
|
97648d798c | ||
|
|
ded7a4a2db | ||
|
|
2078fed520 | ||
|
|
9424411b12 | ||
|
|
16d8f16b25 | ||
|
|
17fcdede5a | ||
|
|
12dfad93bf | ||
|
|
a7c79bb2f2 | ||
|
|
2314f89260 | ||
|
|
085d7d1ffb | ||
|
|
37abe27ebd | ||
|
|
f853270f3e | ||
|
|
966a88ec0d | ||
|
|
9a96df15dd | ||
|
|
5912599769 | ||
|
|
bc90f59909 | ||
|
|
e98b8777c7 | ||
|
|
1c01c2e318 | ||
|
|
b34aae160a | ||
|
|
177fc13282 | ||
|
|
1f8cb28670 | ||
|
|
b19fbf5fa5 | ||
|
|
451de60281 | ||
|
|
451ac69307 | ||
|
|
20090bdf2c | ||
|
|
206bd8d5f7 | ||
|
|
bc3b707050 | ||
|
|
8287de36c7 | ||
|
|
4ddeada0c9 |
@@ -0,0 +1,108 @@
|
||||
name: Bug Report and Feature Request
|
||||
description: Report a HTBD issue or suggest a feature or documentation improvement
|
||||
title: "[Issue]: "
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
Thank you for taking the time to provide feedback. Please include as much information as possible so we can investigate efficiently.
|
||||
Do not include your Baidu Translate App ID, secret key, signatures, cookies, or other sensitive information.
|
||||
|
||||
- type: dropdown
|
||||
id: issue-type
|
||||
attributes:
|
||||
label: Issue type
|
||||
options:
|
||||
- Bug report
|
||||
- Feature request
|
||||
- Documentation issue
|
||||
- Other
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: summary
|
||||
attributes:
|
||||
label: Description
|
||||
description: Clearly describe the issue you encountered or the feature you would like to see.
|
||||
placeholder: Describe the current behavior, expected behavior, and impact.
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: input
|
||||
id: plugin-version
|
||||
attributes:
|
||||
label: HTBD version
|
||||
placeholder: "For example: 0.3.1"
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: input
|
||||
id: wordpress-version
|
||||
attributes:
|
||||
label: WordPress version
|
||||
placeholder: "For example: 6.8.2"
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: input
|
||||
id: php-version
|
||||
attributes:
|
||||
label: PHP version
|
||||
placeholder: "For example: 8.3.20"
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: environment
|
||||
attributes:
|
||||
label: Environment
|
||||
description: Include your theme, relevant plugins, routing mode, source language, and target languages.
|
||||
placeholder: |
|
||||
Theme:
|
||||
Relevant plugins:
|
||||
Routing mode: Subdirectory / Domain binding
|
||||
Source and target languages:
|
||||
|
||||
- type: textarea
|
||||
id: reproduction
|
||||
attributes:
|
||||
label: Steps to reproduce
|
||||
description: For bug reports, provide the minimum steps that consistently reproduce the issue. Enter "Not applicable" for other issue types.
|
||||
placeholder: |
|
||||
1. Open ...
|
||||
2. Configure ...
|
||||
3. Run ...
|
||||
4. Observe ...
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: expected-behavior
|
||||
attributes:
|
||||
label: Expected behavior
|
||||
placeholder: Describe what you expected to happen.
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: logs
|
||||
attributes:
|
||||
label: Logs or screenshots
|
||||
description: Paste error logs with credentials and personal information removed, or drag in screenshots. Enter "None" if unavailable.
|
||||
|
||||
- type: textarea
|
||||
id: additional-context
|
||||
attributes:
|
||||
label: Additional context
|
||||
description: Add any other information that may help us understand or investigate the issue.
|
||||
|
||||
- type: checkboxes
|
||||
id: checks
|
||||
attributes:
|
||||
label: Pre-submission checklist
|
||||
options:
|
||||
- label: I searched the existing issues and did not find the same issue or suggestion.
|
||||
required: true
|
||||
- label: I removed all Baidu Translate credentials, signatures, cookies, and other sensitive information.
|
||||
required: true
|
||||
@@ -0,0 +1,108 @@
|
||||
name: 问题反馈与功能建议
|
||||
description: 反馈 HTBD 的问题,或提出功能和文档改进建议
|
||||
title: "[Issue]: "
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
感谢反馈。请尽量完整填写以下信息,以便快速定位和处理。
|
||||
请勿提交百度翻译 App ID、密钥、签名、Cookie 或其他敏感信息。
|
||||
|
||||
- type: dropdown
|
||||
id: issue-type
|
||||
attributes:
|
||||
label: Issue 类型
|
||||
options:
|
||||
- 缺陷报告
|
||||
- 功能建议
|
||||
- 文档问题
|
||||
- 其他
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: summary
|
||||
attributes:
|
||||
label: 问题描述
|
||||
description: 清楚说明遇到的问题或希望增加的功能。
|
||||
placeholder: 请描述当前行为、预期行为及其影响。
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: input
|
||||
id: plugin-version
|
||||
attributes:
|
||||
label: HTBD 版本
|
||||
placeholder: 例如:0.3.1
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: input
|
||||
id: wordpress-version
|
||||
attributes:
|
||||
label: WordPress 版本
|
||||
placeholder: 例如:6.8.2
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: input
|
||||
id: php-version
|
||||
attributes:
|
||||
label: PHP 版本
|
||||
placeholder: 例如:8.3.20
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: environment
|
||||
attributes:
|
||||
label: 环境信息
|
||||
description: 请填写主题、相关插件、路由模式、源语言和目标语言等信息。
|
||||
placeholder: |
|
||||
主题:
|
||||
相关插件:
|
||||
路由模式:子目录 / 域名绑定
|
||||
源语言与目标语言:
|
||||
|
||||
- type: textarea
|
||||
id: reproduction
|
||||
attributes:
|
||||
label: 复现步骤
|
||||
description: 缺陷报告请提供可稳定复现问题的最少步骤;其他类型可填写“不适用”。
|
||||
placeholder: |
|
||||
1. 打开……
|
||||
2. 配置……
|
||||
3. 执行……
|
||||
4. 出现……
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: expected-behavior
|
||||
attributes:
|
||||
label: 预期行为
|
||||
placeholder: 说明你期望发生什么。
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: logs
|
||||
attributes:
|
||||
label: 日志或截图
|
||||
description: 可粘贴已移除凭据和个人信息的错误日志,或拖入截图。没有可填写“无”。
|
||||
|
||||
- type: textarea
|
||||
id: additional-context
|
||||
attributes:
|
||||
label: 补充信息
|
||||
description: 添加其他可能有助于理解或定位问题的信息。
|
||||
|
||||
- type: checkboxes
|
||||
id: checks
|
||||
attributes:
|
||||
label: 提交前确认
|
||||
options:
|
||||
- label: 我已搜索现有 Issue,未发现相同问题或建议。
|
||||
required: true
|
||||
- label: 我已移除百度翻译凭据、签名、Cookie 和其他敏感信息。
|
||||
required: true
|
||||
@@ -0,0 +1,138 @@
|
||||
name: Deploy to WordPress.org SVN
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
release_tag:
|
||||
description: GitHub Release tag to deploy (for example, v0.3.1)
|
||||
required: true
|
||||
type: string
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
concurrency:
|
||||
group: wordpress-svn-deploy
|
||||
cancel-in-progress: false
|
||||
|
||||
jobs:
|
||||
deploy:
|
||||
name: Deploy release to WordPress.org
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
env:
|
||||
PLUGIN_SLUG: hyx-translator-for-baidu-translate
|
||||
SVN_URL: https://plugins.svn.wordpress.org/hyx-translator-for-baidu-translate
|
||||
SVN_USERNAME: ${{ secrets.SVN_USERNAME }}
|
||||
SVN_PASSWORD: ${{ secrets.SVN_PASSWORD }}
|
||||
|
||||
steps:
|
||||
- name: Resolve release tag and archive name
|
||||
id: release
|
||||
env:
|
||||
MANUAL_TAG: ${{ inputs.release_tag }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
|
||||
release_tag="${MANUAL_TAG}"
|
||||
|
||||
if [[ ! "${release_tag}" =~ ^v?([0-9]+\.[0-9]+\.[0-9]+)$ ]]; then
|
||||
printf 'Release tag must be vX.Y.Z or X.Y.Z, got: %s\n' "${release_tag}" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
version="${BASH_REMATCH[1]}"
|
||||
archive="${PLUGIN_SLUG}-${version}.zip"
|
||||
printf 'tag=%s\nversion=%s\narchive=%s\n' \
|
||||
"${release_tag}" "${version}" "${archive}" >> "${GITHUB_OUTPUT}"
|
||||
|
||||
- name: Install deployment tools
|
||||
run: sudo apt-get update && sudo apt-get install --yes rsync subversion unzip
|
||||
|
||||
- name: Download ZIP from GitHub Release
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
RELEASE_TAG: ${{ steps.release.outputs.tag }}
|
||||
ARCHIVE: ${{ steps.release.outputs.archive }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
mkdir -p release
|
||||
gh release download "${RELEASE_TAG}" \
|
||||
--repo "${GITHUB_REPOSITORY}" \
|
||||
--pattern "${ARCHIVE}" \
|
||||
--dir release
|
||||
test -f "release/${ARCHIVE}"
|
||||
|
||||
- name: Extract and validate plugin package
|
||||
env:
|
||||
ARCHIVE: ${{ steps.release.outputs.archive }}
|
||||
VERSION: ${{ steps.release.outputs.version }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
mkdir -p package
|
||||
unzip -q "release/${ARCHIVE}" -d package
|
||||
|
||||
plugin_dir="package/${PLUGIN_SLUG}"
|
||||
plugin_file="${plugin_dir}/${PLUGIN_SLUG}.php"
|
||||
test -f "${plugin_file}"
|
||||
test -f "${plugin_dir}/readme.txt"
|
||||
|
||||
package_version="$(sed -nE 's/^ \* Version: *([^ ]+) *$/\1/p' "${plugin_file}")"
|
||||
stable_tag="$(sed -nE 's/^Stable tag: *([^ ]+) *$/\1/p' "${plugin_dir}/readme.txt")"
|
||||
if [[ "${package_version}" != "${VERSION}" || "${stable_tag}" != "${VERSION}" ]]; then
|
||||
printf 'Version mismatch: release=%s, plugin=%s, stable tag=%s\n' \
|
||||
"${VERSION}" "${package_version}" "${stable_tag}" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
- name: Check out WordPress.org SVN repository
|
||||
run: |
|
||||
set -euo pipefail
|
||||
svn checkout "${SVN_URL}" wordpress-svn \
|
||||
--non-interactive \
|
||||
--no-auth-cache \
|
||||
--username "${SVN_USERNAME}" \
|
||||
--password "${SVN_PASSWORD}"
|
||||
|
||||
- name: Update trunk and create SVN tag
|
||||
env:
|
||||
VERSION: ${{ steps.release.outputs.version }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
source_dir="${GITHUB_WORKSPACE}/package/${PLUGIN_SLUG}/"
|
||||
trunk_dir="${GITHUB_WORKSPACE}/wordpress-svn/trunk"
|
||||
tag_dir="${GITHUB_WORKSPACE}/wordpress-svn/tags/${VERSION}"
|
||||
|
||||
if svn info "${SVN_URL}/tags/${VERSION}" \
|
||||
--non-interactive \
|
||||
--no-auth-cache \
|
||||
--username "${SVN_USERNAME}" \
|
||||
--password "${SVN_PASSWORD}" >/dev/null 2>&1; then
|
||||
printf 'SVN tag %s already exists; refusing to overwrite it.\n' "${VERSION}" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
rsync -a --delete --exclude='.svn' "${source_dir}" "${trunk_dir}/"
|
||||
svn add --force "${trunk_dir}" --parents
|
||||
|
||||
svn status "${trunk_dir}" | while IFS= read -r status_line; do
|
||||
if [[ "${status_line:0:1}" == '!' ]]; then
|
||||
svn rm --force "${status_line:8}"
|
||||
fi
|
||||
done
|
||||
|
||||
svn copy "${trunk_dir}" "${tag_dir}"
|
||||
|
||||
- name: Commit release to WordPress.org SVN
|
||||
env:
|
||||
VERSION: ${{ steps.release.outputs.version }}
|
||||
working-directory: wordpress-svn
|
||||
run: |
|
||||
set -euo pipefail
|
||||
svn status
|
||||
svn commit . \
|
||||
--message "Release ${VERSION}" \
|
||||
--non-interactive \
|
||||
--no-auth-cache \
|
||||
--username "${SVN_USERNAME}" \
|
||||
--password "${SVN_PASSWORD}"
|
||||
@@ -2,8 +2,6 @@ name: Package Plugin
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
tags:
|
||||
- 'v*'
|
||||
pull_request:
|
||||
@@ -12,6 +10,7 @@ on:
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
actions: write
|
||||
contents: write
|
||||
|
||||
jobs:
|
||||
@@ -23,31 +22,22 @@ jobs:
|
||||
- name: Check out repository
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Install gettext
|
||||
run: sudo apt-get update && sudo apt-get install --yes gettext
|
||||
|
||||
- name: Build installable plugin archive
|
||||
id: package
|
||||
run: |
|
||||
set -eu
|
||||
plugin_slug="btranslate"
|
||||
version="$(grep -E '^ \* Version:' wp-translate.php | sed -E 's/^ \* Version: *//')"
|
||||
archive_name="${plugin_slug}-${version}.zip"
|
||||
|
||||
rm -rf build
|
||||
mkdir -p "build/${plugin_slug}"
|
||||
cp wp-translate.php README.md "build/${plugin_slug}/"
|
||||
cp -R includes "build/${plugin_slug}/"
|
||||
(
|
||||
cd build
|
||||
zip -qr "../${archive_name}" "${plugin_slug}"
|
||||
)
|
||||
|
||||
unzip -p "${archive_name}" "${plugin_slug}/wp-translate.php" | grep -q '^ \* Plugin Name: Btranslate$'
|
||||
|
||||
version="$(grep -E '^ \* Version:' hyx-translator-for-baidu-translate.php | sed -E 's/^ \* Version: *//')"
|
||||
archive_name="hyx-translator-for-baidu-translate-${version}.zip"
|
||||
./build.sh build
|
||||
printf 'archive=%s\n' "${archive_name}" >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: Upload plugin archive
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: btranslate-plugin
|
||||
name: hyx-translator-for-baidu-translate-plugin
|
||||
path: ${{ steps.package.outputs.archive }}
|
||||
if-no-files-found: error
|
||||
retention-days: 30
|
||||
@@ -56,4 +46,14 @@ jobs:
|
||||
if: startsWith( github.ref, 'refs/tags/v' )
|
||||
uses: softprops/action-gh-release@v2
|
||||
with:
|
||||
files: ${{ steps.package.outputs.archive }}
|
||||
files: ${{ steps.package.outputs.archive }}
|
||||
|
||||
- name: Deploy GitHub Release to WordPress.org SVN
|
||||
if: startsWith( github.ref, 'refs/tags/v' )
|
||||
env:
|
||||
DEFAULT_BRANCH: ${{ github.event.repository.default_branch }}
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
run: |
|
||||
gh workflow run deploy-wordpress-svn.yml \
|
||||
--ref "${DEFAULT_BRANCH}" \
|
||||
--field release_tag="${GITHUB_REF_NAME}"
|
||||
+3
-1
@@ -10,4 +10,6 @@
|
||||
/wordpress/
|
||||
/wp-content/
|
||||
.DS_Store
|
||||
*.zip
|
||||
*.zip
|
||||
/languages/*.mo
|
||||
/svn/
|
||||
@@ -1,4 +1,4 @@
|
||||
# wp-translate Agent Guide
|
||||
# hyx-translator-for-baidu-translate Agent Guide
|
||||
|
||||
## Project Scope
|
||||
|
||||
@@ -44,6 +44,16 @@ Unless a user explicitly requests a refresh, each translatable value must be tra
|
||||
- Add focused tests for language selection, routing, translation cache reuse and invalidation, persistence, provider signing/error handling, and content/meta filtering.
|
||||
- Mock Baidu API calls in every test. Tests must not require live credentials or outbound network access.
|
||||
- Keep changes narrow and avoid unrelated refactors or formatting churn.
|
||||
- Plugin slug and PHP prefix: `wp-translate` and `wpt_`/`WPT_`. The current minimum supported PHP version is 8.1 and the current minimum WordPress version is 6.4.
|
||||
- After every file change, review `.github/workflows/package-plugin.yml` and ensure the automated packaging Action includes all runtime files required by the change. Validate the packaging commands locally when practical.
|
||||
- Plugin slug and PHP prefix: `hyx-translator-for-baidu-translate` and `htbd_`/`HTBD_`. The custom translation table is `{$wpdb->prefix}hyx_bd_translations`. The current minimum supported PHP version is 8.1 and the current minimum WordPress version is 6.4.
|
||||
- The repository has no Composer-managed development dependencies or bundled automated test suite. Validate PHP syntax with `find . -path './.git' -prune -o -type f -name '*.php' -print0 | xargs -0 -n1 php -l`. Any future automated tests must mock WordPress API calls and must not require live credentials, external network access, or a WordPress database.
|
||||
- Document supported languages, routing modes, data retention, translation lifecycle, and known limitations in the README whenever the implementation changes.
|
||||
- Whenever any README or readme file is updated, update the corresponding PO translation file under `languages/` in the same change so the translated documentation remains synchronized.
|
||||
|
||||
## Release Workflow
|
||||
|
||||
When the user issues `release x.x.x` or `release vx.x.x`:
|
||||
|
||||
1. Determine the previous version from the latest Git tag reachable from the current commit and output that tag/version before making release changes. Accept the optional leading `v` in the requested version and preserve the repository's existing version/tag convention.
|
||||
2. Search the entire repository, including hidden files such as `.github` and excluding only Git internals and ignored/generated artifacts, for every occurrence of the previous version. Replace all release-version occurrences with the new version, including but not limited to PHP headers/constants, readme files, documentation, workflows, and issue templates. Report the files changed and verify that no stale release-version occurrence remains where the current release version is expected.
|
||||
3. Generate release notes from the previous tag through the current release state, using the commits and resulting diff in that range. Output both an English release log and a Chinese release log in Markdown format, with each complete language version enclosed in its own fenced code block. Keep the two logs semantically aligned and organize notable changes, fixes, compatibility notes, and upgrade considerations when applicable.
|
||||
|
||||
@@ -0,0 +1,674 @@
|
||||
GNU GENERAL PUBLIC LICENSE
|
||||
Version 3, 29 June 2007
|
||||
|
||||
Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>
|
||||
Everyone is permitted to copy and distribute verbatim copies
|
||||
of this license document, but changing it is not allowed.
|
||||
|
||||
Preamble
|
||||
|
||||
The GNU General Public License is a free, copyleft license for
|
||||
software and other kinds of works.
|
||||
|
||||
The licenses for most software and other practical works are designed
|
||||
to take away your freedom to share and change the works. By contrast,
|
||||
the GNU General Public License is intended to guarantee your freedom to
|
||||
share and change all versions of a program--to make sure it remains free
|
||||
software for all its users. We, the Free Software Foundation, use the
|
||||
GNU General Public License for most of our software; it applies also to
|
||||
any other work released this way by its authors. You can apply it to
|
||||
your programs, too.
|
||||
|
||||
When we speak of free software, we are referring to freedom, not
|
||||
price. Our General Public Licenses are designed to make sure that you
|
||||
have the freedom to distribute copies of free software (and charge for
|
||||
them if you wish), that you receive source code or can get it if you
|
||||
want it, that you can change the software or use pieces of it in new
|
||||
free programs, and that you know you can do these things.
|
||||
|
||||
To protect your rights, we need to prevent others from denying you
|
||||
these rights or asking you to surrender the rights. Therefore, you have
|
||||
certain responsibilities if you distribute copies of the software, or if
|
||||
you modify it: responsibilities to respect the freedom of others.
|
||||
|
||||
For example, if you distribute copies of such a program, whether
|
||||
gratis or for a fee, you must pass on to the recipients the same
|
||||
freedoms that you received. You must make sure that they, too, receive
|
||||
or can get the source code. And you must show them these terms so they
|
||||
know their rights.
|
||||
|
||||
Developers that use the GNU GPL protect your rights with two steps:
|
||||
(1) assert copyright on the software, and (2) offer you this License
|
||||
giving you legal permission to copy, distribute and/or modify it.
|
||||
|
||||
For the developers' and authors' protection, the GPL clearly explains
|
||||
that there is no warranty for this free software. For both users' and
|
||||
authors' sake, the GPL requires that modified versions be marked as
|
||||
changed, so that their problems will not be attributed erroneously to
|
||||
authors of previous versions.
|
||||
|
||||
Some devices are designed to deny users access to install or run
|
||||
modified versions of the software inside them, although the manufacturer
|
||||
can do so. This is fundamentally incompatible with the aim of
|
||||
protecting users' freedom to change the software. The systematic
|
||||
pattern of such abuse occurs in the area of products for individuals to
|
||||
use, which is precisely where it is most unacceptable. Therefore, we
|
||||
have designed this version of the GPL to prohibit the practice for those
|
||||
products. If such problems arise substantially in other domains, we
|
||||
stand ready to extend this provision to those domains in future versions
|
||||
of the GPL, as needed to protect the freedom of users.
|
||||
|
||||
Finally, every program is threatened constantly by software patents.
|
||||
States should not allow patents to restrict development and use of
|
||||
software on general-purpose computers, but in those that do, we wish to
|
||||
avoid the special danger that patents applied to a free program could
|
||||
make it effectively proprietary. To prevent this, the GPL assures that
|
||||
patents cannot be used to render the program non-free.
|
||||
|
||||
The precise terms and conditions for copying, distribution and
|
||||
modification follow.
|
||||
|
||||
TERMS AND CONDITIONS
|
||||
|
||||
0. Definitions.
|
||||
|
||||
"This License" refers to version 3 of the GNU General Public License.
|
||||
|
||||
"Copyright" also means copyright-like laws that apply to other kinds of
|
||||
works, such as semiconductor masks.
|
||||
|
||||
"The Program" refers to any copyrightable work licensed under this
|
||||
License. Each licensee is addressed as "you". "Licensees" and
|
||||
"recipients" may be individuals or organizations.
|
||||
|
||||
To "modify" a work means to copy from or adapt all or part of the work
|
||||
in a fashion requiring copyright permission, other than the making of an
|
||||
exact copy. The resulting work is called a "modified version" of the
|
||||
earlier work or a work "based on" the earlier work.
|
||||
|
||||
A "covered work" means either the unmodified Program or a work based
|
||||
on the Program.
|
||||
|
||||
To "propagate" a work means to do anything with it that, without
|
||||
permission, would make you directly or secondarily liable for
|
||||
infringement under applicable copyright law, except executing it on a
|
||||
computer or modifying a private copy. Propagation includes copying,
|
||||
distribution (with or without modification), making available to the
|
||||
public, and in some countries other activities as well.
|
||||
|
||||
To "convey" a work means any kind of propagation that enables other
|
||||
parties to make or receive copies. Mere interaction with a user through
|
||||
a computer network, with no transfer of a copy, is not conveying.
|
||||
|
||||
An interactive user interface displays "Appropriate Legal Notices"
|
||||
to the extent that it includes a convenient and prominently visible
|
||||
feature that (1) displays an appropriate copyright notice, and (2)
|
||||
tells the user that there is no warranty for the work (except to the
|
||||
extent that warranties are provided), that licensees may convey the
|
||||
work under this License, and how to view a copy of this License. If
|
||||
the interface presents a list of user commands or options, such as a
|
||||
menu, a prominent item in the list meets this criterion.
|
||||
|
||||
1. Source Code.
|
||||
|
||||
The "source code" for a work means the preferred form of the work
|
||||
for making modifications to it. "Object code" means any non-source
|
||||
form of a work.
|
||||
|
||||
A "Standard Interface" means an interface that either is an official
|
||||
standard defined by a recognized standards body, or, in the case of
|
||||
interfaces specified for a particular programming language, one that
|
||||
is widely used among developers working in that language.
|
||||
|
||||
The "System Libraries" of an executable work include anything, other
|
||||
than the work as a whole, that (a) is included in the normal form of
|
||||
packaging a Major Component, but which is not part of that Major
|
||||
Component, and (b) serves only to enable use of the work with that
|
||||
Major Component, or to implement a Standard Interface for which an
|
||||
implementation is available to the public in source code form. A
|
||||
"Major Component", in this context, means a major essential component
|
||||
(kernel, window system, and so on) of the specific operating system
|
||||
(if any) on which the executable work runs, or a compiler used to
|
||||
produce the work, or an object code interpreter used to run it.
|
||||
|
||||
The "Corresponding Source" for a work in object code form means all
|
||||
the source code needed to generate, install, and (for an executable
|
||||
work) run the object code and to modify the work, including scripts to
|
||||
control those activities. However, it does not include the work's
|
||||
System Libraries, or general-purpose tools or generally available free
|
||||
programs which are used unmodified in performing those activities but
|
||||
which are not part of the work. For example, Corresponding Source
|
||||
includes interface definition files associated with source files for
|
||||
the work, and the source code for shared libraries and dynamically
|
||||
linked subprograms that the work is specifically designed to require,
|
||||
such as by intimate data communication or control flow between those
|
||||
subprograms and other parts of the work.
|
||||
|
||||
The Corresponding Source need not include anything that users
|
||||
can regenerate automatically from other parts of the Corresponding
|
||||
Source.
|
||||
|
||||
The Corresponding Source for a work in source code form is that
|
||||
same work.
|
||||
|
||||
2. Basic Permissions.
|
||||
|
||||
All rights granted under this License are granted for the term of
|
||||
copyright on the Program, and are irrevocable provided the stated
|
||||
conditions are met. This License explicitly affirms your unlimited
|
||||
permission to run the unmodified Program. The output from running a
|
||||
covered work is covered by this License only if the output, given its
|
||||
content, constitutes a covered work. This License acknowledges your
|
||||
rights of fair use or other equivalent, as provided by copyright law.
|
||||
|
||||
You may make, run and propagate covered works that you do not
|
||||
convey, without conditions so long as your license otherwise remains
|
||||
in force. You may convey covered works to others for the sole purpose
|
||||
of having them make modifications exclusively for you, or provide you
|
||||
with facilities for running those works, provided that you comply with
|
||||
the terms of this License in conveying all material for which you do
|
||||
not control copyright. Those thus making or running the covered works
|
||||
for you must do so exclusively on your behalf, under your direction
|
||||
and control, on terms that prohibit them from making any copies of
|
||||
your copyrighted material outside their relationship with you.
|
||||
|
||||
Conveying under any other circumstances is permitted solely under
|
||||
the conditions stated below. Sublicensing is not allowed; section 10
|
||||
makes it unnecessary.
|
||||
|
||||
3. Protecting Users' Legal Rights From Anti-Circumvention Law.
|
||||
|
||||
No covered work shall be deemed part of an effective technological
|
||||
measure under any applicable law fulfilling obligations under article
|
||||
11 of the WIPO copyright treaty adopted on 20 December 1996, or
|
||||
similar laws prohibiting or restricting circumvention of such
|
||||
measures.
|
||||
|
||||
When you convey a covered work, you waive any legal power to forbid
|
||||
circumvention of technological measures to the extent such circumvention
|
||||
is effected by exercising rights under this License with respect to
|
||||
the covered work, and you disclaim any intention to limit operation or
|
||||
modification of the work as a means of enforcing, against the work's
|
||||
users, your or third parties' legal rights to forbid circumvention of
|
||||
technological measures.
|
||||
|
||||
4. Conveying Verbatim Copies.
|
||||
|
||||
You may convey verbatim copies of the Program's source code as you
|
||||
receive it, in any medium, provided that you conspicuously and
|
||||
appropriately publish on each copy an appropriate copyright notice;
|
||||
keep intact all notices stating that this License and any
|
||||
non-permissive terms added in accord with section 7 apply to the code;
|
||||
keep intact all notices of the absence of any warranty; and give all
|
||||
recipients a copy of this License along with the Program.
|
||||
|
||||
You may charge any price or no price for each copy that you convey,
|
||||
and you may offer support or warranty protection for a fee.
|
||||
|
||||
5. Conveying Modified Source Versions.
|
||||
|
||||
You may convey a work based on the Program, or the modifications to
|
||||
produce it from the Program, in the form of source code under the
|
||||
terms of section 4, provided that you also meet all of these conditions:
|
||||
|
||||
a) The work must carry prominent notices stating that you modified
|
||||
it, and giving a relevant date.
|
||||
|
||||
b) The work must carry prominent notices stating that it is
|
||||
released under this License and any conditions added under section
|
||||
7. This requirement modifies the requirement in section 4 to
|
||||
"keep intact all notices".
|
||||
|
||||
c) You must license the entire work, as a whole, under this
|
||||
License to anyone who comes into possession of a copy. This
|
||||
License will therefore apply, along with any applicable section 7
|
||||
additional terms, to the whole of the work, and all its parts,
|
||||
regardless of how they are packaged. This License gives no
|
||||
permission to license the work in any other way, but it does not
|
||||
invalidate such permission if you have separately received it.
|
||||
|
||||
d) If the work has interactive user interfaces, each must display
|
||||
Appropriate Legal Notices; however, if the Program has interactive
|
||||
interfaces that do not display Appropriate Legal Notices, your
|
||||
work need not make them do so.
|
||||
|
||||
A compilation of a covered work with other separate and independent
|
||||
works, which are not by their nature extensions of the covered work,
|
||||
and which are not combined with it such as to form a larger program,
|
||||
in or on a volume of a storage or distribution medium, is called an
|
||||
"aggregate" if the compilation and its resulting copyright are not
|
||||
used to limit the access or legal rights of the compilation's users
|
||||
beyond what the individual works permit. Inclusion of a covered work
|
||||
in an aggregate does not cause this License to apply to the other
|
||||
parts of the aggregate.
|
||||
|
||||
6. Conveying Non-Source Forms.
|
||||
|
||||
You may convey a covered work in object code form under the terms
|
||||
of sections 4 and 5, provided that you also convey the
|
||||
machine-readable Corresponding Source under the terms of this License,
|
||||
in one of these ways:
|
||||
|
||||
a) Convey the object code in, or embodied in, a physical product
|
||||
(including a physical distribution medium), accompanied by the
|
||||
Corresponding Source fixed on a durable physical medium
|
||||
customarily used for software interchange.
|
||||
|
||||
b) Convey the object code in, or embodied in, a physical product
|
||||
(including a physical distribution medium), accompanied by a
|
||||
written offer, valid for at least three years and valid for as
|
||||
long as you offer spare parts or customer support for that product
|
||||
model, to give anyone who possesses the object code either (1) a
|
||||
copy of the Corresponding Source for all the software in the
|
||||
product that is covered by this License, on a durable physical
|
||||
medium customarily used for software interchange, for a price no
|
||||
more than your reasonable cost of physically performing this
|
||||
conveying of source, or (2) access to copy the
|
||||
Corresponding Source from a network server at no charge.
|
||||
|
||||
c) Convey individual copies of the object code with a copy of the
|
||||
written offer to provide the Corresponding Source. This
|
||||
alternative is allowed only occasionally and noncommercially, and
|
||||
only if you received the object code with such an offer, in accord
|
||||
with subsection 6b.
|
||||
|
||||
d) Convey the object code by offering access from a designated
|
||||
place (gratis or for a charge), and offer equivalent access to the
|
||||
Corresponding Source in the same way through the same place at no
|
||||
further charge. You need not require recipients to copy the
|
||||
Corresponding Source along with the object code. If the place to
|
||||
copy the object code is a network server, the Corresponding Source
|
||||
may be on a different server (operated by you or a third party)
|
||||
that supports equivalent copying facilities, provided you maintain
|
||||
clear directions next to the object code saying where to find the
|
||||
Corresponding Source. Regardless of what server hosts the
|
||||
Corresponding Source, you remain obligated to ensure that it is
|
||||
available for as long as needed to satisfy these requirements.
|
||||
|
||||
e) Convey the object code using peer-to-peer transmission, provided
|
||||
you inform other peers where the object code and Corresponding
|
||||
Source of the work are being offered to the general public at no
|
||||
charge under subsection 6d.
|
||||
|
||||
A separable portion of the object code, whose source code is excluded
|
||||
from the Corresponding Source as a System Library, need not be
|
||||
included in conveying the object code work.
|
||||
|
||||
A "User Product" is either (1) a "consumer product", which means any
|
||||
tangible personal property which is normally used for personal, family,
|
||||
or household purposes, or (2) anything designed or sold for incorporation
|
||||
into a dwelling. In determining whether a product is a consumer product,
|
||||
doubtful cases shall be resolved in favor of coverage. For a particular
|
||||
product received by a particular user, "normally used" refers to a
|
||||
typical or common use of that class of product, regardless of the status
|
||||
of the particular user or of the way in which the particular user
|
||||
actually uses, or expects or is expected to use, the product. A product
|
||||
is a consumer product regardless of whether the product has substantial
|
||||
commercial, industrial or non-consumer uses, unless such uses represent
|
||||
the only significant mode of use of the product.
|
||||
|
||||
"Installation Information" for a User Product means any methods,
|
||||
procedures, authorization keys, or other information required to install
|
||||
and execute modified versions of a covered work in that User Product from
|
||||
a modified version of its Corresponding Source. The information must
|
||||
suffice to ensure that the continued functioning of the modified object
|
||||
code is in no case prevented or interfered with solely because
|
||||
modification has been made.
|
||||
|
||||
If you convey an object code work under this section in, or with, or
|
||||
specifically for use in, a User Product, and the conveying occurs as
|
||||
part of a transaction in which the right of possession and use of the
|
||||
User Product is transferred to the recipient in perpetuity or for a
|
||||
fixed term (regardless of how the transaction is characterized), the
|
||||
Corresponding Source conveyed under this section must be accompanied
|
||||
by the Installation Information. But this requirement does not apply
|
||||
if neither you nor any third party retains the ability to install
|
||||
modified object code on the User Product (for example, the work has
|
||||
been installed in ROM).
|
||||
|
||||
The requirement to provide Installation Information does not include a
|
||||
requirement to continue to provide support service, warranty, or updates
|
||||
for a work that has been modified or installed by the recipient, or for
|
||||
the User Product in which it has been modified or installed. Access to a
|
||||
network may be denied when the modification itself materially and
|
||||
adversely affects the operation of the network or violates the rules and
|
||||
protocols for communication across the network.
|
||||
|
||||
Corresponding Source conveyed, and Installation Information provided,
|
||||
in accord with this section must be in a format that is publicly
|
||||
documented (and with an implementation available to the public in
|
||||
source code form), and must require no special password or key for
|
||||
unpacking, reading or copying.
|
||||
|
||||
7. Additional Terms.
|
||||
|
||||
"Additional permissions" are terms that supplement the terms of this
|
||||
License by making exceptions from one or more of its conditions.
|
||||
Additional permissions that are applicable to the entire Program shall
|
||||
be treated as though they were included in this License, to the extent
|
||||
that they are valid under applicable law. If additional permissions
|
||||
apply only to part of the Program, that part may be used separately
|
||||
under those permissions, but the entire Program remains governed by
|
||||
this License without regard to the additional permissions.
|
||||
|
||||
When you convey a copy of a covered work, you may at your option
|
||||
remove any additional permissions from that copy, or from any part of
|
||||
it. (Additional permissions may be written to require their own
|
||||
removal in certain cases when you modify the work.) You may place
|
||||
additional permissions on material, added by you to a covered work,
|
||||
for which you have or can give appropriate copyright permission.
|
||||
|
||||
Notwithstanding any other provision of this License, for material you
|
||||
add to a covered work, you may (if authorized by the copyright holders of
|
||||
that material) supplement the terms of this License with terms:
|
||||
|
||||
a) Disclaiming warranty or limiting liability differently from the
|
||||
terms of sections 15 and 16 of this License; or
|
||||
|
||||
b) Requiring preservation of specified reasonable legal notices or
|
||||
author attributions in that material or in the Appropriate Legal
|
||||
Notices displayed by works containing it; or
|
||||
|
||||
c) Prohibiting misrepresentation of the origin of that material, or
|
||||
requiring that modified versions of such material be marked in
|
||||
reasonable ways as different from the original version; or
|
||||
|
||||
d) Limiting the use for publicity purposes of names of licensors or
|
||||
authors of the material; or
|
||||
|
||||
e) Declining to grant rights under trademark law for use of some
|
||||
trade names, trademarks, or service marks; or
|
||||
|
||||
f) Requiring indemnification of licensors and authors of that
|
||||
material by anyone who conveys the material (or modified versions of
|
||||
it) with contractual assumptions of liability to the recipient, for
|
||||
any liability that these contractual assumptions directly impose on
|
||||
those licensors and authors.
|
||||
|
||||
All other non-permissive additional terms are considered "further
|
||||
restrictions" within the meaning of section 10. If the Program as you
|
||||
received it, or any part of it, contains a notice stating that it is
|
||||
governed by this License along with a term that is a further
|
||||
restriction, you may remove that term. If a license document contains
|
||||
a further restriction but permits relicensing or conveying under this
|
||||
License, you may add to a covered work material governed by the terms
|
||||
of that license document, provided that the further restriction does
|
||||
not survive such relicensing or conveying.
|
||||
|
||||
If you add terms to a covered work in accord with this section, you
|
||||
must place, in the relevant source files, a statement of the
|
||||
additional terms that apply to those files, or a notice indicating
|
||||
where to find the applicable terms.
|
||||
|
||||
Additional terms, permissive or non-permissive, may be stated in the
|
||||
form of a separately written license, or stated as exceptions;
|
||||
the above requirements apply either way.
|
||||
|
||||
8. Termination.
|
||||
|
||||
You may not propagate or modify a covered work except as expressly
|
||||
provided under this License. Any attempt otherwise to propagate or
|
||||
modify it is void, and will automatically terminate your rights under
|
||||
this License (including any patent licenses granted under the third
|
||||
paragraph of section 11).
|
||||
|
||||
However, if you cease all violation of this License, then your
|
||||
license from a particular copyright holder is reinstated (a)
|
||||
provisionally, unless and until the copyright holder explicitly and
|
||||
finally terminates your license, and (b) permanently, if the copyright
|
||||
holder fails to notify you of the violation by some reasonable means
|
||||
prior to 60 days after the cessation.
|
||||
|
||||
Moreover, your license from a particular copyright holder is
|
||||
reinstated permanently if the copyright holder notifies you of the
|
||||
violation by some reasonable means, this is the first time you have
|
||||
received notice of violation of this License (for any work) from that
|
||||
copyright holder, and you cure the violation prior to 30 days after
|
||||
your receipt of the notice.
|
||||
|
||||
Termination of your rights under this section does not terminate the
|
||||
licenses of parties who have received copies or rights from you under
|
||||
this License. If your rights have been terminated and not permanently
|
||||
reinstated, you do not qualify to receive new licenses for the same
|
||||
material under section 10.
|
||||
|
||||
9. Acceptance Not Required for Having Copies.
|
||||
|
||||
You are not required to accept this License in order to receive or
|
||||
run a copy of the Program. Ancillary propagation of a covered work
|
||||
occurring solely as a consequence of using peer-to-peer transmission
|
||||
to receive a copy likewise does not require acceptance. However,
|
||||
nothing other than this License grants you permission to propagate or
|
||||
modify any covered work. These actions infringe copyright if you do
|
||||
not accept this License. Therefore, by modifying or propagating a
|
||||
covered work, you indicate your acceptance of this License to do so.
|
||||
|
||||
10. Automatic Licensing of Downstream Recipients.
|
||||
|
||||
Each time you convey a covered work, the recipient automatically
|
||||
receives a license from the original licensors, to run, modify and
|
||||
propagate that work, subject to this License. You are not responsible
|
||||
for enforcing compliance by third parties with this License.
|
||||
|
||||
An "entity transaction" is a transaction transferring control of an
|
||||
organization, or substantially all assets of one, or subdividing an
|
||||
organization, or merging organizations. If propagation of a covered
|
||||
work results from an entity transaction, each party to that
|
||||
transaction who receives a copy of the work also receives whatever
|
||||
licenses to the work the party's predecessor in interest had or could
|
||||
give under the previous paragraph, plus a right to possession of the
|
||||
Corresponding Source of the work from the predecessor in interest, if
|
||||
the predecessor has it or can get it with reasonable efforts.
|
||||
|
||||
You may not impose any further restrictions on the exercise of the
|
||||
rights granted or affirmed under this License. For example, you may
|
||||
not impose a license fee, royalty, or other charge for exercise of
|
||||
rights granted under this License, and you may not initiate litigation
|
||||
(including a cross-claim or counterclaim in a lawsuit) alleging that
|
||||
any patent claim is infringed by making, using, selling, offering for
|
||||
sale, or importing the Program or any portion of it.
|
||||
|
||||
11. Patents.
|
||||
|
||||
A "contributor" is a copyright holder who authorizes use under this
|
||||
License of the Program or a work on which the Program is based. The
|
||||
work thus licensed is called the contributor's "contributor version".
|
||||
|
||||
A contributor's "essential patent claims" are all patent claims
|
||||
owned or controlled by the contributor, whether already acquired or
|
||||
hereafter acquired, that would be infringed by some manner, permitted
|
||||
by this License, of making, using, or selling its contributor version,
|
||||
but do not include claims that would be infringed only as a
|
||||
consequence of further modification of the contributor version. For
|
||||
purposes of this definition, "control" includes the right to grant
|
||||
patent sublicenses in a manner consistent with the requirements of
|
||||
this License.
|
||||
|
||||
Each contributor grants you a non-exclusive, worldwide, royalty-free
|
||||
patent license under the contributor's essential patent claims, to
|
||||
make, use, sell, offer for sale, import and otherwise run, modify and
|
||||
propagate the contents of its contributor version.
|
||||
|
||||
In the following three paragraphs, a "patent license" is any express
|
||||
agreement or commitment, however denominated, not to enforce a patent
|
||||
(such as an express permission to practice a patent or covenant not to
|
||||
sue for patent infringement). To "grant" such a patent license to a
|
||||
party means to make such an agreement or commitment not to enforce a
|
||||
patent against the party.
|
||||
|
||||
If you convey a covered work, knowingly relying on a patent license,
|
||||
and the Corresponding Source of the work is not available for anyone
|
||||
to copy, free of charge and under the terms of this License, through a
|
||||
publicly available network server or other readily accessible means,
|
||||
then you must either (1) cause the Corresponding Source to be so
|
||||
available, or (2) arrange to deprive yourself of the benefit of the
|
||||
patent license for this particular work, or (3) arrange, in a manner
|
||||
consistent with the requirements of this License, to extend the patent
|
||||
license to downstream recipients. "Knowingly relying" means you have
|
||||
actual knowledge that, but for the patent license, your conveying the
|
||||
covered work in a country, or your recipient's use of the covered work
|
||||
in a country, would infringe one or more identifiable patents in that
|
||||
country that you have reason to believe are valid.
|
||||
|
||||
If, pursuant to or in connection with a single transaction or
|
||||
arrangement, you convey, or propagate by procuring conveyance of, a
|
||||
covered work, and grant a patent license to some of the parties
|
||||
receiving the covered work authorizing them to use, propagate, modify
|
||||
or convey a specific copy of the covered work, then the patent license
|
||||
you grant is automatically extended to all recipients of the covered
|
||||
work and works based on it.
|
||||
|
||||
A patent license is "discriminatory" if it does not include within
|
||||
the scope of its coverage, prohibits the exercise of, or is
|
||||
conditioned on the non-exercise of one or more of the rights that are
|
||||
specifically granted under this License. You may not convey a covered
|
||||
work if you are a party to an arrangement with a third party that is
|
||||
in the business of distributing software, under which you make payment
|
||||
to the third party based on the extent of your activity of conveying
|
||||
the work, and under which the third party grants, to any of the
|
||||
parties who would receive the covered work from you, a discriminatory
|
||||
patent license (a) in connection with copies of the covered work
|
||||
conveyed by you (or copies made from those copies), or (b) primarily
|
||||
for and in connection with specific products or compilations that
|
||||
contain the covered work, unless you entered into that arrangement,
|
||||
or that patent license was granted, prior to 28 March 2007.
|
||||
|
||||
Nothing in this License shall be construed as excluding or limiting
|
||||
any implied license or other defenses to infringement that may
|
||||
otherwise be available to you under applicable patent law.
|
||||
|
||||
12. No Surrender of Others' Freedom.
|
||||
|
||||
If conditions are imposed on you (whether by court order, agreement or
|
||||
otherwise) that contradict the conditions of this License, they do not
|
||||
excuse you from the conditions of this License. If you cannot convey a
|
||||
covered work so as to satisfy simultaneously your obligations under this
|
||||
License and any other pertinent obligations, then as a consequence you may
|
||||
not convey it at all. For example, if you agree to terms that obligate you
|
||||
to collect a royalty for further conveying from those to whom you convey
|
||||
the Program, the only way you could satisfy both those terms and this
|
||||
License would be to refrain entirely from conveying the Program.
|
||||
|
||||
13. Use with the GNU Affero General Public License.
|
||||
|
||||
Notwithstanding any other provision of this License, you have
|
||||
permission to link or combine any covered work with a work licensed
|
||||
under version 3 of the GNU Affero General Public License into a single
|
||||
combined work, and to convey the resulting work. The terms of this
|
||||
License will continue to apply to the part which is the covered work,
|
||||
but the special requirements of the GNU Affero General Public License,
|
||||
section 13, concerning interaction through a network will apply to the
|
||||
combination as such.
|
||||
|
||||
14. Revised Versions of this License.
|
||||
|
||||
The Free Software Foundation may publish revised and/or new versions of
|
||||
the GNU General Public License from time to time. Such new versions will
|
||||
be similar in spirit to the present version, but may differ in detail to
|
||||
address new problems or concerns.
|
||||
|
||||
Each version is given a distinguishing version number. If the
|
||||
Program specifies that a certain numbered version of the GNU General
|
||||
Public License "or any later version" applies to it, you have the
|
||||
option of following the terms and conditions either of that numbered
|
||||
version or of any later version published by the Free Software
|
||||
Foundation. If the Program does not specify a version number of the
|
||||
GNU General Public License, you may choose any version ever published
|
||||
by the Free Software Foundation.
|
||||
|
||||
If the Program specifies that a proxy can decide which future
|
||||
versions of the GNU General Public License can be used, that proxy's
|
||||
public statement of acceptance of a version permanently authorizes you
|
||||
to choose that version for the Program.
|
||||
|
||||
Later license versions may give you additional or different
|
||||
permissions. However, no additional obligations are imposed on any
|
||||
author or copyright holder as a result of your choosing to follow a
|
||||
later version.
|
||||
|
||||
15. Disclaimer of Warranty.
|
||||
|
||||
THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
|
||||
APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
|
||||
HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
|
||||
OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
|
||||
THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
|
||||
PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
|
||||
IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
|
||||
ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
|
||||
|
||||
16. Limitation of Liability.
|
||||
|
||||
IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
|
||||
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
|
||||
THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
|
||||
GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
|
||||
USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
|
||||
DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
|
||||
PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
|
||||
EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
|
||||
SUCH DAMAGES.
|
||||
|
||||
17. Interpretation of Sections 15 and 16.
|
||||
|
||||
If the disclaimer of warranty and limitation of liability provided
|
||||
above cannot be given local legal effect according to their terms,
|
||||
reviewing courts shall apply local law that most closely approximates
|
||||
an absolute waiver of all civil liability in connection with the
|
||||
Program, unless a warranty or assumption of liability accompanies a
|
||||
copy of the Program in return for a fee.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
How to Apply These Terms to Your New Programs
|
||||
|
||||
If you develop a new program, and you want it to be of the greatest
|
||||
possible use to the public, the best way to achieve this is to make it
|
||||
free software which everyone can redistribute and change under these terms.
|
||||
|
||||
To do so, attach the following notices to the program. It is safest
|
||||
to attach them to the start of each source file to most effectively
|
||||
state the exclusion of warranty; and each file should have at least
|
||||
the "copyright" line and a pointer to where the full notice is found.
|
||||
|
||||
<one line to give the program's name and a brief idea of what it does.>
|
||||
Copyright (C) <year> <name of author>
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
Also add information on how to contact you by electronic and paper mail.
|
||||
|
||||
If the program does terminal interaction, make it output a short
|
||||
notice like this when it starts in an interactive mode:
|
||||
|
||||
<program> Copyright (C) <year> <name of author>
|
||||
This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
|
||||
This is free software, and you are welcome to redistribute it
|
||||
under certain conditions; type `show c' for details.
|
||||
|
||||
The hypothetical commands `show w' and `show c' should show the appropriate
|
||||
parts of the General Public License. Of course, your program's commands
|
||||
might be different; for a GUI interface, you would use an "about box".
|
||||
|
||||
You should also get your employer (if you work as a programmer) or school,
|
||||
if any, to sign a "copyright disclaimer" for the program, if necessary.
|
||||
For more information on this, and how to apply and follow the GNU GPL, see
|
||||
<https://www.gnu.org/licenses/>.
|
||||
|
||||
The GNU General Public License does not permit incorporating your program
|
||||
into proprietary programs. If your program is a subroutine library, you
|
||||
may consider it more useful to permit linking proprietary applications with
|
||||
the library. If this is what you want to do, use the GNU Lesser General
|
||||
Public License instead of this License. But first, please read
|
||||
<https://www.gnu.org/licenses/why-not-lgpl.html>.
|
||||
@@ -1,90 +1,195 @@
|
||||
# Btranslate
|
||||
# HTBD: A Free and Open-Source WordPress Multilingual Plugin Powered by the Baidu Translate API
|
||||
|
||||
[中文](README.zh-CN.md)|English
|
||||
[](https://github.com/hanyixuanten/HTBD)
|
||||
|
||||
Btranslate is a WordPress plugin skeleton for persistent multilingual content using the Baidu Translate API.
|
||||
HTBD is a multilingual translation plugin for WordPress. It uses the Baidu Translate API to translate posts, pages, and other website content into different languages, with a dedicated URL for each translated version.
|
||||
|
||||
## Requirements
|
||||
Translations are stored on the server in `{$wpdb->prefix}hyx_bd_translations`. Unless a user explicitly refreshes a translation, the same content is translated only once for each target language. Existing `{$wpdb->prefix}btranslate_translations` data is migrated when the renamed plugin is activated. This avoids calling the translation API on every page visit, reducing API usage and improving page load performance.
|
||||
|
||||
- WordPress 6.4 or later.
|
||||
- PHP 8.1 or later.
|
||||
- A Baidu Translate application ID and secret key.
|
||||
- WordPress cron enabled, or a real server cron that calls WordPress cron.
|
||||
## Project Links
|
||||
|
||||
## Installation
|
||||
- Plugin homepage: [https://www.vblg.top/index.php/archives/147](https://www.vblg.top/index.php/archives/147)
|
||||
- GitHub repository: [https://github.com/hanyixuanten/HTBD](https://github.com/hanyixuanten/HTBD)
|
||||
- WordPress plugin page: [https://wordpress.org/plugins/hyx-translator-for-baidu-translate/](https://wordpress.org/plugins/hyx-translator-for-baidu-translate/)
|
||||
|
||||
1. Copy this directory to `wp-content/plugins/wp-btranslate`.
|
||||
2. Activate **Btranslate** in WordPress Admin.
|
||||
3. Open **Settings > Btranslate**.
|
||||
4. Enter the Baidu credentials, source language, target-language codes, and URL routing mode.
|
||||
5. Save a post or page to schedule translation jobs for every configured target language.
|
||||
> **Review status:** HTBD has not yet been approved for the WordPress.org Plugin Directory, so it cannot currently be installed directly from the WordPress admin plugin marketplace. For now, download it from GitHub and install it manually.
|
||||
|
||||
## Translation Lifecycle
|
||||
## Key Features
|
||||
|
||||
Translations are generated by the `wpt_process_translation` cron task after a post or page is saved. For content published before the plugin was activated, open **Settings > Btranslate** and use **Translate existing posts and pages** to schedule all published posts and pages. A translation has a deterministic identity based on the source fingerprint, source language, target language, field context, and provider version. The plugin first looks up a completed result in the `{$wpdb->prefix}wpt_translations` table. Only a cache miss causes a request to Baidu.
|
||||
### Baidu Translate API Integration
|
||||
|
||||
Changing source text produces a new fingerprint and therefore schedules a new translation without deleting a valid prior translation. Front-end rendering never contacts Baidu; it reads a completed value from the table and otherwise falls back to the source value. WordPress cron must run after queuing content; production sites should configure a real server cron to call WordPress cron regularly.
|
||||
HTBD uses the translation API provided by the Baidu Translate Open Platform. Users can configure their own App ID and secret key in WordPress Admin, and the plugin handles request signing, API calls, and error handling.
|
||||
|
||||
## Supported Content
|
||||
### Post and Page Translation
|
||||
|
||||
- Post and page titles, content, and excerpts.
|
||||
- Attached image `alt` text.
|
||||
- Assigned categories and tags, including their descriptions.
|
||||
- Yoast SEO title and description fields.
|
||||
- Rank Math title and description fields.
|
||||
- All in One SEO and SEOPress title and description output filters.
|
||||
- WordPress AI plugin fallback meta description (`wpai_meta_description`).
|
||||
The plugin supports translating the main content of WordPress posts and pages, including:
|
||||
|
||||
Only the listed SEO meta fields and the generated title/description output from supported SEO plugins are sent for translation. Browser titles generated through the WordPress document-title API use the persisted translated post title. Arbitrary metadata, serialized data, credentials, and code are excluded.
|
||||
- Titles
|
||||
- Body content
|
||||
- Excerpts
|
||||
- SEO fields
|
||||
- Image alternative text
|
||||
- Tags
|
||||
- Categories
|
||||
- Selected interface text
|
||||
|
||||
When post content contains HTML links, the plugin preserves each link's `href` and attributes, translates its visible text as a separate value, and inserts that translated text back into the original link. Re-run a post translation after upgrading to regenerate previously translated content that contains links.
|
||||
### Multilingual URLs
|
||||
|
||||
Category and tag names are translated when they are created, edited, or included in a full retranslation. On a translated site, term objects used by archives, navigation, and post metadata use the persisted translated name and description.
|
||||
HTBD can generate dedicated URLs for different languages, with support for:
|
||||
|
||||
## Routing
|
||||
- Language subdirectories such as `/en/` and `/jp/`
|
||||
- A separate domain for each language
|
||||
- Language-specific permalinks based on the configured routing mode
|
||||
|
||||
The **Subdirectory** mode resolves configured language paths such as `/en/example-post/`. The **Domain binding** mode maps a request host to a language using one `domain=language` entry per line in Settings. Use one canonical strategy for each language to avoid duplicate URLs. In domain mode, the plugin does not register front-end rewrite rules, so the primary site URL is not affected.
|
||||
### Persistent Translations
|
||||
|
||||
Post, page, term, and home links are generated for the active language. For example, a theme's "back to home" link that uses `home_url()` resolves to the bound language domain or language subdirectory.
|
||||
After a translation is generated, it is stored on the WordPress server. Future requests for the same translation use the stored result instead of calling the Baidu Translate API again.
|
||||
|
||||
For a bound subdomain, enter a value such as `en.example.com=en`. The server and DNS must route that hostname to the same WordPress installation before WordPress can resolve the language.
|
||||
When the source content changes, the corresponding translation is marked as requiring an update, preventing an outdated translation from being used for changed content.
|
||||
|
||||
After changing routing settings, rewrite rules are refreshed. The initial implementation does not yet generate translated permalinks, canonical URLs, `hreflang` alternates, redirects, or sitemap entries.
|
||||
### WordPress Content Compatibility
|
||||
|
||||
## Security And Data Retention
|
||||
When translating content, the plugin makes every effort to preserve:
|
||||
|
||||
Credentials are stored in the WordPress `wpt_settings` option and are not rendered on the front end. The plugin stores the translation identity, language pair, field context, source fingerprint, translated value, status, and timestamps. It does not store full Baidu response payloads.
|
||||
- Gutenberg block markup
|
||||
- HTML structure
|
||||
- Shortcodes
|
||||
- Placeholders
|
||||
- URLs
|
||||
- Content inside `<pre>` and `<code>` elements, which is left untranslated
|
||||
- Protected special content
|
||||
|
||||
The **Log every Baidu translation request** setting is disabled by default. When enabled, each request writes safe metadata to the PHP error log: language pair, field context, source fingerprint and length, result status, and an error code when applicable. Credentials, signatures, source text, translated text, and complete API responses are never logged.
|
||||
### Multilingual SEO
|
||||
|
||||
Use HTTPS and restrict database access appropriately because WordPress options are database-backed. Production sites should configure a real cron runner for reliable translation processing.
|
||||
HTBD provides essential SEO support for multilingual pages, including:
|
||||
|
||||
## Uninstalling
|
||||
- Multilingual permalinks
|
||||
- Canonical URLs
|
||||
- `hreflang` alternate links
|
||||
- Sitemap compatibility
|
||||
- SEO field translation
|
||||
|
||||
Uninstalling Btranslate permanently deletes its settings and credentials, retranslation batch state, all pending Btranslate cron events, and every site's `{$wpdb->prefix}wpt_translations` table. This removes all persisted translations. Deactivating the plugin does not delete these records.
|
||||
### Failure Fallback
|
||||
|
||||
## Development
|
||||
If a Baidu Translate API request fails, the plugin does not cause the website page to fail:
|
||||
|
||||
The repository has no Composer or PHPUnit dependency. Validate PHP syntax with the PHP executable available on your system:
|
||||
- If no translation is available, the website's source-language content is displayed.
|
||||
- If a previously saved successful translation exists, the previous translation continues to be used.
|
||||
|
||||
```sh
|
||||
find . -path './.git' -prune -o -type f -name '*.php' -print0 | xargs -0 -n1 php -l
|
||||
## Quick Start
|
||||
|
||||
### 1. Get Access to the Baidu Translate API
|
||||
|
||||
1. Open the [Baidu Translate Open Platform](https://fanyi-api.baidu.com/).
|
||||
2. Register or sign in with a Baidu account.
|
||||
3. Open the management console.
|
||||
4. Activate the General Text Translation API service.
|
||||
5. Create an application and enter the required application information.
|
||||
6. Obtain the **APP ID** and **secret key** from the application management page.
|
||||
|
||||
> Your APP ID and secret key are sensitive credentials. Do not commit them to a public GitHub repository or expose them in front-end code or public logs.
|
||||
|
||||
For activation instructions, pricing, and usage limits, see the [official Baidu Translate API documentation](https://fanyi-api.baidu.com/doc/23).
|
||||
|
||||
### 2. Install the Plugin
|
||||
|
||||
Because the plugin has not yet been approved for the WordPress.org Plugin Directory, it must currently be installed manually from GitHub.
|
||||
|
||||
#### Method 1: Upload the ZIP Package
|
||||
|
||||
1. Visit the [HTBD GitHub repository](https://github.com/hanyixuanten/HTBD).
|
||||
2. Select **Releases**.
|
||||
3. Download the **hyx-translator-for-baidu-translate-*.zip** file from the latest release.
|
||||
4. Sign in to WordPress Admin.
|
||||
5. Open **Plugins > Add New Plugin**.
|
||||
6. Select **Upload Plugin**.
|
||||
7. Choose the downloaded ZIP file and start the installation.
|
||||
8. Activate HTBD after installation is complete.
|
||||
|
||||
#### Method 2: Upload to the Server
|
||||
|
||||
1. Visit the [HTBD GitHub repository](https://github.com/hanyixuanten/HTBD).
|
||||
2. Select **Releases**.
|
||||
3. Download the **hyx-translator-for-baidu-translate-*.zip** file from the latest release.
|
||||
4. Upload the `hyx-translator-for-baidu-translate` directory to the WordPress plugin directory: `wp-content/plugins/hyx-translator-for-baidu-translate/`.
|
||||
5. Sign in to WordPress Admin.
|
||||
6. Open **Plugins > Installed Plugins**.
|
||||
7. Find HTBD and select **Activate**.
|
||||
|
||||
### 3. Configure the Plugin
|
||||
|
||||
1. Sign in to WordPress Admin.
|
||||
2. Open the HTBD settings page.
|
||||
3. Enter the **APP ID** and **secret key** provided by the Baidu Translate Open Platform.
|
||||
4. Set the website's source language, or enable **Automatically detect the source language** to use Baidu's `auto` source language mode.
|
||||
5. Add the target languages you want to enable.
|
||||
6. Select a language URL mode, such as language subdirectories or domain binding.
|
||||
7. Save the settings.
|
||||
|
||||
After changing language routing settings, open **Settings > Permalinks** in WordPress Admin and confirm that the permalink configuration is working correctly.
|
||||
|
||||
Automatic detection is stored as a separate setting. Entering `auto` in the source language field without enabling automatic detection remains a distinct configuration state.
|
||||
|
||||
### 4. Start Translating
|
||||
|
||||
1. Open the post or page you want to translate in WordPress Admin.
|
||||
2. Save or update the content.
|
||||
3. Use the translation feature provided by HTBD to generate translations for the target languages.
|
||||
4. Wait for the translation tasks to finish.
|
||||
5. Check each translation at its language-specific URL.
|
||||
|
||||
For example, suppose the source-language page is available at:
|
||||
|
||||
```txt
|
||||
https://example.com/about/
|
||||
```
|
||||
|
||||
Before production use, test the plugin in a WordPress staging environment. Cover language resolution, rewrite behavior, persistence reuse and invalidation, Baidu request signing and errors, content filtering, and fallback rendering. Mock `wp_remote_post` responses for any automated tests so they do not need live credentials, a database, or outbound network access.
|
||||
When English uses the `/en/` subdirectory, its translated URL is:
|
||||
|
||||
## Retranslation
|
||||
```txt
|
||||
https://example.com/en/about/
|
||||
```
|
||||
|
||||
The Posts and Pages lists show a status column for every target language, including the latest successful translation time and a translation action. The Settings page provides **重新翻译所有内容**, **翻译所有文章**, and **翻译所有分类标签**. Each action shows a confirmation before scheduling a forced refresh of all supported content, only published posts and pages, or only categories and tags; this bypasses persisted translations and consumes Baidu API quota.
|
||||
When Japanese uses the `/jp/` subdirectory, its translated URL is:
|
||||
|
||||
The Settings page also shows a live progress panel. It refreshes every five seconds and reports the latest translation task: full translation, post/page translation, category/tag translation, or a single-post translation. It counts only completed items that belong to that task's queued content and target languages after the task began, so historical translations cannot make a new task appear complete. Forced retranslation jobs begin immediately and are scheduled two seconds apart, so a single WP-Cron request processes at most one task item instead of timing out while processing every post. Progress reads only local WordPress and translation-table data; it does not send additional requests to Baidu.
|
||||
```txt
|
||||
https://example.com/jp/about/
|
||||
```
|
||||
|
||||
The **清除已翻译的缓存** action deletes every persisted translation, cancels pending translation jobs, and resets batch progress. The front end then falls back to source content until translations are explicitly queued again; regenerating them consumes Baidu API quota.
|
||||
Requests for `wp-login.php` or `wp-admin` through a target-language subdirectory or bound domain are redirected to the corresponding source-site URL. Query parameters are preserved.
|
||||
|
||||
## Known Limitations
|
||||
The plugin prioritizes saved translations. It calls the translation API again only when the source content changes, a translation becomes invalid, or a user explicitly requests a refresh.
|
||||
|
||||
- The settings UI accepts language codes but does not yet validate them against a maintained Baidu language-code list.
|
||||
- API calls translate raw WordPress content. HTML, blocks, shortcodes, placeholders, and URLs need a protected-token pipeline before production use.
|
||||
- Taxonomy and SEO values are queued but the current rendering filters cover only a subset of SEO plugins and term views.
|
||||
- Domain binding assumes the WordPress site and server virtual hosts are already configured for every bound domain.
|
||||
- Translation failures are stored as failed attempts and render the source value; retry management and an admin job monitor are not implemented yet.
|
||||
## Recommendations
|
||||
|
||||
- Test permalinks and multilingual routing in a staging environment before enabling the plugin in production.
|
||||
- Set appropriate usage quotas and security policies for the Baidu Translate API.
|
||||
- Do not store API credentials in source code, front-end JavaScript, or public logs.
|
||||
- Manually review brand names, technical terminology, and important pages before publishing translations.
|
||||
- After changing source content, check whether the corresponding translations need to be updated.
|
||||
- When configuring domain binding, also configure DNS, HTTPS certificates, and the server's domain settings.
|
||||
- After changing language routing, verify that canonical URLs, `hreflang` links, and sitemaps are correct.
|
||||
|
||||
## System Requirements
|
||||
|
||||
- WordPress 6.4 or later
|
||||
- PHP 8.1 or later
|
||||
- A server environment that can connect to the Baidu Translate API
|
||||
- A valid Baidu Translate Open Platform APP ID and secret key
|
||||
|
||||
## Feedback and Contributions
|
||||
|
||||
To report a problem or suggest a feature, visit the GitHub repository:
|
||||
|
||||
[https://github.com/hanyixuanten/HTBD](https://github.com/hanyixuanten/HTBD)
|
||||
|
||||
When opening an issue, consider including the following information:
|
||||
|
||||
- WordPress version
|
||||
- PHP version
|
||||
- HTBD version
|
||||
- Languages and routing mode in use
|
||||
- Steps to reproduce the problem
|
||||
- Error logs with sensitive information removed
|
||||
|
||||
Contributions through issues and pull requests are welcome.
|
||||
|
||||
+159
-53
@@ -1,89 +1,195 @@
|
||||
# Btranslate
|
||||
# HTBD:基于百度翻译 API 的免费开源 Wordpress 多语言插件
|
||||
|
||||
中文|[English](README.md)
|
||||
[](https://github.com/hanyixuanten/HTBD)
|
||||
|
||||
Btranslate 是一个 WordPress 插件骨架,使用百度翻译 API 持久化管理多语言内容。
|
||||
HTBD 是一款 WordPress 多语言翻译插件,通过百度翻译开放平台 API,将网站中的文章、页面及其他内容翻译为不同语言,并为译文生成独立的语言访问地址。
|
||||
|
||||
## 要求
|
||||
插件会将翻译结果保存在服务器的 `{$wpdb->prefix}hyx_bd_translations` 表中。除非用户主动刷新翻译,否则同一内容在同一目标语言下只需翻译一次。启用改名后的插件时,已有 `{$wpdb->prefix}btranslate_translations` 数据会自动迁移,无需在每次访问页面时重复调用翻译 API,有助于减少 API 请求量并提高页面加载速度。
|
||||
|
||||
- WordPress 6.4 或更高版本。
|
||||
- PHP 8.1 或更高版本。
|
||||
- 百度翻译应用 ID 和密钥。
|
||||
- 已启用 WordPress Cron,或可调用 WordPress Cron 的服务器定时任务。
|
||||
## 项目地址
|
||||
|
||||
## 安装
|
||||
- 插件主页:[https://www.vblg.top/index.php/archives/147](https://www.vblg.top/index.php/archives/147)
|
||||
- GitHub 仓库:[https://github.com/hanyixuanten/HTBD](https://github.com/hanyixuanten/HTBD)
|
||||
- WordPress 插件主页:[https://wordpress.org/plugins/hyx-translator-for-baidu-translate/](https://wordpress.org/plugins/hyx-translator-for-baidu-translate/)
|
||||
|
||||
1. 将此目录复制到 `wp-content/plugins/wp-btranslate`。
|
||||
2. 在 WordPress 后台启用 **Btranslate**。
|
||||
3. 打开 **设置 > Btranslate**。
|
||||
4. 填写百度翻译凭据、源语言、目标语言代码和 URL 路由模式。
|
||||
5. 保存文章或页面,为每种已配置的目标语言安排翻译任务。
|
||||
> **审核状态:** HTBD 目前尚未通过 WordPress.org 插件目录审核,因此暂时无法直接通过 WordPress 后台插件市场安装。现阶段请从 GitHub 仓库下载并手动安装。
|
||||
|
||||
## 翻译生命周期
|
||||
## 主要功能
|
||||
|
||||
保存文章或页面后,`wpt_process_translation` Cron 任务会生成翻译。对于插件启用前已发布的内容,请打开 **设置 > Btranslate**,使用 **翻译现有文章和页面** 为全部已发布文章和页面安排任务。翻译具有确定性标识,标识由源内容指纹、源语言、目标语言、字段上下文和提供程序版本构成。插件会先在 `{$wpdb->prefix}wpt_translations` 表中查找已完成的结果;只有缓存未命中时才会向百度发起请求。
|
||||
### 百度翻译 API 集成
|
||||
|
||||
源文本变更会生成新的指纹,因此会安排新的翻译任务,而不会删除此前有效的翻译。前端渲染不会请求百度 API;它只读取表中已完成的值,若不存在则回退显示源文本。内容入队后必须运行 WordPress Cron;生产环境应配置真实的服务器 Cron,定期调用 WordPress Cron。
|
||||
HTBD 使用百度翻译开放平台提供的翻译 API。用户可以在 WordPress 后台配置自己的 App ID 和密钥,由插件完成请求签名、翻译调用及错误处理。
|
||||
|
||||
## 支持的内容
|
||||
### 文章和页面翻译
|
||||
|
||||
- 文章和页面的标题、正文及摘要。
|
||||
- 附件图片的 `alt` 文本。
|
||||
- 已分配的分类和标签,包括其描述。
|
||||
- Yoast SEO 的标题和描述字段。
|
||||
- Rank Math 的标题和描述字段。
|
||||
- All in One SEO 与 SEOPress 的标题和描述输出过滤器。
|
||||
插件支持翻译 WordPress 文章与页面中的主要内容,包括:
|
||||
|
||||
只有列出的 SEO 元字段,以及受支持 SEO 插件生成的标题/描述输出会被发送翻译。通过 WordPress 文档标题 API 生成的浏览器标题会使用已持久化的文章标题翻译。任意元数据、序列化数据、凭据和代码均会被排除。
|
||||
- 标题
|
||||
- 正文
|
||||
- 摘要
|
||||
- SEO 字段
|
||||
- 图片替代文本
|
||||
- 标签
|
||||
- 分类目录
|
||||
- 部分界面文本
|
||||
|
||||
当文章内容包含 HTML 链接时,插件会保留每个链接的 `href` 和属性,将可见文本作为独立值翻译,再把翻译后的文本写回原始链接。升级后,请重新翻译包含链接的文章以重新生成内容。
|
||||
### 多语言访问地址
|
||||
|
||||
分类和标签名称会在创建、编辑或完整重新翻译时进行翻译。在翻译站点上,用于归档、导航和文章元数据的术语对象会使用已持久化的翻译名称和描述。
|
||||
HTBD 可以为不同语言生成独立的访问地址,支持:
|
||||
|
||||
## 路由
|
||||
- 语言子目录,例如 `/en/`、`/jp/`
|
||||
- 不同语言绑定不同域名
|
||||
- 根据配置生成对应语言的固定链接
|
||||
|
||||
**子目录** 模式会解析配置语言对应的路径,例如 `/en/example-post/`。**域名绑定** 模式通过主机名映射语言:在设置中每行填写一条 `domain=language`。每种语言应只使用一种规范 URL 策略,以避免重复内容。在域名模式下,插件不会注册前端重写规则,因此不会影响主站 URL。
|
||||
### 翻译结果持久化
|
||||
|
||||
文章、页面、术语和首页链接会按当前语言生成。例如,主题中使用 `home_url()` 的“返回首页”链接会解析为已绑定语言的域名或语言子目录。
|
||||
翻译完成后,结果会保存在 WordPress 服务器中。后续访问相同译文时,插件优先读取已经保存的结果,而不是重新请求百度翻译 API。
|
||||
|
||||
对于已绑定的子域名,请填写类似 `en.example.com=en` 的值。服务器和 DNS 必须先将该主机名指向同一 WordPress 安装,WordPress 才能解析该语言。
|
||||
当原文发生变化时,对应译文会被标记为需要更新,从而避免继续使用与原文不一致的旧译文。
|
||||
|
||||
路由设置变更后会刷新重写规则。初始实现尚不生成翻译固定链接、规范 URL、`hreflang` 替代链接、重定向或站点地图条目。
|
||||
### WordPress 内容兼容
|
||||
|
||||
## 安全和数据保留
|
||||
插件在翻译内容时会尽量保留:
|
||||
|
||||
凭据保存在 WordPress 的 `wpt_settings` 选项中,不会在前端输出。插件会存储翻译标识、语言对、字段上下文、源内容指纹、翻译值、状态和时间戳;不会保存完整的百度响应负载。
|
||||
- Gutenberg 区块标记
|
||||
- HTML 结构
|
||||
- Shortcode 短代码
|
||||
- 占位符
|
||||
- URL
|
||||
- `<pre>` 和 `<code>` 元素内的代码内容(不翻译)
|
||||
- 受保护的特殊内容
|
||||
|
||||
**记录每次百度翻译请求** 默认关闭。开启后,每次请求都会向 PHP 错误日志写入安全元数据:语言对、字段上下文、源文本指纹和长度、结果状态以及适用时的错误码。日志绝不会包含凭据、签名、原文、译文或完整 API 响应。
|
||||
### 多语言 SEO
|
||||
|
||||
由于 WordPress 选项由数据库保存,请使用 HTTPS 并适当限制数据库访问权限。生产站点应配置真实的 Cron 执行器,确保翻译任务可靠运行。
|
||||
HTBD 为多语言页面提供必要的 SEO 支持,包括:
|
||||
|
||||
## 卸载
|
||||
- 多语言固定链接
|
||||
- Canonical URL
|
||||
- `hreflang` 多语言链接
|
||||
- 站点地图兼容
|
||||
- SEO 字段翻译
|
||||
|
||||
卸载 Btranslate 会永久删除其设置和凭据、重新翻译批处理状态、所有待执行的 Btranslate Cron 事件,以及每个站点的 `{$wpdb->prefix}wpt_translations` 表。这会删除所有已持久化的翻译。停用插件不会删除这些记录。
|
||||
### 失败回退
|
||||
|
||||
## 开发
|
||||
如果百度翻译 API 请求失败,插件不会使网站页面报错:
|
||||
|
||||
仓库不依赖 Composer 或 PHPUnit。使用系统中可用的 PHP 可执行文件验证 PHP 语法:
|
||||
- 如果没有可用译文,则显示网站的源语言内容。
|
||||
- 如果存在以前成功保存的译文,则继续使用旧译文。
|
||||
|
||||
```sh
|
||||
find . -path './.git' -prune -o -type f -name '*.php' -print0 | xargs -0 -n1 php -l
|
||||
## 快速开始
|
||||
|
||||
### 一、获取百度翻译 API
|
||||
|
||||
1. 打开[百度翻译开放平台](https://fanyi-api.baidu.com/)。
|
||||
2. 注册或登录百度账号。
|
||||
3. 进入管理控制台。
|
||||
4. 开通通用文本翻译 API 服务。
|
||||
5. 创建应用并填写应用信息。
|
||||
6. 在应用管理页面获取 **APP ID** 和 **密钥**。
|
||||
|
||||
> APP ID 和密钥属于敏感信息,请勿提交到公开的 GitHub 仓库,也不要通过网页前端代码或公开日志暴露。
|
||||
|
||||
百度翻译 API 的具体开通方式、计费规则和调用限制,请参考[百度翻译 API 官方文档](https://fanyi-api.baidu.com/doc/23)。
|
||||
|
||||
### 二、安装插件
|
||||
|
||||
由于插件目前尚未通过 WordPress.org 审核,需要从 GitHub 手动安装。
|
||||
|
||||
#### 方法一:上传 ZIP 压缩包
|
||||
|
||||
1. 访问 [HTBD GitHub 仓库](https://github.com/hanyixuanten/HTBD)。
|
||||
2. 点击 **Releases**。
|
||||
3. 从最新版本下载 zip 文件 **hyx-translator-for-baidu-translate-*.zip**
|
||||
4. 登录 WordPress 管理后台。
|
||||
5. 打开“插件” → “安装插件”。
|
||||
6. 点击“上传插件”。
|
||||
7. 选择下载的 ZIP 文件并开始安装。
|
||||
8. 安装完成后启用 HTBD。
|
||||
|
||||
#### 方法二:上传到服务器
|
||||
|
||||
1. 访问 [HTBD GitHub 仓库](https://github.com/hanyixuanten/HTBD)。
|
||||
2. 点击 **Releases**。
|
||||
3. 从最新版本下载 zip 文件 **hyx-translator-for-baidu-translate-*.zip**
|
||||
4. 将 `hyx-translator-for-baidu-translate` 目录上传到 WordPress 的插件目录: `wp-content/plugins/hyx-translator-for-baidu-translate/`
|
||||
5. 登录 WordPress 管理后台。
|
||||
6. 打开“插件” → “已安装插件”。
|
||||
7. 找到 HTBD 并点击“启用”。
|
||||
|
||||
### 三、配置插件
|
||||
|
||||
1. 登录 WordPress 管理后台。
|
||||
2. 打开 HTBD 设置页面。
|
||||
3. 填写百度翻译开放平台提供的 **APP ID** 和 **密钥**。
|
||||
4. 设置网站的源语言,或勾选“自动识别源语言”以使用百度翻译的 `auto` 源语言模式。
|
||||
5. 添加需要启用的目标语言。
|
||||
6. 选择语言 URL 模式,例如语言子目录或域名绑定。
|
||||
7. 保存设置。
|
||||
|
||||
修改语言路由设置后,建议在 WordPress 后台打开“设置” → “固定链接”,确认固定链接配置已经正确生效。
|
||||
|
||||
自动识别状态使用独立选项保存。未勾选自动识别时在源语言输入框中主动填写 `auto`,仍属于不同的配置状态。
|
||||
|
||||
### 四、开始使用
|
||||
|
||||
1. 在 WordPress 后台打开需要翻译的文章或页面。
|
||||
2. 保存或更新内容。
|
||||
3. 使用 HTBD 提供的翻译功能生成目标语言译文。
|
||||
4. 等待翻译任务完成。
|
||||
5. 通过对应语言的访问地址检查译文。
|
||||
|
||||
例如,假设源语言页面地址为:
|
||||
|
||||
```txt
|
||||
https://example.com/about/
|
||||
```
|
||||
|
||||
投入生产前,应在 WordPress 预发布环境中测试插件,覆盖语言解析、重写行为、持久化复用与失效、百度请求签名和错误处理、内容过滤和回退渲染。任何自动化测试都应模拟 `wp_remote_post` 响应,避免依赖真实凭据、数据库或外部网络访问。
|
||||
英语使用 `/en/` 子目录时,译文地址为:
|
||||
|
||||
## 重新翻译
|
||||
```txt
|
||||
https://example.com/en/about/
|
||||
```
|
||||
|
||||
文章和页面列表会显示每种目标语言的状态列,包括最近成功翻译时间和翻译操作。设置页提供 **重新翻译所有内容**、**翻译所有文章** 和 **翻译所有分类标签**。它们会在为相应内容安排强制刷新前显示确认警告;此操作会绕过已持久化的翻译并消耗百度 API 配额。
|
||||
日语使用 `/jp/` 子目录时,译文地址为:
|
||||
|
||||
设置页还显示实时进度面板。面板每五秒刷新一次,显示最近一次翻译任务的进度,覆盖全量翻译、文章和页面翻译、分类和标签翻译,以及单篇文章翻译。进度仅统计该任务启动后、且属于其已入队内容和目标语言的完成项目,因此历史翻译不会让新的任务看似已完成。强制重翻任务会立即开始,并按两秒间隔排队;单次 WP-Cron 请求最多处理一个内容项,避免一次处理全部内容而超时。进度只读取本地 WordPress 和翻译表数据,不会发送额外请求。
|
||||
```txt
|
||||
https://example.com/jp/about/
|
||||
```
|
||||
|
||||
设置页的 **清除已翻译的缓存** 会删除所有持久化翻译记录、取消待执行的翻译任务并重置进度。清除后前台将显示源文,需使用相应翻译操作重新生成翻译;重新生成会再次消耗百度 API 配额。
|
||||
通过目标语言子目录或绑定域名请求 `wp-login.php`、`wp-admin` 时,插件会重定向到源站对应地址,并保留查询参数。
|
||||
|
||||
## 已知限制
|
||||
插件会优先使用已保存的译文。只有在原文发生变化、译文失效或用户主动要求刷新时,才需要重新调用翻译 API。
|
||||
|
||||
- 设置界面接受语言代码,但尚未依据维护中的百度语言代码列表进行验证。
|
||||
- API 调用会翻译原始 WordPress 内容。生产使用前,需要为 HTML、区块、短代码、占位符和 URL 提供受保护令牌处理管线。
|
||||
- 分类法和 SEO 值会入队,但当前渲染过滤器只覆盖部分 SEO 插件和术语视图。
|
||||
- 域名绑定假定 WordPress 站点和服务器虚拟主机已为每个绑定域名配置完成。
|
||||
- 翻译失败会作为失败尝试保存,并显示源值;尚未实现重试管理和后台任务监控器。
|
||||
## 使用建议
|
||||
|
||||
- 正式启用前,请先在测试环境中检查固定链接和多语言路由。
|
||||
- 为百度翻译 API 设置合理的调用额度和安全策略。
|
||||
- 不要在源代码、前端 JavaScript或公开日志中保存 API 密钥。
|
||||
- 发布译文前,建议人工检查品牌名、专业术语和重要页面。
|
||||
- 修改原文后,应检查对应语言的译文是否需要更新。
|
||||
- 配置域名绑定时,需要同时完成 DNS、HTTPS 证书和服务器域名设置。
|
||||
- 修改语言路由后,请检查 Canonical URL、`hreflang` 和站点地图是否正确。
|
||||
|
||||
## 系统要求
|
||||
|
||||
- WordPress 6.4 或更高版本
|
||||
- PHP 8.1 或更高版本
|
||||
- 可正常访问百度翻译 API 的服务器网络环境
|
||||
- 有效的百度翻译开放平台 APP ID 和密钥
|
||||
|
||||
## 问题反馈与贡献
|
||||
|
||||
如果在使用过程中遇到问题,或者希望提交功能建议,可以前往 GitHub 仓库反馈:
|
||||
|
||||
[https://github.com/hanyixuanten/HTBD](https://github.com/hanyixuanten/HTBD)
|
||||
|
||||
提交问题时,建议提供以下信息:
|
||||
|
||||
- WordPress 版本
|
||||
- PHP 版本
|
||||
- HTBD 版本
|
||||
- 使用的语言和路由模式
|
||||
- 问题复现步骤
|
||||
- 已隐藏敏感信息的错误日志
|
||||
|
||||
欢迎通过 Issue 或 Pull Request 参与 HTBD 的改进。
|
||||
|
||||
@@ -0,0 +1,113 @@
|
||||
(function () {
|
||||
'use strict';
|
||||
|
||||
var domainMode = document.querySelector('#htbd-routing-mode input[value="domain"]');
|
||||
var bindingsRow = document.getElementById('htbd-domain-bindings-row');
|
||||
var autoDetectSourceLanguage = document.getElementById('htbd-auto-detect-source-language');
|
||||
var sourceLanguageRow = document.getElementById('htbd-source-language-row');
|
||||
|
||||
if (domainMode && bindingsRow) {
|
||||
domainMode.addEventListener('change', function () {
|
||||
bindingsRow.style.display = domainMode.checked ? '' : 'none';
|
||||
});
|
||||
}
|
||||
|
||||
if (autoDetectSourceLanguage && sourceLanguageRow) {
|
||||
autoDetectSourceLanguage.addEventListener('change', function () {
|
||||
sourceLanguageRow.style.display = autoDetectSourceLanguage.checked ? 'none' : '';
|
||||
});
|
||||
}
|
||||
|
||||
document.querySelectorAll('form[data-htbd-confirm]').forEach(function (form) {
|
||||
form.addEventListener('submit', function (event) {
|
||||
if (!window.confirm(form.getAttribute('data-htbd-confirm'))) {
|
||||
event.preventDefault();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
var container = document.getElementById('htbd-translation-progress');
|
||||
if (!container || typeof htbdAdmin === 'undefined') {
|
||||
return;
|
||||
}
|
||||
|
||||
function appendText(parent, text) {
|
||||
parent.appendChild(document.createTextNode(text));
|
||||
}
|
||||
|
||||
function format(template, values) {
|
||||
return template.replace(/%([1-9]\d*)\$s/g, function (match, position) {
|
||||
return values[Number(position) - 1];
|
||||
});
|
||||
}
|
||||
|
||||
function renderProgress(progress) {
|
||||
var summary = document.createElement('p');
|
||||
var percent = document.createElement('strong');
|
||||
percent.textContent = progress.percent + '%';
|
||||
summary.appendChild(percent);
|
||||
appendText(summary, ' ' + format(htbdAdmin.i18n.contentItems, [progress.completed, progress.total]));
|
||||
|
||||
var progressBar = document.createElement('div');
|
||||
progressBar.style.cssText = 'max-width:480px;height:12px;background:#dcdcde';
|
||||
var progressValue = document.createElement('div');
|
||||
progressValue.style.cssText = 'height:12px;background:#2271b1';
|
||||
progressValue.style.width = progress.percent + '%';
|
||||
progressBar.appendChild(progressValue);
|
||||
|
||||
var description = document.createElement('p');
|
||||
description.className = 'description';
|
||||
description.textContent = format(htbdAdmin.i18n.latestTask, [progress.task_label, progress.posts_completed, progress.posts_total, progress.terms_completed, progress.terms_total]);
|
||||
|
||||
var children = [summary, progressBar, description];
|
||||
if (progress.failed_items.length) {
|
||||
var failedHeading = document.createElement('h3');
|
||||
failedHeading.textContent = htbdAdmin.i18n.failedItems;
|
||||
children.push(failedHeading);
|
||||
|
||||
var failedList = document.createElement('ul');
|
||||
progress.failed_items.forEach(function (item) {
|
||||
var failedItem = document.createElement('li');
|
||||
appendText(failedItem, item.name + ' (' + item.language + ') ');
|
||||
|
||||
var retryLink = document.createElement('a');
|
||||
retryLink.className = 'button button-small';
|
||||
retryLink.href = item.retry_url;
|
||||
retryLink.textContent = htbdAdmin.i18n.retranslate;
|
||||
failedItem.appendChild(retryLink);
|
||||
failedList.appendChild(failedItem);
|
||||
});
|
||||
children.push(failedList);
|
||||
}
|
||||
|
||||
container.replaceChildren.apply(container, children);
|
||||
}
|
||||
|
||||
function renderProgressError() {
|
||||
var message = document.createElement('p');
|
||||
message.textContent = htbdAdmin.i18n.progressError;
|
||||
container.replaceChildren(message);
|
||||
}
|
||||
|
||||
function updateProgress() {
|
||||
var url = htbdAdmin.progressUrl + '?action=htbd_translation_progress&_ajax_nonce=' + encodeURIComponent(htbdAdmin.progressNonce);
|
||||
|
||||
window.fetch(url, { credentials: 'same-origin' })
|
||||
.then(function (response) {
|
||||
if (!response.ok) {
|
||||
throw new Error('progress_request_failed');
|
||||
}
|
||||
return response.json();
|
||||
})
|
||||
.then(function (response) {
|
||||
if (!response.success) {
|
||||
throw new Error('progress_request_failed');
|
||||
}
|
||||
renderProgress(response.data);
|
||||
})
|
||||
.catch(renderProgressError);
|
||||
}
|
||||
|
||||
updateProgress();
|
||||
window.setInterval(updateProgress, 5000);
|
||||
}());
|
||||
@@ -0,0 +1,90 @@
|
||||
#!/usr/bin/env sh
|
||||
|
||||
set -eu
|
||||
|
||||
plugin_slug="hyx-translator-for-baidu-translate"
|
||||
plugin_file="${plugin_slug}.php"
|
||||
script_dir="$(CDPATH='' cd -- "$(dirname -- "$0")" && pwd)"
|
||||
build_dir="${script_dir}/build"
|
||||
plugin_dir="${build_dir}/${plugin_slug}"
|
||||
|
||||
clean() {
|
||||
find "${script_dir}" -maxdepth 1 -type f -name "${plugin_slug}-*.zip" -delete
|
||||
rm -rf "${build_dir}"
|
||||
}
|
||||
|
||||
build() {
|
||||
for command in grep sed msgfmt zip unzip; do
|
||||
if ! command -v "${command}" >/dev/null 2>&1; then
|
||||
printf 'Required command not found: %s\n' "${command}" >&2
|
||||
exit 1
|
||||
fi
|
||||
done
|
||||
|
||||
version="$(grep -E '^ \* Version:' "${script_dir}/${plugin_file}" | sed -E 's/^ \* Version: *//')"
|
||||
archive_name="${plugin_slug}-${version}.zip"
|
||||
archive_path="${script_dir}/${archive_name}"
|
||||
|
||||
clean
|
||||
mkdir -p "${plugin_dir}"
|
||||
cp "${script_dir}/${plugin_file}" \
|
||||
"${script_dir}/uninstall.php" \
|
||||
"${script_dir}/readme.txt" \
|
||||
"${script_dir}/LICENSE" \
|
||||
"${plugin_dir}/"
|
||||
cp -R "${script_dir}/includes" "${plugin_dir}/"
|
||||
cp -R "${script_dir}/assets" "${plugin_dir}/"
|
||||
mkdir -p "${plugin_dir}/languages"
|
||||
cp "${script_dir}/languages/${plugin_slug}.pot" \
|
||||
"${script_dir}/languages/${plugin_slug}-zh_CN.po" \
|
||||
"${plugin_dir}/languages/"
|
||||
msgfmt --check --check-format \
|
||||
--output-file="${plugin_dir}/languages/${plugin_slug}-zh_CN.mo" \
|
||||
"${script_dir}/languages/${plugin_slug}-zh_CN.po"
|
||||
(
|
||||
cd "${build_dir}"
|
||||
zip -qr "${archive_path}" "${plugin_slug}"
|
||||
)
|
||||
|
||||
unzip -p "${archive_path}" "${plugin_slug}/${plugin_file}" | grep -q '^ \* Plugin Name: HTBD - hyx Translator powered by Baidu Translate$'
|
||||
unzip -p "${archive_path}" "${plugin_slug}/readme.txt" | grep -q "^Stable tag: ${version}$"
|
||||
unzip -Z1 "${archive_path}" | grep -q "^${plugin_slug}/languages/${plugin_slug}-zh_CN.mo$"
|
||||
if unzip -Z1 "${archive_path}" | grep -q '/wp-plugins-.*\.po$'; then
|
||||
printf 'Unexpected GlotPress submission file found in archive.\n' >&2
|
||||
exit 1
|
||||
fi
|
||||
unzip -Z1 "${archive_path}" | grep -q "^${plugin_slug}/includes/interface-translation-provider.php$"
|
||||
unzip -Z1 "${archive_path}" | grep -q "^${plugin_slug}/assets/js/admin.js$"
|
||||
if unzip -Z1 "${archive_path}" | grep -Eq '/class-btranslate-[^/]+\.php$'; then
|
||||
printf 'Unexpected legacy class filename found in archive.\n' >&2
|
||||
exit 1
|
||||
fi
|
||||
if unzip -Z1 "${archive_path}" | grep -Eq '/(interface-btranslate-translation-provider\.php|btranslate-admin\.js)$'; then
|
||||
printf 'Unexpected legacy runtime filename found in archive.\n' >&2
|
||||
exit 1
|
||||
fi
|
||||
if grep -R -q 'BTRANSLATE' "${plugin_dir}" --include='*.php'; then
|
||||
printf 'Unexpected legacy PHP prefix found in archive.\n' >&2
|
||||
exit 1
|
||||
fi
|
||||
if unzip -Z1 "${archive_path}" | grep -Eq '/README(\.zh-CN)?\.md$'; then
|
||||
printf 'Unexpected development README found in archive.\n' >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
printf 'Built %s\n' "${archive_path}"
|
||||
}
|
||||
|
||||
case "${1:-build}" in
|
||||
build)
|
||||
build
|
||||
;;
|
||||
clean)
|
||||
clean
|
||||
printf 'Removed local build artifacts.\n'
|
||||
;;
|
||||
*)
|
||||
printf 'Usage: %s [build|clean]\n' "$0" >&2
|
||||
exit 2
|
||||
;;
|
||||
esac
|
||||
@@ -0,0 +1,59 @@
|
||||
<?php
|
||||
/**
|
||||
* Plugin Name: HTBD - hyx Translator powered by Baidu Translate
|
||||
* Plugin URI: https://www.vblg.top/index.php/archives/147
|
||||
* Description: Persistent multilingual WordPress translations powered by Baidu Translate.
|
||||
* Version: 0.3.1
|
||||
* Requires at least: 6.4
|
||||
* Requires PHP: 8.1
|
||||
* Author: hanyixuanten
|
||||
* Author URI: https://www.vblg.top
|
||||
* License: GPL-3.0-only
|
||||
* License URI: https://www.gnu.org/licenses/gpl-3.0.html
|
||||
* Text Domain: hyx-translator-for-baidu-translate
|
||||
* Domain Path: /languages
|
||||
*/
|
||||
|
||||
defined( 'ABSPATH' ) || exit;
|
||||
|
||||
define( 'HTBD_VERSION', '0.3.1' );
|
||||
define( 'HTBD_FILE', __FILE__ );
|
||||
define( 'HTBD_PATH', plugin_dir_path( __FILE__ ) );
|
||||
|
||||
require_once HTBD_PATH . 'includes/interface-translation-provider.php';
|
||||
require_once HTBD_PATH . 'includes/class-translation-result.php';
|
||||
require_once HTBD_PATH . 'includes/class-translation-identity.php';
|
||||
require_once HTBD_PATH . 'includes/class-translation-store.php';
|
||||
require_once HTBD_PATH . 'includes/class-baidu-provider.php';
|
||||
require_once HTBD_PATH . 'includes/class-translation-service.php';
|
||||
require_once HTBD_PATH . 'includes/class-content-translator.php';
|
||||
require_once HTBD_PATH . 'includes/class-settings.php';
|
||||
require_once HTBD_PATH . 'includes/class-language-router.php';
|
||||
require_once HTBD_PATH . 'includes/class-sitemap-controller.php';
|
||||
require_once HTBD_PATH . 'includes/class-content-controller.php';
|
||||
require_once HTBD_PATH . 'includes/class-admin.php';
|
||||
require_once HTBD_PATH . 'includes/class-plugin.php';
|
||||
require_once HTBD_PATH . 'includes/class-uninstaller.php';
|
||||
|
||||
register_activation_hook( HTBD_FILE, array( 'HTBD_Plugin', 'activate' ) );
|
||||
register_deactivation_hook( HTBD_FILE, array( 'HTBD_Plugin', 'deactivate' ) );
|
||||
|
||||
function htbd_load_textdomain() {
|
||||
load_plugin_textdomain( 'hyx-translator-for-baidu-translate', false, dirname( plugin_basename( HTBD_FILE ) ) . '/languages' );
|
||||
}
|
||||
|
||||
function htbd_add_plugin_row_meta( $plugin_meta, $plugin_file ) {
|
||||
if ( plugin_basename( HTBD_FILE ) === $plugin_file ) {
|
||||
$plugin_meta[] = sprintf(
|
||||
'<a href="%1$s" target="_blank" rel="noopener noreferrer">%2$s</a>',
|
||||
esc_url( 'https://github.com/hanyixuanten/HTBD' ),
|
||||
esc_html__( 'GitHub Repository', 'hyx-translator-for-baidu-translate' )
|
||||
);
|
||||
}
|
||||
|
||||
return $plugin_meta;
|
||||
}
|
||||
|
||||
add_action( 'init', 'htbd_load_textdomain', 0 );
|
||||
add_action( 'plugins_loaded', array( 'HTBD_Plugin', 'instance' ) );
|
||||
add_filter( 'plugin_row_meta', 'htbd_add_plugin_row_meta', 10, 2 );
|
||||
@@ -0,0 +1,590 @@
|
||||
<?php
|
||||
|
||||
defined( 'ABSPATH' ) || exit;
|
||||
|
||||
class HTBD_Admin {
|
||||
public function register() {
|
||||
add_action( 'admin_menu', array( $this, 'add_settings_page' ) );
|
||||
add_action( 'admin_init', array( $this, 'register_settings' ) );
|
||||
add_action( 'admin_enqueue_scripts', array( $this, 'enqueue_settings_assets' ) );
|
||||
add_action( 'admin_post_htbd_queue_existing_translations', array( $this, 'queue_existing_translations' ) );
|
||||
add_action( 'admin_post_htbd_clear_translation_cache', array( $this, 'clear_translation_cache' ) );
|
||||
add_action( 'admin_post_htbd_translate_post', array( $this, 'queue_post_translation' ) );
|
||||
add_action( 'admin_post_htbd_retry_failed_translation', array( $this, 'retry_failed_translation' ) );
|
||||
add_action( 'add_meta_boxes', array( $this, 'add_translation_meta_box' ) );
|
||||
add_action( 'save_post', array( $this, 'save_translation_meta_box' ), 30, 3 );
|
||||
add_action( 'wp_ajax_htbd_translation_progress', array( $this, 'translation_progress' ) );
|
||||
add_filter( 'manage_post_posts_columns', array( $this, 'add_translation_column' ) );
|
||||
add_filter( 'manage_page_posts_columns', array( $this, 'add_translation_column' ) );
|
||||
add_action( 'manage_post_posts_custom_column', array( $this, 'render_translation_column' ), 10, 2 );
|
||||
add_action( 'manage_page_posts_custom_column', array( $this, 'render_translation_column' ), 10, 2 );
|
||||
add_filter( 'plugin_action_links_' . plugin_basename( HTBD_FILE ), array( $this, 'add_plugin_settings_link' ) );
|
||||
}
|
||||
|
||||
public function add_translation_meta_box() {
|
||||
foreach ( array( 'post', 'page' ) as $post_type ) {
|
||||
add_meta_box( 'htbd-translation-editor', __( 'Manual translations', 'hyx-translator-for-baidu-translate' ), array( $this, 'render_translation_meta_box' ), $post_type, 'normal', 'default' );
|
||||
}
|
||||
}
|
||||
|
||||
public function render_translation_meta_box( $post ) {
|
||||
wp_nonce_field( 'htbd_save_translation_' . $post->ID, 'htbd_translation_nonce' );
|
||||
$fields = $this->get_post_translation_fields( $post );
|
||||
if ( empty( $fields ) ) {
|
||||
echo '<p>' . esc_html__( 'Generate a translation first, then edit it here.', 'hyx-translator-for-baidu-translate' ) . '</p>';
|
||||
return;
|
||||
}
|
||||
echo '<p class="description">' . esc_html__( 'Changes are saved as manual translations and remain until you explicitly retranslate this content.', 'hyx-translator-for-baidu-translate' ) . '</p>';
|
||||
foreach ( $fields as $language => $language_fields ) {
|
||||
echo '<h4>' . esc_html( strtoupper( $language ) ) . '</h4>';
|
||||
foreach ( $language_fields as $field ) {
|
||||
$input_id = 'htbd-translation-' . md5( $field['context'] );
|
||||
echo '<p><label for="' . esc_attr( $input_id ) . '"><strong>' . esc_html( $field['label'] ) . '</strong></label><textarea class="widefat" rows="' . esc_attr( $field['rows'] ) . '" id="' . esc_attr( $input_id ) . '" name="htbd_translation[' . esc_attr( $language ) . '][' . esc_attr( $field['context'] ) . ']">' . esc_textarea( $field['translated_value'] ) . '</textarea></p>';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public function save_translation_meta_box( $post_id, $post, $update ) {
|
||||
if ( ! $update || wp_is_post_revision( $post_id ) || wp_is_post_autosave( $post_id ) || ! in_array( $post->post_type, array( 'post', 'page' ), true ) || ! current_user_can( 'edit_post', $post_id ) || empty( $_POST['htbd_translation_nonce'] ) ) {
|
||||
return;
|
||||
}
|
||||
check_admin_referer( 'htbd_save_translation_' . $post_id, 'htbd_translation_nonce' );
|
||||
$submitted = isset( $_POST['htbd_translation'] ) ? (array) wp_unslash( $_POST['htbd_translation'] ) : array();
|
||||
$allowed = $this->get_post_translation_fields( $post );
|
||||
$settings = HTBD_Settings::get();
|
||||
$provider = new HTBD_Baidu_Provider( $settings['baidu_app_id'], $settings['baidu_secret_key'] );
|
||||
$store = new HTBD_Translation_Store();
|
||||
foreach ( $allowed as $language => $language_fields ) {
|
||||
foreach ( $language_fields as $field ) {
|
||||
if ( ! isset( $submitted[ $language ][ $field['context'] ] ) || ! is_string( $submitted[ $language ][ $field['context'] ] ) ) {
|
||||
continue;
|
||||
}
|
||||
$value = 'post_content' === $field['field'] ? wp_kses_post( $submitted[ $language ][ $field['context'] ] ) : sanitize_textarea_field( $submitted[ $language ][ $field['context'] ] );
|
||||
if ( '' === trim( $value ) ) {
|
||||
continue;
|
||||
}
|
||||
$identity = HTBD_Translation_Identity::key( $field['source_value'], $settings['source_language'], $language, $field['context'], $provider->get_version() );
|
||||
$store->save_manual( $identity, $settings['source_language'], $language, $field['context'], HTBD_Translation_Identity::source_fingerprint( $field['source_value'] ), $value );
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private function get_post_translation_fields( $post ) {
|
||||
$settings = HTBD_Settings::get();
|
||||
$definitions = array(
|
||||
'post_title' => array( 'label' => __( 'Title', 'hyx-translator-for-baidu-translate' ), 'rows' => 2, 'source' => $post->post_title ),
|
||||
'post_excerpt' => array( 'label' => __( 'Excerpt', 'hyx-translator-for-baidu-translate' ), 'rows' => 3, 'source' => $post->post_excerpt ),
|
||||
);
|
||||
if ( '' !== $post->post_content ) {
|
||||
$definitions['post_content'] = array( 'label' => __( 'Content', 'hyx-translator-for-baidu-translate' ), 'rows' => 3, 'source' => $post->post_content );
|
||||
}
|
||||
foreach ( array( 'wpai_meta_description', '_yoast_wpseo_title', '_yoast_wpseo_metadesc', 'rank_math_title', 'rank_math_description' ) as $meta_key ) {
|
||||
$meta_value = get_post_meta( $post->ID, $meta_key, true );
|
||||
if ( is_string( $meta_value ) && '' !== $meta_value ) {
|
||||
$definitions[ 'meta:' . $meta_key ] = array( 'label' => $meta_key, 'rows' => 3, 'source' => $meta_value );
|
||||
}
|
||||
}
|
||||
$result = array();
|
||||
$store = new HTBD_Translation_Store();
|
||||
$provider = new HTBD_Baidu_Provider( $settings['baidu_app_id'], $settings['baidu_secret_key'] );
|
||||
foreach ( $settings['target_languages'] as $language ) {
|
||||
foreach ( $definitions as $field => $definition ) {
|
||||
$segments = 'post_content' === $field ? HTBD_Content_Translator::segments( $definition['source'] ) : array( $definition['source'] );
|
||||
$text_index = 0;
|
||||
foreach ( (array) $segments as $segment ) {
|
||||
if ( 'post_content' === $field ) {
|
||||
if ( HTBD_Content_Translator::is_tag( $segment ) || '' === trim( $segment ) ) { continue; }
|
||||
$source = HTBD_Content_Translator::surrounding_whitespace( $segment )['text'];
|
||||
$context = 'post:' . $post->ID . ':post_content:text:' . $text_index;
|
||||
++$text_index;
|
||||
} else {
|
||||
$source = $segment;
|
||||
$context = 'post:' . $post->ID . ':' . $field;
|
||||
}
|
||||
if ( '' === trim( $source ) ) { continue; }
|
||||
$identity = HTBD_Translation_Identity::key( $source, $settings['source_language'], $language, $context, $provider->get_version() );
|
||||
$translation = $store->find_valid( $identity );
|
||||
if ( empty( $translation['translated_value'] ) ) { continue; }
|
||||
$result[ $language ][] = array( 'field' => $field, 'context' => $context, 'source_value' => $source, 'translated_value' => $translation['translated_value'], 'label' => $definition['label'] . ( 'post_content' === $field ? ' #' . $text_index : '' ), 'rows' => $definition['rows'] );
|
||||
}
|
||||
}
|
||||
}
|
||||
return $result;
|
||||
}
|
||||
|
||||
public function add_settings_page() {
|
||||
add_options_page( __( 'HTBD', 'hyx-translator-for-baidu-translate' ), __( 'HTBD', 'hyx-translator-for-baidu-translate' ), 'manage_options', 'htbd', array( $this, 'render_settings_page' ) );
|
||||
}
|
||||
|
||||
public function register_settings() {
|
||||
register_setting( 'htbd_settings', 'htbd_settings', array( $this, 'sanitize_settings' ) );
|
||||
}
|
||||
|
||||
public function enqueue_settings_assets( $hook_suffix ) {
|
||||
if ( 'settings_page_htbd' !== $hook_suffix ) {
|
||||
return;
|
||||
}
|
||||
|
||||
wp_enqueue_script(
|
||||
'htbd-admin',
|
||||
plugins_url( 'assets/js/admin.js', HTBD_FILE ),
|
||||
array(),
|
||||
HTBD_VERSION,
|
||||
true
|
||||
);
|
||||
wp_localize_script(
|
||||
'htbd-admin',
|
||||
'htbdAdmin',
|
||||
array(
|
||||
'progressUrl' => admin_url( 'admin-ajax.php' ),
|
||||
'progressNonce' => wp_create_nonce( 'htbd_translation_progress' ),
|
||||
'i18n' => array(
|
||||
/* translators: 1: translated content item count, 2: total content item count. */
|
||||
'contentItems' => __( '(%1$s / %2$s content items)', 'hyx-translator-for-baidu-translate' ),
|
||||
/* translators: 1: latest task status, 2: translated post/page count, 3: total post/page count, 4: translated category/tag count, 5: total category/tag count. */
|
||||
'latestTask' => __( 'Latest task: %1$s. Posts and pages: %2$s / %3$s; categories and tags: %4$s / %5$s. Updates automatically every 5 seconds.', 'hyx-translator-for-baidu-translate' ),
|
||||
'failedItems' => __( 'Failed translations', 'hyx-translator-for-baidu-translate' ),
|
||||
'retranslate' => __( 'Retranslate', 'hyx-translator-for-baidu-translate' ),
|
||||
'progressError' => __( 'Unable to load translation progress at this time.', 'hyx-translator-for-baidu-translate' ),
|
||||
),
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
public function sanitize_settings( $settings ) {
|
||||
$settings = (array) $settings;
|
||||
$bindings = array();
|
||||
$auto_detect_source_language = ! empty( $settings['auto_detect_source_language'] );
|
||||
$routing_modes = HTBD_Settings::routing_modes( $settings['routing_mode'] ?? array() );
|
||||
$target_languages = array_values( array_filter( array_map( 'sanitize_key', explode( ',', (string) ( $settings['target_languages'] ?? 'en' ) ) ) ) );
|
||||
|
||||
foreach ( preg_split( '/\r\n|\r|\n/', (string) ( $settings['domain_bindings'] ?? '' ) ) as $line ) {
|
||||
$parts = array_map( 'trim', explode( '=', $line, 2 ) );
|
||||
$domain = 2 === count( $parts ) ? HTBD_Settings::normalize_domain( $parts[0] ) : '';
|
||||
$language = 2 === count( $parts ) ? sanitize_key( $parts[1] ) : '';
|
||||
if ( '' !== $domain && in_array( $language, $target_languages, true ) && ! in_array( $language, $bindings, true ) ) {
|
||||
$bindings[ $domain ] = $language;
|
||||
}
|
||||
}
|
||||
|
||||
return array(
|
||||
'auto_detect_source_language' => $auto_detect_source_language,
|
||||
'source_language' => $auto_detect_source_language ? 'auto' : sanitize_key( $settings['source_language'] ?? 'zh' ),
|
||||
'target_languages' => $target_languages,
|
||||
'routing_mode' => $routing_modes,
|
||||
'domain_bindings' => $bindings,
|
||||
'fallback_language' => sanitize_key( $settings['fallback_language'] ?? 'zh' ),
|
||||
'baidu_app_id' => sanitize_text_field( $settings['baidu_app_id'] ?? '' ),
|
||||
'baidu_secret_key' => sanitize_text_field( $settings['baidu_secret_key'] ?? '' ),
|
||||
'log_requests' => ! empty( $settings['log_requests'] ),
|
||||
);
|
||||
}
|
||||
|
||||
public function render_settings_page() {
|
||||
$settings = HTBD_Settings::get();
|
||||
$bindings = array();
|
||||
foreach ( (array) $settings['domain_bindings'] as $domain => $language ) {
|
||||
$bindings[] = $domain . '=' . $language;
|
||||
}
|
||||
?>
|
||||
<div class="wrap">
|
||||
<h1>HTBD</h1>
|
||||
<form action="options.php" method="post">
|
||||
<?php settings_fields( 'htbd_settings' ); ?>
|
||||
<table class="form-table" role="presentation">
|
||||
<tr><th scope="row"><?php esc_html_e( 'Source language detection', 'hyx-translator-for-baidu-translate' ); ?></th><td><label for="htbd-auto-detect-source-language"><input id="htbd-auto-detect-source-language" name="htbd_settings[auto_detect_source_language]" type="checkbox" value="1" <?php checked( $settings['auto_detect_source_language'] ); ?> /> <?php esc_html_e( 'Automatically detect the source language', 'hyx-translator-for-baidu-translate' ); ?></label></td></tr>
|
||||
<tr id="htbd-source-language-row"<?php echo $settings['auto_detect_source_language'] ? ' style="display:none"' : ''; ?>><th scope="row"><label for="htbd-source-language"><?php esc_html_e( 'Source language', 'hyx-translator-for-baidu-translate' ); ?></label></th><td><input id="htbd-source-language" name="htbd_settings[source_language]" type="text" value="<?php echo esc_attr( $settings['source_language'] ); ?>" /><p class="description"><?php esc_html_e( 'Use a Baidu Translate language code, for example zh.', 'hyx-translator-for-baidu-translate' ); ?></p></td></tr>
|
||||
<tr><th scope="row"><label for="htbd-target-languages"><?php esc_html_e( 'Target languages', 'hyx-translator-for-baidu-translate' ); ?></label></th><td><input id="htbd-target-languages" name="htbd_settings[target_languages]" type="text" value="<?php echo esc_attr( implode( ',', $settings['target_languages'] ) ); ?>" class="regular-text" /><p class="description"><?php esc_html_e( 'Separate multiple language codes with commas, for example en,jp.', 'hyx-translator-for-baidu-translate' ); ?></p></td></tr>
|
||||
<tr><th scope="row"><?php esc_html_e( 'URL mode', 'hyx-translator-for-baidu-translate' ); ?></th><td><fieldset id="htbd-routing-mode"><label><input type="checkbox" name="htbd_settings[routing_mode][]" value="subdirectory" <?php checked( in_array( 'subdirectory', $settings['routing_mode'], true ) ); ?> /> <?php esc_html_e( 'Subdirectory', 'hyx-translator-for-baidu-translate' ); ?></label><br /><label><input type="checkbox" name="htbd_settings[routing_mode][]" value="domain" <?php checked( in_array( 'domain', $settings['routing_mode'], true ) ); ?> /> <?php esc_html_e( 'Domain', 'hyx-translator-for-baidu-translate' ); ?></label></fieldset></td></tr>
|
||||
<tr id="htbd-domain-bindings-row"<?php echo in_array( 'domain', $settings['routing_mode'], true ) ? '' : ' style="display:none"'; ?>><th scope="row"><label for="htbd-domain-bindings"><?php esc_html_e( 'Domain bindings', 'hyx-translator-for-baidu-translate' ); ?></label></th><td><textarea id="htbd-domain-bindings" name="htbd_settings[domain_bindings]" rows="4" class="large-text" placeholder="en.example.com=en"><?php echo esc_textarea( implode( "\n", $bindings ) ); ?></textarea><p class="description"><?php esc_html_e( 'Enter one domain=language code pair per line, for example en.example.com=en. Do not include https://, a path, or a port.', 'hyx-translator-for-baidu-translate' ); ?></p></td></tr>
|
||||
<tr><th scope="row"><label for="htbd-baidu-app-id"><?php esc_html_e( 'Baidu application ID', 'hyx-translator-for-baidu-translate' ); ?></label></th><td><input id="htbd-baidu-app-id" name="htbd_settings[baidu_app_id]" type="text" value="<?php echo esc_attr( $settings['baidu_app_id'] ); ?>" class="regular-text" /></td></tr>
|
||||
<tr><th scope="row"><label for="htbd-baidu-secret-key"><?php esc_html_e( 'Baidu secret key', 'hyx-translator-for-baidu-translate' ); ?></label></th><td><input id="htbd-baidu-secret-key" name="htbd_settings[baidu_secret_key]" type="password" value="<?php echo esc_attr( $settings['baidu_secret_key'] ); ?>" class="regular-text" autocomplete="new-password" /></td></tr>
|
||||
<tr><th scope="row"><?php esc_html_e( 'Request logging', 'hyx-translator-for-baidu-translate' ); ?></th><td><label for="htbd-log-requests"><input id="htbd-log-requests" name="htbd_settings[log_requests]" type="checkbox" value="1" <?php checked( $settings['log_requests'] ); ?> /> <?php esc_html_e( 'Log each Baidu Translate request', 'hyx-translator-for-baidu-translate' ); ?></label><p class="description"><?php esc_html_e( 'When enabled, fires the htbd_translation_request_logged action with the language, field, text fingerprint, length, and result status. Credentials, source text, translated text, and full API responses are never included.', 'hyx-translator-for-baidu-translate' ); ?></p></td></tr>
|
||||
</table>
|
||||
<?php submit_button(); ?>
|
||||
<div class="notice notice-warning inline">
|
||||
<p><strong><?php esc_html_e( 'After saving settings for the first time, manually run "Retranslate all content" below.', 'hyx-translator-for-baidu-translate' ); ?></strong></p>
|
||||
</div>
|
||||
</form>
|
||||
<hr />
|
||||
<h2><?php esc_html_e( 'Retranslate content', 'hyx-translator-for-baidu-translate' ); ?></h2>
|
||||
<p><?php esc_html_e( 'Queue all published posts, pages, categories, and tags for translation again.', 'hyx-translator-for-baidu-translate' ); ?></p>
|
||||
<form action="<?php echo esc_url( admin_url( 'admin-post.php' ) ); ?>" method="post" data-htbd-confirm="<?php echo esc_attr__( 'This will retranslate all content and use your API quota. Continue?', 'hyx-translator-for-baidu-translate' ); ?>">
|
||||
<input type="hidden" name="action" value="htbd_queue_existing_translations" />
|
||||
<?php wp_nonce_field( 'htbd_queue_existing_translations' ); ?>
|
||||
<?php submit_button( __( 'Retranslate all content', 'hyx-translator-for-baidu-translate' ), 'secondary', 'submit', false ); ?>
|
||||
</form>
|
||||
<p><?php esc_html_e( 'You can also retranslate a single content type.', 'hyx-translator-for-baidu-translate' ); ?></p>
|
||||
<form action="<?php echo esc_url( admin_url( 'admin-post.php' ) ); ?>" method="post" style="display:inline-block;margin-right:8px" data-htbd-confirm="<?php echo esc_attr__( 'This will retranslate all posts and pages and use your API quota. Continue?', 'hyx-translator-for-baidu-translate' ); ?>">
|
||||
<input type="hidden" name="action" value="htbd_queue_existing_translations" />
|
||||
<input type="hidden" name="scope" value="posts" />
|
||||
<?php wp_nonce_field( 'htbd_queue_existing_translations' ); ?>
|
||||
<?php submit_button( __( 'Translate all posts', 'hyx-translator-for-baidu-translate' ), 'secondary', 'submit', false ); ?>
|
||||
</form>
|
||||
<form action="<?php echo esc_url( admin_url( 'admin-post.php' ) ); ?>" method="post" style="display:inline-block" data-htbd-confirm="<?php echo esc_attr__( 'This will retranslate all categories and tags and use your API quota. Continue?', 'hyx-translator-for-baidu-translate' ); ?>">
|
||||
<input type="hidden" name="action" value="htbd_queue_existing_translations" />
|
||||
<input type="hidden" name="scope" value="terms" />
|
||||
<?php wp_nonce_field( 'htbd_queue_existing_translations' ); ?>
|
||||
<?php submit_button( __( 'Translate all categories and tags', 'hyx-translator-for-baidu-translate' ), 'secondary', 'submit', false ); ?>
|
||||
</form>
|
||||
<?php if ( count( $settings['target_languages'] ) > 1 ) : ?>
|
||||
<?php foreach ( $settings['target_languages'] as $target_language ) : ?>
|
||||
<hr />
|
||||
<h3><?php echo esc_html( strtoupper( $target_language ) ); ?></h3>
|
||||
<form action="<?php echo esc_url( admin_url( 'admin-post.php' ) ); ?>" method="post" data-htbd-confirm="<?php echo esc_attr__( 'This will retranslate all content and use your API quota. Continue?', 'hyx-translator-for-baidu-translate' ); ?>">
|
||||
<input type="hidden" name="action" value="htbd_queue_existing_translations" />
|
||||
<input type="hidden" name="language" value="<?php echo esc_attr( $target_language ); ?>" />
|
||||
<?php wp_nonce_field( 'htbd_queue_existing_translations' ); ?>
|
||||
<?php submit_button( sprintf( '%s %s', $target_language, __( 'Retranslate all content', 'hyx-translator-for-baidu-translate' ) ), 'secondary', 'submit', false ); ?>
|
||||
</form>
|
||||
<form action="<?php echo esc_url( admin_url( 'admin-post.php' ) ); ?>" method="post" style="display:inline-block;margin-right:8px" data-htbd-confirm="<?php echo esc_attr__( 'This will retranslate all posts and pages and use your API quota. Continue?', 'hyx-translator-for-baidu-translate' ); ?>">
|
||||
<input type="hidden" name="action" value="htbd_queue_existing_translations" />
|
||||
<input type="hidden" name="scope" value="posts" />
|
||||
<input type="hidden" name="language" value="<?php echo esc_attr( $target_language ); ?>" />
|
||||
<?php wp_nonce_field( 'htbd_queue_existing_translations' ); ?>
|
||||
<?php submit_button( sprintf( '%s %s', $target_language, __( 'Translate all posts', 'hyx-translator-for-baidu-translate' ) ), 'secondary', 'submit', false ); ?>
|
||||
</form>
|
||||
<form action="<?php echo esc_url( admin_url( 'admin-post.php' ) ); ?>" method="post" style="display:inline-block" data-htbd-confirm="<?php echo esc_attr__( 'This will retranslate all categories and tags and use your API quota. Continue?', 'hyx-translator-for-baidu-translate' ); ?>">
|
||||
<input type="hidden" name="action" value="htbd_queue_existing_translations" />
|
||||
<input type="hidden" name="scope" value="terms" />
|
||||
<input type="hidden" name="language" value="<?php echo esc_attr( $target_language ); ?>" />
|
||||
<?php wp_nonce_field( 'htbd_queue_existing_translations' ); ?>
|
||||
<?php submit_button( sprintf( '%s %s', $target_language, __( 'Translate all categories and tags', 'hyx-translator-for-baidu-translate' ) ), 'secondary', 'submit', false ); ?>
|
||||
</form>
|
||||
<form action="<?php echo esc_url( admin_url( 'admin-post.php' ) ); ?>" method="post" data-htbd-confirm="<?php echo esc_attr( sprintf( /* translators: %s: target language code. */ __( 'This will delete all saved %s translations. Continue?', 'hyx-translator-for-baidu-translate' ), $target_language ) ); ?>">
|
||||
<input type="hidden" name="action" value="htbd_clear_translation_cache" />
|
||||
<input type="hidden" name="language" value="<?php echo esc_attr( $target_language ); ?>" />
|
||||
<?php wp_nonce_field( 'htbd_clear_translation_cache_' . $target_language ); ?>
|
||||
<?php submit_button( sprintf( '%s %s', $target_language, __( 'Clear translation cache', 'hyx-translator-for-baidu-translate' ) ), 'delete', 'submit', false ); ?>
|
||||
</form>
|
||||
<?php endforeach; ?>
|
||||
<?php endif; ?>
|
||||
<hr />
|
||||
<h2><?php esc_html_e( 'Translation cache', 'hyx-translator-for-baidu-translate' ); ?></h2>
|
||||
<p><?php esc_html_e( 'After clearing the cache, the front end temporarily displays source content. Baidu Translate API calls resume when content is translated again.', 'hyx-translator-for-baidu-translate' ); ?></p>
|
||||
<form action="<?php echo esc_url( admin_url( 'admin-post.php' ) ); ?>" method="post" data-htbd-confirm="<?php echo esc_attr__( 'This will delete all saved translations, cancel pending translation tasks, and reset progress. Continue?', 'hyx-translator-for-baidu-translate' ); ?>">
|
||||
<input type="hidden" name="action" value="htbd_clear_translation_cache" />
|
||||
<?php wp_nonce_field( 'htbd_clear_translation_cache' ); ?>
|
||||
<?php submit_button( __( 'Clear translation cache', 'hyx-translator-for-baidu-translate' ), 'delete', 'submit', false ); ?>
|
||||
</form>
|
||||
<hr />
|
||||
<h2><?php esc_html_e( 'Translation progress', 'hyx-translator-for-baidu-translate' ); ?></h2>
|
||||
<div id="htbd-translation-progress" aria-live="polite">
|
||||
<p><?php esc_html_e( 'Loading translation progress...', 'hyx-translator-for-baidu-translate' ); ?></p>
|
||||
</div>
|
||||
</div>
|
||||
<?php
|
||||
}
|
||||
|
||||
public function queue_existing_translations() {
|
||||
if ( ! current_user_can( 'manage_options' ) ) {
|
||||
wp_die( esc_html__( 'You do not have permission to perform this action.', 'hyx-translator-for-baidu-translate' ) );
|
||||
}
|
||||
|
||||
check_admin_referer( 'htbd_queue_existing_translations' );
|
||||
$scope = isset( $_POST['scope'] ) ? sanitize_key( wp_unslash( $_POST['scope'] ) ) : 'all';
|
||||
if ( ! in_array( $scope, array( 'all', 'posts', 'terms' ), true ) ) {
|
||||
wp_die( esc_html__( 'Invalid translation scope.', 'hyx-translator-for-baidu-translate' ) );
|
||||
}
|
||||
$target_language = isset( $_POST['language'] ) ? sanitize_key( wp_unslash( $_POST['language'] ) ) : '';
|
||||
$languages = HTBD_Settings::target_languages();
|
||||
if ( '' !== $target_language ) {
|
||||
if ( ! in_array( $target_language, $languages, true ) ) {
|
||||
wp_die( esc_html__( 'Invalid translation request.', 'hyx-translator-for-baidu-translate' ) );
|
||||
}
|
||||
$languages = array( $target_language );
|
||||
}
|
||||
|
||||
$scheduled = 0;
|
||||
$next_run = time();
|
||||
$post_ids = array();
|
||||
$term_ids = array();
|
||||
|
||||
if ( 'all' === $scope || 'posts' === $scope ) {
|
||||
$post_ids = get_posts(
|
||||
array(
|
||||
'post_type' => array( 'post', 'page' ),
|
||||
'post_status' => 'publish',
|
||||
'posts_per_page' => -1,
|
||||
'fields' => 'ids',
|
||||
)
|
||||
);
|
||||
|
||||
}
|
||||
|
||||
if ( 'all' === $scope || 'terms' === $scope ) {
|
||||
$term_ids = get_terms(
|
||||
array(
|
||||
'taxonomy' => array( 'category', 'post_tag' ),
|
||||
'hide_empty' => false,
|
||||
'fields' => 'ids',
|
||||
)
|
||||
);
|
||||
if ( is_wp_error( $term_ids ) ) {
|
||||
$term_ids = array();
|
||||
}
|
||||
}
|
||||
|
||||
$this->update_translation_task( $post_ids, $term_ids, $languages );
|
||||
|
||||
foreach ( $post_ids as $post_id ) {
|
||||
foreach ( $languages as $target_language ) {
|
||||
$this->clear_scheduled_event( 'htbd_process_translation', array( $post_id, $target_language, true ) );
|
||||
wp_schedule_single_event( $next_run, 'htbd_process_translation', array( $post_id, $target_language, true ) );
|
||||
$next_run += 2;
|
||||
++$scheduled;
|
||||
}
|
||||
}
|
||||
|
||||
foreach ( $term_ids as $term_id ) {
|
||||
foreach ( $languages as $target_language ) {
|
||||
$this->clear_scheduled_event( 'htbd_process_term_translation', array( $term_id, $target_language, true ) );
|
||||
wp_schedule_single_event( $next_run, 'htbd_process_term_translation', array( $term_id, $target_language, true ) );
|
||||
$next_run += 2;
|
||||
++$scheduled;
|
||||
}
|
||||
}
|
||||
|
||||
wp_safe_redirect( add_query_arg( 'htbd_queued', $scheduled, admin_url( 'options-general.php?page=htbd' ) ) );
|
||||
exit;
|
||||
}
|
||||
|
||||
public function clear_translation_cache() {
|
||||
if ( ! current_user_can( 'manage_options' ) ) {
|
||||
wp_die( esc_html__( 'You do not have permission to perform this action.', 'hyx-translator-for-baidu-translate' ) );
|
||||
}
|
||||
|
||||
$target_language = isset( $_POST['language'] ) ? sanitize_key( wp_unslash( $_POST['language'] ) ) : '';
|
||||
if ( '' !== $target_language && ! in_array( $target_language, HTBD_Settings::target_languages(), true ) ) {
|
||||
wp_die( esc_html__( 'Invalid translation request.', 'hyx-translator-for-baidu-translate' ) );
|
||||
}
|
||||
|
||||
check_admin_referer( 'htbd_clear_translation_cache' . ( $target_language ? '_' . $target_language : '' ) );
|
||||
$deleted = ( new HTBD_Translation_Store() )->clear( $target_language );
|
||||
|
||||
if ( false === $deleted ) {
|
||||
wp_die( esc_html__( 'Failed to clear the translation cache.', 'hyx-translator-for-baidu-translate' ) );
|
||||
}
|
||||
|
||||
if ( '' === $target_language ) {
|
||||
wp_clear_scheduled_hook( 'htbd_process_translation' );
|
||||
wp_clear_scheduled_hook( 'htbd_process_term_translation' );
|
||||
wp_clear_scheduled_hook( 'htbd_process_seo_output_translation' );
|
||||
delete_option( 'htbd_translation_task' );
|
||||
delete_option( 'htbd_retranslation_batch' );
|
||||
}
|
||||
|
||||
wp_safe_redirect( add_query_arg( 'htbd_cache_cleared', $target_language ? $target_language : 1, admin_url( 'options-general.php?page=htbd' ) ) );
|
||||
exit;
|
||||
}
|
||||
|
||||
private function clear_scheduled_event( $hook, $args ) {
|
||||
while ( false !== ( $timestamp = wp_next_scheduled( $hook, $args ) ) ) {
|
||||
wp_unschedule_event( $timestamp, $hook, $args );
|
||||
}
|
||||
}
|
||||
|
||||
public function add_plugin_settings_link( $links ) {
|
||||
array_unshift( $links, '<a href="' . esc_url( admin_url( 'options-general.php?page=htbd' ) ) . '">' . esc_html__( 'Settings', 'hyx-translator-for-baidu-translate' ) . '</a>' );
|
||||
|
||||
return $links;
|
||||
}
|
||||
|
||||
public function translation_progress() {
|
||||
check_ajax_referer( 'htbd_translation_progress' );
|
||||
|
||||
if ( ! current_user_can( 'manage_options' ) ) {
|
||||
wp_send_json_error( array( 'message' => 'forbidden' ), 403 );
|
||||
}
|
||||
|
||||
$task = (array) get_option( 'htbd_translation_task', array() );
|
||||
$post_ids = array_values( array_filter( array_map( 'absint', (array) ( $task['post_ids'] ?? array() ) ) ) );
|
||||
$term_ids = array_values( array_filter( array_map( 'absint', (array) ( $task['term_ids'] ?? array() ) ) ) );
|
||||
$languages = array_values( array_filter( array_map( 'sanitize_key', (array) ( $task['target_languages'] ?? array() ) ) ) );
|
||||
$started_at = isset( $task['started_at'] ) ? sanitize_text_field( $task['started_at'] ) : '';
|
||||
$store = new HTBD_Translation_Store();
|
||||
$counts = $store->get_completed_item_counts( $languages, $post_ids, $term_ids, $started_at );
|
||||
$failures = $this->prepare_failed_items( $store->get_failed_items( $languages, $post_ids, $term_ids, $started_at ) );
|
||||
|
||||
$posts_total = count( $post_ids ) * count( $languages );
|
||||
$terms_total = count( $term_ids ) * count( $languages );
|
||||
$total = $posts_total + $terms_total;
|
||||
$posts_completed = min( $counts['posts'], $posts_total );
|
||||
$terms_completed = min( $counts['terms'], $terms_total );
|
||||
$completed = $posts_completed + $terms_completed;
|
||||
$percent = $total ? (int) floor( ( $completed / $total ) * 100 ) : 100;
|
||||
|
||||
wp_send_json_success(
|
||||
array(
|
||||
'posts_completed' => $posts_completed,
|
||||
'posts_total' => $posts_total,
|
||||
'terms_completed' => $terms_completed,
|
||||
'terms_total' => $terms_total,
|
||||
'completed' => $completed,
|
||||
'total' => $total,
|
||||
'percent' => $percent,
|
||||
'batch_started_at' => $started_at,
|
||||
'task_label' => $this->translation_task_label( $post_ids, $term_ids ),
|
||||
'failed_items' => $failures,
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
public function add_translation_column( $columns ) {
|
||||
$columns['htbd_translation'] = __( 'Translation status', 'hyx-translator-for-baidu-translate' );
|
||||
|
||||
return $columns;
|
||||
}
|
||||
|
||||
public function render_translation_column( $column, $post_id ) {
|
||||
if ( 'htbd_translation' !== $column ) {
|
||||
return;
|
||||
}
|
||||
|
||||
$store = new HTBD_Translation_Store();
|
||||
|
||||
foreach ( HTBD_Settings::target_languages() as $target_language ) {
|
||||
$status = $store->get_post_language_status( $post_id, $target_language );
|
||||
$translated = ! empty( $status['translated_fields'] );
|
||||
$button_text = $translated ? __( 'Retranslate', 'hyx-translator-for-baidu-translate' ) : __( 'Translate', 'hyx-translator-for-baidu-translate' );
|
||||
$action_url = wp_nonce_url(
|
||||
add_query_arg(
|
||||
array(
|
||||
'action' => 'htbd_translate_post',
|
||||
'post_id' => $post_id,
|
||||
'language' => $target_language,
|
||||
'force' => $translated ? '1' : '0',
|
||||
),
|
||||
admin_url( 'admin-post.php' )
|
||||
),
|
||||
'htbd_translate_post_' . $post_id . '_' . $target_language
|
||||
);
|
||||
|
||||
echo '<p><strong>' . esc_html( strtoupper( $target_language ) ) . '</strong>: ';
|
||||
echo $translated ? '<span style="color:#008a20">' . esc_html__( 'Translated', 'hyx-translator-for-baidu-translate' ) . '</span>' : '<span>' . esc_html__( 'Not translated', 'hyx-translator-for-baidu-translate' ) . '</span>';
|
||||
if ( $translated && ! empty( $status['last_translated_at'] ) ) {
|
||||
echo '<br><small>' . esc_html__( 'Last translated:', 'hyx-translator-for-baidu-translate' ) . ' ' . esc_html( get_date_from_gmt( $status['last_translated_at'], 'Y-m-d H:i' ) ) . '</small>';
|
||||
}
|
||||
echo '<br><a class="button button-small" href="' . esc_url( $action_url ) . '">' . esc_html( $button_text ) . '</a></p>';
|
||||
}
|
||||
}
|
||||
|
||||
public function queue_post_translation() {
|
||||
$post_id = isset( $_GET['post_id'] ) ? absint( $_GET['post_id'] ) : 0;
|
||||
$target_language = isset( $_GET['language'] ) ? sanitize_key( wp_unslash( $_GET['language'] ) ) : '';
|
||||
$force_refresh = ! empty( $_GET['force'] );
|
||||
|
||||
if ( ! $post_id || ! current_user_can( 'edit_post', $post_id ) || ! HTBD_Settings::is_supported_language( $target_language ) || $target_language === HTBD_Settings::get()['source_language'] ) {
|
||||
wp_die( esc_html__( 'Invalid translation request.', 'hyx-translator-for-baidu-translate' ) );
|
||||
}
|
||||
|
||||
check_admin_referer( 'htbd_translate_post_' . $post_id . '_' . $target_language );
|
||||
$this->update_translation_task( array( $post_id ), array(), array( $target_language ) );
|
||||
wp_schedule_single_event( time() + 5, 'htbd_process_translation', array( $post_id, $target_language, $force_refresh ) );
|
||||
|
||||
wp_safe_redirect( wp_get_referer() ? wp_get_referer() : admin_url( 'edit.php?post_type=' . get_post_type( $post_id ) ) );
|
||||
exit;
|
||||
}
|
||||
|
||||
public function retry_failed_translation() {
|
||||
$item_type = isset( $_GET['item_type'] ) ? sanitize_key( wp_unslash( $_GET['item_type'] ) ) : '';
|
||||
$item_id = isset( $_GET['item_id'] ) ? absint( $_GET['item_id'] ) : 0;
|
||||
$target_language = isset( $_GET['language'] ) ? sanitize_key( wp_unslash( $_GET['language'] ) ) : '';
|
||||
|
||||
if ( ! current_user_can( 'manage_options' ) || ! $item_id || ! in_array( $item_type, array( 'post', 'term' ), true ) || ! in_array( $target_language, HTBD_Settings::target_languages(), true ) ) {
|
||||
wp_die( esc_html__( 'Invalid translation request.', 'hyx-translator-for-baidu-translate' ) );
|
||||
}
|
||||
|
||||
check_admin_referer( 'htbd_retry_failed_translation_' . $item_type . '_' . $item_id . '_' . $target_language );
|
||||
|
||||
if ( 'post' === $item_type ) {
|
||||
wp_schedule_single_event( time() + 5, 'htbd_process_translation', array( $item_id, $target_language, true ) );
|
||||
} else {
|
||||
wp_schedule_single_event( time() + 5, 'htbd_process_term_translation', array( $item_id, $target_language, true ) );
|
||||
}
|
||||
|
||||
wp_safe_redirect( admin_url( 'options-general.php?page=htbd' ) );
|
||||
exit;
|
||||
}
|
||||
|
||||
private function prepare_failed_items( $failures ) {
|
||||
$items = array();
|
||||
|
||||
foreach ( (array) $failures as $failure ) {
|
||||
$context_parts = explode( ':', $failure['item_context'], 2 );
|
||||
$item_type = $context_parts[0] ?? '';
|
||||
$item_id = isset( $context_parts[1] ) ? absint( $context_parts[1] ) : 0;
|
||||
$target_language = sanitize_key( $failure['target_language'] ?? '' );
|
||||
|
||||
if ( ! $item_id || ! in_array( $item_type, array( 'post', 'term' ), true ) || '' === $target_language ) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if ( 'post' === $item_type ) {
|
||||
$name = get_the_title( $item_id );
|
||||
} else {
|
||||
$term = get_term( $item_id );
|
||||
$name = $term && ! is_wp_error( $term ) ? $term->name : '';
|
||||
}
|
||||
|
||||
if ( '' === $name ) {
|
||||
/* translators: %d: content item ID. */
|
||||
$name = sprintf( __( 'Content #%d', 'hyx-translator-for-baidu-translate' ), $item_id );
|
||||
}
|
||||
|
||||
$retry_url = wp_nonce_url(
|
||||
add_query_arg(
|
||||
array(
|
||||
'action' => 'htbd_retry_failed_translation',
|
||||
'item_type' => $item_type,
|
||||
'item_id' => $item_id,
|
||||
'language' => $target_language,
|
||||
),
|
||||
admin_url( 'admin-post.php' )
|
||||
),
|
||||
'htbd_retry_failed_translation_' . $item_type . '_' . $item_id . '_' . $target_language
|
||||
);
|
||||
|
||||
$items[] = array(
|
||||
'name' => $name,
|
||||
'language' => strtoupper( $target_language ),
|
||||
'retry_url' => $retry_url,
|
||||
);
|
||||
}
|
||||
|
||||
return $items;
|
||||
}
|
||||
|
||||
private function update_translation_task( $post_ids, $term_ids, $target_languages ) {
|
||||
update_option(
|
||||
'htbd_translation_task',
|
||||
array(
|
||||
'started_at' => current_time( 'mysql', true ),
|
||||
'post_ids' => array_values( array_filter( array_map( 'absint', (array) $post_ids ) ) ),
|
||||
'term_ids' => array_values( array_filter( array_map( 'absint', (array) $term_ids ) ) ),
|
||||
'target_languages' => array_values( array_filter( array_map( 'sanitize_key', (array) $target_languages ) ) ),
|
||||
),
|
||||
false
|
||||
);
|
||||
}
|
||||
|
||||
private function translation_task_label( $post_ids, $term_ids ) {
|
||||
if ( ! empty( $post_ids ) && ! empty( $term_ids ) ) {
|
||||
return __( 'Full translation', 'hyx-translator-for-baidu-translate' );
|
||||
}
|
||||
|
||||
if ( 1 === count( $post_ids ) ) {
|
||||
return __( 'Single post translation', 'hyx-translator-for-baidu-translate' );
|
||||
}
|
||||
|
||||
if ( ! empty( $post_ids ) ) {
|
||||
return __( 'Posts and pages translation', 'hyx-translator-for-baidu-translate' );
|
||||
}
|
||||
|
||||
if ( ! empty( $term_ids ) ) {
|
||||
return __( 'Categories and tags translation', 'hyx-translator-for-baidu-translate' );
|
||||
}
|
||||
|
||||
return __( 'No translation task', 'hyx-translator-for-baidu-translate' );
|
||||
}
|
||||
}
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
defined( 'ABSPATH' ) || exit;
|
||||
|
||||
class WPT_Baidu_Provider implements WPT_Translation_Provider {
|
||||
class HTBD_Baidu_Provider implements HTBD_Translation_Provider {
|
||||
private $app_id;
|
||||
private $secret_key;
|
||||
|
||||
@@ -19,7 +19,7 @@ class WPT_Baidu_Provider implements WPT_Translation_Provider {
|
||||
if ( '' === $this->app_id || '' === $this->secret_key ) {
|
||||
$this->log_request( $source_value, $source_language, $target_language, $context, 'missing_credentials' );
|
||||
|
||||
return WPT_Translation_Result::failure( 'missing_credentials', __( 'Baidu Translate credentials are not configured.', 'wp-btranslate' ) );
|
||||
return HTBD_Translation_Result::failure( 'missing_credentials', __( 'Baidu Translate credentials are not configured.', 'hyx-translator-for-baidu-translate' ) );
|
||||
}
|
||||
|
||||
$salt = (string) wp_rand( 100000, 999999 );
|
||||
@@ -42,27 +42,27 @@ class WPT_Baidu_Provider implements WPT_Translation_Provider {
|
||||
if ( is_wp_error( $response ) ) {
|
||||
$this->log_request( $source_value, $source_language, $target_language, $context, 'request_failed', $response->get_error_code() );
|
||||
|
||||
return WPT_Translation_Result::failure( 'request_failed', $response->get_error_message() );
|
||||
return HTBD_Translation_Result::failure( 'request_failed', $response->get_error_message() );
|
||||
}
|
||||
|
||||
$body = json_decode( wp_remote_retrieve_body( $response ), true );
|
||||
|
||||
if ( ! is_array( $body ) || empty( $body['trans_result'] ) || ! is_array( $body['trans_result'] ) ) {
|
||||
$error_code = isset( $body['error_code'] ) ? sanitize_key( $body['error_code'] ) : 'invalid_response';
|
||||
$error_message = isset( $body['error_msg'] ) ? sanitize_text_field( $body['error_msg'] ) : __( 'Baidu Translate returned an invalid response.', 'wp-btranslate' );
|
||||
$error_message = isset( $body['error_msg'] ) ? sanitize_text_field( $body['error_msg'] ) : __( 'Baidu Translate returned an invalid response.', 'hyx-translator-for-baidu-translate' );
|
||||
$this->log_request( $source_value, $source_language, $target_language, $context, 'failed', $error_code );
|
||||
|
||||
return WPT_Translation_Result::failure( $error_code, $error_message );
|
||||
return HTBD_Translation_Result::failure( $error_code, $error_message );
|
||||
}
|
||||
|
||||
$translated_parts = wp_list_pluck( $body['trans_result'], 'dst' );
|
||||
$this->log_request( $source_value, $source_language, $target_language, $context, 'complete' );
|
||||
|
||||
return WPT_Translation_Result::success( implode( "\n", $translated_parts ) );
|
||||
return HTBD_Translation_Result::success( implode( "\n", $translated_parts ) );
|
||||
}
|
||||
|
||||
private function log_request( $source_value, $source_language, $target_language, $context, $status, $error_code = '' ) {
|
||||
$settings = WPT_Settings::get();
|
||||
$settings = HTBD_Settings::get();
|
||||
|
||||
if ( empty( $settings['log_requests'] ) ) {
|
||||
return;
|
||||
@@ -73,12 +73,12 @@ class WPT_Baidu_Provider implements WPT_Translation_Provider {
|
||||
'source_language' => sanitize_key( $source_language ),
|
||||
'target_language' => sanitize_key( $target_language ),
|
||||
'context' => sanitize_key( $context ),
|
||||
'source_fingerprint' => WPT_Translation_Identity::source_fingerprint( (string) $source_value ),
|
||||
'source_fingerprint' => HTBD_Translation_Identity::source_fingerprint( (string) $source_value ),
|
||||
'source_length' => strlen( (string) $source_value ),
|
||||
'status' => sanitize_key( $status ),
|
||||
'error_code' => sanitize_key( $error_code ),
|
||||
);
|
||||
|
||||
error_log( 'WPT translation request: ' . wp_json_encode( $entry ) );
|
||||
do_action( 'htbd_translation_request_logged', $entry );
|
||||
}
|
||||
}
|
||||
@@ -2,20 +2,20 @@
|
||||
|
||||
defined( 'ABSPATH' ) || exit;
|
||||
|
||||
class WPT_Content_Controller {
|
||||
class HTBD_Content_Controller {
|
||||
private $store;
|
||||
private $router;
|
||||
|
||||
public function __construct( WPT_Translation_Store $store, WPT_Language_Router $router ) {
|
||||
public function __construct( HTBD_Translation_Store $store, HTBD_Language_Router $router ) {
|
||||
$this->store = $store;
|
||||
$this->router = $router;
|
||||
}
|
||||
|
||||
public function register() {
|
||||
add_action( 'save_post', array( $this, 'schedule_post_translation' ), 20, 3 );
|
||||
add_action( 'wpt_process_translation', array( $this, 'translate_post' ), 10, 3 );
|
||||
add_action( 'wpt_process_term_translation', array( $this, 'translate_term' ), 10, 3 );
|
||||
add_action( 'wpt_process_seo_output_translation', array( $this, 'translate_seo_output' ), 10, 4 );
|
||||
add_action( 'htbd_process_translation', array( $this, 'translate_post' ), 10, 3 );
|
||||
add_action( 'htbd_process_term_translation', array( $this, 'translate_term' ), 10, 3 );
|
||||
add_action( 'htbd_process_seo_output_translation', array( $this, 'translate_seo_output' ), 10, 4 );
|
||||
add_action( 'created_term', array( $this, 'schedule_term_translation' ), 20, 3 );
|
||||
add_action( 'edited_term', array( $this, 'schedule_term_translation' ), 20, 3 );
|
||||
add_filter( 'the_title', array( $this, 'translate_title' ), 20, 2 );
|
||||
@@ -43,28 +43,28 @@ class WPT_Content_Controller {
|
||||
}
|
||||
|
||||
public function schedule_post_translation( $post_id, $post, $update ) {
|
||||
if ( wp_is_post_revision( $post_id ) || wp_is_post_autosave( $post_id ) || ! in_array( $post->post_type, array( 'post', 'page' ), true ) ) {
|
||||
if ( wp_is_post_revision( $post_id ) || wp_is_post_autosave( $post_id ) || 'publish' !== $post->post_status || ! in_array( $post->post_type, array( 'post', 'page' ), true ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
foreach ( WPT_Settings::target_languages() as $target_language ) {
|
||||
if ( ! wp_next_scheduled( 'wpt_process_translation', array( $post_id, $target_language ) ) ) {
|
||||
wp_schedule_single_event( time() + 30, 'wpt_process_translation', array( $post_id, $target_language ) );
|
||||
foreach ( HTBD_Settings::target_languages() as $target_language ) {
|
||||
if ( ! wp_next_scheduled( 'htbd_process_translation', array( $post_id, $target_language ) ) ) {
|
||||
wp_schedule_single_event( time() + 10, 'htbd_process_translation', array( $post_id, $target_language ) );
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public function translate_post( $post_id, $target_language, $force_refresh = false ) {
|
||||
$post = get_post( $post_id );
|
||||
$settings = WPT_Settings::get();
|
||||
$settings = HTBD_Settings::get();
|
||||
|
||||
if ( ! $post || ! WPT_Settings::is_supported_language( $target_language ) ) {
|
||||
if ( ! $post || 'publish' !== $post->post_status || ! HTBD_Settings::is_supported_language( $target_language ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
$service = new WPT_Translation_Service(
|
||||
$service = new HTBD_Translation_Service(
|
||||
$this->store,
|
||||
new WPT_Baidu_Provider( $settings['baidu_app_id'], $settings['baidu_secret_key'] )
|
||||
new HTBD_Baidu_Provider( $settings['baidu_app_id'], $settings['baidu_secret_key'] )
|
||||
);
|
||||
|
||||
$fields = array(
|
||||
@@ -91,7 +91,7 @@ class WPT_Content_Controller {
|
||||
if ( '' !== $value ) {
|
||||
$context = 'post:' . $post_id . ':' . $field;
|
||||
if ( 'post_content' === $field ) {
|
||||
( new WPT_Content_Translator( $service ) )->translate( $value, $settings['source_language'], $target_language, $context, $force_refresh );
|
||||
( new HTBD_Content_Translator( $service ) )->translate( $value, $settings['source_language'], $target_language, $context, $force_refresh );
|
||||
} else {
|
||||
$service->get_or_translate( $value, $settings['source_language'], $target_language, $context, $force_refresh );
|
||||
}
|
||||
@@ -107,24 +107,24 @@ class WPT_Content_Controller {
|
||||
return;
|
||||
}
|
||||
|
||||
foreach ( WPT_Settings::target_languages() as $target_language ) {
|
||||
if ( ! wp_next_scheduled( 'wpt_process_term_translation', array( $term_id, $target_language, false ) ) ) {
|
||||
wp_schedule_single_event( time() + 30, 'wpt_process_term_translation', array( $term_id, $target_language, false ) );
|
||||
foreach ( HTBD_Settings::target_languages() as $target_language ) {
|
||||
if ( ! wp_next_scheduled( 'htbd_process_term_translation', array( $term_id, $target_language, false ) ) ) {
|
||||
wp_schedule_single_event( time() + 10, 'htbd_process_term_translation', array( $term_id, $target_language, false ) );
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public function translate_term( $term_id, $target_language, $force_refresh = false ) {
|
||||
$term = get_term( $term_id );
|
||||
$settings = WPT_Settings::get();
|
||||
$settings = HTBD_Settings::get();
|
||||
|
||||
if ( ! $term || is_wp_error( $term ) || ! in_array( $term->taxonomy, array( 'category', 'post_tag' ), true ) || ! WPT_Settings::is_supported_language( $target_language ) ) {
|
||||
if ( ! $term || is_wp_error( $term ) || ! in_array( $term->taxonomy, array( 'category', 'post_tag' ), true ) || ! HTBD_Settings::is_supported_language( $target_language ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
$service = new WPT_Translation_Service(
|
||||
$service = new HTBD_Translation_Service(
|
||||
$this->store,
|
||||
new WPT_Baidu_Provider( $settings['baidu_app_id'], $settings['baidu_secret_key'] )
|
||||
new HTBD_Baidu_Provider( $settings['baidu_app_id'], $settings['baidu_secret_key'] )
|
||||
);
|
||||
|
||||
if ( '' !== $term->name ) {
|
||||
@@ -136,43 +136,65 @@ class WPT_Content_Controller {
|
||||
}
|
||||
|
||||
public function translate_seo_output( $post_id, $source_value, $target_language, $context ) {
|
||||
$settings = WPT_Settings::get();
|
||||
$settings = HTBD_Settings::get();
|
||||
|
||||
if ( ! get_post( $post_id ) || ! WPT_Settings::is_supported_language( $target_language ) || '' === $source_value ) {
|
||||
if ( ! get_post( $post_id ) || ! HTBD_Settings::is_supported_language( $target_language ) || '' === $source_value ) {
|
||||
return;
|
||||
}
|
||||
|
||||
$service = new WPT_Translation_Service(
|
||||
$service = new HTBD_Translation_Service(
|
||||
$this->store,
|
||||
new WPT_Baidu_Provider( $settings['baidu_app_id'], $settings['baidu_secret_key'] )
|
||||
new HTBD_Baidu_Provider( $settings['baidu_app_id'], $settings['baidu_secret_key'] )
|
||||
);
|
||||
|
||||
$service->get_or_translate( $source_value, $settings['source_language'], $target_language, $context );
|
||||
}
|
||||
|
||||
public function translate_title( $title, $post_id ) {
|
||||
return $this->translated_value( $title, 'post:' . $post_id . ':post_title' );
|
||||
$post_id = absint( $post_id );
|
||||
if ( ! $post_id && is_singular() ) {
|
||||
$post_id = get_queried_object_id();
|
||||
}
|
||||
|
||||
if ( ! $post_id ) {
|
||||
return $title;
|
||||
}
|
||||
|
||||
$source_title = get_post_field( 'post_title', $post_id, 'raw' );
|
||||
if ( ! is_string( $source_title ) || '' === $source_title ) {
|
||||
return $title;
|
||||
}
|
||||
|
||||
$translated_title = $this->translated_value( $source_title, 'post:' . $post_id . ':post_title' );
|
||||
|
||||
return $translated_title !== $source_title ? esc_html( $translated_title ) : $title;
|
||||
}
|
||||
|
||||
public function translate_content( $content ) {
|
||||
$post_id = get_the_ID();
|
||||
if ( ! $post_id || false === strpos( $content, '<' ) ) {
|
||||
return $this->translated_value( $content, 'post:' . $post_id . ':post_content' );
|
||||
return $this->escaped_translated_value( $content, 'post:' . $post_id . ':post_content' );
|
||||
}
|
||||
|
||||
$segments = WPT_Content_Translator::segments( $content );
|
||||
$segments = HTBD_Content_Translator::segments( $content );
|
||||
if ( false === $segments ) {
|
||||
return $content;
|
||||
}
|
||||
|
||||
$text_index = 0;
|
||||
$text_index = 0;
|
||||
$protected_depth = 0;
|
||||
foreach ( $segments as $index => $segment ) {
|
||||
if ( WPT_Content_Translator::is_tag( $segment ) || '' === trim( $segment ) ) {
|
||||
if ( HTBD_Content_Translator::is_tag( $segment ) ) {
|
||||
$protected_depth = HTBD_Content_Translator::protected_depth_after_tag( $segment, $protected_depth );
|
||||
continue;
|
||||
}
|
||||
|
||||
$whitespace = WPT_Content_Translator::surrounding_whitespace( $segment );
|
||||
$translated = $this->translated_value( $whitespace['text'], 'post:' . $post_id . ':post_content:text:' . $text_index );
|
||||
if ( 0 < $protected_depth || '' === trim( $segment ) ) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$whitespace = HTBD_Content_Translator::surrounding_whitespace( $segment );
|
||||
$translated = $this->escaped_translated_value( $whitespace['text'], 'post:' . $post_id . ':post_content:text:' . $text_index );
|
||||
$segments[ $index ] = $whitespace['leading'] . $translated . $whitespace['trailing'];
|
||||
++$text_index;
|
||||
}
|
||||
@@ -181,7 +203,7 @@ class WPT_Content_Controller {
|
||||
}
|
||||
|
||||
public function translate_excerpt( $excerpt, $post ) {
|
||||
return $this->translated_value( $excerpt, 'post:' . $post->ID . ':post_excerpt' );
|
||||
return $this->kses_translated_value( $excerpt, 'post:' . $post->ID . ':post_excerpt' );
|
||||
}
|
||||
|
||||
public function translate_image_alt( $attributes, $attachment ) {
|
||||
@@ -195,11 +217,11 @@ class WPT_Content_Controller {
|
||||
}
|
||||
|
||||
public function translate_term_title( $term_name, $term ) {
|
||||
return $this->translated_value( $term_name, 'term:' . $term->term_id . ':name' );
|
||||
return $this->escaped_translated_value( $term_name, 'term:' . $term->term_id . ':name' );
|
||||
}
|
||||
|
||||
public function translate_term_description( $description, $term_id, $taxonomy ) {
|
||||
return $this->translated_value( $description, 'term:' . $term_id . ':description' );
|
||||
return $this->kses_translated_value( $description, 'term:' . $term_id . ':description' );
|
||||
}
|
||||
|
||||
public function translate_document_title_parts( $parts ) {
|
||||
@@ -258,8 +280,8 @@ class WPT_Content_Controller {
|
||||
}
|
||||
|
||||
$translated_term = clone $term;
|
||||
$translated_term->name = $this->translated_value( $term->name, 'term:' . $term->term_id . ':name' );
|
||||
$translated_term->description = $this->translated_value( $term->description, 'term:' . $term->term_id . ':description' );
|
||||
$translated_term->name = sanitize_text_field( $this->translated_value( $term->name, 'term:' . $term->term_id . ':name' ) );
|
||||
$translated_term->description = $this->kses_translated_value( $term->description, 'term:' . $term->term_id . ':description' );
|
||||
|
||||
return $translated_term;
|
||||
}
|
||||
@@ -306,22 +328,34 @@ class WPT_Content_Controller {
|
||||
}
|
||||
|
||||
private function translated_value( $source_value, $context ) {
|
||||
$settings = WPT_Settings::get();
|
||||
$settings = HTBD_Settings::get();
|
||||
$target_language = $this->router->current_language();
|
||||
|
||||
if ( $target_language === $settings['source_language'] || '' === $source_value ) {
|
||||
return $source_value;
|
||||
}
|
||||
|
||||
$provider = new WPT_Baidu_Provider( $settings['baidu_app_id'], $settings['baidu_secret_key'] );
|
||||
$identity_key = WPT_Translation_Identity::key( $source_value, $settings['source_language'], $target_language, $context, $provider->get_version() );
|
||||
$provider = new HTBD_Baidu_Provider( $settings['baidu_app_id'], $settings['baidu_secret_key'] );
|
||||
$identity_key = HTBD_Translation_Identity::key( $source_value, $settings['source_language'], $target_language, $context, $provider->get_version() );
|
||||
$translation = $this->store->find_valid( $identity_key );
|
||||
|
||||
return ! empty( $translation['translated_value'] ) ? $translation['translated_value'] : $source_value;
|
||||
}
|
||||
|
||||
private function escaped_translated_value( $source_value, $context ) {
|
||||
$translated_value = $this->translated_value( $source_value, $context );
|
||||
|
||||
return esc_html( $translated_value );
|
||||
}
|
||||
|
||||
private function kses_translated_value( $source_value, $context ) {
|
||||
$translated_value = $this->translated_value( $source_value, $context );
|
||||
|
||||
return wp_kses_post( $translated_value );
|
||||
}
|
||||
|
||||
private function translated_or_schedule_seo_output( $source_value, $post_id, $field_context ) {
|
||||
$settings = WPT_Settings::get();
|
||||
$settings = HTBD_Settings::get();
|
||||
$target_language = $this->router->current_language();
|
||||
$context = 'post:' . $post_id . ':' . $field_context;
|
||||
|
||||
@@ -335,8 +369,8 @@ class WPT_Content_Controller {
|
||||
}
|
||||
|
||||
$event_args = array( $post_id, $source_value, $target_language, $context );
|
||||
if ( ! wp_next_scheduled( 'wpt_process_seo_output_translation', $event_args ) ) {
|
||||
wp_schedule_single_event( time() + 5, 'wpt_process_seo_output_translation', $event_args );
|
||||
if ( ! wp_next_scheduled( 'htbd_process_seo_output_translation', $event_args ) ) {
|
||||
wp_schedule_single_event( time() + 5, 'htbd_process_seo_output_translation', $event_args );
|
||||
}
|
||||
|
||||
return $source_value;
|
||||
@@ -2,22 +2,28 @@
|
||||
|
||||
defined( 'ABSPATH' ) || exit;
|
||||
|
||||
class WPT_Content_Translator {
|
||||
class HTBD_Content_Translator {
|
||||
private $service;
|
||||
|
||||
public function __construct( WPT_Translation_Service $service ) {
|
||||
public function __construct( HTBD_Translation_Service $service ) {
|
||||
$this->service = $service;
|
||||
}
|
||||
|
||||
public function translate( $content, $source_language, $target_language, $context, $force_refresh = false ) {
|
||||
$segments = self::segments( $content );
|
||||
if ( false === $segments ) {
|
||||
return WPT_Translation_Result::failure( 'content_parse_failed', 'Unable to protect links before translation.' );
|
||||
return HTBD_Translation_Result::failure( 'content_parse_failed', 'Unable to protect links before translation.' );
|
||||
}
|
||||
|
||||
$text_index = 0;
|
||||
$text_index = 0;
|
||||
$protected_depth = 0;
|
||||
foreach ( $segments as $segment ) {
|
||||
if ( self::is_tag( $segment ) || '' === trim( $segment ) ) {
|
||||
if ( self::is_tag( $segment ) ) {
|
||||
$protected_depth = self::protected_depth_after_tag( $segment, $protected_depth );
|
||||
continue;
|
||||
}
|
||||
|
||||
if ( 0 < $protected_depth || '' === trim( $segment ) ) {
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -29,7 +35,7 @@ class WPT_Content_Translator {
|
||||
++$text_index;
|
||||
}
|
||||
|
||||
return WPT_Translation_Result::success( $content );
|
||||
return HTBD_Translation_Result::success( $content );
|
||||
}
|
||||
|
||||
public static function segments( $content ) {
|
||||
@@ -48,6 +54,18 @@ class WPT_Content_Translator {
|
||||
return 1 === preg_match( '/^<[^>]+>$/s', $segment );
|
||||
}
|
||||
|
||||
public static function protected_depth_after_tag( $segment, $protected_depth ) {
|
||||
if ( 1 !== preg_match( '/^<\s*(\/?)\s*(?:pre|code)\b[^>]*>/i', $segment, $matches ) ) {
|
||||
return $protected_depth;
|
||||
}
|
||||
|
||||
if ( '/' === $matches[1] ) {
|
||||
return max( 0, $protected_depth - 1 );
|
||||
}
|
||||
|
||||
return str_ends_with( rtrim( $segment ), '/>' ) ? $protected_depth : $protected_depth + 1;
|
||||
}
|
||||
|
||||
public static function surrounding_whitespace( $segment ) {
|
||||
preg_match( '/^(\s*)(.*?)(\s*)$/s', $segment, $matches );
|
||||
|
||||
@@ -0,0 +1,344 @@
|
||||
<?php
|
||||
|
||||
defined( 'ABSPATH' ) || exit;
|
||||
|
||||
class HTBD_Language_Router {
|
||||
private $request_language = '';
|
||||
|
||||
public function register() {
|
||||
$this->redirect_admin_request_to_source();
|
||||
add_filter( 'query_vars', array( $this, 'query_vars' ) );
|
||||
add_filter( 'do_parse_request', array( $this, 'strip_language_prefix' ), 1, 3 );
|
||||
add_action( 'parse_request', array( $this, 'resolve_domain_language' ) );
|
||||
add_filter( 'home_url', array( $this, 'localize_home_url' ), 20, 4 );
|
||||
add_filter( 'site_url', array( $this, 'localize_comment_post_url' ), 20, 4 );
|
||||
add_filter( 'allowed_redirect_hosts', array( $this, 'allow_language_redirect_hosts' ) );
|
||||
add_filter( 'comment_post_redirect', array( $this, 'localize_comment_redirect' ) );
|
||||
}
|
||||
|
||||
private function redirect_admin_request_to_source() {
|
||||
if ( empty( $_SERVER['REQUEST_URI'] ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
$request_uri = sanitize_text_field( wp_unslash( $_SERVER['REQUEST_URI'] ) );
|
||||
$path = wp_parse_url( $request_uri, PHP_URL_PATH );
|
||||
$query = wp_parse_url( $request_uri, PHP_URL_QUERY );
|
||||
|
||||
if ( ! is_string( $path ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
$relative_path = $this->relative_path( $path );
|
||||
$segments = explode( '/', $relative_path, 2 );
|
||||
$language = sanitize_key( $segments[0] );
|
||||
$uses_language_path = HTBD_Settings::is_routing_mode_enabled( 'subdirectory' ) && in_array( $language, HTBD_Settings::subdirectory_languages(), true );
|
||||
$uses_language_host = HTBD_Settings::is_routing_mode_enabled( 'domain' ) && $this->request_uses_bound_domain();
|
||||
|
||||
if ( ! $uses_language_path && ! $uses_language_host ) {
|
||||
return;
|
||||
}
|
||||
|
||||
if ( $uses_language_path ) {
|
||||
$relative_path = isset( $segments[1] ) ? $segments[1] : '';
|
||||
}
|
||||
|
||||
if ( ! preg_match( '#^(?:wp-login\.php|wp-admin)(?:/|$)#', $relative_path ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
$source_url = untrailingslashit( (string) get_option( 'home', '' ) );
|
||||
if ( '' === $source_url ) {
|
||||
return;
|
||||
}
|
||||
|
||||
$redirect_url = $source_url . '/' . ltrim( $relative_path, '/' );
|
||||
if ( is_string( $query ) && '' !== $query ) {
|
||||
$redirect_url .= '?' . $query;
|
||||
}
|
||||
|
||||
wp_redirect( esc_url_raw( $redirect_url ), 302, 'HTBD' );
|
||||
exit;
|
||||
}
|
||||
|
||||
public function register_rewrite_rules() {
|
||||
// Subdirectory requests reuse WordPress's existing rewrite rules after their language prefix is removed.
|
||||
}
|
||||
|
||||
public function query_vars( $query_vars ) {
|
||||
$query_vars[] = 'htbd_language';
|
||||
|
||||
return $query_vars;
|
||||
}
|
||||
|
||||
public function strip_language_prefix( $do_parse_request, $wp, $extra_query_vars ) {
|
||||
if ( ! $do_parse_request || ! HTBD_Settings::is_routing_mode_enabled( 'subdirectory' ) || empty( $_SERVER['REQUEST_URI'] ) ) {
|
||||
return $do_parse_request;
|
||||
}
|
||||
|
||||
$request_uri = sanitize_text_field( wp_unslash( $_SERVER['REQUEST_URI'] ) );
|
||||
$path = wp_parse_url( $request_uri, PHP_URL_PATH );
|
||||
$query = wp_parse_url( $request_uri, PHP_URL_QUERY );
|
||||
|
||||
if ( ! is_string( $path ) ) {
|
||||
return $do_parse_request;
|
||||
}
|
||||
|
||||
$home_path = $this->home_path();
|
||||
$relative = ltrim( (string) preg_replace( '#^' . preg_quote( $home_path, '#' ) . '(?=/|$)#', '', $path, 1 ), '/' );
|
||||
$segments = explode( '/', $relative, 2 );
|
||||
$language = sanitize_key( $segments[0] );
|
||||
|
||||
if ( ! in_array( $language, HTBD_Settings::subdirectory_languages(), true ) ) {
|
||||
return $do_parse_request;
|
||||
}
|
||||
|
||||
$this->request_language = $language;
|
||||
$remaining_path = isset( $segments[1] ) ? $segments[1] : '';
|
||||
$source_path = '/' . ( '' !== $home_path ? trim( $home_path, '/' ) . '/' : '' ) . ltrim( $remaining_path, '/' );
|
||||
$_SERVER['REQUEST_URI'] = $source_path . ( is_string( $query ) && '' !== $query ? '?' . $query : '' );
|
||||
|
||||
return $do_parse_request;
|
||||
}
|
||||
|
||||
public function resolve_domain_language( $wp ) {
|
||||
$settings = HTBD_Settings::get();
|
||||
$host = isset( $_SERVER['HTTP_HOST'] ) ? HTBD_Settings::normalize_domain( sanitize_text_field( wp_unslash( $_SERVER['HTTP_HOST'] ) ) ) : '';
|
||||
$bindings = (array) $settings['domain_bindings'];
|
||||
|
||||
if ( '' !== $this->request_language ) {
|
||||
$wp->query_vars['htbd_language'] = $this->request_language;
|
||||
}
|
||||
|
||||
if ( HTBD_Settings::is_routing_mode_enabled( 'domain' ) && '' !== $host && isset( $bindings[ $host ] ) && HTBD_Settings::is_supported_language( $bindings[ $host ] ) ) {
|
||||
if ( '' !== $this->request_language ) {
|
||||
$wp->query_vars = array( 'error' => '404' );
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$wp->query_vars['htbd_language'] = sanitize_key( $bindings[ $host ] );
|
||||
}
|
||||
}
|
||||
|
||||
public function current_language() {
|
||||
$settings = HTBD_Settings::get();
|
||||
$language = get_query_var( 'htbd_language' );
|
||||
|
||||
if ( $language && HTBD_Settings::is_supported_language( $language ) ) {
|
||||
return sanitize_key( $language );
|
||||
}
|
||||
|
||||
return sanitize_key( $settings['source_language'] );
|
||||
}
|
||||
|
||||
public function localized_url( $url, $language = '' ) {
|
||||
$settings = HTBD_Settings::get();
|
||||
$language = '' === $language ? $this->current_language() : sanitize_key( $language );
|
||||
|
||||
if ( $language === $settings['source_language'] || ! HTBD_Settings::is_supported_language( $language ) ) {
|
||||
return $url;
|
||||
}
|
||||
|
||||
if ( HTBD_Settings::is_routing_mode_enabled( 'domain' ) && $this->request_uses_domain( $language ) ) {
|
||||
return $this->localized_domain_url( $url, $language );
|
||||
}
|
||||
|
||||
if ( ! HTBD_Settings::is_routing_mode_enabled( 'subdirectory' ) ) {
|
||||
return $url;
|
||||
}
|
||||
|
||||
$parts = wp_parse_url( $url );
|
||||
if ( ! is_array( $parts ) || empty( $parts['path'] ) ) {
|
||||
return $url;
|
||||
}
|
||||
$parts = $this->source_origin_parts( $parts );
|
||||
|
||||
$scheme = isset( $parts['scheme'] ) ? $parts['scheme'] . '://' : '//';
|
||||
$host = isset( $parts['host'] ) ? $parts['host'] : '';
|
||||
$port = isset( $parts['port'] ) ? ':' . $parts['port'] : '';
|
||||
$path = $this->localized_path( $parts['path'], $language );
|
||||
$query = isset( $parts['query'] ) ? '?' . $parts['query'] : '';
|
||||
$fragment = isset( $parts['fragment'] ) ? '#' . $parts['fragment'] : '';
|
||||
|
||||
return $scheme . $host . $port . $path . $query . $fragment;
|
||||
}
|
||||
|
||||
public function localize_home_url( $url, $path, $scheme, $blog_id ) {
|
||||
return $this->localized_url( $url );
|
||||
}
|
||||
|
||||
public function localize_comment_post_url( $url, $path, $scheme, $blog_id ) {
|
||||
if ( 'wp-comments-post.php' !== basename( (string) wp_parse_url( $url, PHP_URL_PATH ) ) ) {
|
||||
return $url;
|
||||
}
|
||||
|
||||
$language = $this->current_language();
|
||||
|
||||
if ( ! HTBD_Settings::is_routing_mode_enabled( 'domain' ) || ! $this->request_uses_domain( $language ) ) {
|
||||
return $url;
|
||||
}
|
||||
|
||||
return $this->localized_domain_url( $url, $language );
|
||||
}
|
||||
|
||||
public function allow_language_redirect_hosts( $hosts ) {
|
||||
if ( ! HTBD_Settings::is_routing_mode_enabled( 'domain' ) ) {
|
||||
return $hosts;
|
||||
}
|
||||
|
||||
foreach ( (array) HTBD_Settings::get()['domain_bindings'] as $domain => $language ) {
|
||||
$domain = HTBD_Settings::normalize_domain( $domain );
|
||||
|
||||
if ( '' !== $domain && HTBD_Settings::is_supported_language( $language ) ) {
|
||||
$hosts[] = $domain;
|
||||
}
|
||||
}
|
||||
|
||||
return array_values( array_unique( $hosts ) );
|
||||
}
|
||||
|
||||
public function localize_comment_redirect( $location ) {
|
||||
$referer = wp_get_raw_referer();
|
||||
|
||||
if ( ! is_string( $referer ) || '' === $referer ) {
|
||||
return $location;
|
||||
}
|
||||
|
||||
$language = $this->language_from_url( $referer );
|
||||
|
||||
if ( '' === $language ) {
|
||||
return $location;
|
||||
}
|
||||
|
||||
$referer_host = HTBD_Settings::normalize_domain( (string) wp_parse_url( $referer, PHP_URL_HOST ) );
|
||||
$bindings = (array) HTBD_Settings::get()['domain_bindings'];
|
||||
|
||||
if ( HTBD_Settings::is_routing_mode_enabled( 'domain' ) && isset( $bindings[ $referer_host ] ) ) {
|
||||
return $this->localized_domain_url( $location, $language );
|
||||
}
|
||||
|
||||
return $this->localized_url( $location, $language );
|
||||
}
|
||||
|
||||
private function localized_domain_url( $url, $language ) {
|
||||
$domain = array_search( $language, (array) HTBD_Settings::get()['domain_bindings'], true );
|
||||
$parts = wp_parse_url( $url );
|
||||
|
||||
if ( false === $domain || ! is_array( $parts ) || empty( $parts['host'] ) ) {
|
||||
return $url;
|
||||
}
|
||||
|
||||
$scheme = isset( $parts['scheme'] ) ? $parts['scheme'] : 'https';
|
||||
$path = $this->source_path( isset( $parts['path'] ) ? $parts['path'] : '/' );
|
||||
$query = isset( $parts['query'] ) ? '?' . $parts['query'] : '';
|
||||
$fragment = isset( $parts['fragment'] ) ? '#' . $parts['fragment'] : '';
|
||||
|
||||
return $scheme . '://' . $domain . $path . $query . $fragment;
|
||||
}
|
||||
|
||||
private function language_from_url( $url ) {
|
||||
$parts = wp_parse_url( $url );
|
||||
|
||||
if ( ! is_array( $parts ) || empty( $parts['host'] ) ) {
|
||||
return '';
|
||||
}
|
||||
|
||||
$settings = HTBD_Settings::get();
|
||||
$host = HTBD_Settings::normalize_domain( $parts['host'] . ( isset( $parts['port'] ) ? ':' . $parts['port'] : '' ) );
|
||||
$bindings = (array) $settings['domain_bindings'];
|
||||
|
||||
if ( HTBD_Settings::is_routing_mode_enabled( 'domain' ) && isset( $bindings[ $host ] ) && HTBD_Settings::is_supported_language( $bindings[ $host ] ) ) {
|
||||
return sanitize_key( $bindings[ $host ] );
|
||||
}
|
||||
|
||||
$home_host = HTBD_Settings::normalize_domain( (string) wp_parse_url( (string) get_option( 'home', '' ), PHP_URL_HOST ) );
|
||||
if ( $host !== $home_host || ! HTBD_Settings::is_routing_mode_enabled( 'subdirectory' ) ) {
|
||||
return '';
|
||||
}
|
||||
|
||||
$path = isset( $parts['path'] ) ? $parts['path'] : '/';
|
||||
$segments = explode( '/', $this->relative_path( $path ), 2 );
|
||||
$language = sanitize_key( $segments[0] );
|
||||
$languages = HTBD_Settings::subdirectory_languages();
|
||||
|
||||
return in_array( $language, $languages, true ) ? $language : '';
|
||||
}
|
||||
|
||||
private function request_uses_domain( $language ) {
|
||||
$host = isset( $_SERVER['HTTP_HOST'] ) ? HTBD_Settings::normalize_domain( sanitize_text_field( wp_unslash( $_SERVER['HTTP_HOST'] ) ) ) : '';
|
||||
$bindings = (array) HTBD_Settings::get()['domain_bindings'];
|
||||
|
||||
return '' !== $host && isset( $bindings[ $host ] ) && sanitize_key( $bindings[ $host ] ) === $language;
|
||||
}
|
||||
|
||||
private function request_uses_bound_domain() {
|
||||
$host = isset( $_SERVER['HTTP_HOST'] ) ? HTBD_Settings::normalize_domain( sanitize_text_field( wp_unslash( $_SERVER['HTTP_HOST'] ) ) ) : '';
|
||||
$bindings = (array) HTBD_Settings::get()['domain_bindings'];
|
||||
|
||||
return '' !== $host && isset( $bindings[ $host ] ) && HTBD_Settings::is_supported_language( $bindings[ $host ] );
|
||||
}
|
||||
|
||||
private function source_origin_parts( $parts ) {
|
||||
$host = isset( $_SERVER['HTTP_HOST'] ) ? HTBD_Settings::normalize_domain( sanitize_text_field( wp_unslash( $_SERVER['HTTP_HOST'] ) ) ) : '';
|
||||
$bindings = (array) HTBD_Settings::get()['domain_bindings'];
|
||||
|
||||
if ( '' === $host || ! isset( $bindings[ $host ] ) ) {
|
||||
return $parts;
|
||||
}
|
||||
|
||||
$home_parts = wp_parse_url( (string) get_option( 'home', '' ) );
|
||||
if ( ! is_array( $home_parts ) || empty( $home_parts['host'] ) ) {
|
||||
return $parts;
|
||||
}
|
||||
|
||||
$parts['scheme'] = isset( $home_parts['scheme'] ) ? $home_parts['scheme'] : 'https';
|
||||
$parts['host'] = $home_parts['host'];
|
||||
|
||||
if ( isset( $home_parts['port'] ) ) {
|
||||
$parts['port'] = $home_parts['port'];
|
||||
} else {
|
||||
unset( $parts['port'] );
|
||||
}
|
||||
|
||||
return $parts;
|
||||
}
|
||||
|
||||
private function localized_path( $path, $language ) {
|
||||
$home_path = $this->home_path();
|
||||
$relative = $this->relative_path( $path );
|
||||
$languages = array_map( 'preg_quote', HTBD_Settings::target_languages() );
|
||||
|
||||
if ( ! empty( $languages ) ) {
|
||||
$relative = preg_replace( '#^(?:' . implode( '|', $languages ) . ')(?=/|$)#', $language, $relative, 1 );
|
||||
}
|
||||
|
||||
if ( $language !== strtok( $relative, '/' ) ) {
|
||||
$relative = $language . '/' . $relative;
|
||||
}
|
||||
|
||||
return '/' . ( '' !== $home_path ? trim( $home_path, '/' ) . '/' : '' ) . ltrim( $relative, '/' );
|
||||
}
|
||||
|
||||
private function source_path( $path ) {
|
||||
$home_path = $this->home_path();
|
||||
$relative = $this->relative_path( $path );
|
||||
$languages = array_map( 'preg_quote', HTBD_Settings::target_languages() );
|
||||
|
||||
if ( ! empty( $languages ) ) {
|
||||
$relative = preg_replace( '#^(?:' . implode( '|', $languages ) . ')(?=/|$)/?#', '', $relative, 1 );
|
||||
}
|
||||
|
||||
return '/' . ( '' !== $home_path ? trim( $home_path, '/' ) . '/' : '' ) . ltrim( $relative, '/' );
|
||||
}
|
||||
|
||||
private function relative_path( $path ) {
|
||||
return ltrim( (string) preg_replace( '#^' . preg_quote( $this->home_path(), '#' ) . '(?=/|$)#', '', $path, 1 ), '/' );
|
||||
}
|
||||
|
||||
private function home_path() {
|
||||
$home_path = wp_parse_url( (string) get_option( 'home', '' ), PHP_URL_PATH );
|
||||
|
||||
return is_string( $home_path ) ? untrailingslashit( $home_path ) : '';
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,81 @@
|
||||
<?php
|
||||
|
||||
defined( 'ABSPATH' ) || exit;
|
||||
|
||||
class HTBD_Plugin {
|
||||
private static $instance;
|
||||
private $router;
|
||||
private $sitemap_controller;
|
||||
private $content_controller;
|
||||
private $admin;
|
||||
|
||||
private function __construct() {
|
||||
$this->router = new HTBD_Language_Router();
|
||||
$this->sitemap_controller = new HTBD_Sitemap_Controller( $this->router );
|
||||
$this->content_controller = new HTBD_Content_Controller( new HTBD_Translation_Store(), $this->router );
|
||||
$this->admin = new HTBD_Admin();
|
||||
|
||||
add_action( 'init', array( $this, 'register' ) );
|
||||
add_action( 'update_option_htbd_settings', array( $this, 'refresh_rewrite_rules' ), 10, 2 );
|
||||
$this->admin->register();
|
||||
}
|
||||
|
||||
public static function instance() {
|
||||
if ( null === self::$instance ) {
|
||||
self::$instance = new self();
|
||||
}
|
||||
|
||||
return self::$instance;
|
||||
}
|
||||
|
||||
public static function activate() {
|
||||
HTBD_Translation_Store::install();
|
||||
wp_clear_scheduled_hook( 'btranslate_process_translation' );
|
||||
wp_clear_scheduled_hook( 'btranslate_process_term_translation' );
|
||||
wp_clear_scheduled_hook( 'btranslate_process_seo_output_translation' );
|
||||
$legacy_settings = get_option( 'btranslate_settings', null );
|
||||
if ( null !== $legacy_settings && false === get_option( 'htbd_settings', false ) ) {
|
||||
add_option( 'htbd_settings', $legacy_settings, '', false );
|
||||
}
|
||||
add_option(
|
||||
'htbd_settings',
|
||||
array(
|
||||
'auto_detect_source_language' => false,
|
||||
'source_language' => 'zh',
|
||||
'target_languages' => array( 'en' ),
|
||||
'routing_mode' => array( 'subdirectory' ),
|
||||
'domain_bindings' => array(),
|
||||
'fallback_language' => 'zh',
|
||||
),
|
||||
'',
|
||||
false
|
||||
);
|
||||
$router = new HTBD_Language_Router();
|
||||
$router->register_rewrite_rules();
|
||||
flush_rewrite_rules();
|
||||
}
|
||||
|
||||
public static function deactivate() {
|
||||
wp_clear_scheduled_hook( 'htbd_process_translation' );
|
||||
wp_clear_scheduled_hook( 'htbd_process_term_translation' );
|
||||
wp_clear_scheduled_hook( 'htbd_process_seo_output_translation' );
|
||||
flush_rewrite_rules();
|
||||
}
|
||||
|
||||
public function register() {
|
||||
$this->router->register();
|
||||
$this->sitemap_controller->register();
|
||||
$this->content_controller->register();
|
||||
|
||||
if ( get_option( 'htbd_flush_rewrite_rules', false ) ) {
|
||||
delete_option( 'htbd_flush_rewrite_rules' );
|
||||
flush_rewrite_rules();
|
||||
}
|
||||
}
|
||||
|
||||
public function refresh_rewrite_rules( $old_value, $new_value ) {
|
||||
if ( (array) $old_value !== (array) $new_value ) {
|
||||
update_option( 'htbd_flush_rewrite_rules', 1, false );
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
<?php
|
||||
|
||||
defined( 'ABSPATH' ) || exit;
|
||||
|
||||
class HTBD_Settings {
|
||||
public static function get() {
|
||||
$defaults = array(
|
||||
'auto_detect_source_language' => false,
|
||||
'source_language' => 'zh',
|
||||
'target_languages' => array( 'en' ),
|
||||
'routing_mode' => array( 'subdirectory' ),
|
||||
'domain_bindings' => array(),
|
||||
'fallback_language' => 'zh',
|
||||
'baidu_app_id' => '',
|
||||
'baidu_secret_key' => '',
|
||||
'log_requests' => false,
|
||||
);
|
||||
|
||||
$stored_settings = get_option( 'htbd_settings', false );
|
||||
if ( false === $stored_settings ) {
|
||||
$legacy_settings = get_option( 'btranslate_settings', false );
|
||||
if ( false !== $legacy_settings ) {
|
||||
$stored_settings = $legacy_settings;
|
||||
add_option( 'htbd_settings', $legacy_settings, '', false );
|
||||
}
|
||||
}
|
||||
|
||||
$settings = wp_parse_args( (array) $stored_settings, $defaults );
|
||||
$settings['routing_mode'] = self::routing_modes( $settings['routing_mode'] );
|
||||
|
||||
return $settings;
|
||||
}
|
||||
|
||||
public static function routing_modes( $routing_mode = null ) {
|
||||
if ( null === $routing_mode ) {
|
||||
$routing_mode = self::get()['routing_mode'];
|
||||
}
|
||||
|
||||
return array_values( array_intersect( (array) $routing_mode, array( 'subdirectory', 'domain' ) ) );
|
||||
}
|
||||
|
||||
public static function is_routing_mode_enabled( $mode ) {
|
||||
return in_array( $mode, self::routing_modes(), true );
|
||||
}
|
||||
|
||||
public static function target_languages() {
|
||||
return array_values( array_filter( array_map( 'sanitize_key', (array) self::get()['target_languages'] ) ) );
|
||||
}
|
||||
|
||||
public static function subdirectory_languages() {
|
||||
return self::target_languages();
|
||||
}
|
||||
|
||||
public static function is_supported_language( $language ) {
|
||||
$settings = self::get();
|
||||
$languages = array_merge( array( sanitize_key( $settings['source_language'] ) ), self::target_languages() );
|
||||
|
||||
return in_array( sanitize_key( $language ), $languages, true );
|
||||
}
|
||||
|
||||
public static function normalize_domain( $domain ) {
|
||||
$domain = strtolower( trim( (string) $domain ) );
|
||||
$domain = preg_replace( '#^https?://#', '', $domain );
|
||||
$domain = explode( '/', $domain, 2 )[0];
|
||||
$domain = preg_replace( '/:\d+$/', '', $domain );
|
||||
|
||||
return preg_match( '/^(?=.{1,253}$)(?:[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?\.)+[a-z]{2,63}$/', $domain ) ? $domain : '';
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,193 @@
|
||||
<?php
|
||||
|
||||
defined( 'ABSPATH' ) || exit;
|
||||
|
||||
class HTBD_Sitemap_Controller {
|
||||
private $router;
|
||||
private $target_language = '';
|
||||
|
||||
public function __construct( HTBD_Language_Router $router ) {
|
||||
$this->router = $router;
|
||||
}
|
||||
|
||||
public function register() {
|
||||
add_filter( 'do_parse_request', array( $this, 'intercept_request' ), 0, 3 );
|
||||
}
|
||||
|
||||
public function intercept_request( $do_parse_request, $wp, $extra_query_vars ) {
|
||||
$settings = HTBD_Settings::get();
|
||||
$language = $this->requested_language( $this->request_path(), $settings );
|
||||
|
||||
if ( '' === $language ) {
|
||||
return $do_parse_request;
|
||||
}
|
||||
|
||||
$this->target_language = $language;
|
||||
$this->serve_localized_sitemap();
|
||||
|
||||
return $do_parse_request;
|
||||
}
|
||||
|
||||
private function serve_localized_sitemap() {
|
||||
$source_url = $this->source_sitemap_url();
|
||||
if ( '' === $source_url ) {
|
||||
return;
|
||||
}
|
||||
|
||||
$source_xml = $this->source_sitemap_xml( $source_url );
|
||||
if ( '' === $source_xml ) {
|
||||
return;
|
||||
}
|
||||
|
||||
$xml = $this->rewrite_sitemap( $source_xml, $source_url );
|
||||
if ( '' === $xml ) {
|
||||
return;
|
||||
}
|
||||
|
||||
status_header( 200 );
|
||||
header( 'Content-Type: application/xml; charset=UTF-8' );
|
||||
header( 'X-Robots-Tag: noindex, follow', true );
|
||||
echo $xml; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Serialized XML from DOMDocument.
|
||||
exit;
|
||||
}
|
||||
|
||||
private function source_sitemap_xml( $source_url ) {
|
||||
$cache_key = 'htbd_sitemap_' . md5( $source_url );
|
||||
$cached = get_transient( $cache_key );
|
||||
|
||||
if ( is_string( $cached ) && '' !== $cached ) {
|
||||
return $cached;
|
||||
}
|
||||
|
||||
$response = wp_safe_remote_get(
|
||||
$source_url,
|
||||
array(
|
||||
'timeout' => 10,
|
||||
'redirection' => 0,
|
||||
'limit_response_size' => 5 * MB_IN_BYTES,
|
||||
'headers' => array(
|
||||
'Accept' => 'application/xml, text/xml;q=0.9',
|
||||
),
|
||||
)
|
||||
);
|
||||
|
||||
if ( is_wp_error( $response ) || 200 !== wp_remote_retrieve_response_code( $response ) ) {
|
||||
return '';
|
||||
}
|
||||
|
||||
$source_xml = wp_remote_retrieve_body( $response );
|
||||
if ( '' !== $source_xml ) {
|
||||
set_transient( $cache_key, $source_xml, 5 * MINUTE_IN_SECONDS );
|
||||
}
|
||||
|
||||
return $source_xml;
|
||||
}
|
||||
|
||||
private function request_path() {
|
||||
$request_uri = isset( $_SERVER['REQUEST_URI'] ) ? sanitize_text_field( wp_unslash( $_SERVER['REQUEST_URI'] ) ) : '';
|
||||
$path = wp_parse_url( $request_uri, PHP_URL_PATH );
|
||||
|
||||
return is_string( $path ) ? '/' . ltrim( $path, '/' ) : '';
|
||||
}
|
||||
|
||||
private function requested_language( $path, $settings ) {
|
||||
$home_path = wp_parse_url( (string) get_option( 'home', '' ), PHP_URL_PATH );
|
||||
|
||||
if ( is_string( $home_path ) && '' !== untrailingslashit( $home_path ) ) {
|
||||
$path = preg_replace( '#^' . preg_quote( untrailingslashit( $home_path ), '#' ) . '(?=/|$)#', '', $path, 1 );
|
||||
$path = '/' . ltrim( (string) $path, '/' );
|
||||
}
|
||||
|
||||
if ( HTBD_Settings::is_routing_mode_enabled( 'domain' ) && '/sitemap.xml' === untrailingslashit( $path ) ) {
|
||||
$host = isset( $_SERVER['HTTP_HOST'] ) ? HTBD_Settings::normalize_domain( sanitize_text_field( wp_unslash( $_SERVER['HTTP_HOST'] ) ) ) : '';
|
||||
$bindings = (array) $settings['domain_bindings'];
|
||||
$language = isset( $bindings[ $host ] ) ? sanitize_key( $bindings[ $host ] ) : '';
|
||||
|
||||
if ( in_array( $language, HTBD_Settings::target_languages(), true ) ) {
|
||||
return $language;
|
||||
}
|
||||
}
|
||||
|
||||
if ( HTBD_Settings::is_routing_mode_enabled( 'subdirectory' ) ) {
|
||||
foreach ( HTBD_Settings::subdirectory_languages() as $language ) {
|
||||
$language = sanitize_key( $language );
|
||||
|
||||
if ( '/' . $language . '/sitemap.xml' === untrailingslashit( $path ) ) {
|
||||
return $language;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return '';
|
||||
}
|
||||
|
||||
private function source_sitemap_url() {
|
||||
$home_url = (string) get_option( 'home', '' );
|
||||
$parts = wp_parse_url( $home_url );
|
||||
|
||||
if ( ! is_array( $parts ) || empty( $parts['scheme'] ) || empty( $parts['host'] ) ) {
|
||||
return '';
|
||||
}
|
||||
|
||||
$port = isset( $parts['port'] ) ? ':' . $parts['port'] : '';
|
||||
$path = isset( $parts['path'] ) ? '/' . trim( $parts['path'], '/' ) : '';
|
||||
|
||||
return $parts['scheme'] . '://' . $parts['host'] . $port . $path . '/sitemap.xml';
|
||||
}
|
||||
|
||||
private function rewrite_sitemap( $xml, $source_url ) {
|
||||
if ( '' === trim( $xml ) || ! class_exists( 'DOMDocument' ) ) {
|
||||
return '';
|
||||
}
|
||||
|
||||
$document = new DOMDocument();
|
||||
$document->preserveWhiteSpace = true;
|
||||
$previous_errors = libxml_use_internal_errors( true );
|
||||
$loaded = $document->loadXML( $xml, LIBXML_NONET );
|
||||
libxml_clear_errors();
|
||||
libxml_use_internal_errors( $previous_errors );
|
||||
|
||||
if ( ! $loaded ) {
|
||||
return '';
|
||||
}
|
||||
|
||||
$xpath = new DOMXPath( $document );
|
||||
$nodes = $xpath->query( '//text() | //@* | //processing-instruction("xml-stylesheet")' );
|
||||
|
||||
if ( false === $nodes ) {
|
||||
return '';
|
||||
}
|
||||
|
||||
foreach ( $nodes as $node ) {
|
||||
$node->nodeValue = $this->rewrite_source_urls( $node->nodeValue, $source_url );
|
||||
}
|
||||
|
||||
$result = $document->saveXML();
|
||||
|
||||
return false === $result ? '' : $result;
|
||||
}
|
||||
|
||||
private function rewrite_source_urls( $value, $source_url ) {
|
||||
$source_parts = wp_parse_url( $source_url );
|
||||
|
||||
if ( ! is_array( $source_parts ) || empty( $source_parts['host'] ) ) {
|
||||
return $value;
|
||||
}
|
||||
|
||||
$rewritten = preg_replace_callback(
|
||||
'#https?://[^\s<>"\']+#i',
|
||||
function ( $matches ) use ( $source_parts ) {
|
||||
$url_parts = wp_parse_url( $matches[0] );
|
||||
|
||||
if ( ! is_array( $url_parts ) || empty( $url_parts['host'] ) || 0 !== strcasecmp( $url_parts['host'], $source_parts['host'] ) ) {
|
||||
return $matches[0];
|
||||
}
|
||||
|
||||
return $this->router->localized_url( $matches[0], $this->target_language );
|
||||
},
|
||||
$value
|
||||
);
|
||||
|
||||
return null === $rewritten ? $value : $rewritten;
|
||||
}
|
||||
}
|
||||
+1
-1
@@ -2,7 +2,7 @@
|
||||
|
||||
defined( 'ABSPATH' ) || exit;
|
||||
|
||||
class WPT_Translation_Identity {
|
||||
class HTBD_Translation_Identity {
|
||||
public static function source_fingerprint( $source_value ) {
|
||||
return hash( 'sha256', (string) $source_value );
|
||||
}
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
defined( 'ABSPATH' ) || exit;
|
||||
|
||||
class WPT_Translation_Result {
|
||||
class HTBD_Translation_Result {
|
||||
public $success;
|
||||
public $value;
|
||||
public $error_code;
|
||||
@@ -2,30 +2,30 @@
|
||||
|
||||
defined( 'ABSPATH' ) || exit;
|
||||
|
||||
class WPT_Translation_Service {
|
||||
class HTBD_Translation_Service {
|
||||
private $store;
|
||||
private $provider;
|
||||
|
||||
public function __construct( WPT_Translation_Store $store, WPT_Translation_Provider $provider ) {
|
||||
public function __construct( HTBD_Translation_Store $store, HTBD_Translation_Provider $provider ) {
|
||||
$this->store = $store;
|
||||
$this->provider = $provider;
|
||||
}
|
||||
|
||||
public function get_or_translate( $source_value, $source_language, $target_language, $context, $force_refresh = false ) {
|
||||
$source_value = (string) $source_value;
|
||||
$source_fingerprint = WPT_Translation_Identity::source_fingerprint( $source_value );
|
||||
$identity_key = WPT_Translation_Identity::key( $source_value, $source_language, $target_language, $context, $this->provider->get_version() );
|
||||
$existing = $force_refresh ? null : $this->store->find_valid( $identity_key );
|
||||
$source_fingerprint = HTBD_Translation_Identity::source_fingerprint( $source_value );
|
||||
$identity_key = HTBD_Translation_Identity::key( $source_value, $source_language, $target_language, $context, $this->provider->get_version() );
|
||||
$existing = $this->store->find_valid( $identity_key );
|
||||
|
||||
if ( ! empty( $existing['translated_value'] ) ) {
|
||||
return WPT_Translation_Result::success( $existing['translated_value'] );
|
||||
if ( ! $force_refresh && ! empty( $existing['translated_value'] ) ) {
|
||||
return HTBD_Translation_Result::success( $existing['translated_value'] );
|
||||
}
|
||||
|
||||
$result = $this->provider->translate( $source_value, $source_language, $target_language, $context );
|
||||
|
||||
if ( $result->success ) {
|
||||
$this->store->save( $identity_key, $source_language, $target_language, $context, $source_fingerprint, $result->value, 'complete' );
|
||||
} else {
|
||||
} elseif ( empty( $existing['translated_value'] ) ) {
|
||||
$this->store->save( $identity_key, $source_language, $target_language, $context, $source_fingerprint, '', 'failed' );
|
||||
}
|
||||
|
||||
@@ -0,0 +1,218 @@
|
||||
<?php
|
||||
|
||||
defined( 'ABSPATH' ) || exit;
|
||||
|
||||
class HTBD_Translation_Store {
|
||||
public static function table_name() {
|
||||
global $wpdb;
|
||||
|
||||
return $wpdb->prefix . 'hyx_bd_translations';
|
||||
}
|
||||
|
||||
public static function install() {
|
||||
global $wpdb;
|
||||
|
||||
require_once ABSPATH . 'wp-admin/includes/upgrade.php';
|
||||
|
||||
$table_name = self::table_name();
|
||||
$legacy_table = $wpdb->prefix . 'btranslate_translations';
|
||||
$table_exists = $wpdb->get_var( $wpdb->prepare( 'SHOW TABLES LIKE %s', $wpdb->esc_like( $table_name ) ) ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- Checks the plugin-owned table during activation.
|
||||
$legacy_exists = $wpdb->get_var( $wpdb->prepare( 'SHOW TABLES LIKE %s', $wpdb->esc_like( $legacy_table ) ) ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- Checks for the legacy plugin-owned table during activation.
|
||||
if ( $legacy_exists === $legacy_table && $table_exists !== $table_name ) {
|
||||
$wpdb->query( $wpdb->prepare( 'RENAME TABLE %i TO %i', $legacy_table, $table_name ) ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- Migrates persisted translations to the renamed table.
|
||||
}
|
||||
|
||||
$charset_collate = $wpdb->get_charset_collate();
|
||||
$sql = "CREATE TABLE {$table_name} (
|
||||
id bigint(20) unsigned NOT NULL AUTO_INCREMENT,
|
||||
identity_key char(64) NOT NULL,
|
||||
source_language varchar(20) NOT NULL,
|
||||
target_language varchar(20) NOT NULL,
|
||||
field_context varchar(191) NOT NULL,
|
||||
source_fingerprint char(64) NOT NULL,
|
||||
translated_value longtext NULL,
|
||||
status varchar(20) NOT NULL,
|
||||
created_at datetime NOT NULL,
|
||||
updated_at datetime NOT NULL,
|
||||
PRIMARY KEY (id),
|
||||
UNIQUE KEY identity_key (identity_key),
|
||||
KEY target_language (target_language),
|
||||
KEY source_fingerprint (source_fingerprint)
|
||||
) {$charset_collate};";
|
||||
|
||||
dbDelta( $sql );
|
||||
}
|
||||
|
||||
public function find_valid( $identity_key ) {
|
||||
global $wpdb;
|
||||
|
||||
return $wpdb->get_row( // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- The custom translation table is the plugin's persistent cache.
|
||||
$wpdb->prepare(
|
||||
"SELECT * FROM %i WHERE identity_key = %s AND status IN ('complete', 'manual') LIMIT 1",
|
||||
self::table_name(),
|
||||
$identity_key
|
||||
),
|
||||
ARRAY_A
|
||||
);
|
||||
}
|
||||
|
||||
public function save( $identity_key, $source_language, $target_language, $field_context, $source_fingerprint, $translated_value, $status ) {
|
||||
global $wpdb;
|
||||
|
||||
$now = current_time( 'mysql', true );
|
||||
|
||||
return $wpdb->replace( // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- Writes to the plugin-owned persistent translation cache.
|
||||
self::table_name(),
|
||||
array(
|
||||
'identity_key' => $identity_key,
|
||||
'source_language' => $source_language,
|
||||
'target_language' => $target_language,
|
||||
'field_context' => $field_context,
|
||||
'source_fingerprint' => $source_fingerprint,
|
||||
'translated_value' => $translated_value,
|
||||
'status' => $status,
|
||||
'created_at' => $now,
|
||||
'updated_at' => $now,
|
||||
),
|
||||
array( '%s', '%s', '%s', '%s', '%s', '%s', '%s', '%s', '%s' )
|
||||
);
|
||||
}
|
||||
|
||||
public function save_manual( $identity_key, $source_language, $target_language, $field_context, $source_fingerprint, $translated_value ) {
|
||||
return $this->save( $identity_key, $source_language, $target_language, $field_context, $source_fingerprint, $translated_value, 'manual' );
|
||||
}
|
||||
|
||||
public function clear( $target_language = '' ) {
|
||||
global $wpdb;
|
||||
|
||||
if ( '' !== $target_language ) {
|
||||
return $wpdb->query( // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- This explicitly clears one language from the plugin's persistent translation cache.
|
||||
$wpdb->prepare(
|
||||
'DELETE FROM %i WHERE target_language = %s',
|
||||
self::table_name(),
|
||||
sanitize_key( $target_language )
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
return $wpdb->query( // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- This explicitly clears the plugin's persistent translation cache.
|
||||
$wpdb->prepare( 'DELETE FROM %i', self::table_name() )
|
||||
);
|
||||
}
|
||||
|
||||
public function get_post_language_status( $post_id, $target_language ) {
|
||||
global $wpdb;
|
||||
|
||||
return $wpdb->get_row( // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- The custom translation table is the plugin's persistent cache.
|
||||
$wpdb->prepare(
|
||||
"SELECT COUNT(*) AS translated_fields, MAX(updated_at) AS last_translated_at
|
||||
FROM %i
|
||||
WHERE target_language = %s
|
||||
AND status IN ('complete', 'manual')
|
||||
AND field_context LIKE %s",
|
||||
self::table_name(),
|
||||
$target_language,
|
||||
'post:' . absint( $post_id ) . ':%'
|
||||
),
|
||||
ARRAY_A
|
||||
);
|
||||
}
|
||||
|
||||
public function get_completed_item_counts( $target_languages, $post_ids, $term_ids, $since = '' ) {
|
||||
$target_languages = array_values( array_filter( array_map( 'sanitize_key', (array) $target_languages ) ) );
|
||||
$post_ids = array_values( array_filter( array_map( 'absint', (array) $post_ids ) ) );
|
||||
$term_ids = array_values( array_filter( array_map( 'absint', (array) $term_ids ) ) );
|
||||
if ( empty( $target_languages ) ) {
|
||||
return array(
|
||||
'posts' => 0,
|
||||
'terms' => 0,
|
||||
);
|
||||
}
|
||||
|
||||
$language_placeholders = implode( ',', array_fill( 0, count( $target_languages ), '%s' ) );
|
||||
|
||||
return array(
|
||||
'posts' => $this->get_completed_item_count( 'post', $post_ids, $target_languages, $language_placeholders, $since ),
|
||||
'terms' => $this->get_completed_item_count( 'term', $term_ids, $target_languages, $language_placeholders, $since ),
|
||||
);
|
||||
}
|
||||
|
||||
public function get_failed_items( $target_languages, $post_ids, $term_ids, $since = '' ) {
|
||||
global $wpdb;
|
||||
|
||||
$target_languages = array_values( array_filter( array_map( 'sanitize_key', (array) $target_languages ) ) );
|
||||
$item_contexts = array_merge(
|
||||
array_map(
|
||||
static function ( $post_id ) {
|
||||
return 'post:' . absint( $post_id );
|
||||
},
|
||||
(array) $post_ids
|
||||
),
|
||||
array_map(
|
||||
static function ( $term_id ) {
|
||||
return 'term:' . absint( $term_id );
|
||||
},
|
||||
(array) $term_ids
|
||||
)
|
||||
);
|
||||
|
||||
if ( empty( $target_languages ) || empty( $item_contexts ) ) {
|
||||
return array();
|
||||
}
|
||||
|
||||
$language_placeholders = implode( ',', array_fill( 0, count( $target_languages ), '%s' ) );
|
||||
$context_placeholders = implode( ',', array_fill( 0, count( $item_contexts ), '%s' ) );
|
||||
$query_args = array_merge( $target_languages, $item_contexts );
|
||||
if ( '' !== $since ) {
|
||||
$query_args[] = $since;
|
||||
}
|
||||
|
||||
$since_clause = '' === $since ? '' : ' AND updated_at >= %s';
|
||||
|
||||
return $wpdb->get_results( // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- Reads task failures from the plugin-owned translation table.
|
||||
$wpdb->prepare( // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- IN clauses contain only generated %s placeholders; the optional clause is fixed SQL.
|
||||
"SELECT target_language, SUBSTRING_INDEX(field_context, ':', 2) AS item_context, MAX(updated_at) AS failed_at
|
||||
FROM %i
|
||||
WHERE status = 'failed'
|
||||
AND target_language IN ({$language_placeholders})
|
||||
AND SUBSTRING_INDEX(field_context, ':', 2) IN ({$context_placeholders}){$since_clause}
|
||||
GROUP BY target_language, item_context
|
||||
ORDER BY failed_at DESC",
|
||||
array_merge( array( self::table_name() ), $query_args )
|
||||
),
|
||||
ARRAY_A
|
||||
);
|
||||
}
|
||||
|
||||
private function get_completed_item_count( $item_type, $item_ids, $target_languages, $language_placeholders, $since ) {
|
||||
global $wpdb;
|
||||
|
||||
if ( empty( $item_ids ) ) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
$contexts = array_map(
|
||||
static function ( $item_id ) use ( $item_type ) {
|
||||
return $item_type . ':' . $item_id;
|
||||
},
|
||||
$item_ids
|
||||
);
|
||||
$context_placeholders = implode( ',', array_fill( 0, count( $contexts ), '%s' ) );
|
||||
$query_args = array_merge( $target_languages, $contexts );
|
||||
if ( '' !== $since ) {
|
||||
$query_args[] = $since;
|
||||
}
|
||||
|
||||
$since_clause = '' === $since ? '' : ' AND updated_at >= %s';
|
||||
return (int) $wpdb->get_var( // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- The custom translation table is the plugin's persistent cache.
|
||||
$wpdb->prepare( // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- IN clauses contain only generated %s placeholders; the optional clause is a fixed SQL fragment.
|
||||
"SELECT COUNT(DISTINCT CONCAT(target_language, '|', SUBSTRING_INDEX(field_context, ':', 2)))
|
||||
FROM %i
|
||||
WHERE status IN ('complete', 'manual')
|
||||
AND target_language IN ({$language_placeholders})
|
||||
AND SUBSTRING_INDEX(field_context, ':', 2) IN ({$context_placeholders}){$since_clause}",
|
||||
array_merge( array( self::table_name() ), $query_args )
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
<?php
|
||||
|
||||
defined( 'ABSPATH' ) || exit;
|
||||
|
||||
class HTBD_Uninstaller {
|
||||
private const OPTION_NAMES = array(
|
||||
'htbd_settings',
|
||||
'htbd_flush_rewrite_rules',
|
||||
'htbd_translation_task',
|
||||
'htbd_retranslation_batch',
|
||||
'btranslate_settings',
|
||||
'btranslate_flush_rewrite_rules',
|
||||
'btranslate_translation_task',
|
||||
'btranslate_retranslation_batch',
|
||||
);
|
||||
|
||||
private const CRON_HOOKS = array(
|
||||
'htbd_process_translation',
|
||||
'htbd_process_term_translation',
|
||||
'htbd_process_seo_output_translation',
|
||||
'btranslate_process_translation',
|
||||
'btranslate_process_term_translation',
|
||||
'btranslate_process_seo_output_translation',
|
||||
);
|
||||
|
||||
public static function uninstall() {
|
||||
if ( is_multisite() ) {
|
||||
$site_ids = get_sites(
|
||||
array(
|
||||
'fields' => 'ids',
|
||||
'number' => 0,
|
||||
)
|
||||
);
|
||||
|
||||
foreach ( $site_ids as $site_id ) {
|
||||
switch_to_blog( $site_id );
|
||||
self::delete_site_data();
|
||||
restore_current_blog();
|
||||
}
|
||||
} else {
|
||||
self::delete_site_data();
|
||||
}
|
||||
|
||||
foreach ( self::OPTION_NAMES as $option_name ) {
|
||||
delete_site_option( $option_name );
|
||||
}
|
||||
}
|
||||
|
||||
private static function delete_site_data() {
|
||||
global $wpdb;
|
||||
|
||||
foreach ( self::CRON_HOOKS as $hook_name ) {
|
||||
wp_clear_scheduled_hook( $hook_name );
|
||||
}
|
||||
|
||||
foreach ( self::OPTION_NAMES as $option_name ) {
|
||||
delete_option( $option_name );
|
||||
}
|
||||
|
||||
$table_names = array(
|
||||
$wpdb->prefix . 'hyx_bd_translations',
|
||||
$wpdb->prefix . 'btranslate_translations',
|
||||
);
|
||||
foreach ( $table_names as $table_name ) {
|
||||
$wpdb->query( $wpdb->prepare( 'DROP TABLE IF EXISTS %i', $table_name ) );
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,365 +0,0 @@
|
||||
<?php
|
||||
|
||||
defined( 'ABSPATH' ) || exit;
|
||||
|
||||
class WPT_Admin {
|
||||
public function register() {
|
||||
add_action( 'admin_menu', array( $this, 'add_settings_page' ) );
|
||||
add_action( 'admin_init', array( $this, 'register_settings' ) );
|
||||
add_action( 'admin_post_wpt_queue_existing_translations', array( $this, 'queue_existing_translations' ) );
|
||||
add_action( 'admin_post_wpt_clear_translation_cache', array( $this, 'clear_translation_cache' ) );
|
||||
add_action( 'admin_post_wpt_translate_post', array( $this, 'queue_post_translation' ) );
|
||||
add_action( 'wp_ajax_wpt_translation_progress', array( $this, 'translation_progress' ) );
|
||||
add_filter( 'manage_post_posts_columns', array( $this, 'add_translation_column' ) );
|
||||
add_filter( 'manage_page_posts_columns', array( $this, 'add_translation_column' ) );
|
||||
add_action( 'manage_post_posts_custom_column', array( $this, 'render_translation_column' ), 10, 2 );
|
||||
add_action( 'manage_page_posts_custom_column', array( $this, 'render_translation_column' ), 10, 2 );
|
||||
add_filter( 'plugin_action_links_' . plugin_basename( WPT_FILE ), array( $this, 'add_plugin_settings_link' ) );
|
||||
}
|
||||
|
||||
public function add_settings_page() {
|
||||
add_options_page( __( 'Btranslate', 'wp-btranslate' ), __( 'Btranslate', 'wp-btranslate' ), 'manage_options', 'wp-btranslate', array( $this, 'render_settings_page' ) );
|
||||
}
|
||||
|
||||
public function register_settings() {
|
||||
register_setting( 'wpt_settings', 'wpt_settings', array( $this, 'sanitize_settings' ) );
|
||||
}
|
||||
|
||||
public function sanitize_settings( $settings ) {
|
||||
$settings = (array) $settings;
|
||||
$bindings = array();
|
||||
|
||||
foreach ( preg_split( '/\r\n|\r|\n/', (string) ( $settings['domain_bindings'] ?? '' ) ) as $line ) {
|
||||
$parts = array_map( 'trim', explode( '=', $line, 2 ) );
|
||||
$domain = 2 === count( $parts ) ? WPT_Settings::normalize_domain( $parts[0] ) : '';
|
||||
$language = 2 === count( $parts ) ? sanitize_key( $parts[1] ) : '';
|
||||
if ( '' !== $domain && '' !== $language ) {
|
||||
$bindings[ $domain ] = $language;
|
||||
}
|
||||
}
|
||||
|
||||
return array(
|
||||
'source_language' => sanitize_key( $settings['source_language'] ?? 'zh' ),
|
||||
'target_languages' => array_values( array_filter( array_map( 'sanitize_key', explode( ',', (string) ( $settings['target_languages'] ?? 'en' ) ) ) ) ),
|
||||
'routing_mode' => in_array( $settings['routing_mode'] ?? '', array( 'subdirectory', 'domain' ), true ) ? $settings['routing_mode'] : 'subdirectory',
|
||||
'domain_bindings' => $bindings,
|
||||
'fallback_language' => sanitize_key( $settings['fallback_language'] ?? 'zh' ),
|
||||
'baidu_app_id' => sanitize_text_field( $settings['baidu_app_id'] ?? '' ),
|
||||
'baidu_secret_key' => sanitize_text_field( $settings['baidu_secret_key'] ?? '' ),
|
||||
'log_requests' => ! empty( $settings['log_requests'] ),
|
||||
);
|
||||
}
|
||||
|
||||
public function render_settings_page() {
|
||||
$settings = WPT_Settings::get();
|
||||
$bindings = array();
|
||||
foreach ( (array) $settings['domain_bindings'] as $domain => $language ) {
|
||||
$bindings[] = $domain . '=' . $language;
|
||||
}
|
||||
?>
|
||||
<div class="wrap">
|
||||
<h1>Btranslate</h1>
|
||||
<form action="options.php" method="post">
|
||||
<?php settings_fields( 'wpt_settings' ); ?>
|
||||
<table class="form-table" role="presentation">
|
||||
<tr><th scope="row"><label for="wpt-source-language">源语言</label></th><td><input id="wpt-source-language" name="wpt_settings[source_language]" type="text" value="<?php echo esc_attr( $settings['source_language'] ); ?>" /><p class="description">使用百度翻译语言代码,例如 <code>zh</code>。</p></td></tr>
|
||||
<tr><th scope="row"><label for="wpt-target-languages">目标语言</label></th><td><input id="wpt-target-languages" name="wpt_settings[target_languages]" type="text" value="<?php echo esc_attr( implode( ',', $settings['target_languages'] ) ); ?>" class="regular-text" /><p class="description">多个语言代码用英文逗号分隔,例如 <code>en,ja</code>。</p></td></tr>
|
||||
<tr><th scope="row"><label for="wpt-routing-mode">网址模式</label></th><td><select id="wpt-routing-mode" name="wpt_settings[routing_mode]"><option value="subdirectory" <?php selected( $settings['routing_mode'], 'subdirectory' ); ?>>子目录</option><option value="domain" <?php selected( $settings['routing_mode'], 'domain' ); ?>>绑定域名</option></select></td></tr>
|
||||
<tr><th scope="row"><label for="wpt-domain-bindings">域名绑定</label></th><td><textarea id="wpt-domain-bindings" name="wpt_settings[domain_bindings]" rows="4" class="large-text" placeholder="en.example.com=en"><?php echo esc_textarea( implode( "\n", $bindings ) ); ?></textarea><p class="description">每行一个 <code>域名=语言代码</code>,例如 <code>en.example.com=en</code>。请勿填写 <code>https://</code>、路径或端口。</p></td></tr>
|
||||
<tr><th scope="row"><label for="wpt-baidu-app-id">百度应用 ID</label></th><td><input id="wpt-baidu-app-id" name="wpt_settings[baidu_app_id]" type="text" value="<?php echo esc_attr( $settings['baidu_app_id'] ); ?>" class="regular-text" /></td></tr>
|
||||
<tr><th scope="row"><label for="wpt-baidu-secret-key">百度密钥</label></th><td><input id="wpt-baidu-secret-key" name="wpt_settings[baidu_secret_key]" type="password" value="<?php echo esc_attr( $settings['baidu_secret_key'] ); ?>" class="regular-text" autocomplete="new-password" /></td></tr>
|
||||
<tr><th scope="row">请求日志</th><td><label for="wpt-log-requests"><input id="wpt-log-requests" name="wpt_settings[log_requests]" type="checkbox" value="1" <?php checked( $settings['log_requests'] ); ?> /> 记录每次百度翻译请求</label><p class="description">日志写入 PHP 错误日志,包含语言、字段、文本指纹、长度和结果状态;不会记录密钥、原文、译文或完整 API 响应。</p></td></tr>
|
||||
</table>
|
||||
<?php submit_button(); ?>
|
||||
</form>
|
||||
<hr />
|
||||
<h2>重新翻译所有内容</h2>
|
||||
<p>此操作会将所有已发布文章、页面、分类和标签重新加入翻译队列。</p>
|
||||
<form action="<?php echo esc_url( admin_url( 'admin-post.php' ) ); ?>" method="post" onsubmit="return window.confirm('这将重新翻译所有内容,请注意 API 消耗。确认继续吗?');">
|
||||
<input type="hidden" name="action" value="wpt_queue_existing_translations" />
|
||||
<?php wp_nonce_field( 'wpt_queue_existing_translations' ); ?>
|
||||
<?php submit_button( '重新翻译所有内容', 'secondary', 'submit', false ); ?>
|
||||
</form>
|
||||
<p>也可以只重新翻译一种内容类型。</p>
|
||||
<form action="<?php echo esc_url( admin_url( 'admin-post.php' ) ); ?>" method="post" style="display:inline-block;margin-right:8px" onsubmit="return window.confirm('这将重新翻译所有文章和页面,请注意 API 消耗。确认继续吗?');">
|
||||
<input type="hidden" name="action" value="wpt_queue_existing_translations" />
|
||||
<input type="hidden" name="scope" value="posts" />
|
||||
<?php wp_nonce_field( 'wpt_queue_existing_translations' ); ?>
|
||||
<?php submit_button( '翻译所有文章', 'secondary', 'submit', false ); ?>
|
||||
</form>
|
||||
<form action="<?php echo esc_url( admin_url( 'admin-post.php' ) ); ?>" method="post" style="display:inline-block" onsubmit="return window.confirm('这将重新翻译所有分类和标签,请注意 API 消耗。确认继续吗?');">
|
||||
<input type="hidden" name="action" value="wpt_queue_existing_translations" />
|
||||
<input type="hidden" name="scope" value="terms" />
|
||||
<?php wp_nonce_field( 'wpt_queue_existing_translations' ); ?>
|
||||
<?php submit_button( '翻译所有分类标签', 'secondary', 'submit', false ); ?>
|
||||
</form>
|
||||
<hr />
|
||||
<h2>翻译缓存</h2>
|
||||
<p>清除后,前台会暂时显示源文;重新翻译时将再次调用百度翻译 API。</p>
|
||||
<form action="<?php echo esc_url( admin_url( 'admin-post.php' ) ); ?>" method="post" onsubmit="return window.confirm('这将删除所有已保存的翻译、取消待执行翻译任务并重置进度。确认继续吗?');">
|
||||
<input type="hidden" name="action" value="wpt_clear_translation_cache" />
|
||||
<?php wp_nonce_field( 'wpt_clear_translation_cache' ); ?>
|
||||
<?php submit_button( '清除已翻译的缓存', 'delete', 'submit', false ); ?>
|
||||
</form>
|
||||
<hr />
|
||||
<h2>翻译进度</h2>
|
||||
<div id="wpt-translation-progress" aria-live="polite">
|
||||
<p>正在读取翻译进度...</p>
|
||||
</div>
|
||||
<script>
|
||||
(function () {
|
||||
var container = document.getElementById('wpt-translation-progress');
|
||||
var endpoint = <?php echo wp_json_encode( admin_url( 'admin-ajax.php' ) ); ?>;
|
||||
var nonce = <?php echo wp_json_encode( wp_create_nonce( 'wpt_translation_progress' ) ); ?>;
|
||||
|
||||
function updateProgress() {
|
||||
fetch(endpoint + '?action=wpt_translation_progress&_ajax_nonce=' + encodeURIComponent(nonce), { credentials: 'same-origin' })
|
||||
.then(function (response) { return response.json(); })
|
||||
.then(function (response) {
|
||||
if (!response.success) {
|
||||
throw new Error('progress_request_failed');
|
||||
}
|
||||
var progress = response.data;
|
||||
container.innerHTML = '<p><strong>' + progress.percent + '%</strong>(' + progress.completed + ' / ' + progress.total + ' 个内容项)</p>' +
|
||||
'<div style="max-width:480px;height:12px;background:#dcdcde"><div style="height:12px;width:' + progress.percent + '%;background:#2271b1"></div></div>' +
|
||||
'<p class="description">最近任务:' + progress.task_label + '。文章和页面:' + progress.posts_completed + ' / ' + progress.posts_total + ';分类和标签:' + progress.terms_completed + ' / ' + progress.terms_total + '。每 5 秒自动更新。</p>';
|
||||
})
|
||||
.catch(function () {
|
||||
container.innerHTML = '<p>暂时无法读取翻译进度。</p>';
|
||||
});
|
||||
}
|
||||
|
||||
updateProgress();
|
||||
window.setInterval(updateProgress, 5000);
|
||||
}());
|
||||
</script>
|
||||
</div>
|
||||
<?php
|
||||
}
|
||||
|
||||
public function queue_existing_translations() {
|
||||
if ( ! current_user_can( 'manage_options' ) ) {
|
||||
wp_die( '您没有执行此操作的权限。' );
|
||||
}
|
||||
|
||||
check_admin_referer( 'wpt_queue_existing_translations' );
|
||||
$scope = isset( $_POST['scope'] ) ? sanitize_key( wp_unslash( $_POST['scope'] ) ) : 'all';
|
||||
if ( ! in_array( $scope, array( 'all', 'posts', 'terms' ), true ) ) {
|
||||
wp_die( '无效的翻译范围。' );
|
||||
}
|
||||
|
||||
$scheduled = 0;
|
||||
$next_run = time();
|
||||
$post_ids = array();
|
||||
$term_ids = array();
|
||||
$languages = WPT_Settings::target_languages();
|
||||
|
||||
if ( 'all' === $scope || 'posts' === $scope ) {
|
||||
$post_ids = get_posts(
|
||||
array(
|
||||
'post_type' => array( 'post', 'page' ),
|
||||
'post_status' => 'publish',
|
||||
'posts_per_page' => -1,
|
||||
'fields' => 'ids',
|
||||
)
|
||||
);
|
||||
|
||||
}
|
||||
|
||||
if ( 'all' === $scope || 'terms' === $scope ) {
|
||||
$term_ids = get_terms(
|
||||
array(
|
||||
'taxonomy' => array( 'category', 'post_tag' ),
|
||||
'hide_empty' => false,
|
||||
'fields' => 'ids',
|
||||
)
|
||||
);
|
||||
if ( is_wp_error( $term_ids ) ) {
|
||||
$term_ids = array();
|
||||
}
|
||||
}
|
||||
|
||||
$this->update_translation_task( $post_ids, $term_ids, $languages );
|
||||
|
||||
foreach ( $post_ids as $post_id ) {
|
||||
foreach ( $languages as $target_language ) {
|
||||
$this->clear_scheduled_event( 'wpt_process_translation', array( $post_id, $target_language, true ) );
|
||||
wp_schedule_single_event( $next_run, 'wpt_process_translation', array( $post_id, $target_language, true ) );
|
||||
$next_run += 2;
|
||||
++$scheduled;
|
||||
}
|
||||
}
|
||||
|
||||
foreach ( $term_ids as $term_id ) {
|
||||
foreach ( $languages as $target_language ) {
|
||||
$this->clear_scheduled_event( 'wpt_process_term_translation', array( $term_id, $target_language, true ) );
|
||||
wp_schedule_single_event( $next_run, 'wpt_process_term_translation', array( $term_id, $target_language, true ) );
|
||||
$next_run += 2;
|
||||
++$scheduled;
|
||||
}
|
||||
}
|
||||
|
||||
wp_safe_redirect( add_query_arg( 'wpt_queued', $scheduled, admin_url( 'options-general.php?page=wp-btranslate' ) ) );
|
||||
exit;
|
||||
}
|
||||
|
||||
public function clear_translation_cache() {
|
||||
if ( ! current_user_can( 'manage_options' ) ) {
|
||||
wp_die( '您没有执行此操作的权限。' );
|
||||
}
|
||||
|
||||
check_admin_referer( 'wpt_clear_translation_cache' );
|
||||
$deleted = ( new WPT_Translation_Store() )->clear();
|
||||
|
||||
if ( false === $deleted ) {
|
||||
wp_die( '翻译缓存清除失败。' );
|
||||
}
|
||||
|
||||
wp_clear_scheduled_hook( 'wpt_process_translation' );
|
||||
wp_clear_scheduled_hook( 'wpt_process_term_translation' );
|
||||
wp_clear_scheduled_hook( 'wpt_process_seo_output_translation' );
|
||||
delete_option( 'wpt_translation_task' );
|
||||
delete_option( 'wpt_retranslation_batch' );
|
||||
|
||||
wp_safe_redirect( add_query_arg( 'wpt_cache_cleared', 1, admin_url( 'options-general.php?page=wp-btranslate' ) ) );
|
||||
exit;
|
||||
}
|
||||
|
||||
private function clear_scheduled_event( $hook, $args ) {
|
||||
while ( false !== ( $timestamp = wp_next_scheduled( $hook, $args ) ) ) {
|
||||
wp_unschedule_event( $timestamp, $hook, $args );
|
||||
}
|
||||
}
|
||||
|
||||
public function add_plugin_settings_link( $links ) {
|
||||
array_unshift( $links, '<a href="' . esc_url( admin_url( 'options-general.php?page=wp-btranslate' ) ) . '">设置</a>' );
|
||||
|
||||
return $links;
|
||||
}
|
||||
|
||||
public function translation_progress() {
|
||||
check_ajax_referer( 'wpt_translation_progress' );
|
||||
|
||||
if ( ! current_user_can( 'manage_options' ) ) {
|
||||
wp_send_json_error( array( 'message' => 'forbidden' ), 403 );
|
||||
}
|
||||
|
||||
$task = (array) get_option( 'wpt_translation_task', array() );
|
||||
$post_ids = array_values( array_filter( array_map( 'absint', (array) ( $task['post_ids'] ?? array() ) ) ) );
|
||||
$term_ids = array_values( array_filter( array_map( 'absint', (array) ( $task['term_ids'] ?? array() ) ) ) );
|
||||
$languages = array_values( array_filter( array_map( 'sanitize_key', (array) ( $task['target_languages'] ?? array() ) ) ) );
|
||||
$started_at = isset( $task['started_at'] ) ? sanitize_text_field( $task['started_at'] ) : '';
|
||||
$counts = ( new WPT_Translation_Store() )->get_completed_item_counts( $languages, $post_ids, $term_ids, $started_at );
|
||||
|
||||
$posts_total = count( $post_ids ) * count( $languages );
|
||||
$terms_total = count( $term_ids ) * count( $languages );
|
||||
$total = $posts_total + $terms_total;
|
||||
$posts_completed = min( $counts['posts'], $posts_total );
|
||||
$terms_completed = min( $counts['terms'], $terms_total );
|
||||
$completed = $posts_completed + $terms_completed;
|
||||
$percent = $total ? (int) floor( ( $completed / $total ) * 100 ) : 100;
|
||||
|
||||
wp_send_json_success(
|
||||
array(
|
||||
'posts_completed' => $posts_completed,
|
||||
'posts_total' => $posts_total,
|
||||
'terms_completed' => $terms_completed,
|
||||
'terms_total' => $terms_total,
|
||||
'completed' => $completed,
|
||||
'total' => $total,
|
||||
'percent' => $percent,
|
||||
'batch_started_at' => $started_at,
|
||||
'task_label' => $this->translation_task_label( $post_ids, $term_ids ),
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
public function add_translation_column( $columns ) {
|
||||
$columns['wpt_translation'] = '翻译状态';
|
||||
|
||||
return $columns;
|
||||
}
|
||||
|
||||
public function render_translation_column( $column, $post_id ) {
|
||||
if ( 'wpt_translation' !== $column ) {
|
||||
return;
|
||||
}
|
||||
|
||||
$store = new WPT_Translation_Store();
|
||||
|
||||
foreach ( WPT_Settings::target_languages() as $target_language ) {
|
||||
$status = $store->get_post_language_status( $post_id, $target_language );
|
||||
$translated = ! empty( $status['translated_fields'] );
|
||||
$button_text = $translated ? '重新翻译' : '翻译';
|
||||
$action_url = wp_nonce_url(
|
||||
add_query_arg(
|
||||
array(
|
||||
'action' => 'wpt_translate_post',
|
||||
'post_id' => $post_id,
|
||||
'language' => $target_language,
|
||||
'force' => $translated ? '1' : '0',
|
||||
),
|
||||
admin_url( 'admin-post.php' )
|
||||
),
|
||||
'wpt_translate_post_' . $post_id . '_' . $target_language
|
||||
);
|
||||
|
||||
echo '<p><strong>' . esc_html( strtoupper( $target_language ) ) . '</strong>: ';
|
||||
echo $translated ? '<span style="color:#008a20">已翻译</span>' : '<span>未翻译</span>';
|
||||
if ( $translated && ! empty( $status['last_translated_at'] ) ) {
|
||||
echo '<br><small>上次翻译:' . esc_html( get_date_from_gmt( $status['last_translated_at'], 'Y-m-d H:i' ) ) . '</small>';
|
||||
}
|
||||
echo '<br><a class="button button-small" href="' . esc_url( $action_url ) . '">' . esc_html( $button_text ) . '</a></p>';
|
||||
}
|
||||
}
|
||||
|
||||
public function queue_post_translation() {
|
||||
$post_id = isset( $_GET['post_id'] ) ? absint( $_GET['post_id'] ) : 0;
|
||||
$target_language = isset( $_GET['language'] ) ? sanitize_key( wp_unslash( $_GET['language'] ) ) : '';
|
||||
$force_refresh = ! empty( $_GET['force'] );
|
||||
|
||||
if ( ! $post_id || ! current_user_can( 'edit_post', $post_id ) || ! WPT_Settings::is_supported_language( $target_language ) || $target_language === WPT_Settings::get()['source_language'] ) {
|
||||
wp_die( '无效的翻译请求。' );
|
||||
}
|
||||
|
||||
check_admin_referer( 'wpt_translate_post_' . $post_id . '_' . $target_language );
|
||||
$this->update_translation_task( array( $post_id ), array(), array( $target_language ) );
|
||||
wp_schedule_single_event( time() + 5, 'wpt_process_translation', array( $post_id, $target_language, $force_refresh ) );
|
||||
|
||||
wp_safe_redirect( wp_get_referer() ? wp_get_referer() : admin_url( 'edit.php?post_type=' . get_post_type( $post_id ) ) );
|
||||
exit;
|
||||
}
|
||||
|
||||
private function update_translation_task( $post_ids, $term_ids, $target_languages ) {
|
||||
update_option(
|
||||
'wpt_translation_task',
|
||||
array(
|
||||
'started_at' => current_time( 'mysql', true ),
|
||||
'post_ids' => array_values( array_filter( array_map( 'absint', (array) $post_ids ) ) ),
|
||||
'term_ids' => array_values( array_filter( array_map( 'absint', (array) $term_ids ) ) ),
|
||||
'target_languages' => array_values( array_filter( array_map( 'sanitize_key', (array) $target_languages ) ) ),
|
||||
),
|
||||
false
|
||||
);
|
||||
}
|
||||
|
||||
private function translation_task_label( $post_ids, $term_ids ) {
|
||||
if ( ! empty( $post_ids ) && ! empty( $term_ids ) ) {
|
||||
return '全量翻译';
|
||||
}
|
||||
|
||||
if ( 1 === count( $post_ids ) ) {
|
||||
return '单篇文章翻译';
|
||||
}
|
||||
|
||||
if ( ! empty( $post_ids ) ) {
|
||||
return '文章和页面翻译';
|
||||
}
|
||||
|
||||
if ( ! empty( $term_ids ) ) {
|
||||
return '分类和标签翻译';
|
||||
}
|
||||
|
||||
return '暂无翻译任务';
|
||||
}
|
||||
}
|
||||
@@ -1,98 +0,0 @@
|
||||
<?php
|
||||
|
||||
defined( 'ABSPATH' ) || exit;
|
||||
|
||||
class WPT_Language_Router {
|
||||
public function register() {
|
||||
add_filter( 'query_vars', array( $this, 'query_vars' ) );
|
||||
add_action( 'parse_request', array( $this, 'resolve_domain_language' ) );
|
||||
add_filter( 'home_url', array( $this, 'localize_home_url' ), 20, 4 );
|
||||
$this->register_rewrite_rules();
|
||||
}
|
||||
|
||||
public function register_rewrite_rules() {
|
||||
if ( 'subdirectory' !== WPT_Settings::get()['routing_mode'] ) {
|
||||
return;
|
||||
}
|
||||
|
||||
$language_pattern = implode( '|', array_map( 'preg_quote', array_map( 'strtolower', WPT_Settings::target_languages() ) ) );
|
||||
|
||||
if ( '' === $language_pattern ) {
|
||||
return;
|
||||
}
|
||||
|
||||
add_rewrite_tag( '%wpt_language%', '(' . $language_pattern . ')' );
|
||||
add_rewrite_rule( '^(' . $language_pattern . ')/(.*)$', 'index.php?wpt_language=$matches[1]&pagename=$matches[2]', 'top' );
|
||||
add_rewrite_rule( '^(' . $language_pattern . ')/?$', 'index.php?wpt_language=$matches[1]', 'top' );
|
||||
}
|
||||
|
||||
public function query_vars( $query_vars ) {
|
||||
$query_vars[] = 'wpt_language';
|
||||
|
||||
return $query_vars;
|
||||
}
|
||||
|
||||
public function resolve_domain_language( $wp ) {
|
||||
$settings = WPT_Settings::get();
|
||||
$host = isset( $_SERVER['HTTP_HOST'] ) ? WPT_Settings::normalize_domain( wp_unslash( $_SERVER['HTTP_HOST'] ) ) : '';
|
||||
$bindings = (array) $settings['domain_bindings'];
|
||||
|
||||
if ( 'domain' === $settings['routing_mode'] && '' !== $host && isset( $bindings[ $host ] ) && WPT_Settings::is_supported_language( $bindings[ $host ] ) ) {
|
||||
$wp->query_vars['wpt_language'] = sanitize_key( $bindings[ $host ] );
|
||||
}
|
||||
}
|
||||
|
||||
public function current_language() {
|
||||
$settings = WPT_Settings::get();
|
||||
$language = get_query_var( 'wpt_language' );
|
||||
|
||||
if ( $language && WPT_Settings::is_supported_language( $language ) ) {
|
||||
return sanitize_key( $language );
|
||||
}
|
||||
|
||||
return sanitize_key( $settings['source_language'] );
|
||||
}
|
||||
|
||||
public function localized_url( $url ) {
|
||||
$settings = WPT_Settings::get();
|
||||
$language = $this->current_language();
|
||||
|
||||
if ( $language === $settings['source_language'] || ! WPT_Settings::is_supported_language( $language ) ) {
|
||||
return $url;
|
||||
}
|
||||
|
||||
if ( 'domain' === $settings['routing_mode'] ) {
|
||||
$domain = array_search( $language, (array) $settings['domain_bindings'], true );
|
||||
$parts = wp_parse_url( $url );
|
||||
|
||||
if ( false === $domain || ! is_array( $parts ) || empty( $parts['host'] ) ) {
|
||||
return $url;
|
||||
}
|
||||
|
||||
$scheme = isset( $parts['scheme'] ) ? $parts['scheme'] : 'https';
|
||||
$path = isset( $parts['path'] ) ? $parts['path'] : '/';
|
||||
$query = isset( $parts['query'] ) ? '?' . $parts['query'] : '';
|
||||
$fragment = isset( $parts['fragment'] ) ? '#' . $parts['fragment'] : '';
|
||||
|
||||
return $scheme . '://' . $domain . $path . $query . $fragment;
|
||||
}
|
||||
|
||||
$parts = wp_parse_url( $url );
|
||||
if ( ! is_array( $parts ) || empty( $parts['path'] ) ) {
|
||||
return $url;
|
||||
}
|
||||
|
||||
$scheme = isset( $parts['scheme'] ) ? $parts['scheme'] . '://' : '//';
|
||||
$host = isset( $parts['host'] ) ? $parts['host'] : '';
|
||||
$port = isset( $parts['port'] ) ? ':' . $parts['port'] : '';
|
||||
$path = '/' . $language . '/' . ltrim( $parts['path'], '/' );
|
||||
$query = isset( $parts['query'] ) ? '?' . $parts['query'] : '';
|
||||
$fragment = isset( $parts['fragment'] ) ? '#' . $parts['fragment'] : '';
|
||||
|
||||
return $scheme . $host . $port . $path . $query . $fragment;
|
||||
}
|
||||
|
||||
public function localize_home_url( $url, $path, $scheme, $blog_id ) {
|
||||
return $this->localized_url( $url );
|
||||
}
|
||||
}
|
||||
@@ -1,64 +0,0 @@
|
||||
<?php
|
||||
|
||||
defined( 'ABSPATH' ) || exit;
|
||||
|
||||
class WPT_Plugin {
|
||||
private static $instance;
|
||||
private $router;
|
||||
private $content_controller;
|
||||
private $admin;
|
||||
|
||||
private function __construct() {
|
||||
$this->router = new WPT_Language_Router();
|
||||
$this->content_controller = new WPT_Content_Controller( new WPT_Translation_Store(), $this->router );
|
||||
$this->admin = new WPT_Admin();
|
||||
|
||||
add_action( 'init', array( $this, 'register' ) );
|
||||
add_action( 'update_option_wpt_settings', array( $this, 'refresh_rewrite_rules' ), 10, 2 );
|
||||
$this->admin->register();
|
||||
}
|
||||
|
||||
public static function instance() {
|
||||
if ( null === self::$instance ) {
|
||||
self::$instance = new self();
|
||||
}
|
||||
|
||||
return self::$instance;
|
||||
}
|
||||
|
||||
public static function activate() {
|
||||
WPT_Translation_Store::install();
|
||||
add_option(
|
||||
'wpt_settings',
|
||||
array(
|
||||
'source_language' => 'zh',
|
||||
'target_languages' => array( 'en' ),
|
||||
'routing_mode' => 'subdirectory',
|
||||
'domain_bindings' => array(),
|
||||
'fallback_language' => 'zh',
|
||||
),
|
||||
'',
|
||||
false
|
||||
);
|
||||
$router = new WPT_Language_Router();
|
||||
$router->register_rewrite_rules();
|
||||
flush_rewrite_rules();
|
||||
}
|
||||
|
||||
public static function deactivate() {
|
||||
wp_clear_scheduled_hook( 'wpt_process_translation' );
|
||||
flush_rewrite_rules();
|
||||
}
|
||||
|
||||
public function register() {
|
||||
$this->router->register();
|
||||
$this->content_controller->register();
|
||||
}
|
||||
|
||||
public function refresh_rewrite_rules( $old_value, $new_value ) {
|
||||
if ( (array) $old_value !== (array) $new_value ) {
|
||||
$this->router->register_rewrite_rules();
|
||||
flush_rewrite_rules();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,40 +0,0 @@
|
||||
<?php
|
||||
|
||||
defined( 'ABSPATH' ) || exit;
|
||||
|
||||
class WPT_Settings {
|
||||
public static function get() {
|
||||
$defaults = array(
|
||||
'source_language' => 'zh',
|
||||
'target_languages' => array( 'en' ),
|
||||
'routing_mode' => 'subdirectory',
|
||||
'domain_bindings' => array(),
|
||||
'fallback_language' => 'zh',
|
||||
'baidu_app_id' => '',
|
||||
'baidu_secret_key' => '',
|
||||
'log_requests' => false,
|
||||
);
|
||||
|
||||
return wp_parse_args( (array) get_option( 'wpt_settings', array() ), $defaults );
|
||||
}
|
||||
|
||||
public static function target_languages() {
|
||||
return array_values( array_filter( array_map( 'sanitize_key', (array) self::get()['target_languages'] ) ) );
|
||||
}
|
||||
|
||||
public static function is_supported_language( $language ) {
|
||||
$settings = self::get();
|
||||
$languages = array_merge( array( sanitize_key( $settings['source_language'] ) ), self::target_languages() );
|
||||
|
||||
return in_array( sanitize_key( $language ), $languages, true );
|
||||
}
|
||||
|
||||
public static function normalize_domain( $domain ) {
|
||||
$domain = strtolower( trim( (string) $domain ) );
|
||||
$domain = preg_replace( '#^https?://#', '', $domain );
|
||||
$domain = explode( '/', $domain, 2 )[0];
|
||||
$domain = preg_replace( '/:\d+$/', '', $domain );
|
||||
|
||||
return preg_match( '/^(?=.{1,253}$)(?:[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?\.)+[a-z]{2,63}$/', $domain ) ? $domain : '';
|
||||
}
|
||||
}
|
||||
@@ -1,149 +0,0 @@
|
||||
<?php
|
||||
|
||||
defined( 'ABSPATH' ) || exit;
|
||||
|
||||
class WPT_Translation_Store {
|
||||
public static function table_name() {
|
||||
global $wpdb;
|
||||
|
||||
return $wpdb->prefix . 'wpt_translations';
|
||||
}
|
||||
|
||||
public static function install() {
|
||||
global $wpdb;
|
||||
|
||||
require_once ABSPATH . 'wp-admin/includes/upgrade.php';
|
||||
|
||||
$table_name = self::table_name();
|
||||
$charset_collate = $wpdb->get_charset_collate();
|
||||
$sql = "CREATE TABLE {$table_name} (
|
||||
id bigint(20) unsigned NOT NULL AUTO_INCREMENT,
|
||||
identity_key char(64) NOT NULL,
|
||||
source_language varchar(20) NOT NULL,
|
||||
target_language varchar(20) NOT NULL,
|
||||
field_context varchar(191) NOT NULL,
|
||||
source_fingerprint char(64) NOT NULL,
|
||||
translated_value longtext NULL,
|
||||
status varchar(20) NOT NULL,
|
||||
created_at datetime NOT NULL,
|
||||
updated_at datetime NOT NULL,
|
||||
PRIMARY KEY (id),
|
||||
UNIQUE KEY identity_key (identity_key),
|
||||
KEY target_language (target_language),
|
||||
KEY source_fingerprint (source_fingerprint)
|
||||
) {$charset_collate};";
|
||||
|
||||
dbDelta( $sql );
|
||||
}
|
||||
|
||||
public function find_valid( $identity_key ) {
|
||||
global $wpdb;
|
||||
|
||||
$table_name = self::table_name();
|
||||
$sql = $wpdb->prepare(
|
||||
"SELECT * FROM {$table_name} WHERE identity_key = %s AND status = 'complete' LIMIT 1",
|
||||
$identity_key
|
||||
);
|
||||
|
||||
return $wpdb->get_row( $sql, ARRAY_A );
|
||||
}
|
||||
|
||||
public function save( $identity_key, $source_language, $target_language, $field_context, $source_fingerprint, $translated_value, $status ) {
|
||||
global $wpdb;
|
||||
|
||||
$now = current_time( 'mysql', true );
|
||||
|
||||
return $wpdb->replace(
|
||||
self::table_name(),
|
||||
array(
|
||||
'identity_key' => $identity_key,
|
||||
'source_language' => $source_language,
|
||||
'target_language' => $target_language,
|
||||
'field_context' => $field_context,
|
||||
'source_fingerprint' => $source_fingerprint,
|
||||
'translated_value' => $translated_value,
|
||||
'status' => $status,
|
||||
'created_at' => $now,
|
||||
'updated_at' => $now,
|
||||
),
|
||||
array( '%s', '%s', '%s', '%s', '%s', '%s', '%s', '%s', '%s' )
|
||||
);
|
||||
}
|
||||
|
||||
public function clear() {
|
||||
global $wpdb;
|
||||
|
||||
return $wpdb->query( 'DELETE FROM ' . self::table_name() );
|
||||
}
|
||||
|
||||
public function get_post_language_status( $post_id, $target_language ) {
|
||||
global $wpdb;
|
||||
|
||||
$table_name = self::table_name();
|
||||
$sql = $wpdb->prepare(
|
||||
"SELECT COUNT(*) AS translated_fields, MAX(updated_at) AS last_translated_at
|
||||
FROM {$table_name}
|
||||
WHERE target_language = %s
|
||||
AND status = 'complete'
|
||||
AND field_context LIKE %s",
|
||||
$target_language,
|
||||
'post:' . absint( $post_id ) . ':%'
|
||||
);
|
||||
|
||||
return $wpdb->get_row( $sql, ARRAY_A );
|
||||
}
|
||||
|
||||
public function get_completed_item_counts( $target_languages, $post_ids, $term_ids, $since = '' ) {
|
||||
global $wpdb;
|
||||
|
||||
$target_languages = array_values( array_filter( array_map( 'sanitize_key', (array) $target_languages ) ) );
|
||||
$post_ids = array_values( array_filter( array_map( 'absint', (array) $post_ids ) ) );
|
||||
$term_ids = array_values( array_filter( array_map( 'absint', (array) $term_ids ) ) );
|
||||
if ( empty( $target_languages ) ) {
|
||||
return array(
|
||||
'posts' => 0,
|
||||
'terms' => 0,
|
||||
);
|
||||
}
|
||||
|
||||
$table_name = self::table_name();
|
||||
$language_placeholders = implode( ',', array_fill( 0, count( $target_languages ), '%s' ) );
|
||||
$since_clause = '' === $since ? '' : ' AND updated_at >= %s';
|
||||
$post_sql = $this->get_completed_item_count_sql( 'post', $post_ids, $target_languages, $language_placeholders, $since, $since_clause, $table_name );
|
||||
$term_sql = $this->get_completed_item_count_sql( 'term', $term_ids, $target_languages, $language_placeholders, $since, $since_clause, $table_name );
|
||||
|
||||
return array(
|
||||
'posts' => (int) $wpdb->get_var( $post_sql ),
|
||||
'terms' => (int) $wpdb->get_var( $term_sql ),
|
||||
);
|
||||
}
|
||||
|
||||
private function get_completed_item_count_sql( $item_type, $item_ids, $target_languages, $language_placeholders, $since, $since_clause, $table_name ) {
|
||||
global $wpdb;
|
||||
|
||||
if ( empty( $item_ids ) ) {
|
||||
return 'SELECT 0';
|
||||
}
|
||||
|
||||
$contexts = array_map(
|
||||
static function ( $item_id ) use ( $item_type ) {
|
||||
return $item_type . ':' . $item_id;
|
||||
},
|
||||
$item_ids
|
||||
);
|
||||
$context_placeholders = implode( ',', array_fill( 0, count( $contexts ), '%s' ) );
|
||||
$query_args = array_merge( $target_languages, $contexts );
|
||||
if ( '' !== $since ) {
|
||||
$query_args[] = $since;
|
||||
}
|
||||
|
||||
return $wpdb->prepare(
|
||||
"SELECT COUNT(DISTINCT CONCAT(target_language, '|', SUBSTRING_INDEX(field_context, ':', 2)))
|
||||
FROM {$table_name}
|
||||
WHERE status = 'complete'
|
||||
AND target_language IN ({$language_placeholders})
|
||||
AND SUBSTRING_INDEX(field_context, ':', 2) IN ({$context_placeholders}){$since_clause}",
|
||||
$query_args
|
||||
);
|
||||
}
|
||||
}
|
||||
+1
-1
@@ -2,7 +2,7 @@
|
||||
|
||||
defined( 'ABSPATH' ) || exit;
|
||||
|
||||
interface WPT_Translation_Provider {
|
||||
interface HTBD_Translation_Provider {
|
||||
public function get_version();
|
||||
|
||||
public function translate( $source_value, $source_language, $target_language, $context );
|
||||
@@ -0,0 +1,209 @@
|
||||
# Simplified Chinese translations for HTBD.
|
||||
# Copyright (C) 2026 HTBD contributors
|
||||
# This file is distributed under the same license as the hyx-translator-for-baidu-translate package.
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: hyx-translator-for-baidu-translate 0.3.1\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2026-07-28 14:41+0800\n"
|
||||
"PO-Revision-Date: 2026-07-28 14:45+0800\n"
|
||||
"Last-Translator: HTBD contributors\n"
|
||||
"Language-Team: Chinese (Simplified)\n"
|
||||
"Language: zh_CN\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
"Plural-Forms: nplurals=1; plural=0;\n"
|
||||
|
||||
msgid "HTBD"
|
||||
msgstr "HTBD"
|
||||
|
||||
#, php-format
|
||||
msgid "(%1$s / %2$s content items)"
|
||||
msgstr "(%1$s / %2$s 个内容项)"
|
||||
|
||||
#, php-format
|
||||
msgid "Latest task: %1$s. Posts and pages: %2$s / %3$s; categories and tags: %4$s / %5$s. Updates automatically every 5 seconds."
|
||||
msgstr "最近任务:%1$s。文章和页面:%2$s / %3$s;分类和标签:%4$s / %5$s。每 5 秒自动更新。"
|
||||
|
||||
msgid "Unable to load translation progress at this time."
|
||||
msgstr "暂时无法读取翻译进度。"
|
||||
|
||||
msgid "Failed translations"
|
||||
msgstr "翻译失败的内容"
|
||||
|
||||
msgid "Source language detection"
|
||||
msgstr "源语言识别"
|
||||
|
||||
msgid "Automatically detect the source language"
|
||||
msgstr "自动识别源语言"
|
||||
|
||||
msgid "Source language"
|
||||
msgstr "源语言"
|
||||
|
||||
msgid "Use a Baidu Translate language code, for example zh."
|
||||
msgstr "使用百度翻译语言代码,例如 zh。"
|
||||
|
||||
msgid "Target languages"
|
||||
msgstr "目标语言"
|
||||
|
||||
msgid "Separate multiple language codes with commas, for example en,jp."
|
||||
msgstr "多个语言代码用英文逗号分隔,例如 en,jp。"
|
||||
|
||||
msgid "URL mode"
|
||||
msgstr "网址模式"
|
||||
|
||||
msgid "Subdirectory"
|
||||
msgstr "子目录"
|
||||
|
||||
msgid "Domain"
|
||||
msgstr "域名"
|
||||
|
||||
msgid "Domain bindings"
|
||||
msgstr "域名绑定"
|
||||
|
||||
msgid "Enter one domain=language code pair per line, for example en.example.com=en. Do not include https://, a path, or a port."
|
||||
msgstr "每行一个域名=语言代码,例如 en.example.com=en。请勿填写 https://、路径或端口。"
|
||||
|
||||
msgid "Baidu application ID"
|
||||
msgstr "百度应用 ID"
|
||||
|
||||
msgid "Baidu secret key"
|
||||
msgstr "百度密钥"
|
||||
|
||||
msgid "After saving settings for the first time, manually run \"Retranslate all content\" below."
|
||||
msgstr "第一次保存设置后,请手动执行下方的“重新翻译所有内容”。"
|
||||
|
||||
msgid "Request logging"
|
||||
msgstr "请求日志"
|
||||
|
||||
msgid "Log each Baidu Translate request"
|
||||
msgstr "记录每次百度翻译请求"
|
||||
|
||||
msgid "When enabled, fires the htbd_translation_request_logged action with the language, field, text fingerprint, length, and result status. Credentials, source text, translated text, and full API responses are never included."
|
||||
msgstr "启用后触发 htbd_translation_request_logged 操作,提供语言、字段、文本指纹、长度和结果状态;不会提供密钥、原文、译文或完整 API 响应。"
|
||||
|
||||
msgid "Retranslate content"
|
||||
msgstr "重新翻译内容"
|
||||
|
||||
msgid "Queue all published posts, pages, categories, and tags for translation again."
|
||||
msgstr "将所有已发布文章、页面、分类和标签重新加入翻译队列。"
|
||||
|
||||
msgid "This will retranslate all content and use your API quota. Continue?"
|
||||
msgstr "这将重新翻译所有内容,请注意 API 消耗。确认继续吗?"
|
||||
|
||||
msgid "Retranslate all content"
|
||||
msgstr "重新翻译所有内容"
|
||||
|
||||
msgid "You can also retranslate a single content type."
|
||||
msgstr "也可以只重新翻译一种内容类型。"
|
||||
|
||||
msgid "This will retranslate all posts and pages and use your API quota. Continue?"
|
||||
msgstr "这将重新翻译所有文章和页面,请注意 API 消耗。确认继续吗?"
|
||||
|
||||
msgid "Translate all posts"
|
||||
msgstr "翻译所有文章"
|
||||
|
||||
msgid "This will retranslate all categories and tags and use your API quota. Continue?"
|
||||
msgstr "这将重新翻译所有分类和标签,请注意 API 消耗。确认继续吗?"
|
||||
|
||||
msgid "Translate all categories and tags"
|
||||
msgstr "翻译所有分类和标签"
|
||||
|
||||
msgid "Translation cache"
|
||||
msgstr "翻译缓存"
|
||||
|
||||
msgid "After clearing the cache, the front end temporarily displays source content. Baidu Translate API calls resume when content is translated again."
|
||||
msgstr "清除后,前台会暂时显示源文;重新翻译时将再次调用百度翻译 API。"
|
||||
|
||||
msgid "This will delete all saved translations, cancel pending translation tasks, and reset progress. Continue?"
|
||||
msgstr "这将删除所有已保存的翻译、取消待执行翻译任务并重置进度。确认继续吗?"
|
||||
|
||||
#, php-format
|
||||
msgid "This will delete all saved %s translations. Continue?"
|
||||
msgstr "这将删除所有已保存的 %s 翻译。确认继续吗?"
|
||||
|
||||
msgid "Clear translation cache"
|
||||
msgstr "清除已翻译的缓存"
|
||||
|
||||
msgid "Translation progress"
|
||||
msgstr "翻译进度"
|
||||
|
||||
msgid "Loading translation progress..."
|
||||
msgstr "正在读取翻译进度..."
|
||||
|
||||
msgid "You do not have permission to perform this action."
|
||||
msgstr "您没有执行此操作的权限。"
|
||||
|
||||
msgid "Invalid translation scope."
|
||||
msgstr "无效的翻译范围。"
|
||||
|
||||
msgid "Failed to clear the translation cache."
|
||||
msgstr "翻译缓存清除失败。"
|
||||
|
||||
msgid "Settings"
|
||||
msgstr "设置"
|
||||
|
||||
msgid "Translation status"
|
||||
msgstr "翻译状态"
|
||||
|
||||
msgid "Retranslate"
|
||||
msgstr "重新翻译"
|
||||
|
||||
msgid "Translate"
|
||||
msgstr "翻译"
|
||||
|
||||
msgid "Translated"
|
||||
msgstr "已翻译"
|
||||
|
||||
msgid "Not translated"
|
||||
msgstr "未翻译"
|
||||
|
||||
msgid "Last translated:"
|
||||
msgstr "上次翻译:"
|
||||
|
||||
msgid "Invalid translation request."
|
||||
msgstr "无效的翻译请求。"
|
||||
|
||||
#, php-format
|
||||
msgid "Content #%d"
|
||||
msgstr "内容 #%d"
|
||||
|
||||
msgid "Full translation"
|
||||
msgstr "全量翻译"
|
||||
|
||||
msgid "Single post translation"
|
||||
msgstr "单篇文章翻译"
|
||||
|
||||
msgid "Posts and pages translation"
|
||||
msgstr "文章和页面翻译"
|
||||
|
||||
msgid "Categories and tags translation"
|
||||
msgstr "分类和标签翻译"
|
||||
|
||||
msgid "No translation task"
|
||||
msgstr "暂无翻译任务"
|
||||
|
||||
msgid "Baidu Translate credentials are not configured."
|
||||
msgstr "尚未配置百度翻译凭据。"
|
||||
|
||||
msgid "Baidu Translate returned an invalid response."
|
||||
msgstr "百度翻译返回了无效响应。"
|
||||
|
||||
msgid "GitHub Repository"
|
||||
msgstr "GitHub 代码仓库"
|
||||
|
||||
msgid "https://www.vblg.top"
|
||||
msgstr "https://www.vblg.top"
|
||||
|
||||
msgid "hanyixuanten"
|
||||
msgstr "hanyixuanten"
|
||||
|
||||
msgid "Persistent multilingual WordPress translations powered by Baidu Translate."
|
||||
msgstr "由百度翻译提供支持,为 WordPress 生成并持久化保存多语言翻译。"
|
||||
|
||||
msgid "https://www.vblg.top/index.php/archives/147"
|
||||
msgstr "https://www.vblg.top/index.php/archives/147"
|
||||
|
||||
msgid "HTBD - hyx Translator powered by Baidu Translate"
|
||||
msgstr "HTBD - hyx 百度翻译多语言插件"
|
||||
@@ -0,0 +1,262 @@
|
||||
# SOME DESCRIPTIVE TITLE.
|
||||
# Copyright (C) YEAR HTBD contributors
|
||||
# This file is distributed under the same license as the hyx-translator-for-baidu-translate package.
|
||||
# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
|
||||
#
|
||||
#, fuzzy
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: hyx-translator-for-baidu-translate 0.3.1\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2026-07-28 14:41+0800\n"
|
||||
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
|
||||
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
|
||||
"Language-Team: LANGUAGE <[email protected]>\n"
|
||||
"Language: \n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=CHARSET\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
|
||||
#: includes/class-admin.php:22
|
||||
msgid "HTBD"
|
||||
msgstr ""
|
||||
|
||||
#: includes/class-admin.php:48
|
||||
#, php-format
|
||||
msgid "(%1$s / %2$s content items)"
|
||||
msgstr ""
|
||||
|
||||
#: includes/class-admin.php:49
|
||||
#, php-format
|
||||
msgid ""
|
||||
"Latest task: %1$s. Posts and pages: %2$s / %3$s; categories and tags: %4$s / "
|
||||
"%5$s. Updates automatically every 5 seconds."
|
||||
msgstr ""
|
||||
|
||||
#: includes/class-admin.php:50
|
||||
msgid "Unable to load translation progress at this time."
|
||||
msgstr ""
|
||||
|
||||
msgid "Failed translations"
|
||||
msgstr ""
|
||||
|
||||
msgid "Source language detection"
|
||||
msgstr ""
|
||||
|
||||
msgid "Automatically detect the source language"
|
||||
msgstr ""
|
||||
|
||||
#: includes/class-admin.php:95
|
||||
msgid "Source language"
|
||||
msgstr ""
|
||||
|
||||
#: includes/class-admin.php:95
|
||||
msgid "Use a Baidu Translate language code, for example zh."
|
||||
msgstr ""
|
||||
|
||||
#: includes/class-admin.php:96
|
||||
msgid "Target languages"
|
||||
msgstr ""
|
||||
|
||||
#: includes/class-admin.php:96
|
||||
msgid "Separate multiple language codes with commas, for example en,jp."
|
||||
msgstr ""
|
||||
|
||||
#: includes/class-admin.php:97
|
||||
msgid "URL mode"
|
||||
msgstr ""
|
||||
|
||||
#: includes/class-admin.php:97
|
||||
msgid "Subdirectory"
|
||||
msgstr ""
|
||||
|
||||
#: includes/class-admin.php:97
|
||||
msgid "Domain"
|
||||
msgstr ""
|
||||
|
||||
#: includes/class-admin.php:98
|
||||
msgid "Domain bindings"
|
||||
msgstr ""
|
||||
|
||||
#: includes/class-admin.php:98
|
||||
msgid ""
|
||||
"Enter one domain=language code pair per line, for example en.example.com=en. "
|
||||
"Do not include https://, a path, or a port."
|
||||
msgstr ""
|
||||
|
||||
#: includes/class-admin.php:99
|
||||
msgid "Baidu application ID"
|
||||
msgstr ""
|
||||
|
||||
#: includes/class-admin.php:100
|
||||
msgid "Baidu secret key"
|
||||
msgstr ""
|
||||
|
||||
msgid "After saving settings for the first time, manually run \"Retranslate all content\" below."
|
||||
msgstr ""
|
||||
|
||||
#: includes/class-admin.php:101
|
||||
msgid "Request logging"
|
||||
msgstr ""
|
||||
|
||||
#: includes/class-admin.php:101
|
||||
msgid "Log each Baidu Translate request"
|
||||
msgstr ""
|
||||
|
||||
#: includes/class-admin.php:101
|
||||
msgid ""
|
||||
"When enabled, fires the htbd_translation_request_logged action with "
|
||||
"the language, field, text fingerprint, length, and result status. "
|
||||
"Credentials, source text, translated text, and full API responses are never "
|
||||
"included."
|
||||
msgstr ""
|
||||
|
||||
#: includes/class-admin.php:106
|
||||
msgid "Retranslate content"
|
||||
msgstr ""
|
||||
|
||||
#: includes/class-admin.php:107
|
||||
msgid ""
|
||||
"Queue all published posts, pages, categories, and tags for translation again."
|
||||
msgstr ""
|
||||
|
||||
#: includes/class-admin.php:108
|
||||
msgid "This will retranslate all content and use your API quota. Continue?"
|
||||
msgstr ""
|
||||
|
||||
#: includes/class-admin.php:111
|
||||
msgid "Retranslate all content"
|
||||
msgstr ""
|
||||
|
||||
#: includes/class-admin.php:113
|
||||
msgid "You can also retranslate a single content type."
|
||||
msgstr ""
|
||||
|
||||
#: includes/class-admin.php:114
|
||||
msgid ""
|
||||
"This will retranslate all posts and pages and use your API quota. Continue?"
|
||||
msgstr ""
|
||||
|
||||
#: includes/class-admin.php:118
|
||||
msgid "Translate all posts"
|
||||
msgstr ""
|
||||
|
||||
#: includes/class-admin.php:120
|
||||
msgid ""
|
||||
"This will retranslate all categories and tags and use your API quota. "
|
||||
"Continue?"
|
||||
msgstr ""
|
||||
|
||||
#: includes/class-admin.php:124
|
||||
msgid "Translate all categories and tags"
|
||||
msgstr ""
|
||||
|
||||
#: includes/class-admin.php:127
|
||||
msgid "Translation cache"
|
||||
msgstr ""
|
||||
|
||||
#: includes/class-admin.php:128
|
||||
msgid ""
|
||||
"After clearing the cache, the front end temporarily displays source content. "
|
||||
"Baidu Translate API calls resume when content is translated again."
|
||||
msgstr ""
|
||||
|
||||
#: includes/class-admin.php:129
|
||||
msgid ""
|
||||
"This will delete all saved translations, cancel pending translation tasks, "
|
||||
"and reset progress. Continue?"
|
||||
msgstr ""
|
||||
|
||||
#. translators: %s: target language code.
|
||||
#, php-format
|
||||
msgid "This will delete all saved %s translations. Continue?"
|
||||
msgstr ""
|
||||
|
||||
#: includes/class-admin.php:132
|
||||
msgid "Clear translation cache"
|
||||
msgstr ""
|
||||
|
||||
#: includes/class-admin.php:135
|
||||
msgid "Translation progress"
|
||||
msgstr ""
|
||||
|
||||
#: includes/class-admin.php:137
|
||||
msgid "Loading translation progress..."
|
||||
msgstr ""
|
||||
|
||||
#: includes/class-admin.php:145
|
||||
#: includes/class-admin.php:211
|
||||
msgid "You do not have permission to perform this action."
|
||||
msgstr ""
|
||||
|
||||
#: includes/class-admin.php:151
|
||||
msgid "Invalid translation scope."
|
||||
msgstr ""
|
||||
|
||||
#: includes/class-admin.php:218
|
||||
msgid "Failed to clear the translation cache."
|
||||
msgstr ""
|
||||
|
||||
#: includes/class-admin.php:238
|
||||
msgid "Settings"
|
||||
msgstr ""
|
||||
|
||||
#: includes/class-admin.php:281
|
||||
msgid "Translation status"
|
||||
msgstr ""
|
||||
|
||||
#: includes/class-admin.php:296
|
||||
msgid "Retranslate"
|
||||
msgstr ""
|
||||
|
||||
#: includes/class-admin.php:296
|
||||
msgid "Translate"
|
||||
msgstr ""
|
||||
|
||||
#: includes/class-admin.php:311
|
||||
msgid "Translated"
|
||||
msgstr ""
|
||||
|
||||
#: includes/class-admin.php:311
|
||||
msgid "Not translated"
|
||||
msgstr ""
|
||||
|
||||
#: includes/class-admin.php:313
|
||||
msgid "Last translated:"
|
||||
msgstr ""
|
||||
|
||||
#: includes/class-admin.php:325
|
||||
msgid "Invalid translation request."
|
||||
msgstr ""
|
||||
|
||||
#, php-format
|
||||
msgid "Content #%d"
|
||||
msgstr ""
|
||||
|
||||
#: includes/class-admin.php:351
|
||||
msgid "Full translation"
|
||||
msgstr ""
|
||||
|
||||
#: includes/class-admin.php:355
|
||||
msgid "Single post translation"
|
||||
msgstr ""
|
||||
|
||||
#: includes/class-admin.php:359
|
||||
msgid "Posts and pages translation"
|
||||
msgstr ""
|
||||
|
||||
#: includes/class-admin.php:363
|
||||
msgid "Categories and tags translation"
|
||||
msgstr ""
|
||||
|
||||
#: includes/class-admin.php:366
|
||||
msgid "No translation task"
|
||||
msgstr ""
|
||||
|
||||
#: includes/class-baidu-provider.php:22
|
||||
msgid "Baidu Translate credentials are not configured."
|
||||
msgstr ""
|
||||
|
||||
#: includes/class-baidu-provider.php:52
|
||||
msgid "Baidu Translate returned an invalid response."
|
||||
msgstr ""
|
||||
@@ -0,0 +1,199 @@
|
||||
# Translation of Plugins - HTBD – hyx Translator powered by Baidu Translate - Stable Readme (latest release) in Chinese (China)
|
||||
# This file is distributed under the same license as the Plugins - HTBD – hyx Translator powered by Baidu Translate - Stable Readme (latest release) package.
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"PO-Revision-Date: 2026-08-06 00:00+0000\n"
|
||||
"Last-Translator: HTBD contributors\n"
|
||||
"Language-Team: Chinese (China)\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
"Plural-Forms: nplurals=1; plural=0;\n"
|
||||
"X-Generator: GlotPress/4.0.3\n"
|
||||
"Language: zh_CN\n"
|
||||
"Project-Id-Version: Plugins - HTBD – hyx Translator powered by Baidu Translate - Stable Readme (latest release)\n"
|
||||
|
||||
#. Short description.
|
||||
#, gp-priority: high
|
||||
msgid "Persistent multilingual WordPress translations powered by the Baidu Translate API."
|
||||
msgstr "由百度翻译 API 提供支持,为 WordPress 生成并持久化保存多语言翻译。"
|
||||
|
||||
#. Plugin name.
|
||||
#, gp-priority: high
|
||||
msgid "HTBD - hyx Translator powered by Baidu Translate"
|
||||
msgstr "HTBD - hyx 百度翻译多语言插件"
|
||||
|
||||
#. Found in faq paragraph.
|
||||
msgid "Uninstalling removes plugin settings, scheduled tasks, and the custom translation table, including all persisted translations."
|
||||
msgstr "卸载会删除插件设置、计划任务和自建翻译表,包括所有已持久化的翻译。"
|
||||
|
||||
#. Found in faq paragraph.
|
||||
msgid "Failed posts, pages, categories, or tags appear below the progress bar on the Settings page. Use the Retranslate button beside an item to retry it."
|
||||
msgstr "翻译失败的文章、页面、分类或标签会显示在设置页面的进度条下方。点击对应项目旁的“重新翻译”按钮即可重试。"
|
||||
|
||||
#. Found in faq paragraph.
|
||||
msgid "No. Content inside HTML <code><pre></code> and <code><code></code> elements is preserved in the source language and is not sent for translation."
|
||||
msgstr "不会。HTML <code><pre></code> 和 <code><code></code> 元素内的内容会保留源语言,不会发送进行翻译。"
|
||||
|
||||
#. Found in faq paragraph.
|
||||
msgid "No. Each value is translated by a scheduled task and persisted for reuse. Front-end requests read stored translations only."
|
||||
msgstr "不会。每个值都由计划任务翻译并持久化保存以供重复使用。前端请求只读取已保存的翻译。"
|
||||
|
||||
#. Found in faq header.
|
||||
msgid "What data is removed during uninstall?"
|
||||
msgstr "卸载时会删除哪些数据?"
|
||||
|
||||
#. Found in faq header.
|
||||
msgid "How do I retry a failed translation?"
|
||||
msgstr "如何重试失败的翻译?"
|
||||
|
||||
#. Found in faq header.
|
||||
msgid "Does the plugin translate code in posts or pages?"
|
||||
msgstr "插件会翻译文章或页面中的代码吗?"
|
||||
|
||||
#. Found in faq header.
|
||||
msgid "Does the plugin translate content on every page request?"
|
||||
msgstr "插件会在每次页面请求时翻译内容吗?"
|
||||
|
||||
#. Found in installation paragraph.
|
||||
msgid "Automatic detection is stored independently from the source language value. Manually entering <code>auto</code> without enabling automatic detection remains a distinct configuration state."
|
||||
msgstr "自动识别状态与源语言值分开保存。在未启用自动识别时手动输入 <code>auto</code>,仍属于不同的配置状态。"
|
||||
|
||||
#. Found in installation list item.
|
||||
msgid "After saving settings for the first time, manually run Retranslate all content to queue existing content."
|
||||
msgstr "第一次保存设置后,手动执行“重新翻译所有内容”,将现有内容加入队列。"
|
||||
|
||||
#. Found in installation list item.
|
||||
msgid "Open Settings > HTBD and configure the Baidu credentials, languages, and routing mode. Enable automatic source-language detection to use Baidu's <code>auto</code> source language mode."
|
||||
msgstr "打开“设置 > HTBD”,配置百度翻译凭据、语言和路由模式。启用自动识别源语言可使用百度翻译的 <code>auto</code> 源语言模式。"
|
||||
|
||||
#. Found in installation list item.
|
||||
msgid "Activate HTBD through the Plugins screen in WordPress."
|
||||
msgstr "在 WordPress 的“插件”页面启用 HTBD。"
|
||||
|
||||
#. Found in installation list item.
|
||||
msgid "Upload the <code>hyx-translator-for-baidu-translate</code> directory to <code>/wp-content/plugins/</code>."
|
||||
msgstr "将 <code>hyx-translator-for-baidu-translate</code> 目录上传到 <code>/wp-content/plugins/</code>。"
|
||||
|
||||
#. Found in description paragraph.
|
||||
msgid "HTBD stores the returned translated text in the WordPress database but does not store complete Baidu response payloads. Data sent to Baidu is subject to Baidu's own terms and privacy practices:"
|
||||
msgstr "HTBD 将百度返回的译文保存在 WordPress 数据库中,但不保存完整的百度响应内容。发送给百度的数据受百度自身条款和隐私规则约束:"
|
||||
|
||||
#. Found in description paragraph.
|
||||
msgid "For each translation request, the plugin sends the text being translated, the configured source and target language codes, the Baidu application ID, a random salt, and a request signature. Depending on the content being translated, the text may contain published post or page titles, body text, excerpts, selected SEO title and description values, attached image alternative text, and category or tag names and descriptions. Standard network information, including the originating server IP address, is also visible to Baidu as part of the HTTPS request. The secret key is used locally to create the signature and is not sent as a separate request field."
|
||||
msgstr "每次翻译请求会发送待翻译文本、配置的源语言和目标语言代码、百度应用 ID、随机盐值和请求签名。根据待翻译内容,文本可能包含已发布文章或页面的标题、正文、摘要、指定的 SEO 标题和描述、附件图片替代文本,以及分类或标签的名称和描述。作为 HTTPS 请求的一部分,百度也能看到来源服务器 IP 地址等标准网络信息。密钥只在本地用于生成签名,不会作为独立的请求字段发送。"
|
||||
|
||||
#. Found in description paragraph.
|
||||
msgid "The API is contacted by scheduled tasks after a published post or page is saved, after a supported category or tag is created or edited, or after an administrator explicitly queues or refreshes translations. Front-end page views reuse stored translations and do not contact Baidu. The plugin also makes no Baidu request when valid persisted translations are already available, unless an administrator explicitly requests a refresh."
|
||||
msgstr "已发布的文章或页面保存后、支持的分类或标签创建或编辑后,或者管理员明确将翻译加入队列或刷新翻译时,计划任务会请求该 API。前端页面访问只读取已保存的翻译,不会请求百度。已有有效的持久化翻译时,插件也不会请求百度,除非管理员明确要求刷新。"
|
||||
|
||||
#. Found in description paragraph.
|
||||
msgid "HTBD connects to the Baidu Translate Open Platform API, a service provided by Baidu, to translate WordPress content. The plugin cannot generate new translations without this service. A Baidu Translate account, application ID, and secret key are required. Baidu may apply request quotas or usage charges according to the service plan selected by the site administrator."
|
||||
msgstr "HTBD 连接由百度提供的百度翻译开放平台 API,以翻译 WordPress 内容。没有该外部服务,插件无法生成新的翻译。站点管理员需要百度翻译账户、应用 ID 和密钥。百度可能根据站点管理员选择的服务方案实施请求配额或收取使用费用。"
|
||||
|
||||
#. Found in description paragraph.
|
||||
msgid "Subdirectory routing supports paths such as <code>/en/example-post/</code>. Domain routing maps configured hostnames to target languages. Requests for <code>wp-login.php</code> or <code>wp-admin</code> through a target-language route are redirected to the corresponding source-site URL while preserving query parameters. Translation failures are non-fatal and fall back to source-language content."
|
||||
msgstr "子目录路由支持 <code>/en/example-post/</code> 等路径。域名路由可将配置的主机名映射到目标语言。通过目标语言路由请求 <code>wp-login.php</code> 或 <code>wp-admin</code> 时,插件会重定向到源站对应地址并保留查询参数。翻译失败不会中断页面请求,而会回退显示源语言内容。"
|
||||
|
||||
#. Found in description paragraph.
|
||||
msgid "Supported values include published post and page titles, content, excerpts, image alternative text, categories, tags, and selected SEO fields. Automatic translation is only scheduled for posts and pages with the <code>publish</code> status. Completed translations are stored in the <code>{$wpdb->prefix}hyx_bd_translations</code> custom database table. Existing <code>{$wpdb->prefix}btranslate_translations</code> data is migrated when the renamed plugin is activated. Front-end rendering reuses stored values and never calls the translation provider dynamically."
|
||||
msgstr "支持的值包括已发布文章和页面的标题、正文、摘要、图片替代文本、分类、标签及部分 SEO 字段。插件只为 <code>publish</code> 状态的文章和页面自动安排翻译。完成的翻译保存在 <code>{$wpdb->prefix}hyx_bd_translations</code> 自建数据表中;启用改名后的插件时,已有 <code>{$wpdb->prefix}btranslate_translations</code> 数据会自动迁移。前端渲染只读取已保存的值,不会动态请求翻译服务。"
|
||||
|
||||
#. Found in description paragraph.
|
||||
msgid "The WordPress admin interface follows the current WordPress locale. English is the default. Approved WordPress.org language packs take priority, with the bundled Simplified Chinese translation used as a fallback when WordPress is set to <code>zh_CN</code>."
|
||||
msgstr "WordPress 后台界面会跟随当前 WordPress 语言。默认显示英文;WordPress.org 已批准的语言包优先,WordPress 设为 <code>zh_CN</code> 时,插件内置的简体中文翻译用作后备。"
|
||||
|
||||
#. Found in description paragraph.
|
||||
msgid "HTBD translates supported WordPress content with the Baidu Translate API and persists each translation for reuse. It supports language-specific subdirectory URLs and domain bindings."
|
||||
msgstr "HTBD 使用百度翻译 API 翻译支持的 WordPress 内容,并将每个翻译结果持久化保存以供重复使用。插件支持语言子目录 URL 和语言域名绑定。"
|
||||
|
||||
#. Found in description list item.
|
||||
msgid "Baidu privacy policy: https://privacy.baidu.com/policy"
|
||||
msgstr "百度隐私政策:https://privacy.baidu.com/policy"
|
||||
|
||||
#. Found in description list item.
|
||||
msgid "Baidu Translate Open Platform service agreement: https://fanyi-api.baidu.com/doc/6"
|
||||
msgstr "百度翻译开放平台服务协议:https://fanyi-api.baidu.com/doc/6"
|
||||
|
||||
#. Found in description list item.
|
||||
msgid "Baidu Translate API documentation: https://fanyi-api.baidu.com/doc/23"
|
||||
msgstr "百度翻译 API 文档:https://fanyi-api.baidu.com/doc/23"
|
||||
|
||||
#. Found in description header.
|
||||
msgid "External Services"
|
||||
msgstr "外部服务"
|
||||
|
||||
#. Found in changelog list item.
|
||||
#, gp-priority: low
|
||||
msgid "Improve WordPress.org localization packaging and release deployment workflows."
|
||||
msgstr "改进 WordPress.org 本地化资源打包和发布部署工作流。"
|
||||
|
||||
#. Found in changelog list item.
|
||||
#, gp-priority: low
|
||||
msgid "Preserve target-language routing when submitting comments and following comment redirects."
|
||||
msgstr "提交评论及处理评论重定向时保留目标语言路由。"
|
||||
|
||||
#. Found in changelog list item.
|
||||
#, gp-priority: low
|
||||
msgid "Fix Baidu Japanese routing to use the supported <code>jp</code> language code."
|
||||
msgstr "修复百度日语路由,改用受支持的 <code>jp</code> 语言代码。"
|
||||
|
||||
#. Found in changelog list item.
|
||||
#, gp-priority: low
|
||||
msgid "Add per-language controls for translating posts, terms, and clearing persisted translations."
|
||||
msgstr "新增按语言翻译文章和分类项以及清除持久化翻译的控制项。"
|
||||
|
||||
#. Found in changelog list item.
|
||||
#, gp-priority: low
|
||||
msgid "Add automated GitHub Release packaging and WordPress.org SVN deployment workflows."
|
||||
msgstr "新增 GitHub Release 自动打包及 WordPress.org SVN 自动部署工作流。"
|
||||
|
||||
#. Found in changelog list item.
|
||||
#, gp-priority: low
|
||||
msgid "Redirect target-language <code>wp-admin</code> and <code>wp-login.php</code> requests to the source site while preserving query parameters."
|
||||
msgstr "将目标语言路由中的 <code>wp-admin</code> 和 <code>wp-login.php</code> 请求重定向到源站,并保留查询参数。"
|
||||
|
||||
#. Found in changelog list item.
|
||||
#, gp-priority: low
|
||||
msgid "Add automatic source-language detection using Baidu's <code>auto</code> mode."
|
||||
msgstr "新增使用百度翻译 <code>auto</code> 模式自动识别源语言的功能。"
|
||||
|
||||
#. Found in changelog list item.
|
||||
#, gp-priority: low
|
||||
msgid "Fix the release archive build workflow."
|
||||
msgstr "修复发布归档构建流程。"
|
||||
|
||||
#. Found in changelog list item.
|
||||
#, gp-priority: low
|
||||
msgid "Update packaging, localization, documentation, and issue templates for the new plugin identity."
|
||||
msgstr "更新打包流程、本地化资源、文档和问题模板以适配新的插件标识。"
|
||||
|
||||
#. Found in changelog list item.
|
||||
#, gp-priority: low
|
||||
msgid "Migrate existing translation data from the previous custom database table."
|
||||
msgstr "迁移旧版自建数据表中已有的翻译数据。"
|
||||
|
||||
#. Found in changelog list item.
|
||||
#, gp-priority: low
|
||||
msgid "Rename the plugin and codebase to HTBD / hyx-translator-for-baidu-translate."
|
||||
msgstr "将插件及代码库重命名为 HTBD / hyx-translator-for-baidu-translate。"
|
||||
|
||||
#. Found in changelog list item.
|
||||
#, gp-priority: low
|
||||
msgid "Update the plugin homepage URL."
|
||||
msgstr "更新插件主页 URL。"
|
||||
|
||||
#. Found in changelog list item.
|
||||
#, gp-priority: low
|
||||
msgid "Improve plugin security metadata and translator attribution."
|
||||
msgstr "完善插件安全元数据和译者署名。"
|
||||
|
||||
#. Found in changelog list item.
|
||||
#, gp-priority: low
|
||||
msgid "Preserve content inside HTML <code><pre></code> and <code><code></code> elements instead of sending it for translation."
|
||||
msgstr "保留 HTML <code><pre></code> 和 <code><code></code> 元素内的内容,不再将其发送进行翻译。"
|
||||
|
||||
#. Found in changelog list item.
|
||||
#, gp-priority: low
|
||||
msgid "Fix translated titles so singular views use the current post context."
|
||||
msgstr "修复翻译标题,使单篇内容视图使用当前文章上下文。"
|
||||
+91
@@ -0,0 +1,91 @@
|
||||
=== HTBD - hyx Translator powered by Baidu Translate ===
|
||||
Contributors: hanyixuanten
|
||||
Homepage: https://www.vblg.top/index.php/archives/147
|
||||
Requires at least: 6.4
|
||||
Tested up to: 7.0
|
||||
Requires PHP: 8.1
|
||||
Stable tag: 0.3.1
|
||||
License: GPL-3.0-only
|
||||
License URI: https://www.gnu.org/licenses/gpl-3.0.html
|
||||
|
||||
Persistent multilingual WordPress translations powered by the Baidu Translate API.
|
||||
|
||||
== Description ==
|
||||
|
||||
HTBD translates supported WordPress content with the Baidu Translate API and persists each translation for reuse. It supports language-specific subdirectory URLs and domain bindings.
|
||||
|
||||
The WordPress admin interface follows the current WordPress locale. English is the default. Approved WordPress.org language packs take priority, with the bundled Simplified Chinese translation used as a fallback when WordPress is set to `zh_CN`.
|
||||
|
||||
Supported values include published post and page titles, content, excerpts, image alternative text, categories, tags, and selected SEO fields. Automatic translation is only scheduled for posts and pages with the `publish` status. Completed translations are stored in the `{$wpdb->prefix}hyx_bd_translations` custom database table. Existing `{$wpdb->prefix}btranslate_translations` data is migrated when the renamed plugin is activated. Front-end rendering reuses stored values and never calls the translation provider dynamically.
|
||||
|
||||
Subdirectory routing supports paths such as `/en/example-post/`. Domain routing maps configured hostnames to target languages. Requests for `wp-login.php` or `wp-admin` through a target-language route are redirected to the corresponding source-site URL while preserving query parameters. Translation failures are non-fatal and fall back to source-language content.
|
||||
|
||||
== Installation ==
|
||||
|
||||
1. Upload the `hyx-translator-for-baidu-translate` directory to `/wp-content/plugins/`.
|
||||
2. Activate HTBD through the Plugins screen in WordPress.
|
||||
3. Open Settings > HTBD and configure the Baidu credentials, languages, and routing mode. Enable automatic source-language detection to use Baidu's `auto` source language mode.
|
||||
4. After saving settings for the first time, manually run Retranslate all content to queue existing content.
|
||||
|
||||
Automatic detection is stored independently from the source language value. Manually entering `auto` without enabling automatic detection remains a distinct configuration state.
|
||||
|
||||
== External Services ==
|
||||
|
||||
HTBD connects to the Baidu Translate Open Platform API, a service provided by Baidu, to translate WordPress content. The plugin cannot generate new translations without this service. A Baidu Translate account, application ID, and secret key are required. Baidu may apply request quotas or usage charges according to the service plan selected by the site administrator.
|
||||
|
||||
The API is contacted by scheduled tasks after a published post or page is saved, after a supported category or tag is created or edited, or after an administrator explicitly queues or refreshes translations. Front-end page views reuse stored translations and do not contact Baidu. The plugin also makes no Baidu request when valid persisted translations are already available, unless an administrator explicitly requests a refresh.
|
||||
|
||||
For each translation request, the plugin sends the text being translated, the configured source and target language codes, the Baidu application ID, a random salt, and a request signature. Depending on the content being translated, the text may contain published post or page titles, body text, excerpts, selected SEO title and description values, attached image alternative text, and category or tag names and descriptions. Standard network information, including the originating server IP address, is also visible to Baidu as part of the HTTPS request. The secret key is used locally to create the signature and is not sent as a separate request field.
|
||||
|
||||
HTBD stores the returned translated text in the WordPress database but does not store complete Baidu response payloads. Data sent to Baidu is subject to Baidu's own terms and privacy practices:
|
||||
|
||||
* Baidu Translate API documentation: https://fanyi-api.baidu.com/doc/23
|
||||
* Baidu Translate Open Platform service agreement: https://fanyi-api.baidu.com/doc/6
|
||||
* Baidu privacy policy: https://privacy.baidu.com/policy
|
||||
|
||||
== Frequently Asked Questions ==
|
||||
|
||||
= Does the plugin translate content on every page request? =
|
||||
|
||||
No. Each value is translated by a scheduled task and persisted for reuse. Front-end requests read stored translations only.
|
||||
|
||||
= Does the plugin translate code in posts or pages? =
|
||||
|
||||
No. Content inside HTML `<pre>` and `<code>` elements is preserved in the source language and is not sent for translation.
|
||||
|
||||
= How do I retry a failed translation? =
|
||||
|
||||
Failed posts, pages, categories, or tags appear below the progress bar on the Settings page. Use the Retranslate button beside an item to retry it.
|
||||
|
||||
= What data is removed during uninstall? =
|
||||
|
||||
Uninstalling removes plugin settings, scheduled tasks, and the custom translation table, including all persisted translations.
|
||||
|
||||
== Changelog ==
|
||||
|
||||
= 0.3.1 =
|
||||
|
||||
* Add per-language controls for translating posts, terms, and clearing persisted translations.
|
||||
* Fix Baidu Japanese routing to use the supported `jp` language code.
|
||||
* Preserve target-language routing when submitting comments and following comment redirects.
|
||||
* Improve WordPress.org localization packaging and release deployment workflows.
|
||||
|
||||
= 0.3.0 =
|
||||
|
||||
* Add automatic source-language detection using Baidu's `auto` mode.
|
||||
* Redirect target-language `wp-admin` and `wp-login.php` requests to the source site while preserving query parameters.
|
||||
* Add automated GitHub Release packaging and WordPress.org SVN deployment workflows.
|
||||
|
||||
= 0.2.4 =
|
||||
|
||||
* Fix translated titles so singular views use the current post context.
|
||||
* Preserve content inside HTML `<pre>` and `<code>` elements instead of sending it for translation.
|
||||
* Improve plugin security metadata and translator attribution.
|
||||
* Update the plugin homepage URL.
|
||||
|
||||
= 0.2.3 =
|
||||
|
||||
* Rename the plugin and codebase to HTBD / hyx-translator-for-baidu-translate.
|
||||
* Migrate existing translation data from the previous custom database table.
|
||||
* Update packaging, localization, documentation, and issue templates for the new plugin identity.
|
||||
* Fix the release archive build workflow.
|
||||
+4
-31
@@ -1,41 +1,14 @@
|
||||
<?php
|
||||
/**
|
||||
* Removes all data created by Btranslate.
|
||||
* Removes all data created by HTBD.
|
||||
*
|
||||
* @package WPT
|
||||
* @package HTBD
|
||||
*/
|
||||
|
||||
if ( ! defined( 'WP_UNINSTALL_PLUGIN' ) ) {
|
||||
exit;
|
||||
}
|
||||
|
||||
function wpt_uninstall_site_data() {
|
||||
global $wpdb;
|
||||
require_once __DIR__ . '/includes/class-uninstaller.php';
|
||||
|
||||
wp_clear_scheduled_hook( 'wpt_process_translation' );
|
||||
wp_clear_scheduled_hook( 'wpt_process_term_translation' );
|
||||
wp_clear_scheduled_hook( 'wpt_process_seo_output_translation' );
|
||||
delete_option( 'wpt_settings' );
|
||||
delete_option( 'wpt_translation_task' );
|
||||
delete_option( 'wpt_retranslation_batch' );
|
||||
|
||||
$table_name = $wpdb->prefix . 'wpt_translations';
|
||||
$wpdb->query( "DROP TABLE IF EXISTS {$table_name}" );
|
||||
}
|
||||
|
||||
if ( is_multisite() ) {
|
||||
$site_ids = get_sites(
|
||||
array(
|
||||
'fields' => 'ids',
|
||||
'number' => 0,
|
||||
)
|
||||
);
|
||||
|
||||
foreach ( $site_ids as $site_id ) {
|
||||
switch_to_blog( $site_id );
|
||||
wpt_uninstall_site_data();
|
||||
restore_current_blog();
|
||||
}
|
||||
} else {
|
||||
wpt_uninstall_site_data();
|
||||
}
|
||||
HTBD_Uninstaller::uninstall();
|
||||
@@ -1,34 +0,0 @@
|
||||
<?php
|
||||
/**
|
||||
* Plugin Name: Btranslate
|
||||
* Description: Persistent multilingual WordPress translations powered by Baidu Translate.
|
||||
* Version: 0.2.0
|
||||
* Requires at least: 6.4
|
||||
* Requires PHP: 8.1
|
||||
* Author: hanyixuanten
|
||||
* Text Domain: wp-btranslate
|
||||
*/
|
||||
|
||||
defined( 'ABSPATH' ) || exit;
|
||||
|
||||
define( 'WPT_VERSION', '0.2.0' );
|
||||
define( 'WPT_FILE', __FILE__ );
|
||||
define( 'WPT_PATH', plugin_dir_path( __FILE__ ) );
|
||||
|
||||
require_once WPT_PATH . 'includes/interface-wpt-translation-provider.php';
|
||||
require_once WPT_PATH . 'includes/class-wpt-translation-result.php';
|
||||
require_once WPT_PATH . 'includes/class-wpt-translation-identity.php';
|
||||
require_once WPT_PATH . 'includes/class-wpt-translation-store.php';
|
||||
require_once WPT_PATH . 'includes/class-wpt-baidu-provider.php';
|
||||
require_once WPT_PATH . 'includes/class-wpt-translation-service.php';
|
||||
require_once WPT_PATH . 'includes/class-wpt-content-translator.php';
|
||||
require_once WPT_PATH . 'includes/class-wpt-settings.php';
|
||||
require_once WPT_PATH . 'includes/class-wpt-language-router.php';
|
||||
require_once WPT_PATH . 'includes/class-wpt-content-controller.php';
|
||||
require_once WPT_PATH . 'includes/class-wpt-admin.php';
|
||||
require_once WPT_PATH . 'includes/class-wpt-plugin.php';
|
||||
|
||||
register_activation_hook( WPT_FILE, array( 'WPT_Plugin', 'activate' ) );
|
||||
register_deactivation_hook( WPT_FILE, array( 'WPT_Plugin', 'deactivate' ) );
|
||||
|
||||
add_action( 'plugins_loaded', array( 'WPT_Plugin', 'instance' ) );
|
||||
Reference in New Issue
Block a user