Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,7 @@
{"from": "runtime/models/whisper-base.json", "to": "manifests/model-manifest.json"}
],
"win": {"target": "nsis"},
"mac": {"target": "dmg", "icon": "build/icon.icns"},
"mac": {"target": "dmg", "icon": "build/icon.icns", "electronLanguages": ["en", "en_GB", "es", "es_419"]},
"linux": {"target": "AppImage"}
}
}
95 changes: 95 additions & 0 deletions scripts/check-packaged-electron-locales.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
#!/usr/bin/env node

const fs = require('fs');
const path = require('path');
const packageJson = require('../package.json');
const { newestApp } = require('./check-packaged-runtime');

const root = path.join(__dirname, '..');

function optionValue(name) {
const index = process.argv.indexOf(name);
return index >= 0 ? process.argv[index + 1] : undefined;
}

function fail(message) {
throw new Error(`Packaged Electron locale check failed: ${message}`);
}

function configuredLanguages() {
const languages = packageJson.build?.mac?.electronLanguages;
if (!Array.isArray(languages) || languages.length === 0) {
fail('package.json build.mac.electronLanguages must be a non-empty array');
}
const normalized = languages.map((language) => String(language).trim()).filter(Boolean);
if (normalized.length !== languages.length || new Set(normalized).size !== normalized.length) {
fail('package.json build.mac.electronLanguages contains an empty or duplicate locale');
}
return normalized;
}

function localeDirectories(directory) {
if (!fs.existsSync(directory) || !fs.statSync(directory).isDirectory()) {
fail(`locale resource directory is missing: ${path.relative(root, directory)}`);
}
return fs.readdirSync(directory, { withFileTypes: true })
.filter((entry) => entry.isDirectory() && entry.name.endsWith('.lproj'))
.map((entry) => entry.name.slice(0, -'.lproj'.length))
.sort();
}

function inspectLocaleRoot(directory, allowedLanguages, { requireAll = false } = {}) {
const locales = localeDirectories(directory);
const allowed = new Set(allowedLanguages.map((language) => language.toLowerCase()));
const unexpected = locales.filter((locale) => !allowed.has(locale.toLowerCase()));
if (unexpected.length > 0) {
fail(`${path.relative(root, directory)} contains unexpected Electron locales: ${unexpected.join(', ')}`);
}
if (requireAll) {
const present = new Set(locales.map((locale) => locale.toLowerCase()));
const missing = allowedLanguages.filter((language) => !present.has(language.toLowerCase()));
if (missing.length > 0) {
fail(`${path.relative(root, directory)} is missing configured Electron locales: ${missing.join(', ')}`);
}
}
return locales;
}

function inspectPackage(appPath) {
const allowedLanguages = configuredLanguages();
const resourcesPath = path.join(appPath, 'Contents', 'Resources');
const frameworkResourcesPath = path.join(
appPath,
'Contents',
'Frameworks',
'Electron Framework.framework',
'Versions',
'A',
'Resources',
);
const appLocales = inspectLocaleRoot(resourcesPath, allowedLanguages);
const frameworkLocales = inspectLocaleRoot(frameworkResourcesPath, allowedLanguages, { requireAll: true });
console.log(`Configured Electron locales: ${allowedLanguages.join(', ')}`);
console.log(`App resource locales: ${appLocales.join(', ') || 'none'}`);
console.log(`Electron Framework locales: ${frameworkLocales.join(', ')}`);
console.log('Packaged Electron locale check passed.');
return { appPath, allowedLanguages, appLocales, frameworkLocales };
}

function main() {
if (process.platform !== 'darwin') fail(`requires macOS, received ${process.platform}`);
const appPath = optionValue('--app') ? path.resolve(optionValue('--app')) : newestApp('arm64');
if (!appPath.endsWith('.app') || !fs.existsSync(appPath)) fail(`app does not exist: ${appPath}`);
return inspectPackage(appPath);
}

if (require.main === module) {
try {
main();
} catch (error) {
console.error(error instanceof Error ? error.message : String(error));
process.exit(1);
}
}

module.exports = { configuredLanguages, inspectPackage };
8 changes: 8 additions & 0 deletions scripts/check-release-workflow.js
Original file line number Diff line number Diff line change
Expand Up @@ -229,11 +229,19 @@ function main() {
assert(/entitlementsInherit:\s*['"]build\/entitlements\.mac\.inherit\.plist['"]/.test(signedReleaseConfig), 'signed release config must set inherited entitlements');
assert(!/identity:\s*['"]-['"]/.test(signedReleaseConfig), 'signed release config must not use ad-hoc identity');
const packageText = fs.readFileSync(path.join(root, 'package.json'), 'utf8');
const packageJson = JSON.parse(packageText);
assert(
JSON.stringify(packageJson.build?.mac?.electronLanguages) === JSON.stringify(['en', 'en_GB', 'es', 'es_419']),
'macOS Electron locale allowlist must remain en/en_GB/es/es_419',
);
const releaseAlphaScript = fs.readFileSync(releaseAlphaPath, 'utf8');
assert(releaseAlphaScript.includes('check-packaged-electron-locales.js'), 'candidate release path is missing packaged Electron locale gate');
assert(/release:signed:arm64/.test(packageText) && /scripts\/release-signed\.js/.test(packageText), 'signed release command is missing');
assert(/check:signed-release/.test(packageText) && /check-macos-signed-release\.js/.test(packageText), 'signed release verification command is missing');
const signedReleaseScript = fs.readFileSync(path.join(root, 'scripts', 'release-signed.js'), 'utf8');
for (const requiredGate of [
'check-packaged-runtime.js',
'check-packaged-electron-locales.js',
'smoke-packaged-release-identity.js',
'check-packaged-ffmpeg.js',
'smoke-packaged-backend.js',
Expand Down
1 change: 1 addition & 0 deletions scripts/release-alpha.js
Original file line number Diff line number Diff line change
Expand Up @@ -306,6 +306,7 @@ async function main() {
const outputs = candidateOutputs(pkg);
const packageInfo = inspectPackage(outputs.appPath);
runPackagedGate('Packaged runtime gate', 'scripts/check-packaged-runtime.js', ['--arch', 'arm64', '--app', outputs.appPath], env);
runPackagedGate('Packaged Electron locale gate', 'scripts/check-packaged-electron-locales.js', ['--app', outputs.appPath], env);
runPackagedGate('Packaged release identity gate', 'scripts/smoke-packaged-release-identity.js', ['--app', outputs.appPath], env);
runPackagedGate('Packaged FFmpeg gate', 'scripts/check-packaged-ffmpeg.js', ['--arch', 'arm64', '--app', outputs.appPath], env);
runPackagedGate('Packaged backend gate', 'scripts/smoke-packaged-backend.js', ['--arch', 'arm64', '--app', outputs.appPath], env);
Expand Down
1 change: 1 addition & 0 deletions scripts/release-signed.js
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ function main() {
const appPath = path.join(outputRoot, 'mac-arm64', 'ScriptCut.app');
const dmgPath = path.join(outputRoot, `ScriptCut-${version}-arm64.dmg`);
run('Packaged runtime gate', 'node', ['scripts/check-packaged-runtime.js', '--arch', 'arm64', '--app', appPath]);
run('Packaged Electron locale gate', 'node', ['scripts/check-packaged-electron-locales.js', '--app', appPath]);
run('Packaged release identity gate', 'node', ['scripts/smoke-packaged-release-identity.js', '--app', appPath]);
run('Packaged FFmpeg gate', 'node', ['scripts/check-packaged-ffmpeg.js', '--arch', 'arm64', '--app', appPath]);
run('Packaged backend gate', 'node', ['scripts/smoke-packaged-backend.js', '--arch', 'arm64', '--app', appPath]);
Expand Down
Loading