Skip to content
This repository was archived by the owner on Mar 5, 2026. It is now read-only.
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
a8b127f
add collectJsonStrings util
Aug 22, 2025
cf8db24
remove comments
Aug 22, 2025
e10c358
parallelize requesting string translation for each language
Aug 22, 2025
b684c12
adds buildOutputJson and writes to file for each language
Aug 23, 2025
f73d407
add a more robust speed test
tylerthehaas Aug 23, 2025
6fabce7
add translateWithRetry with exponential back off
tylerthehaas Aug 23, 2025
b48146c
cleanup code comments
tylerthehaas Aug 23, 2025
106af14
change return of translateText to return lang
tylerthehaas Aug 23, 2025
7a34143
account for multiple requests for same language
tylerthehaas Aug 23, 2025
c1f6b83
update action name and author
tylerthehaas Aug 24, 2025
c2df7c8
fixes problem with keep tags being kept in output
tylerthehaas Sep 15, 2025
170eb0c
2.2.1
tylerthehaas Sep 15, 2025
c6d903a
Added new `model_type` parameter that can be passed to the translateO…
joe-l-bd Dec 4, 2025
968ef67
Update model type to 'prefer_quality_optimized' in main.ts and index.…
joe-l-bd Dec 4, 2025
cfa19cf
Update deepl-node dependency to version 1.22.0 and add tests for ES-4…
joe-l-bd Dec 4, 2025
439e114
Refactor test assertions in main.test.ts for ES-419 translation to us…
joe-l-bd Dec 4, 2025
2ba7b20
Merge pull request #1 from joe-l-bd/feat/add-model-type-param
tylerthehaas Dec 4, 2025
22325e0
Add timeout input to action.yml and implement timeout handling in ind…
joe-l-bd Dec 4, 2025
47088d7
Refactor timeout handling in index.ts by utilizing createTranslatorOp…
joe-l-bd Dec 4, 2025
7668daa
add missing function
joe-l-bd Dec 4, 2025
3e5ae71
Refactor createTranslatorOptions to accept timeout parameter and enha…
joe-l-bd Dec 10, 2025
ce98aff
Merge pull request #2 from joe-l-bd/feat/add-model-type-param
bit-tyler Dec 10, 2025
c6ecd1d
Translate only changed source segments and fix PR branch commits.
tylerthehaas Mar 10, 2026
2fa7fd2
Allow workflows to pin the translation base branch.
tylerthehaas Mar 10, 2026
1e0a2d3
Use the branch merge-base for incremental translations.
tylerthehaas Mar 10, 2026
a972ab8
Fetch the workflow repo base branch for merge-base diffing.
tylerthehaas Mar 10, 2026
9dd2c67
fix review feedback
tylerthehaas Mar 10, 2026
2b1c4bf
adds safe fallback
tylerthehaas Mar 10, 2026
508a593
Add translateTexts function with unit tests for default and custom pr…
tylerthehaas Mar 10, 2026
46e7dca
Enhance getTextLineMetadata function to handle empty lines and missin…
tylerthehaas Mar 11, 2026
4b97a01
Merge pull request #3 from tylerthehaas/fix/incremental-pr-translation
bit-tyler Mar 11, 2026
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
24 changes: 20 additions & 4 deletions action.yml
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
name: 'DeepL Translate Github Action'
author: 'Estee Tey'
name: 'DeepL API Translate Github Action'
author: 'Tyler Haas'
Comment on lines +1 to +2
Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

hmm o_O

description: 'Translate any document using DeepL Translate API'

inputs:
Expand All @@ -15,6 +15,9 @@ inputs:
output_file_name_pattern:
description: "Pattern of the output file name, including the folder name"
required: true
base_ref:
description: "Optional git branch to diff the source file against before translating. Defaults to the pull request base branch when available."
required: false
deepl_api_key:
description: "API Key for DeepL API"
required: true
Expand All @@ -29,25 +32,38 @@ inputs:
description: "End tag to ignore when translating"
required: false
default: <!-- /notranslate -->
model_type:
description: "Model type to use for translation. Valid values: quality_optimized, prefer_quality_optimized, latency_optimized"
required: false
timeout:
description: "Connection timeout for each HTTP request retry, in milliseconds. If not provided, uses the default value (5000ms)"
required: false

runs:
using: composite
steps:
- name: Checkout
uses: actions/checkout@v3
uses: actions/checkout@v4
with:
repository: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name || github.repository }}
ref: ${{ github.event_name == 'pull_request' && github.head_ref || github.ref }}
fetch-depth: 0
- name: Set up node
uses: actions/setup-node@v3
uses: actions/setup-node@v4
- name: Run translation script
shell: bash
env:
target_languages: ${{ inputs.target_languages }}
input_file_path: ${{ inputs.input_file_path }}
output_file_name_prefix: ${{ inputs.output_file_name_prefix }}
output_file_name_pattern: ${{ inputs.output_file_name_pattern }}
base_ref: ${{ inputs.base_ref }}
deepl_api_key: ${{ inputs.deepl_api_key }}
ignore_terms: ${{ inputs.ignore_terms }}
no_translate_start_tag: ${{ inputs.no_translate_start_tag }}
no_translate_end_tag: ${{ inputs.no_translate_end_tag }}
model_type: ${{ inputs.model_type }}
timeout: ${{ inputs.timeout }}
run: |
cd ${{github.action_path}} && yarn install && yarn start
- name: remove unused temp file if it exists
Expand Down
6 changes: 3 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "deepl-translate-github-action",
"version": "2.2.0",
"version": "2.2.1",
"author": "Estee <estee.tsw@gmail.com>",
"license": "MIT",
"scripts": {
Expand All @@ -10,12 +10,12 @@
"coverage": "vitest run --coverage"
},
"dependencies": {
"deepl-node": "^1.14.0"
"deepl-node": "^1.22.0"
},
"devDependencies": {
"@types/node": "^22.7.6",
"ts-node": "^10.9.2",
"typescript": "^5.6.3",
"vitest": "^2.1.3"
}
}
}
37 changes: 36 additions & 1 deletion playground/local.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,42 @@ const endTagForNoTranslate = "<!-- /keep -->";

const tempFilePath = path.join(playgroundPath, "to_translate.txt");
const fileExtensionsThatAllowForIgnoringBlocks = [".html", ".xml", ".md", ".txt"];
const targetLanguages = ["ja"] as deepl.TargetLanguageCode[];

// All supported DeepL target languages as of April 2025
const targetLanguages: deepl.TargetLanguageCode[] = [
"ar", // Arabic
"bg", // Bulgarian
"cs", // Czech
"da", // Danish
"de", // German
"el", // Greek
"en-GB", // English (British)
"en-US", // English (American)
"es", // Spanish
"et", // Estonian
"fi", // Finnish
"fr", // French
"hu", // Hungarian
"id", // Indonesian
"it", // Italian
"ja", // Japanese
"ko", // Korean
"lt", // Lithuanian
"lv", // Latvian
"nb", // Norwegian Bokmål
"nl", // Dutch
"pl", // Polish
"pt-BR", // Portuguese (Brazilian)
"pt-PT", // Portuguese (all Portuguese variants excluding Brazilian Portuguese)
"ro", // Romanian
"ru", // Russian
"sk", // Slovak
"sl", // Slovenian
"sv", // Swedish
"tr", // Turkish
"uk", // Ukrainian
"zh" // Chinese (unspecified variant for backward compatibility)
];

(async () => {
await main({
Expand Down
1 change: 0 additions & 1 deletion playground/locales/ja/nested.json

This file was deleted.

997 changes: 992 additions & 5 deletions playground/nested.json

Large diffs are not rendered by default.

108 changes: 108 additions & 0 deletions src/git.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
import { execFile } from 'child_process'
import { promisify } from 'util'

const execFileAsync = promisify(execFile)

interface BaseFileContentParams {
workspacePath: string
inputFileRelativePath: string
baseRef?: string
}

function isMissingPathError(stderr: string) {
return (
stderr.includes('exists on disk, but not in') ||
stderr.includes('pathspec') ||
stderr.includes('fatal: path') ||
stderr.includes('does not exist')
)
}

function getWorkflowRepositoryUrl(): string | null {
const serverUrl = process.env.GITHUB_SERVER_URL?.replace(/\/$/, '')
const repository = process.env.GITHUB_REPOSITORY

if (!serverUrl || !repository) {
return null
}

return `${serverUrl}/${repository}.git`
}

export async function getBaseFileContent({
workspacePath,
inputFileRelativePath,
baseRef,
}: BaseFileContentParams): Promise<string | null> {
if (!baseRef) {
return null
}

const baseRepoUrl = getWorkflowRepositoryUrl()
const baseBranchRef = baseRepoUrl
? `refs/remotes/base/${baseRef}`
: `refs/remotes/origin/${baseRef}`
const fetchSource = baseRepoUrl ?? 'origin'

try {
await execFileAsync(
'git',
[
'fetch',
'--no-tags',
fetchSource,
`+refs/heads/${baseRef}:${baseBranchRef}`,
],
{ cwd: workspacePath, maxBuffer: 10 * 1024 * 1024 },
)
} catch (error) {
console.warn(`Failed to fetch base branch ${baseBranchRef}, falling back to local refs if available.`, error)
}

let baseCommitRef: string | null = null

try {
const { stdout } = await execFileAsync(
'git',
['merge-base', 'HEAD', baseBranchRef],
{ cwd: workspacePath, maxBuffer: 10 * 1024 * 1024 },
)

const mergeBaseSha = stdout.trim()
if (mergeBaseSha) {
baseCommitRef = mergeBaseSha
}
} catch (error) {
console.warn(
`Failed to determine merge-base with ${baseBranchRef}, falling back to full translation.`,
error,
)
}

if (!baseCommitRef) {
return null
}

try {
const normalizedInputFileRelativePath = inputFileRelativePath.replace(/\\/g, '/')
const { stdout } = await execFileAsync(
'git',
['show', `${baseCommitRef}:${normalizedInputFileRelativePath}`],
{ cwd: workspacePath, maxBuffer: 10 * 1024 * 1024 },
)

return stdout
} catch (error) {
const stderr = error instanceof Error && 'stderr' in error ? String(error.stderr) : ''

if (isMissingPathError(stderr)) {
return null
}

console.warn(
`Failed to read ${inputFileRelativePath} from ${baseCommitRef}, falling back to full translation.`,
error,
)
return null
}
}
40 changes: 34 additions & 6 deletions src/index.ts
Original file line number Diff line number Diff line change
@@ -1,23 +1,30 @@
import type { TargetLanguageCode } from "deepl-node";
import { Translator } from 'deepl-node';
import path from "path";
import { main } from "./main";
import { getBaseFileContent } from "./git";
import { main, type ModelType } from "./main";
import { createTranslatorOptions } from "./utils";

const authKey = process.env.deepl_api_key as string;
const translator = new Translator(authKey);
const translatorOptions = createTranslatorOptions(process.env.timeout);

const translator = new Translator(authKey, translatorOptions);
const workspacePath = process.env.GITHUB_WORKSPACE as string;
const inputFileRelativePath = process.env.input_file_path as string;
const baseRef = process.env.base_ref || process.env.GITHUB_BASE_REF;
const inputFilePath = path.join(
process.env.GITHUB_WORKSPACE as string,
process.env.input_file_path as string,
workspacePath,
inputFileRelativePath,
);
const outputFileNamePattern = path.join(
process.env.GITHUB_WORKSPACE as string,
workspacePath,
process.env.output_file_name_pattern as string,
)
const startTagForNoTranslate = process.env.no_translate_start_tag as string;
const endTagForNoTranslate = process.env.no_translate_end_tag as string;

const tempFilePath = path.join(
process.env.GITHUB_WORKSPACE as string,
workspacePath,
"to_translate.txt",
);
const fileExtensionsThatAllowForIgnoringBlocks = [".html", ".xml", ".md", ".txt"];
Expand All @@ -34,14 +41,35 @@ const fileExtensionsThatAllowForIgnoringBlocks = [".html", ".xml", ".md", ".txt"

targetLanguages = targetLanguages.filter(lang => !excludedLanguages.includes(lang));

const modelTypeEnv = process.env.model_type;
let modelType: ModelType | undefined;
if (modelTypeEnv) {
const validModelTypes: ModelType[] = ['quality_optimized', 'prefer_quality_optimized', 'latency_optimized'];
if (validModelTypes.includes(modelTypeEnv as ModelType)) {
modelType = modelTypeEnv as ModelType;
} else {
console.warn(`Invalid model_type value: ${modelTypeEnv}. Valid values are: ${validModelTypes.join(', ')}. Ignoring model_type parameter.`);
}
}

const baseFileContent = await getBaseFileContent({
workspacePath,
inputFileRelativePath,
baseRef,
});

await main({
translator,
workspacePath,
inputFileRelativePath,
inputFilePath,
outputFileNamePattern,
startTagForNoTranslate,
endTagForNoTranslate,
tempFilePath,
fileExtensionsThatAllowForIgnoringBlocks,
targetLanguages,
modelType,
baseFileContent,
});
})();
Loading