Skip to content
Merged
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
110 changes: 43 additions & 67 deletions .github/scripts/translate.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ const path = require('path');
// ==================================================================================================

// Path to your original language files (e.g., English source content)
const ORIGIN_LANGUAGE_DIR = path.resolve('.');
const ORIGIN_LANGUAGE_DIR = path.resolve('.');

// Path where translated files will be saved (Base directory)
const TRANSLATION_OUTPUT_DIR = path.resolve('.');
Expand All @@ -20,15 +20,13 @@ const EXCLUDED_FILES = ['package.json', 'package-lock.json', 'node_modules', 'do
// ============================================================

(async () => {
// Dynamically import the ESM-only module
const sdkModule = await import('@frenglish/sdk');
const FrenglishSDK = sdkModule.FrenglishSDK;
if (!FrenglishSDK) throw new Error('FrenglishSDK not found in module exports.');

// Retrieve the API key from environment variable
const FRENGLISH_API_KEY = process.env.FRENGLISH_API_KEY;
if (!FRENGLISH_API_KEY) {
console.error('Error: FRENGLISH_API_KEY environment variable not set.');
console.error('FRENGLISH_API_KEY environment variable not set. Aborting action.');
process.exit(1);
}
const frenglish = FrenglishSDK(FRENGLISH_API_KEY);
Expand All @@ -44,53 +42,46 @@ const EXCLUDED_FILES = ['package.json', 'package-lock.json', 'node_modules', 'do
const data = await response.json();
return data.default_branch;
} catch (error) {
console.log('Error getting default branch:', error.message);
console.error(`❌ Failed to retrieve default branch: ${error.message}`);
return 'main';
}
}

async function isSupportedFile(filePath) {
try {
// Ensure the file is within the intended source directory structure for relative path logic
const relativeToOrigin = path.relative(ORIGIN_LANGUAGE_DIR, path.resolve(filePath));
if (relativeToOrigin.startsWith('..') || relativeToOrigin === '') {
return false;
return false;
}

// Check against general excluded files/paths
if (EXCLUDED_FILES.some(excluded => filePath.includes(excluded))) {
console.log(`File ${filePath} is explicitly excluded.`);
console.log(`⏭️ Skipping (excluded): ${filePath}`);
return false;
}

// Get the configuration to check origin language
const config = await frenglish.getDefaultConfiguration();
const languageCodes = await frenglish.getSupportedLanguages();
const originLanguage = config.originLanguage.toLowerCase();

// Check if the file is in a translated language directory
const pathParts = filePath.split(path.sep);
const languageDirIndex = pathParts.findIndex(part =>
part.toLowerCase() === originLanguage ||
const languageDirIndex = pathParts.findIndex(part =>
part.toLowerCase() === originLanguage ||
languageCodes.some(lang => lang.toLowerCase() === part.toLowerCase())
);

// If the file is in a language directory and it's not the origin language, exclude it
if (languageDirIndex !== -1 && pathParts[languageDirIndex].toLowerCase() !== originLanguage) {
console.log(`File ${filePath} is in a translated language directory. Excluding.`);
console.log(`⏭️ Skipping (translated dir): ${filePath}`);
return false;
}

// Check extension against Frenglish supported types
const supportedFileTypes = await frenglish.getSupportedFileTypes();
const validFileTypes = supportedFileTypes.filter(type => type && type.length > 0);
const ext = path.extname(filePath).toLowerCase().replace('.', '');

const isSupported = ext && validFileTypes.includes(ext);
return isSupported;

} catch (error) {
console.error(`Error checking file support for ${filePath}:`, error.message);
console.error(`Error checking file support for ${filePath}: ${error.message}`);
return false;
}
}
Expand All @@ -99,47 +90,39 @@ const EXCLUDED_FILES = ['package.json', 'package-lock.json', 'node_modules', 'do
async function getChangedFiles() {
try {
const isPR = !!process.env.GITHUB_BASE_REF;
const currentBranch = process.env.GITHUB_HEAD_REF // PR
|| process.env.GITHUB_REF.replace('refs/heads/', ''); // Push to default branch
const currentBranch = process.env.GITHUB_HEAD_REF || process.env.GITHUB_REF.replace('refs/heads/', '');
const defaultBranch = await getDefaultBranch();

// Skip non‑PR pushes that are NOT on default branch

if (!isPR && currentBranch !== defaultBranch) {
return [];
return [];
}

// Figure out what we’re diffing against
const baseBranch = isPR ? process.env.GITHUB_BASE_REF : defaultBranch;

let baseSha;
if (isPR) {
// bring in the base branch locally
execSync(`git fetch --depth=1 origin ${baseBranch}:${baseBranch}`);

// HEAD is already at the PR tip
baseSha = execSync(`git merge-base ${baseBranch} HEAD`).toString().trim();
} else {
// push on default branch: compare to the previous commit on that branch
baseSha = process.env.GITHUB_EVENT_BEFORE || execSync('git rev-parse HEAD^').toString().trim();
}

console.log(`Diff base: ${baseBranch} @ ${baseSha}`);
console.log(`Head : ${currentBranch} @ HEAD`);

const output = execSync(
`git diff --diff-filter=ACM --name-only ${baseSha} HEAD`
).toString().trim();

const changedFiles = output ? output.split('\n') : [];

console.log(`🔀 Diff base: ${baseBranch} @ ${baseSha}`);
console.log(`🔝 Head : ${currentBranch} @ HEAD`);

const output = execSync(`git diff --diff-filter=ACM --name-only ${baseSha} HEAD`).toString().trim();
const changedFiles = output ? output.split('\n') : [];
const supportedFiles = [];

for (const file of changedFiles) {
if (await isSupportedFile(file)) supportedFiles.push(file);
}
console.log('Files sent for translation:', supportedFiles);

console.log(`📦 Files queued for translation (${supportedFiles.length}): ${supportedFiles.join(', ') || 'None'}`);
return supportedFiles;
} catch (error) {
console.error('Error getting changed files:', error.message);
console.error(`❌ Error getting changed files: ${error.message}`);
return [];
}
}
Expand All @@ -150,92 +133,85 @@ const EXCLUDED_FILES = ['package.json', 'package-lock.json', 'node_modules', 'do
const originLanguage = config.originLanguage.toLowerCase();
const filesToTranslate = await getChangedFiles();

if (!filesToTranslate || filesToTranslate.length === 0) {
console.log('No supported files found in the diff within the origin language directory to translate.');
return; // Exit gracefully if no files need translation
if (!filesToTranslate.length) {
console.log('ℹ️ No eligible files found for translation. Exiting.');
return;
}

// Prepare content for translation
const fileContents = await Promise.all(filesToTranslate.map(async (file) => {
try {
const content = await fs.readFile(file, 'utf-8');
// Use path relative to ORIGIN_LANGUAGE_DIR as the fileId
const fileId = path.relative(ORIGIN_LANGUAGE_DIR, file);
return { fileId: fileId, content: content };
} catch (readError) {
console.error(`Error reading file ${file}:`, readError.message);
console.error(`Error reading file ${file}:`, readError.message);
return null;
}
}));

// Filter out any nulls from read errors
const validFileContents = fileContents.filter(fc => fc !== null);
if (validFileContents.length === 0) {
console.log('No valid file contents could be read for translation.');
console.log('⚠️ No readable file contents detected. Exiting.');
return;
}

const filenames = validFileContents.map(file => file.fileId);
const contents = validFileContents.map(file => file.content);

console.log(`Initiating translation for ${filenames.length} files:`, filenames);
console.log(`🚀 Initiating translation for ${filenames.length} file(s).`);
const translation = await frenglish.translate(contents, false, filenames);
console.log(`Translation requested with ID: ${translation.translationId}`);
console.log(`📤 Translation request submitted. ID: ${translation.translationId}`);

// Process translated content
for (const languageData of translation.content) {
const language = languageData.language;
// Skip writing files for the origin language if they are returned
if (language === originLanguage) {
console.log(`Skipping write for origin language: ${language}`);
console.log(`⏩ Skipping origin language (${language}).`);
continue;
}

const languageOutputDir = path.join(TRANSLATION_OUTPUT_DIR, language);
try {
await fs.mkdir(languageOutputDir, { recursive: true });
} catch (mkdirError) {
console.error(`Error creating directory ${languageOutputDir}:`, mkdirError.message);
continue; // Skip this language if directory creation fails
console.error(`❌ Unable to create directory ${languageOutputDir}: ${mkdirError.message}`);
continue;
}

for (const translatedFile of languageData.files) {
// Construct the full path for the translated file
const translatedFilePath = path.join(languageOutputDir, translatedFile.fileId);

// Ensure the subdirectory for the file exists (e.g., for nested structures)
try {
await fs.mkdir(path.dirname(translatedFilePath), { recursive: true });
} catch (mkdirError) {
console.error(`Error creating subdirectory ${path.dirname(translatedFilePath)}:`, mkdirError.message);
continue; // Skip this file if subdirectory creation fails
console.error(`❌ Unable to create subdirectory ${path.dirname(translatedFilePath)}: ${mkdirError.message}`);
continue;
}

// Write the file content if not empty
if (translatedFile.content && translatedFile.content.length > 0) {
try {
await fs.writeFile(translatedFilePath, translatedFile.content, 'utf8');
console.log(`Translated file written: ${translatedFilePath}`);
console.log(`✅ Written: ${translatedFilePath}`);
} catch (writeError) {
console.error(`Error writing translated file ${translatedFilePath}:`, writeError.message);
console.error(`Error writing ${translatedFilePath}: ${writeError.message}`);
}
} else {
console.warn(`Empty content for translated file: ${translatedFile.fileId} in language ${language}. Skipping write.`);
console.warn(`⚠️ Empty content for ${translatedFile.fileId} (${language}). Skipping.`);
}
}
}

console.log("Translation file writing complete. Git operations will be handled by the workflow.");

console.log('🏁 Translation workflow complete. Git operations will be handled by the Action.');
} catch (error) {
console.error('Error during translation and file writing process:', error);
if (error.response && error.response.data) {
console.error('Frenglish API Error Details:', error.response.data);
console.error('❌ Translation process failed:', error);
if (error.response?.data) {
console.error('🔍 Frenglish API details:', error.response.data);
}
process.exit(1);
}
}

// Run the modified translation process
translateAndWriteFiles();
})();