Skip to content

Commit f82cdc6

Browse files
committed
add browser test for translation
1 parent 45e720a commit f82cdc6

9 files changed

Lines changed: 265 additions & 0 deletions

File tree

.github/workflows/browser-tests.yml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -108,6 +108,9 @@ jobs:
108108
- name: Run Pro Kit Tests
109109
run: cd browser-testing && npm run test:ci:browser-pro-kit
110110

111+
- name: Run Admin i18n Tests
112+
run: cd browser-testing && npm run test:ci:browser-i18n
113+
111114
- name: Debug on failure
112115
if: failure()
113116
run: |

browser-testing/README.md

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,38 @@ and the Pro Kit tests like this:
7474
npm run test:ci:browser-pro-kit
7575
```
7676

77+
# Admin i18n Test (PR #304)
78+
79+
`tests-using-mock-fa-api/i18n/adminTranslations.spec.js` is regression coverage
80+
for the admin JavaScript internationalization fix (PR #304): it verifies, in the
81+
browser, that the admin bundle's script translations load and are applied.
82+
83+
Because this bug only manifests in the browser (the PHP hook ordering looked
84+
fine in isolation), it belongs here rather than in the PHPUnit suite.
85+
86+
It runs as its own project (`i18n-chrome`) with a dedicated setup/teardown so it
87+
doesn't disturb the other cells, whose specs match UI text in **English**:
88+
89+
- `setup/i18n.js` forces the locale to `de_DE` (via a direct `wp_options` write,
90+
so no language pack download is needed) and installs a fixture JS translation
91+
file (`support/admin-translations.de_DE.json`) into the WordPress container
92+
under the exact name WordPress derives from the admin bundle's path.
93+
- `setup/i18nCleanup.js` (wired as the `setup-i18n` project's `teardown`) reverts
94+
the locale and removes the fixture file.
95+
96+
The fixture strings are intentionally prefixed with `TEST-DE —` so the browser
97+
assertions are unambiguous; they are test markers, not real German copy.
98+
99+
Run it the way CI does:
100+
101+
```bash
102+
npm run test:ci:browser-i18n
103+
```
104+
105+
The setup installs the fixture file into the running WordPress container via
106+
`docker`. The container name defaults to `wordpress-ci` when `CI=true` and the
107+
local dev container otherwise; override with `WP_CONTAINER` if needed.
108+
77109
# Console Output
78110

79111
The JavaScript console output from the browser is silenced by default to avoid showing in the terminal.

browser-testing/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
"private": true,
55
"scripts": {
66
"test:ci:browser-pro-kit": "CI=true npm run test:browser-mocked-fa-api-pro-kit-all-browsers",
7+
"test:ci:browser-i18n": "CI=true npx playwright test --project=i18n-chrome",
78
"test:ci:browser-non-kit": "CI=true npm run test:browser-mocked-fa-api-legacy-cdn-all-browsers",
89
"test:local:browser-pro-kit": "npm run test:browser-real-fa-api-pro-kit-all-browsers",
910
"test:local:browser-non-kit": "npm run test:browser-real-fa-api-legacy-cdn-all-browsers",

browser-testing/playwright.config.js

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,15 @@ const testConfigs = [
3232
name: 'mock-fa-api-legacy-cdn',
3333
testMatch: 'tests-using-mock-fa-api/using-legacy-cdn/*.spec.js',
3434
dependencies: ['reset']
35+
},
36+
{
37+
// Admin JS translations (PR #304). Runs in a forced de_DE locale with a
38+
// fixture translation file; setup-i18n establishes that and its teardown
39+
// reverts it, so the English-text expectations of the other cells are
40+
// unaffected. Not FA-API dependent.
41+
name: 'i18n',
42+
testMatch: 'tests-using-mock-fa-api/i18n/*.spec.js',
43+
dependencies: ['setup-i18n']
3544
}
3645
]
3746

@@ -81,6 +90,24 @@ export default defineConfig({
8190
},
8291
dependencies: ['wp-login', 'reset']
8392
},
93+
{
94+
name: 'setup-i18n',
95+
testDir,
96+
testMatch: 'setup/i18n.js',
97+
use: {
98+
storageState: authFile
99+
},
100+
dependencies: ['wp-login', 'reset'],
101+
teardown: 'teardown-i18n'
102+
},
103+
{
104+
name: 'teardown-i18n',
105+
testDir,
106+
testMatch: 'setup/i18nCleanup.js',
107+
use: {
108+
storageState: authFile
109+
}
110+
},
84111
...browserProjects
85112
]
86113
})

browser-testing/setup/i18n.js

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
import { test as setup } from '@wordpress/e2e-test-utils-playwright'
2+
import { execFileSync } from 'child_process'
3+
import path from 'path'
4+
import '../support/env.js'
5+
import {
6+
LOCALE,
7+
LANG_PLUGINS_DIR,
8+
translationFileName,
9+
wpContainer,
10+
dbConnection
11+
} from '../support/i18nHelpers.js'
12+
13+
// Establishes the preconditions for the admin JS-translation tests:
14+
// 1. force the site locale to de_DE, and
15+
// 2. install a fixture JS translation file for the admin bundle.
16+
//
17+
// Both are reverted by setup/i18nCleanup.js (wired as this project's teardown),
18+
// so the rest of the suite — whose specs match UI text in English — is
19+
// unaffected. This is a plain admin-page test and does not touch the Font
20+
// Awesome API.
21+
setup('set up admin i18n fixture', async () => {
22+
// 1. Force the locale via a direct DB write. Going through the WPLANG option's
23+
// sanitize (as wp-cli / the Settings screen would) rejects a locale whose
24+
// language pack isn't installed; writing the row directly avoids needing a
25+
// language-pack download, keeping this hermetic.
26+
const connection = await dbConnection()
27+
await connection.execute(
28+
'INSERT INTO `wp_options` (`option_name`, `option_value`, `autoload`) VALUES (?, ?, "yes") ' +
29+
'ON DUPLICATE KEY UPDATE `option_value` = VALUES(`option_value`)',
30+
['WPLANG', LOCALE]
31+
)
32+
await connection.end()
33+
34+
// 2. Install the JS translation file under the exact name WordPress derives
35+
// from the admin bundle's path (see support/i18nHelpers.js). WordPress only
36+
// prints the setLocaleData bridge when such a file exists for the locale,
37+
// so this file is what makes the fix observable in the browser.
38+
const container = wpContainer()
39+
const fixture = path.resolve(__dirname, `../support/admin-translations.${LOCALE}.json`)
40+
execFileSync('docker', ['exec', '-u', 'root', container, 'mkdir', '-p', LANG_PLUGINS_DIR])
41+
execFileSync('docker', [
42+
'cp',
43+
fixture,
44+
`${container}:${LANG_PLUGINS_DIR}/${translationFileName()}`
45+
])
46+
})
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
import { test as teardown } from '@wordpress/e2e-test-utils-playwright'
2+
import { execFileSync } from 'child_process'
3+
import '../support/env.js'
4+
import {
5+
LANG_PLUGINS_DIR,
6+
translationFileName,
7+
wpContainer,
8+
dbConnection
9+
} from '../support/i18nHelpers.js'
10+
11+
// Reverts everything setup/i18n.js established, so the WordPress instance is left
12+
// with its default (English) locale and no fixture translation file. Wired as
13+
// the teardown of the `setup-i18n` project, so it runs after the i18n specs
14+
// finish. Each step is best-effort — teardown must not fail the run.
15+
teardown('tear down admin i18n fixture', async () => {
16+
try {
17+
const connection = await dbConnection()
18+
await connection.execute(
19+
'UPDATE `wp_options` SET `option_value` = "" WHERE `option_name` = "WPLANG"'
20+
)
21+
await connection.end()
22+
} catch (e) {
23+
console.warn(`i18n teardown: reverting WPLANG failed: ${e.message}`)
24+
}
25+
26+
try {
27+
const container = wpContainer()
28+
execFileSync('docker', [
29+
'exec',
30+
'-u',
31+
'root',
32+
container,
33+
'rm',
34+
'-f',
35+
`${LANG_PLUGINS_DIR}/${translationFileName()}`
36+
])
37+
} catch (e) {
38+
console.warn(`i18n teardown: removing translation file failed: ${e.message}`)
39+
}
40+
})
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
{
2+
"translation-revision-date": "2026-07-28 00:00:00+0000",
3+
"generator": "font-awesome e2e fixture",
4+
"domain": "messages",
5+
"locale_data": {
6+
"messages": {
7+
"": {
8+
"domain": "messages",
9+
"lang": "de_DE",
10+
"plural-forms": "nplurals=2; plural=(n != 1);"
11+
},
12+
"How are you using Font Awesome?": [ "TEST-DE — Wie verwendest du Font Awesome?" ],
13+
"Use A Kit": [ "TEST-DE — Kit verwenden" ],
14+
"Use CDN": [ "TEST-DE — CDN verwenden" ],
15+
"Save Changes": [ "TEST-DE — Änderungen speichern" ]
16+
}
17+
}
18+
}
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
import crypto from 'crypto'
2+
import mysql from 'mysql2/promise'
3+
4+
// The locale we force the admin UI into for the translation tests.
5+
export const LOCALE = 'de_DE'
6+
7+
export const TEXT_DOMAIN = 'font-awesome'
8+
9+
// The script handle the plugin registers for its admin bundle
10+
// (FontAwesome::ADMIN_RESOURCE_HANDLE = 'font-awesome-official' . '-admin').
11+
export const ADMIN_HANDLE = 'font-awesome-official-admin'
12+
13+
// Path of the admin bundle relative to the plugin root. The plugin registers it
14+
// from get_webpack_asset_url_base() . 'index.js' (= admin/build/index.js), and
15+
// WordPress hashes exactly this string to name the script's JSON translation
16+
// file: `<text-domain>-<locale>-<md5(relative-path)>.json`. See WordPress core
17+
// load_script_textdomain(). If the build output path changes, update this.
18+
export const RELATIVE_JS_PATH = 'admin/build/index.js'
19+
20+
export const LANG_PLUGINS_DIR = '/var/www/html/wp-content/languages/plugins'
21+
22+
/** The exact filename WordPress looks for when loading the admin bundle's translations. */
23+
export function translationFileName() {
24+
const hash = crypto.createHash('md5').update(RELATIVE_JS_PATH).digest('hex')
25+
return `${TEXT_DOMAIN}-${LOCALE}-${hash}.json`
26+
}
27+
28+
/**
29+
* Name of the running WordPress container to install the translation file into.
30+
* Defaults to the CI container when CI=true, otherwise the local dev container.
31+
* Override with WP_CONTAINER.
32+
*/
33+
export function wpContainer() {
34+
return (
35+
process.env.WP_CONTAINER ||
36+
(process.env.CI === 'true' ? 'wordpress-ci' : 'com.fontawesome.wordpress-latest-dev')
37+
)
38+
}
39+
40+
/** Open a connection to the WordPress DB using the same env the rest of the suite uses. */
41+
export function dbConnection() {
42+
return mysql.createConnection({
43+
host: 'localhost',
44+
user: process.env.WORDPRESS_DB_USER,
45+
password: process.env.WORDPRESS_DB_PASSWORD,
46+
database: process.env.WORDPRESS_DB_NAME,
47+
port: process.env.WORDPRESS_DB_PORT
48+
})
49+
}
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
import { test, expect } from '../../fixtures.js'
2+
import { ADMIN_HANDLE } from '../../support/i18nHelpers.js'
3+
4+
// Regression coverage for PR #304.
5+
//
6+
// The bug: FontAwesome::maybe_enqueue_admin_assets() called
7+
// wp_set_script_translations() for the admin bundle BEFORE the script was
8+
// registered/enqueued, so WordPress never attached the textdomain, never printed
9+
// the setLocaleData bridge, and the React admin UI never received its
10+
// translations. The fix moved the call to run right after
11+
// enqueue_admin_js_assets().
12+
//
13+
// setup/i18n.js forces the locale to de_DE and installs a fixture translation
14+
// file, so with the fix in place these assertions pass; if the bug regresses the
15+
// bridge disappears and the strings stay in English, failing the test.
16+
test.describe('admin JS translations (PR #304)', () => {
17+
test.beforeEach(async ({ page }) => {
18+
await page.goto('/wp-admin/admin.php?page=font-awesome')
19+
})
20+
21+
test('prints the script-translations bridge for the admin bundle', async ({ page }) => {
22+
// The admin bundle must be on the page, otherwise the rest is meaningless.
23+
await expect(page.locator(`#${ADMIN_HANDLE}-js`)).toHaveCount(1)
24+
25+
// The heart of the fix: WordPress only prints this inline setLocaleData
26+
// script when wp_set_script_translations() ran AFTER registration. It's a
27+
// <script> element (no rendered text), so read its textContent directly.
28+
const bridge = page.locator(`#${ADMIN_HANDLE}-js-translations`)
29+
await expect(bridge).toHaveCount(1)
30+
const bridgeSource = await bridge.evaluate((el) => el.textContent)
31+
expect(bridgeSource).toContain('wp.i18n.setLocaleData')
32+
expect(bridgeSource).toContain('font-awesome')
33+
})
34+
35+
test('renders translated strings in the admin UI', async ({ page }) => {
36+
await expect(
37+
page.getByRole('heading', { name: 'TEST-DE — Wie verwendest du Font Awesome?' })
38+
).toBeVisible()
39+
await expect(page.getByText('TEST-DE — Kit verwenden')).toBeVisible()
40+
await expect(page.getByText('TEST-DE — CDN verwenden')).toBeVisible()
41+
})
42+
43+
test('resolves the plugin textdomain through the wp.i18n runtime', async ({ page }) => {
44+
const translated = await page.evaluate(() =>
45+
window.wp.i18n.__('How are you using Font Awesome?', 'font-awesome')
46+
)
47+
expect(translated).toBe('TEST-DE — Wie verwendest du Font Awesome?')
48+
})
49+
})

0 commit comments

Comments
 (0)