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
204 changes: 184 additions & 20 deletions package-lock.json

Large diffs are not rendered by default.

19 changes: 19 additions & 0 deletions packages/cli/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,9 @@ Commands:
l, link [<file>] Get link to target file (default: workshop.md)
-r, --repo Set GitHub repo instead of fetching it from git
-b, --branch <name> Set branch name (default: current branch)
t, translate [<file>] Translate workshop to different languages
-l, --languages <lang1,lang2,...> Comma-separated list of target languages
-m, --model <model_name> Copilot model to use (default: gpt-5.2)

General options:
-v, --version Show version
Expand Down Expand Up @@ -64,3 +67,19 @@ As MOAW requires specific [frontmatter metadata in the Markdown file](../../temp
```

Note that not all AsciiDoc features are supported, and fall back to HTML generation will be used in that case. You can use the `--verbose` option to see if any unsupported feature is used.

#### AI-Generated translations

The `translate` command allows you to translate your workshop to different languages using GitHub Copilot CLI.

GitHub Copilot CLI must be installed and authenticated before you can use this command (see [installation guide](https://docs.github.com/en/copilot/how-tos/set-up/install-copilot-cli)).

You can then use AI models from Copilot to translate your workshop. For example, to translate to Spanish and French:

```bash
moaw translate workshop.md -l es,fr
```

> [!NOTE]
> Translation may take some time depending on the size of your workshop and the number of target languages.
> There's a hard timeout of 30 minutes per translation request, so for very large workshops you may need to separate the translation task into multiple commands.
1 change: 1 addition & 0 deletions packages/cli/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@
"xo": "^0.58.0"
},
"dependencies": {
"@github/copilot-sdk": "^0.1.16",
"asciidoctor": "^3.0.2",
"browser-sync": "^3.0.2",
"debug": "^4.3.4",
Expand Down
22 changes: 19 additions & 3 deletions packages/cli/src/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import process from 'node:process';
import debug from 'debug';
import updateNotifier, { type Package } from 'update-notifier';
import minimist from 'minimist';
import { convert, createNew, link, serve, build } from './commands/index.js';
import { convert, createNew, link, serve, build, translate } from './commands/index.js';
import { getPackageJson } from './util.js';

const help = `Usage: moaw <command> [options]
Expand All @@ -21,6 +21,9 @@ Commands:
l, link [<file>] Get link to target file (default: workshop.md)
-r, --repo Set GitHub repo instead of fetching it from git
-b, --branch <name> Set branch name (default: current branch)
t, translate [<file>] Translate workshop to different languages
-l, --languages <lang1,lang2,...> Comma-separated list of target languages
-m, --model <model_name> Copilot model to use (default: gpt-5.2)

General options:
-v, --version Show version
Expand All @@ -29,7 +32,7 @@ General options:

export async function run(args: string[]) {
const options = minimist(args, {
string: ['host', 'attr', 'dest', 'repo', 'branch'],
string: ['host', 'attr', 'dest', 'repo', 'branch', 'languages', 'model'],
boolean: ['verbose', 'version', 'help', 'open'],
alias: {
v: 'version',
Expand All @@ -39,7 +42,9 @@ export async function run(args: string[]) {
a: 'attr',
d: 'dest',
r: 'repo',
b: 'branch'
b: 'branch',
l: 'languages',
m: 'model'
}
});

Expand Down Expand Up @@ -112,6 +117,17 @@ export async function run(args: string[]) {
break;
}

case 't':
case 'translate': {
await translate({
file: parameters[0],
languages: options.languages as string,
model: options.model as string,
verbose: Boolean(options.verbose)
});
break;
}

default: {
if (command) {
console.error(`Unknown command: ${command}`);
Expand Down
2 changes: 1 addition & 1 deletion packages/cli/src/commands/build.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import fs from 'node:fs/promises';
import process from 'node:process';
import { dirname, parse } from 'node:path';
import createDebug from 'debug';
import { pathExists, readJson } from '../util.js';
import { pathExists } from '../util.js';
import { defaultWorkshopFile } from '../constants.js';
import { processFileIncludes } from '../include.js';

Expand Down
1 change: 1 addition & 0 deletions packages/cli/src/commands/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,4 @@ export * from './build.js';
export * from './serve.js';
export * from './convert.js';
export * from './link.js';
export * from './translate.js';
105 changes: 105 additions & 0 deletions packages/cli/src/commands/translate.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
import process from 'node:process';
import createDebug from 'debug';
import { CopilotClient } from '@github/copilot-sdk';
import { pathExists } from '../util.js';
import { defaultWorkshopFile } from '../constants.js';

const debug = createDebug('translate');
const translationsFolder = 'translations';

const translatePrompt = (file: string, languages: string) => `## Role
You are an expert translator for technical documents. Your task is to translate the provided workshop content into the specified target language while preserving the original formatting, code snippets, and technical terminology. Ensure that the translated content is complete, clear, accurate, and maintains the instructional tone of the original text.
## Task
1. Translate the workshop file \`${file}\` in languages: \`${languages}\`.
Copy link

Copilot AI Jan 23, 2026

Choose a reason for hiding this comment

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

Grammatical error: 'Translate the workshop file in languages' should be 'Translate the workshop file to languages' or 'Translate the workshop file into languages'.

Suggested change
1. Translate the workshop file \`${file}\` in languages: \`${languages}\`.
1. Translate the workshop file \`${file}\` into languages: \`${languages}\`.

Copilot uses AI. Check for mistakes.
2. Create one translated file per language. Each new translated file must be created as \`${translationsFolder}/<filename>.<language>.md\`.
3. Return the list of created files without any additional explanation.
## Instructions
- Preserve ALL markdown, HTML and frontmatter formatting, including headings, lists, links, and code blocks.
- Update the paths in any relative links or image references to point to the correct locations in the translated file.
- Keep technical terms and code snippets unchanged.
- Keep frontmatter metadata properties names unchanged. Only translate titles and descriptions in the frontmatter.
`;

export type TranslateOptions = {
file?: string;
languages?: string;
model?: string;
verbose?: boolean;
Copy link

Copilot AI Jan 23, 2026

Choose a reason for hiding this comment

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

The 'verbose' option is accepted in the TranslateOptions type and passed from the CLI, but it is never used in the translate function implementation. Either implement verbose logging (similar to other commands in the codebase) or remove this unused parameter.

Copilot uses AI. Check for mistakes.
};

export async function translate(options: TranslateOptions = {}): Promise<void> {
try {
options.file = options.file?.trim() || defaultWorkshopFile;
options.model = options.model?.trim() || 'gpt-5.2';
debug('Options %o', options);

const { file, model, languages } = options;
if (!languages) {
throw new Error('No target languages specified. Use the --languages option to specify target languages.');
}

if (!(await pathExists(file))) {
throw new Error(`File not found: ${file}`);
}

const startTime = Date.now();
const client = new CopilotClient();
try {
await client.start();
const session = await client.createSession({ model });
const auth = await client.getAuthStatus();
if (!auth.isAuthenticated) {
throw new Error(
'GitHub Copilot CLI is not authenticated.\nPlease run "copilot" and use "/login" to authenticate.'
);
}

debug('Connected to GitHub Copilot CLI');
console.info(`Started translation agent for file "${file}" to languages: ${languages}...`);

session.on((event) => {
if (event.type === 'assistant.message') {
debug(`Copilot: ${event.data.content}`);
}
});

const response = await session.sendAndWait(
{
prompt: translatePrompt(file, languages),
attachments: [{ type: 'file', path: file }]
},
30 * 60 * 1000 // 30 minutes timeout
);

const elapsed = (Date.now() - startTime) / 1000;
const timeStr = elapsed > 60 ? `${(elapsed / 60).toFixed(1)}m` : `${elapsed.toFixed(1)}s`;
console.info(`Translation agent task completed in ${timeStr}:`);
console.log(response?.data.content);

await session.destroy();
debug('Copilot CLI session destroyed');
} catch (error: unknown) {
const error_ = error as Error;
if (error_.message?.includes('ENOENT')) {
throw new Error(
`GitHub Copilot CLI is not installed.\nSee https://docs.github.com/copilot/how-tos/set-up/install-copilot-cli for installation instructions.`
);
}

throw error_;
} finally {
await client.stop();
debug('Copilot CLI client stopped');
}

// Temp workaround to avoid process hanging
// eslint-disable-next-line unicorn/no-process-exit
process.exit();
} catch (error: unknown) {
const error_ = error as Error;
console.error(error_.message);
process.exitCode = 1;
}
}
3 changes: 2 additions & 1 deletion packages/cli/tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,8 @@
"strict": true,
"esModuleInterop": true,
"forceConsistentCasingInFileNames": true,
"resolveJsonModule": true
"resolveJsonModule": true,
"skipLibCheck": true
},
"include": ["./src"]
}
Loading