[Wordpress] Tạo plugin hỗ trợ Polylang free version: String translation
Hello mọi người, lâu lắm rồi mới nổi hứng vào viblo viết bài 😆. Đúng lúc đang làm dự án wordpress nên mình trở lại bằng 1 bài hướng dẫn về wordpress nhé 🤭
Là người Việt Nam, và cũng là dev, thì mình không thường mua bản quyền hay version pro cho lắm. Hồi trước cũng có mua dùng plugin bản quyền đấy, nhưng thời buổi bây giờ các nhà phát triển bào tiền quá, yêu cầu đóng phí theo tháng/theo năm thay vì bỏ tiền mua đứt như trước đây, mà wordpress version thì update thường xuyên, dẫn đến các plugin hồi xưa mua giờ outdate, lỗi version... nên mình thấy mua thì tốn tiền quá, dùng thì ít (mình không thường được phân dự án wordpress, khách lại không muốn trả tiền nhiều như thế theo tháng) phải tìm cách lươn lẹo thôi 🙈.
Tất nhiên các bạn có thể tìm bản crack trên mạng, nhưng mà rủi ro cũng hên xui. Vì vấn đề security, mình quyết định nhờ sự trợ giúp của ChatGPT (cái này thì mình có đk gói pro 🤡), làm ít code support cho các plugin bản free nhưng chức năng pro. Bài này sẽ là về Plugin Polylang nhé
Về plugin Polylang

Plugin Homepage: https://wordpress.org/plugins/polylang/
Version mình đang sử dụng trong bài: 3.7.6
Tóm tắt chức năng:
- Đây là plugin hỗ trợ đa ngôn ngữ cho website của bạn, khá tiện dụng khi quản lý các bản dịch bài viết, hỗ trợ đa ngôn ngữ cho cả Taxonomies.
- Plugin free có thể add nhiều ngôn ngữ. 1 số plugin khác thì chỉ được 2 ngôn ngữ, ngôn ngữ thứ 3 là phải trả phí rồi. Không hỗ trợ nhiều với Woocommerce
String translation: Basic handle
Hướng dẫn sử dụng string translation
Vì đây là bài hướng dẫn nâng cao, tạo hỗ trợ quản lý string translation của Polylang, nên mình sẽ không dài dòng hướng dẫn sử dụng và cài đặt Polylang ở đây, bắt đầu vào việc chính về translate luôn nhé:
Về cơ bản, ngoại trừ các Translate sẵn có, bạn có thể quản lý danh sách các custom string đã được translate tại màn hình: Admin menu bar -> Language -> Translations (hoặc String translation ở các version cũ), hoặc truy cập URL: /wp-admin/admin.php?page=mlang_strings&s&group=-1&paged=1

Đây chính là màn hình quản lý các string sẽ được dịch với Plugin polylang.
Như bạn thấy trên ảnh, tại đây (bản free) không có nút thêm string mới, vỏn vẹn 3 string được add sẵn. Vậy muốn user khỏe, thêm string rồi tự edit (để dev ko phải tự đi add từng string translate trong TH nhiều ngôn ngữ), mình sẽ thêm string vào màn hình quản lý này, để khách tự xử lý phần dịch bằng cách fill cột / ngôn ngữ tương ứng ở cột Translations.
Target đầu tiên: thêm được các string vào màn hình quản lý này.
Dịch 'string' với polylang
Cách để Plugin polylang có thể dịch string trong code của bạn sẽ là như sau:
echo pll__( 'Hello world' );
Như vậy, chỉ cần từ khóa Hello word có trong màn hình quản lý, và đã được fill bản dịch, thì khi chuyển đổi ngôn ngữ, nó cũng sẽ được dịch theo
Trong trường hợp có đính kèm params:
printf(
pll__( 'Hello %s' ),
esc_html( $username )
);
//Kết quả: $username sẽ được fill vào vị trí của %s
Target 1: Thêm String vào màn hình quản lý string ở admin
Tại functions.php, bạn thử thêm đoạn code sau:
pll_register_string(
'hello_world', // unique name
'Hello world', // default string
'My Theme' // group name in admin
);
Giải thích 1 chút:
unique name: giống như key của string, cần unique để tránh lẫn lộn với các string khác trong bảng quản lý, VD như tránh đồng âm/đồng cách viết nhưng khác nghĩa trong ngôn ngữ khácDefault string: Trong trường hợp các bản dịch (trong màn hình quản lý) chưa được fill, sẽ hiển thị default string này. Mặc địch khi sử dụngpll__( 'Hello world' );thì string cần fill sẽ là text nàygroup name in admin: sử dụng để tiện cho chúng ta phân loại các string, chẳng hạn group theo tên màn hình, cũng dễ để filter
Giờ, quay lại màn hình quản lý kiểm tra, bạn có thể thấy string đã được thêm, và bạn có thể dịch cho nó rồi:

Thế nhưng mà, 1 trang web có bao nhiêu là string cần translate, chả lẽ lại phải thêm từng đấy cái pll_register_string() vào functions.php 😱😱
Tất nhiên là 'không cùng đường thì ai lại làm thế', chúng ta có Target 2: Tạo chức năng add string ngay trên màn admin UI
Giờ có 1 vấn đề, trên là cách add string bằng php, vậy lúc cần dùng JS thì phải hướng dẫn đi chứ???
Sử dụng Polylang với JS
Bước 1: Vẫn phải đăng ký string bằng PHP trước
Như phía trên, hãy vào functions.php thêm đoạn đăng ký:
pll_register_string( 'js_loading', 'Loading...', 'My Theme' );
Để pass đoạn dịch sang JS, cần thêm đoạn code
wp_localize_script(
'my-script',
'i18n',
[
'loading' => pll__( 'Loading...' )
]
);
OK chỉ thế là xong rồi đấy, thử nhé:
console.log(i18n.loading);
Nhớ chú ý key string cho đúng nhé, đừng lộn
default Stringvớiunique name, trong functionpll__()là điền default String nhé
Với ACF/Custom Fields
echo pll__( get_field( 'important_notice', 'option' ) );
Với Shortcodes
function custom_button_shortcode() {
return '<button>' . pll__( 'Go to now' ) . '</button>';
}
add_shortcode( 'custom_button', 'custom_button_shortcode' );
Với custom plugins
<label><?php echo esc_html( pll__( 'Gallery Images' ) ); ?></label>
Cheat seat
| Task | Function |
|---|---|
| Translate string | pll__( 'Text' ) |
| Echo translation | pll_e( 'Text' ) |
| Register string | pll_register_string() |
| Current language | pll_current_language() |
| Default language | pll_default_language() |
Target 2: Build support plugin for Polylang string translation
Mục đích chính của chúng ta là đây: Tạo chức năng support việc add thêm custom string vào màn hình quản lý String Translation. Ở đây mình chọn tạo Plugin, để sau này mình có plugin cắm vào các dự án khác, mà không làm ảnh hưởng tới plugin gốc, đề phòng plugin gốc cập nhật thêm nhé.
Tạo plugin mới
Quy trình tạo plugin mới trong Wordpress thì vẫn là cơ bản thôi:
- Bước 1: Trong folder
pluginstạo 1 folder mới, đặt tên theo tên plugin bạn muốn, VD:wp-content/plugins/polylang-string-support - Bước 2: Tại folder vừa tạo: Tạo file php mới trùng tên với tên folder:
wp-content/plugins/polylang-string-support/polylang-string-support.php - Bước 3: Thêm các line thông tin cơ bản của wordpress plugin vào file:
<?php
/**
* Plugin Name: My Polylang support String Manager
* Description: Admin UI to register Polylang strings (stored in DB) without touching functions.php.
* Version: 1.0.0
* Author: bunny.pi.green
*/
if ( ! defined( 'ABSPATH' ) ) exit;
class Polylang_String_Support {
const OPTION_KEY = 'tb_pll_strings';
}
new Polylang_String_Support();
Bước 4 sẽ là phần chi tiết plugin dưới đây, fill lần lượt vào trong class trên nhé:
Chi tiết plugin:
Đầu tiên, chúng ta tạo function __construct() để thêm hook cho plugin:
public function __construct() {
add_action( 'admin_menu', [ $this, 'admin_menu' ] );
add_action( 'admin_init', [ $this, 'register_settings' ] );
// Register strings on every request early enough for themes/plugins to use pll__()
add_action( 'init', [ $this, 'register_saved_strings' ], 5 );
}
Tiếp theo là đăng ký thêm màn hình quản lý vào admin menu bar cho dễ truy cập. Ở đây mình chọn tạo 1 màn hình mới riêng, chỉ nhằm mục đích thay cho việc phải tự fill function pll_register_string() mỗi khi muốn thêm string. Lúc này, màn hình mới của mình sẽ có:
- 1 Form input group là 3 params phải fill vào function
pll_register_string()như đã nhắc tới ở trên, mỗi input group sẽ thay chúng ta chạy functionpll_register_string()1 lần - List các item đã thêm bằng plugin này. Giờ cần tạo màn quản lý admin trước
public function admin_menu() {
add_menu_page(
'Polylang Strings', // Menu title
'Polylang Strings', //
'manage_options',
'tb-polylang-strings',
[ $this, 'render_page' ],
'dashicons-translation', // menu icon
80
);
}
Đăng ký setting cho plugin:
public function register_settings() {
register_setting(
'tb_pll_string_manager_group',
self::OPTION_KEY,
[
'type' => 'array',
'sanitize_callback' => [ $this, 'sanitize_strings' ], // hàm xử lý string này tiếp theo sẽ xử lý
'default' => [],
]
);
}
Xử lý string cho nội dung input: Thêm chút xử lý string cho 3 input của function pll_register_string() :
public function sanitize_strings( $input ) {
if ( ! is_array( $input ) ) return [];
$output = [];
foreach ( $input as $row ) {
if ( ! is_array( $row ) ) continue;
$name = isset($row['name']) ? sanitize_key( $row['name'] ) : ''; // unique string
$value = isset($row['value']) ? wp_kses_post( $row['value'] ) : ''; // default string
$group = isset($row['group']) ? sanitize_text_field( $row['group'] ) : 'From support plugin' // group
if ( $name === '' || $value === '' ) {
continue;
}
$output[] = [
'name' => $name,
'value' => $value,
'group' => $group ?: 'From support plugin',
];
}
return $output;
}
Input cho function register string đã đủ, giờ có thể pass các params vào function pll_register_string() và chạy được rồi:
public function register_saved_strings() {
if ( ! function_exists( 'pll_register_string' ) ) {
return; // Polylang not active
}
$strings = get_option( self::OPTION_KEY, [] );
if ( ! is_array( $strings ) ) return;
foreach ( $strings as $row ) {
$name = $row['name'] ?? '';
$value = $row['value'] ?? '';
$group = $row['group'] ?? 'From support plugin';
if ( $name && $value ) {
pll_register_string( $name, $value, $group );
}
}
}
Function cuối cùng là tạo ít giao diện để nhập data ở admin:
public function render_page() {
if ( ! current_user_can( 'manage_options' ) ) return;
$strings = get_option( self::OPTION_KEY, [] );
if ( ! is_array( $strings ) ) $strings = [];
?>
<div class="wrap">
<h1>Polylang String Manager</h1>
<?php if ( ! function_exists( 'pll_register_string' ) ): ?>
<div class="notice notice-warning">
<p><strong>Polylang is not active.</strong> Please install/activate Polylang first.</p>
</div>
<?php endif; ?>
<p>
Add your strings here. After saving, go to <strong>Languages → String translations</strong> to translate them.
</p>
<form method="post" action="options.php">
<?php
settings_fields( 'tb_pll_string_manager_group' );
?>
<table class="widefat fixed" id="tb-pll-table">
<thead>
<tr>
<th style="width: 20%;">Name (key)</th>
<th>Default Value</th>
<th style="width: 20%;">Group</th>
<th style="width: 80px;">Remove</th>
</tr>
</thead>
<tbody>
<?php foreach ( $strings as $i => $row ): ?>
<tr>
<td>
<input type="text" name="<?php echo esc_attr(self::OPTION_KEY); ?>[<?php echo esc_attr($i); ?>][name]"
value="<?php echo esc_attr($row['name'] ?? ''); ?>"
class="" placeholder="e.g. book_now" />
</td>
<td>
<textarea name="<?php echo esc_attr(self::OPTION_KEY); ?>[<?php echo esc_attr($i); ?>][value]"
rows="2" style="width:100%;"><?php echo esc_textarea($row['value'] ?? ''); ?></textarea>
</td>
<td>
<input type="text" name="<?php echo esc_attr(self::OPTION_KEY); ?>[<?php echo esc_attr($i); ?>][group]"
value="<?php echo esc_attr($row['group'] ?? 'From support plugin'); ?>"
class="" placeholder="e.g. From support plugin" />
</td>
<td style="text-align:center;">
<button type="button" class="button tb-remove-row">X</button>
</td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
<p style="margin-top:10px;">
<button type="button" class="button" id="tb-add-row">+ Add String</button>
</p>
<?php submit_button( 'Save Strings' ); ?>
</form>
<hr />
<h2>How to use in code</h2>
<pre><code>echo pll__( 'Hello World ); // if you registered value "Hello World"
echo pll__( 'Contact' );</code></pre>
</div>
<script>
(function(){
const tableBody = document.querySelector('#tb-pll-table tbody');
const addBtn = document.getElementById('tb-add-row');
function reindexNames(){
const rows = tableBody.querySelectorAll('tr');
rows.forEach((tr, idx) => {
tr.querySelectorAll('input, textarea').forEach(el => {
el.name = el.name.replace(/tb_pll_strings\[\d+\]/, 'tb_pll_strings['+idx+']');
});
});
}
tableBody.addEventListener('click', function(e){
if(e.target.classList.contains('tb-remove-row')){
e.target.closest('tr').remove();
reindexNames();
}
});
addBtn.addEventListener('click', function(){
const idx = tableBody.querySelectorAll('tr').length;
const tr = document.createElement('tr');
tr.innerHTML = `
<td>
<input type="text" name="tb_pll_strings[${idx}][name]" class="" placeholder="e.g. book_now" />
</td>
<td>
<textarea name="tb_pll_strings[${idx}][value]" rows="2" style="width:100%;" placeholder="Default text (English)"></textarea>
</td>
<td>
<input type="text" name="tb_pll_strings[${idx}][group]" class="" placeholder="e.g. Booking UI" value="TB Plugin" />
</td>
<td style="text-align:center;">
<button type="button" class="button tb-remove-row">X</button>
</td>
`;
tableBody.appendChild(tr);
});
})();
</script>
<?php
}

Xong Plugin rồi, duy nhất 1 file 😆😆
Sử dụng plugin mới nào
Tất nhiên, sau khi thêm vào bạn nhớ vào active plugin nhé. Hiện tại, bạn có thể vào màn hình quản lý mới, thêm thông tin string muốn đăng ký mới. Sau khi save, bạn có thể quay lại màn hình String translation của Polylang, thấy string vừa đăng ký, và có thể dịch cho nó rồi. Cách sử dụng dịch thì vẫn như đã hướng dẫn ở trên, không khác gì. Plugin này chỉ nhằm mục đích hỗ trợ bạn không phải vào functions.php đăng ký cho từng string mới thôi, tuy thao tác hơi cồng kềnh nhưng không có gì can thiệp thay đổi plugin cả, code ngắn gọn dễ đọc, không sợ hack nhé 🙈
Mình tạm đến đây là hết 'chăm' rồi, chuyển sang mode lười nên chưa có link full Plugin, các bạn tự làm nhé 🥱
All Rights Reserved