diff --git a/package-lock.json b/package-lock.json index 7fccf888b9..4e24d951d7 100644 --- a/package-lock.json +++ b/package-lock.json @@ -99,6 +99,7 @@ "@wordpress/core-data": "7.4.0", "@wordpress/data": "10.4.0", "@wordpress/element": "6.4.0", + "@wordpress/i18n": "5.14.0", "@wordpress/notices": "5.4.0", "chalk": "5.2.0", "clsx": "^1.2.1", @@ -118,6 +119,7 @@ "eslint-plugin-playground-dev": "file:packages/meta/src/eslint-plugin-playground-dev", "eslint-plugin-react": "7.32.2", "eslint-plugin-react-hooks": "4.6.0", + "gettext-extractor": "3.8.0", "gh-pages": "5.0.0", "glob": "^9.3.0", "husky": "8.0.3", @@ -2383,9 +2385,10 @@ "dev": true }, "node_modules/@babel/runtime": { - "version": "7.23.4", - "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.23.4.tgz", - "integrity": "sha512-2Yv65nlWnWlSpe3fXEyX5i7fx5kIKo4Qbcj+hMO0odwaneFjfXw5fdum+4yL20O0QiaHpia0cYQ9xpNMqrBwHg==", + "version": "7.25.7", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.25.7.tgz", + "integrity": "sha512-FjoyLe754PMiYsFaN5C94ttGiOmBNYTf6pLr4xXHAT5uctHb092PBszndLDR5XA/jghQvn4n7JMHl7dmTgbm9w==", + "license": "MIT", "dependencies": { "regenerator-runtime": "^0.14.0" }, @@ -18961,13 +18964,13 @@ } }, "node_modules/@wordpress/i18n": { - "version": "5.9.0", - "resolved": "https://registry.npmjs.org/@wordpress/i18n/-/i18n-5.9.0.tgz", - "integrity": "sha512-pKFV9S/l0TFlm0mlWLW51hAoRDNmZPGnfEpNXq43VKZkm1cco3Z1E54PHMGk8HdCECHqYNiJuQJOBOlqcYmnVQ==", + "version": "5.14.0", + "resolved": "https://registry.npmjs.org/@wordpress/i18n/-/i18n-5.14.0.tgz", + "integrity": "sha512-2KHyQ+zoyQggokmoTqfVhl2DOM4E11pF/M1+5Q0kUDAHLIAVDhKCzHNPZreHjJld4Tm7hl2HUOutfPmCVudj7g==", "license": "GPL-2.0-or-later", "dependencies": { - "@babel/runtime": "^7.16.0", - "@wordpress/hooks": "^4.9.0", + "@babel/runtime": "7.25.7", + "@wordpress/hooks": "*", "gettext-parser": "^1.3.1", "memize": "^2.1.0", "sprintf-js": "^1.1.1", @@ -23711,6 +23714,13 @@ "url": "https://github.com/sponsors/fb55" } }, + "node_modules/css-selector-parser": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/css-selector-parser/-/css-selector-parser-1.4.1.tgz", + "integrity": "sha512-HYPSb7y/Z7BNDCOrakL4raGO2zltZkbeXyAd6Tg9obzix6QhzxCotdBl6VT0Dv4vZfJGVz3WL/xaEI9Ly3ul0g==", + "dev": true, + "license": "MIT" + }, "node_modules/css-tree": { "version": "1.1.3", "dev": true, @@ -27758,6 +27768,67 @@ "assert-plus": "^1.0.0" } }, + "node_modules/gettext-extractor": { + "version": "3.8.0", + "resolved": "https://registry.npmjs.org/gettext-extractor/-/gettext-extractor-3.8.0.tgz", + "integrity": "sha512-i/3mDQufQoJd2/EKm/B+VlaYrt3yGjVfLZu8DQpESKH29klNiW6z2S89FVCIEB85bDNgtGCeM/3A/yR1njr/Lw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/glob": "5 - 7", + "@types/parse5": "^5", + "css-selector-parser": "^1.3", + "glob": "5 - 7", + "parse5": "5 - 6", + "pofile": "1.0.x", + "typescript": "4 - 5" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/gettext-extractor/node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Glob versions prior to v9 are no longer supported", + "dev": true, + "license": "ISC", + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/gettext-extractor/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/gettext-extractor/node_modules/parse5": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-6.0.1.tgz", + "integrity": "sha512-Ofn/CTFzRGTTxwpNEs9PP93gXShHcTq255nzRYSKe8AkVpZY7e1fpmTfOyoIvjP5HG7Z2ZM7VS9PPhQGW2pOpw==", + "dev": true, + "license": "MIT" + }, "node_modules/gettext-parser": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/gettext-parser/-/gettext-parser-1.4.0.tgz", @@ -37259,6 +37330,12 @@ "node": ">=18" } }, + "node_modules/pofile": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/pofile/-/pofile-1.0.11.tgz", + "integrity": "sha512-Vy9eH1dRD9wHjYt/QqXcTz+RnX/zg53xK+KljFSX30PvdDMb2z+c6uDUeblUGqqJgz3QFsdlA0IJvHziPmWtQg==", + "dev": true + }, "node_modules/portfinder": { "version": "1.0.32", "resolved": "https://registry.npmjs.org/portfinder/-/portfinder-1.0.32.tgz", diff --git a/package.json b/package.json index 8ec1227d40..970b7c55de 100644 --- a/package.json +++ b/package.json @@ -152,6 +152,7 @@ "@wordpress/core-data": "7.4.0", "@wordpress/data": "10.4.0", "@wordpress/element": "6.4.0", + "@wordpress/i18n": "5.14.0", "@wordpress/notices": "5.4.0", "chalk": "5.2.0", "clsx": "^1.2.1", @@ -171,6 +172,7 @@ "eslint-plugin-playground-dev": "file:packages/meta/src/eslint-plugin-playground-dev", "eslint-plugin-react": "7.32.2", "eslint-plugin-react-hooks": "4.6.0", + "gettext-extractor": "3.8.0", "gh-pages": "5.0.0", "glob": "^9.3.0", "husky": "8.0.3", diff --git a/packages/meta/src/wordpress-i18n-gettext-extractor/index.ts b/packages/meta/src/wordpress-i18n-gettext-extractor/index.ts new file mode 100644 index 0000000000..0f7e422cf4 --- /dev/null +++ b/packages/meta/src/wordpress-i18n-gettext-extractor/index.ts @@ -0,0 +1,70 @@ +import { + GettextExtractor, + HtmlExtractors, + JsExtractors, +} from 'gettext-extractor'; + +export function extractWordPressI18nGettext({ + scriptGlobs, + htmlGlobs, + outputFile: outputFile, +}: { + scriptGlobs: string[]; + htmlGlobs: string[]; + outputFile: string; +}) { + const extractor = new GettextExtractor(); + + const comments = { + otherLineLeading: true, + sameLineLeading: true, + sameLineTrailing: true, + regex: /translators.*/is, + }; + const jsParser = extractor.createJsParser([ + JsExtractors.callExpression('__', { + arguments: { + text: 0, + }, + comments, + }), + JsExtractors.callExpression('_x', { + arguments: { + text: 0, + context: 1, + }, + comments, + }), + JsExtractors.callExpression('_n', { + arguments: { + text: 0, + textPlural: 1, + }, + comments, + }), + JsExtractors.callExpression('_nx', { + arguments: { + text: 0, + textPlural: 1, + context: 3, + }, + comments, + }), + ]); + + for (const scriptGlob of scriptGlobs) { + jsParser.parseFilesGlob(scriptGlob); + } + + const htmlParser = extractor.createHtmlParser([ + HtmlExtractors.embeddedJs('script', jsParser), + ]); + + for (const htmlGlob of htmlGlobs) { + htmlParser.parseFilesGlob(htmlGlob); + } + + extractor.savePotFile(outputFile); + + return extractor.getStats(); +} diff --git a/packages/nx-extensions/executors.json b/packages/nx-extensions/executors.json index 643daf1e14..46b12147c3 100644 --- a/packages/nx-extensions/executors.json +++ b/packages/nx-extensions/executors.json @@ -20,6 +20,11 @@ "implementation": "./src/executors/assert-built-esm-and-cjs/executor", "schema": "./src/executors/assert-built-esm-and-cjs/schema.json", "description": "assert-built-esm-and-cjs executor" + }, + "wordpress-i18n-gettext-extractor": { + "implementation": "./src/executors/wordpress-i18n-gettext-extractor/executor", + "schema": "./src/executors/wordpress-i18n-gettext-extractor/schema.json", + "description": "wordpress-i18n-gettext-extractor executor" } } } diff --git a/packages/nx-extensions/src/executors/wordpress-i18n-gettext-extractor/executor.ts b/packages/nx-extensions/src/executors/wordpress-i18n-gettext-extractor/executor.ts new file mode 100644 index 0000000000..53c0dd3697 --- /dev/null +++ b/packages/nx-extensions/src/executors/wordpress-i18n-gettext-extractor/executor.ts @@ -0,0 +1,31 @@ +import { ExecutorContext } from '@nx/devkit'; +import * as path from 'path'; +import { WordPressI18nGettextExtractorSchema } from './schema'; +import { extractWordPressI18nGettext } from '../../../../meta/src/wordpress-i18n-gettext-extractor'; + +/** + * Extract a POT file from WordPress i18n calls in scripts and HTML files. + * + * @param options + * @param context + * @returns + */ +export default async function runExecutor( + options: WordPressI18nGettextExtractorSchema, + context: ExecutorContext +) { + const outputFile = path.isAbsolute(options.outputFile) + ? options.outputFile + : path.join(context.root, options.outputFile); + + const extractionStats = extractWordPressI18nGettext({ + scriptGlobs: options.scriptGlobs, + htmlGlobs: options.htmlGlobs, + outputFile: outputFile, + }); + + console.log('Extraction stats:'); + console.log(extractionStats); + + return { success: true }; +} diff --git a/packages/nx-extensions/src/executors/wordpress-i18n-gettext-extractor/schema.d.ts b/packages/nx-extensions/src/executors/wordpress-i18n-gettext-extractor/schema.d.ts new file mode 100644 index 0000000000..6ccb40c30d --- /dev/null +++ b/packages/nx-extensions/src/executors/wordpress-i18n-gettext-extractor/schema.d.ts @@ -0,0 +1,5 @@ +export interface WordPressI18nGettextExtractorSchema { + scriptGlobs: string[]; + htmlGlobs: string[]; + outputFile: string; +} diff --git a/packages/nx-extensions/src/executors/wordpress-i18n-gettext-extractor/schema.json b/packages/nx-extensions/src/executors/wordpress-i18n-gettext-extractor/schema.json new file mode 100644 index 0000000000..7401a55c19 --- /dev/null +++ b/packages/nx-extensions/src/executors/wordpress-i18n-gettext-extractor/schema.json @@ -0,0 +1,30 @@ +{ + "$schema": "http://json-schema.org/schema", + "version": 2, + "title": "WordPress i18n gettext extractor", + "description": "", + "type": "object", + "properties": { + "scriptGlobs": { + "type": "array", + "description": "Globs for scripts to parse for gettext functions", + "items": { + "type": "string" + }, + "defaultConfiguration": [] + }, + "htmlGlobs": { + "type": "array", + "description": "Globs for HTML documents to parse for gettext functions", + "items": { + "type": "string" + }, + "defaultConfiguration": [] + }, + "outputFile": { + "type": "string", + "description": "The path to the resulting .pot file" + } + }, + "required": ["outputFile"] +} diff --git a/packages/playground/website/project.json b/packages/playground/website/project.json index 7645148d0a..99f3d652d6 100644 --- a/packages/playground/website/project.json +++ b/packages/playground/website/project.json @@ -116,6 +116,16 @@ ] } }, + "extract-i18n-strings": { + "executor": "@wp-playground/nx-extensions:wordpress-i18n-gettext-extractor", + "options": { + "outputFile": "packages/playground/website/website.pot", + "scriptGlobs": [ + "packages/playground/website/src/**/*.@(ts|js|tsx|jsx)" + ], + "htmlGlobs": ["packages/playground/website/index.html"] + } + }, "e2e": { "executor": "@nx/cypress:cypress", "options": { diff --git a/packages/playground/website/src/components/layout/index.tsx b/packages/playground/website/src/components/layout/index.tsx index bdd9c23ef7..a403ed9b40 100644 --- a/packages/playground/website/src/components/layout/index.tsx +++ b/packages/playground/website/src/components/layout/index.tsx @@ -35,6 +35,7 @@ import { import { ImportFormModal } from '../import-form-modal'; import { PreviewPRModal } from '../../github/preview-pr'; import { MissingSiteModal } from '../missing-site-modal'; +import { __ } from '../../lib/i18n'; acquireOAuthTokenIfNeeded(); @@ -90,7 +91,7 @@ export function Layout() {
{siteManagerIsOpen && (
{ dispatch(setSiteManagerOpen(false)); diff --git a/packages/playground/website/src/components/site-manager/sidebar/index.tsx b/packages/playground/website/src/components/site-manager/sidebar/index.tsx index 80df6c4c81..c13923e562 100644 --- a/packages/playground/website/src/components/site-manager/sidebar/index.tsx +++ b/packages/playground/website/src/components/site-manager/sidebar/index.tsx @@ -28,6 +28,7 @@ import { } from '../../../lib/state/redux/slice-sites'; import { PlaygroundRoute, redirectTo } from '../../../lib/state/url/router'; import { setSiteManagerSection } from '../../../lib/state/redux/slice-ui'; +import { __ } from '../../../lib/i18n'; import { WordPressPRMenuItem } from '../../toolbar-buttons/wordpress-pr-menu-item'; import { GutenbergPRMenuItem } from '../../toolbar-buttons/gutenberg-pr-menu-item'; import { RestoreFromZipMenuItem } from '../../toolbar-buttons/restore-from-zip'; @@ -58,19 +59,19 @@ export function Sidebar({ const resources = [ { - label: 'Preview WordPress PR', + label: __('Preview WordPress PR'), href: '/wordpress.html', }, { - label: 'More demos', + label: __('More demos'), href: '/demos/index.html', }, { - label: 'Documentation', + label: __('Documentation'), href: 'https://wordpress.github.io/wordpress-playground/', }, { - label: 'GitHub', + label: __('GitHub'), href: 'https://github.com/wordpress/wordpress-playground', }, ]; @@ -106,7 +107,10 @@ export function Sidebar({ @@ -150,7 +157,9 @@ export function Sidebar({ isSelected={isTemporarySiteSelected} // eslint-disable-next-line jsx-a11y/aria-role role="" - title="This is a temporary Playground. Your changes will be lost on page refresh." + title={__( + 'This is a temporary Playground. Your changes will be lost on page refresh.' + )} {...(activeSite?.metadata.storage === 'none' ? { 'aria-current': 'page', @@ -168,7 +177,7 @@ export function Sidebar({ - Temporary Playground + {__('Temporary Playground')} @@ -196,7 +205,10 @@ export function Sidebar({ /> - Blueprints Gallery + { + // translators: a gallery of WordPress Playground blueprint files + __('Blueprints Gallery') + } diff --git a/packages/playground/website/src/lib/i18n.ts b/packages/playground/website/src/lib/i18n.ts new file mode 100644 index 0000000000..6cb519ecb9 --- /dev/null +++ b/packages/playground/website/src/lib/i18n.ts @@ -0,0 +1,9 @@ +import { createI18n, sprintf as coreSprintf } from '@wordpress/i18n'; + +const i18n = createI18n(undefined, 'wordpress-playground-website'); + +export const __ = i18n.__; +export const _x = i18n._x; +export const _n = i18n._n; +export const _nx = i18n._nx; +export const sprintf = coreSprintf; diff --git a/packages/playground/website/website.pot b/packages/playground/website/website.pot new file mode 100644 index 0000000000..e8009f234e --- /dev/null +++ b/packages/playground/website/website.pot @@ -0,0 +1,28 @@ +msgid "" +msgstr "" +"Content-Type: text/plain; charset=UTF-8\n" + +#: packages/playground/website/src/components/site-manager/sidebar/index.tsx:70 +msgid "Documentation" +msgstr "" + +#: packages/playground/website/src/components/site-manager/sidebar/index.tsx:74 +msgid "GitHub" +msgstr "" + +#. translators: Different ways to import code or content into Playground +#: packages/playground/website/src/components/site-manager/sidebar/index.tsx:112 +msgid "Import actions" +msgstr "" + +#: packages/playground/website/src/components/site-manager/sidebar/index.tsx:66 +msgid "More demos" +msgstr "" + +#: packages/playground/website/src/components/layout/index.tsx:94 +msgid "Open site" +msgstr "" + +#: packages/playground/website/src/components/site-manager/sidebar/index.tsx:62 +msgid "Preview WordPress PR" +msgstr ""