Skip to content

Commit fdfe788

Browse files
authored
feat: Internationlization (#120)
* build: Add node-fetch dependency Utilized for script requests. * feat: Add locale configuration * feat: Configure locale * build: Add download translations script * task: Add logger utility Standardized logging within the code. * refactor: Utilize logger utility * feat: Match translations supported by Gutenberg Mobile * fix: Repair translations data path * fix: Start demo editor The editor no longer implicitly starts. * build: Add GUTERNBERG_EDITOR_REMOTE_URL environment value Simplify configuration with examples. * fix: Repair script logger import path * task: Reduce translation download script noise * feat: Control log levels for scripts * fix: Use default value from dynamic import * refactor: Rename API fetch utility Consistently name utility files as objects. * refactor: Rename exception parser utility Consistently name utility files as objects. * fix: Postpone importing `@wordpress` packages until after setting locale Required to ensure imported files have the correct locale set. * refactor: Translate strings within `gutenberg-kit` domain Ensure all project-specific strings are translated. * fix: Use existing Gutenberg "Block Settings" string Two benefits: 1. Align with Gutenberg 2. Use the existing translation * fix: Apply `@wordpress` package import postponement to remote editor Required to ensure imported files have the correct locale set. * refactor: Rename GBKit global await utility Clarify the utility's purpose. * fix: Repair localization operations order In order to set the locale before critical `@wordpress` packages load, we must load i18n packages separately from the rest of the `@wordpress` packages. Otherwise, the locale will not be set in time and/or we will encounter collisions from loading packages more than once. * task: Log additional values * refactor: Separate concerns of loading assets and unregistering blocks These feel like two separate concerns. Separating them hopefully reduces complexity. * refactor: Replace verbose log level with debug Two, distinct levels provided little-to-no value. * refactor: Replace redundant logger prefix with unique project prefix Prefixing with the log level duplicates browser log level styles. Prefixing the project name or acronym at least provides additional context. * task: Capture build output * build: Manually chunk editor utility It's unclear why this manual chunk configuration is required, but the editor fails to load without it. The dynamic import of the module never resolves. * fix: Avoid errors loading non-existent, default locale translations There is no reason to attempt loading the default locale. There will never be a translation file. * task: Capture build output * fix: Lazy load bundled `@wordpress` packages after locale is set Using static imports means the packages are loaded before the locale is set, leading to incorrect translation strings in various places. * fix: Ensure foundational styles are available for early error UI If the editor fails early in the load process, these styles need to be present for presenting error message UI. Therefore, we hoist them to the entry file. * refactor: Separate block utilities Limit scope of file responsibilities. * refactor: `initializeEditor` provides default WP packages Simplify initializing the local editor. * build: Prevent unintentional preloading of `@wordpress` modules The locale must be set before most `@wordpress` modules load, as they rely upon globals for retrieving translation strings. However, the manual chunks configuration seemingly led to preloading of all `@wordpress` modules even before they were dynamically imported. vitejs/vite#8617 (comment) * task: Capture build output * fix: Workaround circular dependency A circular dependency causes the dynamic import to never resolve. Using a Promise circumvents the issue, as it appears to function differently than top-level await. Ideally, we resolve the circular dependency. However, using Rollup's `manualChunks` configuration does not appear to be an option, as it results in preloading of `@wordpress` modules. The preloading results in untranslated strings as `@wordpress` modules load before the globals they rely upon for localization, which are set by `setLocaleData`. * task: Capture build output * refactor: Remove unused module Usage was removed in a earlier commit. * build: Cache translation strings Allow for avoiding unnecessary network requests when translation strings are already present. * refactor: Flatten translations directory Avoid unnecessary nesting. * refactor: Clarify translation script cache interaction Communicate the cache is ignored when using the `force` flag. * build: Add clean scripts Simplify resetting build caches. * task: Capture build output * refactor: Remove unused export This is only used within the module. * fix: Organize dynamic imports and chunks to avoid circular dependencies Because we load internationalization modules before the rest of the `@wordpress` modules, we must strategically chunk modules to avoid circular dependencies. Additionally, Vite injects its own helper utilities seem to create their own circular dependencies, at times. Lastly, the correct chunk configuration for the `index` entry is a bit complicated and allusive. Instead, we rely upon a `Promise` rather than `async`/`await`, as the latter will timeout when a circular dependency exists. * refactor: Remove unnecessary dependency injection Because of the latest dynamic import strategy and Vite managing externalizing `@wordpress` modules for the remote editor, we no longer need to use DI for these functions. For the local editor, the imported modules will be bundled and imported. For the remote editor, the imported modules will be replaced with global `window.wp` references. * task: Capture build output
1 parent a508888 commit fdfe788

File tree

95 files changed

+2333
-1303
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

95 files changed

+2333
-1303
lines changed

.gitignore

+3
Original file line numberDiff line numberDiff line change
@@ -193,3 +193,6 @@ local.properties
193193
# /ios/Sources/GutenbergKit/Gutenberg/assets
194194
# /ios/Sources/GutenbergKit/Gutenberg/index.html
195195
# /ios/Sources/GutenbergKit/Gutenberg/remote.html
196+
197+
# Translation files
198+
src/translations/

Makefile

+8-2
Original file line numberDiff line numberDiff line change
@@ -10,12 +10,18 @@ define XCODEBUILD_CMD
1010
endef
1111

1212
npm-dependencies:
13-
@if [ "$(SKIP_DEPS)" != "true" ]; then \
13+
@if [ "$(SKIP_DEPS)" != "true" ] && [ "$(SKIP_DEPS)" != "1" ]; then \
1414
echo "--- :npm: Installing NPM Dependencies"; \
1515
npm ci; \
1616
fi
1717

18-
build: npm-dependencies
18+
prep-translations:
19+
@if [ "$(SKIP_L10N)" != "true" ] && [ "$(SKIP_L10N)" != "1" ]; then \
20+
echo "--- :npm: Preparing Translations"; \
21+
npm run prep-translations -- --force; \
22+
fi
23+
24+
build: npm-dependencies prep-translations
1925
echo "--- :node: Building Gutenberg"
2026

2127
npm run build

android/Gutenberg/src/main/java/org/wordpress/gutenberg/EditorConfiguration.kt

+8-2
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,8 @@ open class EditorConfiguration constructor(
6060
val namespaceExcludedPaths: Array<String>,
6161
val authHeader: String,
6262
val webViewGlobals: List<WebViewGlobal>,
63-
val editorSettings: String?
63+
val editorSettings: String?,
64+
val locale: String?
6465
) : Parcelable {
6566
companion object {
6667
@JvmStatic
@@ -82,6 +83,7 @@ open class EditorConfiguration constructor(
8283
private var authHeader: String = ""
8384
private var webViewGlobals: List<WebViewGlobal> = emptyList()
8485
private var editorSettings: String? = null
86+
private var locale: String? = "en"
8587

8688
fun setTitle(title: String) = apply { this.title = title }
8789
fun setContent(content: String) = apply { this.content = content }
@@ -97,6 +99,7 @@ open class EditorConfiguration constructor(
9799
fun setAuthHeader(authHeader: String) = apply { this.authHeader = authHeader }
98100
fun setWebViewGlobals(webViewGlobals: List<WebViewGlobal>) = apply { this.webViewGlobals = webViewGlobals }
99101
fun setEditorSettings(editorSettings: String?) = apply { this.editorSettings = editorSettings }
102+
fun setLocale(locale: String?) = apply { this.locale = locale }
100103

101104
fun build(): EditorConfiguration = EditorConfiguration(
102105
title = title,
@@ -112,7 +115,8 @@ open class EditorConfiguration constructor(
112115
namespaceExcludedPaths = namespaceExcludedPaths,
113116
authHeader = authHeader,
114117
webViewGlobals = webViewGlobals,
115-
editorSettings = editorSettings
118+
editorSettings = editorSettings,
119+
locale = locale
116120
)
117121
}
118122

@@ -136,6 +140,7 @@ open class EditorConfiguration constructor(
136140
if (authHeader != other.authHeader) return false
137141
if (webViewGlobals != other.webViewGlobals) return false
138142
if (editorSettings != other.editorSettings) return false
143+
if (locale != other.locale) return false
139144

140145
return true
141146
}
@@ -155,6 +160,7 @@ open class EditorConfiguration constructor(
155160
result = 31 * result + authHeader.hashCode()
156161
result = 31 * result + webViewGlobals.hashCode()
157162
result = 31 * result + (editorSettings?.hashCode() ?: 0)
163+
result = 31 * result + (locale?.hashCode() ?: 0)
158164
return result
159165
}
160166
}

android/Gutenberg/src/main/java/org/wordpress/gutenberg/GutenbergView.kt

+1
Original file line numberDiff line numberDiff line change
@@ -271,6 +271,7 @@ class GutenbergView : WebView {
271271
"themeStyles": ${configuration.themeStyles},
272272
"hideTitle": ${configuration.hideTitle},
273273
"editorSettings": $editorSettings,
274+
"locale": "${configuration.locale}",
274275
${if (configuration.postId != null) """
275276
"post": {
276277
"id": ${configuration.postId},

bin/prep-translations.js

+158
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,158 @@
1+
/**
2+
* External dependencies
3+
*/
4+
import fs from 'fs';
5+
import path from 'path';
6+
import fetch from 'node-fetch';
7+
8+
/**
9+
* Internal dependencies
10+
*/
11+
import { info, error, debug } from '../src/utils/logger.js';
12+
13+
const TRANSLATIONS_DIR = path.join( process.cwd(), 'src/translations' );
14+
const SUPPORTED_LOCALES = [
15+
'ar', // Arabic
16+
'bg', // Bulgarian
17+
'bo', // Tibetan
18+
'ca', // Catalan
19+
'cs', // Czech
20+
'cy', // Welsh
21+
'da', // Danish
22+
'de', // German
23+
'en-au', // English (Australia)
24+
'en-ca', // English (Canada)
25+
'en-gb', // English (UK)
26+
'en-nz', // English (New Zealand)
27+
'en-za', // English (South Africa)
28+
'el', // Greek
29+
'es', // Spanish
30+
'es-ar', // Spanish (Argentina)
31+
'es-cl', // Spanish (Chile)
32+
'es-cr', // Spanish (Costa Rica)
33+
'fa', // Persian
34+
'fr', // French
35+
'gl', // Galician
36+
'he', // Hebrew
37+
'hr', // Croatian
38+
'hu', // Hungarian
39+
'id', // Indonesian
40+
'is', // Icelandic
41+
'it', // Italian
42+
'ja', // Japanese
43+
'ka', // Georgian
44+
'ko', // Korean
45+
'nb', // Norwegian (Bokmål)
46+
'nl', // Dutch
47+
'nl-be', // Dutch (Belgium)
48+
'pl', // Polish
49+
'pt', // Portuguese
50+
'pt-br', // Portuguese (Brazil)
51+
'ro', // Romainian
52+
'ru', // Russian
53+
'sk', // Slovak
54+
'sq', // Albanian
55+
'sr', // Serbian
56+
'sv', // Swedish
57+
'th', // Thai
58+
'tr', // Turkish
59+
'uk', // Ukrainian
60+
'ur', // Urdu
61+
'vi', // Vietnamese
62+
'zh-cn', // Chinese (China)
63+
'zh-tw', // Chinese (Taiwan)
64+
];
65+
66+
/**
67+
* Prepare translations for all supported locales.
68+
*
69+
* @param {boolean} force Whether to force download even if cache exists.
70+
*
71+
* @return {Promise<void>} A promise that resolves when translations are prepared.
72+
*/
73+
async function prepareTranslations( force = false ) {
74+
if ( force ) {
75+
info( 'Ignoring cache, downloading translations...' );
76+
} else {
77+
info( 'Verifying translations...' );
78+
}
79+
80+
for ( const locale of SUPPORTED_LOCALES ) {
81+
try {
82+
await downloadTranslations( locale, force );
83+
} catch ( err ) {
84+
error( `✗ Failed to download translations for ${ locale }:`, err );
85+
}
86+
}
87+
88+
info( '✓ Translations ready!' );
89+
}
90+
91+
/**
92+
* Downloads translations for a specific locale from translate.wordpress.org.
93+
*
94+
* @param {string} locale The locale to download translations for.
95+
* @param {boolean} force Whether to force download even if cache exists.
96+
*
97+
* @return {Promise<void>} A promise that resolves when translations are downloaded.
98+
*/
99+
async function downloadTranslations( locale, force = false ) {
100+
if ( ! force && hasValidTranslations( locale ) ) {
101+
debug( `Skipping download of cached translations for ${ locale }` );
102+
return;
103+
}
104+
debug( `Downloading translations for ${ locale }...` );
105+
106+
const url = `https://translate.wordpress.org/projects/wp-plugins/gutenberg/dev/${ locale }/default/export-translations/?format=json`;
107+
const response = await fetch( url );
108+
109+
if ( ! response.ok ) {
110+
throw new Error( `Failed to download translations for ${ locale }` );
111+
}
112+
113+
const translations = await response.json();
114+
const outputPath = path.join( TRANSLATIONS_DIR, `${ locale }.json` );
115+
116+
// Ensure the translations directory exists
117+
if ( ! fs.existsSync( TRANSLATIONS_DIR ) ) {
118+
fs.mkdirSync( TRANSLATIONS_DIR, { recursive: true } );
119+
}
120+
121+
// Write translations to file
122+
fs.writeFileSync( outputPath, JSON.stringify( translations, null, 2 ) );
123+
debug( `✓ Downloaded translations for ${ locale }` );
124+
}
125+
126+
/**
127+
* Checks if translations exist and are valid for a specific locale.
128+
*
129+
* @param {string} locale The locale to check.
130+
*
131+
* @return {boolean} Whether valid translations exist.
132+
*/
133+
function hasValidTranslations( locale ) {
134+
const filePath = path.join( TRANSLATIONS_DIR, `${ locale }.json` );
135+
if ( ! fs.existsSync( filePath ) ) {
136+
return false;
137+
}
138+
139+
try {
140+
const content = fs.readFileSync( filePath, 'utf8' );
141+
const translations = JSON.parse( content );
142+
return translations && typeof translations === 'object';
143+
} catch ( err ) {
144+
return false;
145+
}
146+
}
147+
148+
/**
149+
* Main entry point for the script.
150+
* Parses command line arguments and downloads translations.
151+
*/
152+
const forceDownload =
153+
process.argv.includes( '--force' ) || process.argv.includes( '-f' );
154+
155+
prepareTranslations( forceDownload ).catch( ( err ) => {
156+
error( 'Failed to prepare translations:', err );
157+
process.exit( 1 );
158+
} );

ios/Demo-iOS/Gutenberg.xcodeproj/xcshareddata/xcschemes/Gutenberg.xcscheme

+5
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,11 @@
5656
value = "http://localhost:5173/"
5757
isEnabled = "NO">
5858
</EnvironmentVariable>
59+
<EnvironmentVariable
60+
key = "GUTENBERG_EDITOR_REMOTE_URL"
61+
value = "http://localhost:5174/remote.html"
62+
isEnabled = "NO">
63+
</EnvironmentVariable>
5964
</EnvironmentVariables>
6065
</LaunchAction>
6166
<ProfileAction

ios/Demo-iOS/Sources/EditorView.swift

+1
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,7 @@ private struct _EditorView: UIViewControllerRepresentable {
7777
if #available(iOS 16.4, *) {
7878
viewController.webView.isInspectable = true
7979
}
80+
viewController.startEditorSetup()
8081
return viewController
8182
}
8283

ios/Sources/GutenbergKit/Gutenberg/assets/ar-CyYCT0Yn.js

+12
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

ios/Sources/GutenbergKit/Gutenberg/assets/bg-B2djTlFt.js

+1
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

ios/Sources/GutenbergKit/Gutenberg/assets/bo-DOe3GxAO.js

+1
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

ios/Sources/GutenbergKit/Gutenberg/assets/bridge-B_oa8MnC.js renamed to ios/Sources/GutenbergKit/Gutenberg/assets/bridge-COXq4kB3.js

+2-2
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

ios/Sources/GutenbergKit/Gutenberg/assets/bridge-CYSxxzEK.js

-2
This file was deleted.

ios/Sources/GutenbergKit/Gutenberg/assets/ca--D3R8toT.js

+12
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

ios/Sources/GutenbergKit/Gutenberg/assets/cs-DWmcEIfO.js

+12
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

ios/Sources/GutenbergKit/Gutenberg/assets/cy-CGIlBSk6.js

+12
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

ios/Sources/GutenbergKit/Gutenberg/assets/da-DwB1fLza.js

+1
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

ios/Sources/GutenbergKit/Gutenberg/assets/de-H01QIQZg.js

+12
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)