Skip to content

Commit cffaacb

Browse files
authored
Read bundled language packs directly from wp-files (#3214)
## Related issues - Closes STU-1537 (completes the part deferred from #3125) ## How AI was used in this PR Claude Code reconstructed the April context (#3125's deferred section, this PR's original debug instrumentation, and the language-pack copy races later documented in #3492), re-applied the bundle-read change cleanly on top of current trunk. ## Proposed Changes Language packs were the last site dependency still copied from the read-only CLI bundle into the writable `~/.studio/server-files/` cache on every CLI invocation. The copy was pure overhead: language packs are only ever read at site creation, to copy the matching locale's files into the new site's `wp-content/languages/`, and they only change when the app ships a new bundle. This PR has site creation read language packs directly from the bundled `wp-files/latest/languages/`, and drops the per-invocation copy step — shaving the remaining recursive dir-walk (~1,400 files) off CLI startup. Side benefits: - The copy step needed lockfile serialization (#3492) because concurrent CLI invocations raced on the shared cache (transient `ENOENT` warnings). With no copy and no writable cache, that bug class is gone by construction. - Site creation now skips the language-pack directory scans entirely for locales we don't bundle translations for (`WP_LOCALES`). - The existing `04-cleanup-obsolete-server-files` migration also removes the now-unused `~/.studio/server-files/language-packs/` (and its lockfile) on first run after upgrade. **On the April E2E failures that deferred this:** they never reproduced locally — neither at the CLI level (3/3 ja-locale sites healthy, WPLANG=ja, wp-admin 200) nor with the packaged-app Playwright suites that failed in CI back then. The Playground worker-spawn stack has been rewritten since (#3602), and the failure was never root-caused, so CI on this PR is the deciding signal. If mac E2E reds again, this time it fails on a clean branch with inspectable artifacts. ## Testing Instructions 1. `npm run download-language-packs` (plain `npm install` does not populate `wp-files/latest/languages`), then `npm run cli:build`. 2. Set a non-English locale (e.g. `"locale": "ja"` in `~/.studio/shared.json` or via Studio Settings) and create a site. 3. Verify the site's `wp-content/languages/` contains the locale's files (core + `plugins/` + `themes/`), wp-admin loads in that language, and Settings → General shows the locale selected under Site Language. 4. Verify `~/.studio/server-files/language-packs/` is deleted on first CLI run after upgrade and is not recreated. 5. Covered by e2e `localization.test.ts` (locale site creation + WPLANG) and `blueprints.test.ts` (blueprint provisioning). Verified locally on macOS arm64: typecheck, eslint, full unit suite (1963 tests), packaged-app e2e `localization.test.ts` 4/4 (twice) and `blueprints.test.ts` 6/6. ## Pre-merge Checklist - [x] Have you checked for TypeScript, React or other console errors?
1 parent 92a00a9 commit cffaacb

5 files changed

Lines changed: 14 additions & 137 deletions

File tree

‎apps/cli/lib/dependency-management/paths.ts‎

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -62,8 +62,10 @@ export function getSqliteCommandPath(): string {
6262
return path.join( getWpFilesPath(), SQLITE_COMMAND_DIRNAME );
6363
}
6464

65+
// Language packs ship read-only with the CLI bundle and are copied into each site's
66+
// `wp-content/languages/` directory on site create. No writable cache needed.
6567
export function getLanguagePacksPath(): string {
66-
return path.join( getServerFilesPath(), 'language-packs' );
68+
return path.join( getWpFilesPath(), 'latest', 'languages' );
6769
}
6870

6971
// AI instructions ship read-only with the CLI bundle and are installed into each site's

‎apps/cli/lib/dependency-management/setup.ts‎

Lines changed: 1 addition & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,9 @@
11
import fs from 'fs';
22
import path from 'path';
3-
import { LOCKFILE_STALE_TIME, LOCKFILE_WAIT_TIME } from '@studio/common/constants';
43
import { recursiveCopyDirectory } from '@studio/common/lib/fs-utils';
5-
import { lockFileAsync, unlockFileAsync } from '@studio/common/lib/lockfile';
64
import semver from 'semver';
75
import { readCliConfig, updateCliConfigWithPartial } from 'cli/lib/cli-config/core';
8-
import { getLanguagePacksPath, getWordPressVersionPath, getWpFilesPath } from './paths';
9-
import { areDirectoriesDifferentBySizeAndMtime } from './utils';
6+
import { getWordPressVersionPath, getWpFilesPath } from './paths';
107
import { getWordPressVersionFromInstallation, updateLatestWordPressVersion } from './wordpress';
118

129
// Compare the WordPress version in the bundled `wp-files/latest/wordpress` directory (that ships
@@ -38,40 +35,9 @@ async function copyBundledLatestWpVersion() {
3835
}
3936
}
4037

41-
async function copyBundledLanguagePacks() {
42-
const sourceLanguagePacksPath = path.join( getWpFilesPath(), 'latest', 'languages' );
43-
if ( ! fs.existsSync( sourceLanguagePacksPath ) ) {
44-
return;
45-
}
46-
const targetLanguagePacksPath = getLanguagePacksPath();
47-
const lockPath = `${ targetLanguagePacksPath }.lock`;
48-
await fs.promises.mkdir( path.dirname( lockPath ), { recursive: true } );
49-
await lockFileAsync( lockPath, {
50-
wait: LOCKFILE_WAIT_TIME,
51-
stale: LOCKFILE_STALE_TIME,
52-
} );
53-
try {
54-
const isSourceDirectoryDifferent = await areDirectoriesDifferentBySizeAndMtime(
55-
sourceLanguagePacksPath,
56-
targetLanguagePacksPath
57-
);
58-
if ( isSourceDirectoryDifferent ) {
59-
try {
60-
await fs.promises.rm( targetLanguagePacksPath, { recursive: true, force: true } );
61-
} catch {
62-
// Do nothing if the target directory is missing or corrupted
63-
}
64-
await recursiveCopyDirectory( sourceLanguagePacksPath, targetLanguagePacksPath );
65-
}
66-
} finally {
67-
await unlockFileAsync( lockPath );
68-
}
69-
}
70-
7138
export async function setupServerFiles() {
7239
const steps: [ string, () => Promise< void > ][] = [
7340
[ 'WordPress version', copyBundledLatestWpVersion ],
74-
[ 'language packs', copyBundledLanguagePacks ],
7541
];
7642

7743
for ( const [ name, step ] of steps ) {

‎apps/cli/lib/dependency-management/utils.ts‎

Lines changed: 0 additions & 101 deletions
This file was deleted.

‎apps/cli/lib/language-packs.ts‎

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import fs from 'fs';
22
import path from 'path';
33
import { pathExists } from '@studio/common/lib/fs-utils';
4+
import { WP_LOCALES } from '@studio/common/lib/wp-locales';
45
import { getLanguagePacksPath } from 'cli/lib/dependency-management/paths';
56

67
/**
@@ -51,6 +52,12 @@ export async function copyLanguagePackToSite(
5152
sitePath: string,
5253
wpLocale: string
5354
): Promise< boolean > {
55+
// Translations are only bundled for `WP_LOCALES`; skip the readdir() calls over the
56+
// ~1,400-entry bundled languages directories when no files could match anyway.
57+
if ( ! WP_LOCALES.includes( wpLocale ) ) {
58+
return false;
59+
}
60+
5461
const languagePacksDir = getLanguagePacksPath();
5562
if ( ! ( await pathExists( languagePacksDir ) ) ) {
5663
return false;

‎apps/cli/migrations/04-cleanup-obsolete-server-files.ts‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
* - `phpmyadmin/` (phpMyAdmin)
88
* - `wp-cli.phar` (WP-CLI)
99
* - `wordpress-versions/latest/available-site-translations.json`
10+
* - `language-packs/` (WordPress translations, plus its lockfile)
1011
*
1112
* Safe to re-run: `needsToRun()` checks for the presence of any obsolete
1213
* entry, and `run()` tolerates missing paths.
@@ -31,6 +32,8 @@ function getObsoletePaths(): string[] {
3132
'latest',
3233
'available-site-translations.json'
3334
),
35+
path.join( serverFilesPath, 'language-packs' ),
36+
path.join( serverFilesPath, 'language-packs.lock' ),
3437
];
3538
}
3639

0 commit comments

Comments
 (0)