Skip to content
Closed
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
8 changes: 8 additions & 0 deletions action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,14 @@ inputs:
required: false
# Keep in sync with DEFAULT_CLAUDE_MODEL in src/models.ts
default: 'claude-sonnet-5'
localize:
description: >-
Localisation rules applied to files this run creates for the first time:
comma-separated subset of code-comments, figure-labels, i18n-font-config.
Use "none" to disable. Existing translations are unaffected.
required: false
# Keep in sync with DEFAULT_RULES in src/localization-rules.ts
default: 'code-comments,figure-labels,i18n-font-config'
bibliography:
description: >-
How to handle citations a run introduces that the target bibliography lacks:
Expand Down
154 changes: 148 additions & 6 deletions dist-action/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -31352,6 +31352,112 @@ function isRetryableAnthropicError(error3) {
return error3 instanceof RateLimitError || error3 instanceof APIConnectionError || error3 instanceof APIError && (error3.status !== void 0 && error3.status >= 500 || error3.status === void 0 && error3.message?.includes("overloaded"));
}

// dist/localization-rules.js
var codeCommentsRule = {
id: "code-comments",
label: "Code Comments",
description: "Translate comments inside code cells to the target language",
buildPrompt(targetLanguage) {
const example = getCommentExample(targetLanguage);
return `**Localize code comments**: Translate Python comments (lines starting with #) inside code cells to ${targetLanguage}. Keep variable names, function names, and code unchanged \u2014 only translate the human-readable comment text.${example}`;
}
};
var figureLabelsRule = {
id: "figure-labels",
label: "Figure Labels",
description: "Translate plot titles, axis labels, and legend entries",
buildPrompt(targetLanguage) {
const example = getFigureLabelExample(targetLanguage);
return `**Localize figure labels**: Translate user-visible strings in plotting calls \u2014 including plt.title(), plt.xlabel(), plt.ylabel(), plt.legend() labels, ax.set_title(), ax.set_xlabel(), ax.set_ylabel(), and label= keyword arguments. Keep code structure, variable names, and non-label strings unchanged.${example}`;
}
};
var i18nFontConfigRule = {
id: "i18n-font-config",
label: "Font Configuration",
description: "Inject font configuration for CJK/RTL scripts into first matplotlib code cell",
buildPrompt(targetLanguage) {
const config = getFontConfigSnippet(targetLanguage);
if (!config)
return "";
return `**Inject font configuration**: In the FIRST code cell that imports matplotlib, append the following lines immediately after the existing imports (before any other code). These lines ensure that ${targetLanguage} characters render correctly in plots. Add them even though they do not exist in the source document \u2014 this is intentional localization. Mark the added lines with a \`# i18n\` comment.

Lines to add:
\`\`\`python
${config}
\`\`\``;
}
};
var RULES = {
"code-comments": codeCommentsRule,
"figure-labels": figureLabelsRule,
"i18n-font-config": i18nFontConfigRule
};
var ALL_RULE_IDS = Object.keys(RULES);
var DEFAULT_RULES = [...ALL_RULE_IDS];
function parseLocalizationRules(input) {
const trimmed = input.trim().toLowerCase();
if (trimmed === "none")
return [];
const ids = trimmed.split(",").map((s) => s.trim()).filter(Boolean);
const invalid = ids.filter((id) => !RULES[id]);
if (invalid.length > 0) {
throw new Error(`Unknown localization rule(s): ${invalid.join(", ")}. Available: ${ALL_RULE_IDS.join(", ")}, none`);
}
return ids;
}
function buildLocalizationPrompt(rules, targetLanguage) {
if (rules.length === 0)
return "";
const parts = rules.map((id) => RULES[id].buildPrompt(targetLanguage)).filter(Boolean);
if (parts.length === 0)
return "";
return `
## Code-Cell Localization

The following localization rules OVERRIDE the default "keep code as-is" rule for specific elements within code cells:

${parts.map((p, i) => `${i + 1}. ${p}`).join("\n\n")}`;
}
function getCommentExample(targetLanguage) {
const examples = {
"zh-cn": `
Example:
Before: # Calculate the steady state
After: # \u8BA1\u7B97\u7A33\u6001`,
fa: `
Example:
Before: # Calculate the steady state
After: # \u0645\u062D\u0627\u0633\u0628\u0647 \u062D\u0627\u0644\u062A \u067E\u0627\u06CC\u062F\u0627\u0631`
};
return examples[targetLanguage] || "";
}
function getFontConfigSnippet(targetLanguage) {
const snippets = {
"zh-cn": `import matplotlib as mpl # i18n
FONTPATH = "_fonts/SourceHanSerifSC-SemiBold.otf" # i18n
mpl.font_manager.fontManager.addfont(FONTPATH) # i18n
mpl.rcParams['font.family'] = ['Source Han Serif SC'] # i18n`
};
return snippets[targetLanguage] || "";
}
function getFigureLabelExample(targetLanguage) {
const examples = {
"zh-cn": `
Example:
Before: plt.title('Price Dynamics')
After: plt.title('\u4EF7\u683C\u52A8\u6001')
Before: ax.set_xlabel('Time')
After: ax.set_xlabel('\u65F6\u95F4')`,
fa: `
Example:
Before: plt.title('Price Dynamics')
After: plt.title('\u062F\u06CC\u0646\u0627\u0645\u06CC\u06A9 \u0642\u06CC\u0645\u062A')
Before: ax.set_xlabel('Time')
After: ax.set_xlabel('\u0632\u0645\u0627\u0646')`
};
return examples[targetLanguage] || "";
}

// dist/bibliography.js
var BIBLIOGRAPHY_MODES = ["backfill", "lint", "off"];
var DEFAULT_BIBLIOGRAPHY_MODE = "backfill";
Expand Down Expand Up @@ -31697,6 +31803,8 @@ function getInputs() {
const anthropicApiKey = core.getInput("anthropic-api-key", { required: true });
const claudeModel = core.getInput("claude-model", { required: false }) || DEFAULT_CLAUDE_MODEL;
const githubToken = core.getInput("github-token", { required: true });
const localizeRaw = core.getInput("localize", { required: false });
const localizationRules = localizeRaw ? parseLocalizationRules(localizeRaw) : DEFAULT_RULES;
const bibliographyMode = parseBibliographyMode(core.getInput("bibliography", { required: false }));
const prLabelsRaw = core.getInput("pr-labels", { required: false }) || SYNC_PR_LABELS.join(",");
const prLabels = prLabelsRaw.split(",").map((l) => l.trim()).filter((l) => l.length > 0);
Expand Down Expand Up @@ -31728,6 +31836,7 @@ function getInputs() {
prReviewers,
prTeamReviewers,
testMode,
localizationRules,
bibliographyMode
};
}
Expand All @@ -31738,12 +31847,15 @@ function getRebaseInputs() {
const anthropicApiKey = core.getInput("anthropic-api-key", { required: true });
const githubToken = core.getInput("github-token", { required: true });
const normalizedDocsFolder = docsFolder === "" ? "" : docsFolder.endsWith("/") ? docsFolder : `${docsFolder}/`;
const rebaseLocalizeRaw = core.getInput("localize", { required: false });
const localizationRules = rebaseLocalizeRaw ? parseLocalizationRules(rebaseLocalizeRaw) : DEFAULT_RULES;
return {
docsFolder: normalizedDocsFolder,
glossaryPath,
anthropicApiKey,
githubToken,
rebaseStaleSiblings: core.getInput("rebase-stale-siblings", { required: false }).toLowerCase() === "true",
localizationRules,
bibliographyMode: parseBibliographyMode(core.getInput("bibliography", { required: false }))
};
}
Expand Down Expand Up @@ -38181,9 +38293,9 @@ function applyToProse(line, rule) {
}
return out;
}
var RULES = /* @__PURE__ */ new Map([["fr", applyFrenchSpacing]]);
var RULES2 = /* @__PURE__ */ new Map([["fr", applyFrenchSpacing]]);
function applyTypography(content, language) {
const rule = RULES.get(language);
const rule = RULES2.get(language);
if (!rule)
return content;
const lines = content.split("\n");
Expand Down Expand Up @@ -38557,14 +38669,20 @@ ${bodyLines.join("\n")}`;
}
/**
* Process a full document (for new files)
*
* `customInstructions` carries the localisation rules (code comments, figure
* labels, i18n font config). A new file has no existing localisation to
* preserve, so unless these are passed the document lands with English figure
* labels and no font block — see #178.
*/
async processFull(content, filepath, sourceLanguage, targetLanguage, glossary) {
async processFull(content, filepath, sourceLanguage, targetLanguage, glossary, customInstructions) {
this.log(`Processing full document: ${filepath}`);
const result = await this.translator.translateFullDocument({
sourceLanguage,
targetLanguage,
glossary,
content
content,
customInstructions
});
if (!result.success) {
throw new Error(`Full translation failed: ${result.error}`);
Expand Down Expand Up @@ -38965,7 +39083,7 @@ var SyncOrchestrator = class {
}
let translatedContent;
if (file.isNewFile) {
translatedContent = await this.processor.processFull(file.newContent, file.filename, this.config.sourceLanguage, this.config.targetLanguage, glossary);
translatedContent = await this.processor.processFull(file.newContent, file.filename, this.config.sourceLanguage, this.config.targetLanguage, glossary, this.localizationPromptForNewFile(file.filename));
} else {
const skipped = [];
const dropped = [];
Expand All @@ -38992,6 +39110,28 @@ var SyncOrchestrator = class {
});
await this.maybeGenerateStateFile(file.filename, file.newContent, file.sourceCommitSha, file.isNewFile ? "NEW" : "UPDATE", result);
}
/**
* Localisation instructions for a document this run creates from scratch (#178).
*
* `init` has always applied these rules; sync applied none of them, so every
* lecture arriving through the automated path landed with English figure
* labels and no font config. Only first-time translations need them — an
* existing translation already carries its localisation and the translator
* prompts preserve it (#107).
*
* Returns undefined when the edition has opted out, so the caller can pass it
* straight through.
*/
localizationPromptForNewFile(filename) {
const rules = this.config.localizationRules ?? DEFAULT_RULES;
if (rules.length === 0)
return void 0;
const prompt = buildLocalizationPrompt(rules, this.config.targetLanguage);
if (!prompt)
return void 0;
this.logger.info(`${filename}: first-time translation \u2014 applying localisation rules (${rules.join(", ")})`);
return prompt;
}
/**
* Process a renamed markdown file.
* Preserves existing translation at new path, deletes old path.
Expand All @@ -39015,7 +39155,7 @@ var SyncOrchestrator = class {
this.logger.warning(`${file.filename}: removed ${dropped.length} target-only section(s) with no source counterpart \u2014 correct if upstream deleted them; destructive if human-added (see PR body)`);
}
} else {
translatedContent = await this.processor.processFull(file.newContent, file.filename, this.config.sourceLanguage, this.config.targetLanguage, glossary);
translatedContent = await this.processor.processFull(file.newContent, file.filename, this.config.sourceLanguage, this.config.targetLanguage, glossary, this.localizationPromptForNewFile(file.filename));
}
const parity = checkStructuralParity(file.newContent, translatedContent);
if (!parity.ok) {
Expand Down Expand Up @@ -39503,6 +39643,7 @@ async function rebaseSinglePR(octokit, pr, metadata, inputs) {
claudeModel: metadata.claudeModel,
anthropicApiKey: inputs.anthropicApiKey,
debugMode: true,
localizationRules: inputs.localizationRules,
bibliographyMode: inputs.bibliographyMode
}, coreLogger, stateConfig);
const bibliography = inputs.bibliographyMode === "off" ? void 0 : await fetchBibliographies(octokit, { owner: metadata.sourceRepo.split("/")[0], repo: metadata.sourceRepo.split("/")[1] }, { owner, repo }, inputs.docsFolder);
Expand Down Expand Up @@ -39634,6 +39775,7 @@ async function runSync() {
claudeModel: inputs.claudeModel,
anthropicApiKey: inputs.anthropicApiKey,
debugMode: true,
localizationRules: inputs.localizationRules,
bibliographyMode: inputs.bibliographyMode
}, coreLogger, stateConfig);
const bibliography = inputs.bibliographyMode === "off" ? void 0 : await fetchBibliographies(octokit, { owner: github2.context.repo.owner, repo: github2.context.repo.repo, ref: effectiveSha }, { owner: targetOwner, repo: targetRepo }, inputs.docsFolder);
Expand Down
Loading
Loading