Skip to content
Open
Show file tree
Hide file tree
Changes from 5 commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
c77b54f
feat: add init cursor command
SK8-infi Oct 30, 2025
85a1a0e
feat: add init cursor command
SK8-infi Oct 30, 2025
d59ed3d
Delete package-lock.json
SK8-infi Oct 30, 2025
2239479
Rename cursor.md to agents.md
SK8-infi Oct 30, 2025
85b681b
feat: add init cursor command
SK8-infi Oct 30, 2025
6746cec
Delete package-lock.json
SK8-infi Oct 30, 2025
89ddbff
Merge branch 'main' into main
SK8-infi Nov 8, 2025
6df021e
feat(cli): lingo.dev init cursor Command for .cursorrules Setup (#110…
SK8-infi Nov 10, 2025
890c58f
Merge branch 'main' of https://github.com/SK8-infi/lingo.dev
SK8-infi Nov 10, 2025
8d30df1
Delete agents.md
SK8-infi Nov 10, 2025
55a079a
feat(cli): lingo.dev init cursor Command for .cursorrules Setup (#110…
SK8-infi Nov 10, 2025
e864fbf
Merge branch 'main' of https://github.com/SK8-infi/lingo.dev
SK8-infi Nov 10, 2025
d657c6d
feat(cli): lingo.dev init cursor Command for .cursorrules Setup (#110…
SK8-infi Nov 10, 2025
44f9963
Merge branch 'main' into main
SK8-infi Nov 11, 2025
a80466e
feat(cli): lingo.dev init cursor Command for .cursorrules Setup (#110…
SK8-infi Nov 11, 2025
5a530f3
Merge branch 'main' of https://github.com/SK8-infi/lingo.dev
SK8-infi Nov 11, 2025
4fe3d16
Update packages/cli/src/cli/cmd/init/cursor.ts
SK8-infi Nov 12, 2025
f361eb9
Update packages/cli/src/cli/cmd/init/cursor.ts
SK8-infi Nov 12, 2025
ebe9d6e
Update packages/cli/assets/agents.md
SK8-infi Nov 12, 2025
13d14a9
Update packages/cli/src/cli/cmd/init/cursor.ts
SK8-infi Nov 12, 2025
00714d9
Update packages/cli/src/cli/cmd/init/cursor.ts
SK8-infi Nov 12, 2025
8e567eb
Merge branch 'main' into main
SK8-infi Nov 12, 2025
a068b0e
Merge branch 'main' into main
SK8-infi Nov 12, 2025
cdcaa90
Merge branch 'main' into main
SK8-infi Nov 12, 2025
6273c63
Merge branch 'main' into main
SK8-infi Nov 17, 2025
bd3f99b
Merge branch 'main' into main
SK8-infi Nov 19, 2025
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
60 changes: 30 additions & 30 deletions i18n.json
Original file line number Diff line number Diff line change
@@ -1,31 +1,31 @@
{
"version": "1.10",
"locale": {
"source": "en",
"targets": [
"ru",
"he",
"fr",
"es",
"de",
"zh-Hans",
"ko",
"ja",
"it",
"ar",
"hi",
"pt-BR",
"uk-UA",
"bn",
"fa",
"pl",
"tr"
]
},
"buckets": {
"mdx": {
"include": ["readme/[locale].md"]
}
},
"$schema": "https://lingo.dev/schema/i18n.json"
}
"version": "1.10",
"locale": {
"source": "en",
"targets": [
"ru",
"he",
"fr",
"es",
"de",
"zh-Hans",
"ko",
"ja",
"it",
"ar",
"hi",
"pt-BR",
"uk-UA",
"bn",
"fa",
"pl",
"tr"
]
},
"buckets": {
"mdx": {
"include": ["readme/[locale].md"]
}
},
"$schema": "https://lingo.dev/schema/i18n.json"
}
2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -19,9 +19,11 @@
"@commitlint/cli": "^19.8.0",
"@commitlint/config-conventional": "^19.8.0",
"@types/babel__traverse": "^7.20.7",
"@types/node": "^24.9.2",
"commitlint": "^19.7.1",
"husky": "^9.1.7",
"prettier": "^3.4.2",
"tsx": "^4.7.1",
"turbo": "^2.5.0"
},
"dependencies": {
Expand Down
1 change: 1 addition & 0 deletions packages/cli/agents.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Test template for Cursor AI rules.
265 changes: 7 additions & 258 deletions packages/cli/src/cli/cmd/init.ts
Original file line number Diff line number Diff line change
@@ -1,261 +1,10 @@
import { InteractiveCommand, InteractiveOption } from "interactive-commander";
import Ora from "ora";
import { getConfig, saveConfig } from "../utils/config";
import {
defaultConfig,
LocaleCode,
resolveLocaleCode,
bucketTypes,
} from "@lingo.dev/_spec";
import fs from "fs";
import path from "path";
import _ from "lodash";
import { checkbox, confirm, input } from "@inquirer/prompts";
import { login } from "./login";
import { getSettings, saveSettings } from "../utils/settings";
import { createAuthenticator } from "../utils/auth";
import findLocaleFiles from "../utils/find-locale-paths";
import { ensurePatterns } from "../utils/ensure-patterns";
import updateGitignore from "../utils/update-gitignore";
import initCICD from "../utils/init-ci-cd";
import open from "open";

const openUrl = (path: string) => {
const settings = getSettings(undefined);
open(`${settings.auth.webUrl}${path}`, { wait: false });
};

const throwHelpError = (option: string, value: string) => {
if (value === "help") {
openUrl("/go/call");
}
throw new Error(
`Invalid ${option}: ${value}\n\nDo you need support for ${value} ${option}? Type "help" and we will.`,
);
};
import { InteractiveCommand } from "interactive-commander";
import projectInitCmd from "./init/project";
import cursorInitCmd from "./init/cursor";
Copy link
Contributor

Choose a reason for hiding this comment

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

This should be imported in the packages/cli/src/cli/index.ts file! Basically you should be able to revert the changes in this file and import the cursor command there instead.

Copy link
Author

Choose a reason for hiding this comment

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

Hii @The-Best-Codes
I seem to be a little lost here. Won't adding it in index make it a top level command like "lingo.dev cursor". But we need it to be a subcommand of init like "lingo.dev init cursor".

Copy link
Contributor

Choose a reason for hiding this comment

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

Oh gosh you might be right on that lol. Let me make sure when I get a chance

Copy link
Contributor

Choose a reason for hiding this comment

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

@SK8-infi I was wrong about this!
The current structure of subcommands in the codebase it to give the command a directory, an index.ts with the main command and then subcommands in the directory which are imported into index.ts.

So you might do something like this:

  • Create a init/ directory in cmd/
  • Create the cursor.ts in the init/ directory
  • Create an index.ts in init/ with the current init command logic and import and register the cursor subcommand there
  • Remove init.ts

...at least, that's what I would do. You can ask a maintainer too if you want more clarification!

Copy link
Author

Choose a reason for hiding this comment

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

Seems to be a systematic approach to me too(beneficial for other agent sub commands). But would require directory changes... Maybe I should ask maintainers' views

@sumitsaurabh927 @maxprilutskiy


export default new InteractiveCommand()
.command("init")
.description("Create i18n.json configuration file for a new project")
.helpOption("-h, --help", "Show help")
.addOption(
new InteractiveOption(
"-f --force",
"Overwrite existing Lingo.dev configuration instead of aborting initialization (destructive operation)",
)
.prompt(undefined)
.default(false),
)
.addOption(
new InteractiveOption(
"-s --source <locale>",
"Primary language of your application that content will be translated from. Defaults to 'en'",
)
.argParser((value) => {
try {
resolveLocaleCode(value as LocaleCode);
} catch (e) {
throwHelpError("locale", value);
}
return value;
})
.default("en"),
)
.addOption(
new InteractiveOption(
"-t --targets <locale...>",
"Target languages to translate to. Accepts locale codes like 'es', 'fr', 'de-AT' separated by commas or spaces. Defaults to 'es'",
)
.argParser((value) => {
const values = (
value.includes(",") ? value.split(",") : value.split(" ")
) as LocaleCode[];
values.forEach((value) => {
try {
resolveLocaleCode(value);
} catch (e) {
throwHelpError("locale", value);
}
});
return values;
})
.default("es"),
)
.addOption(
new InteractiveOption(
"-b, --bucket <type>",
"File format for your translation files. Must match a supported type such as json, yaml, or android",
)
.argParser((value) => {
if (!bucketTypes.includes(value as (typeof bucketTypes)[number])) {
throwHelpError("bucket format", value);
}
return value;
})
.default("json"),
)
.addOption(
new InteractiveOption(
"-p, --paths [path...]",
"File paths containing translations when using --no-interactive mode. Specify paths with [locale] placeholder, separated by commas or spaces",
)
.argParser((value) => {
if (!value || value.length === 0) return [];
const values = value.includes(",")
? value.split(",")
: value.split(" ");

for (const p of values) {
try {
const dirPath = path.dirname(p);
const stats = fs.statSync(dirPath);
if (!stats.isDirectory()) {
throw new Error(`${dirPath} is not a directory`);
}
} catch (err) {
throw new Error(`Invalid path: ${p}`);
}
}

return values;
})
.prompt(undefined) // make non-interactive
.default([]),
)
.action(async (options) => {
const settings = getSettings(undefined);
const isInteractive = options.interactive;

const spinner = Ora().start("Initializing Lingo.dev project");

let existingConfig = await getConfig(false);
if (existingConfig && !options.force) {
spinner.fail("Lingo.dev project already initialized");
return process.exit(1);
}

const newConfig = _.cloneDeep(defaultConfig);

newConfig.locale.source = options.source;
newConfig.locale.targets = options.targets;

if (!isInteractive) {
newConfig.buckets = {
[options.bucket]: {
include: options.paths || [],
},
};
} else {
let selectedPatterns: string[] = [];
const localeFiles = findLocaleFiles(options.bucket);

if (!localeFiles) {
spinner.warn(
`Bucket type "${options.bucket}" does not supported automatic initialization. Add paths to "i18n.json" manually.`,
);
newConfig.buckets = {
[options.bucket]: {
include: options.paths || [],
},
};
} else {
const { patterns, defaultPatterns } = localeFiles;

if (patterns.length > 0) {
spinner.succeed("Found existing locale files:");

selectedPatterns = await checkbox({
message: "Select the paths to use",
choices: patterns.map((value) => ({
value,
})),
});
} else {
spinner.succeed("No existing locale files found.");
}

if (selectedPatterns.length === 0) {
const useDefault = await confirm({
message: `Use (and create) default path ${defaultPatterns.join(
", ",
)}?`,
});
if (useDefault) {
ensurePatterns(defaultPatterns, options.source);
selectedPatterns = defaultPatterns;
}
}

if (selectedPatterns.length === 0) {
const customPaths = await input({
message: "Enter paths to use",
});
selectedPatterns = customPaths.includes(",")
? customPaths.split(",")
: customPaths.split(" ");
}

newConfig.buckets = {
[options.bucket]: {
include: selectedPatterns || [],
},
};
}
}

await saveConfig(newConfig);

spinner.succeed("Lingo.dev project initialized");

if (isInteractive) {
await initCICD(spinner);

const openDocs = await confirm({
message: "Would you like to see our docs?",
});
if (openDocs) {
openUrl("/go/docs");
}
}

const authenticator = createAuthenticator({
apiKey: settings.auth.apiKey,
apiUrl: settings.auth.apiUrl,
});
const auth = await authenticator.whoami();
if (!auth) {
if (isInteractive) {
const doAuth = await confirm({
message: "It looks like you are not logged into the CLI. Login now?",
});
if (doAuth) {
const apiKey = await login(settings.auth.webUrl);
settings.auth.apiKey = apiKey;
await saveSettings(settings);

const newAuthenticator = createAuthenticator({
apiKey: settings.auth.apiKey,
apiUrl: settings.auth.apiUrl,
});
const auth = await newAuthenticator.whoami();
if (auth) {
Ora().succeed(`Authenticated as ${auth?.email}`);
} else {
Ora().fail("Authentication failed.");
}
}
} else {
Ora().warn(
"You are not logged in. Run `npx lingo.dev@latest login` to login.",
);
}
} else {
Ora().succeed(`Authenticated as ${auth.email}`);
}

updateGitignore();

if (!isInteractive) {
Ora().info("Please see https://lingo.dev/cli");
}
});
.description("Project initializer commands")
.addCommand(projectInitCmd) // main project initializer
.addCommand(cursorInitCmd) // new cursor subcommand
.helpOption("-h, --help", "Show help");
50 changes: 50 additions & 0 deletions packages/cli/src/cli/cmd/init/cursor.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import { InteractiveCommand, InteractiveOption } from "interactive-commander";
import Ora from "ora";
import fs from "fs";
import path from "path";
import { confirm } from "@inquirer/prompts";

const AGENTS_MD = path.resolve(process.cwd(), "packages/cli/agents.md");
const CURSORRULES = path.resolve(process.cwd(), ".cursorrules");

export default new InteractiveCommand()
.command("cursor")
.description("Initialize .cursorrules with i18n-specific instructions for Cursor AI.")
.addOption(
new InteractiveOption("-f, --force", "Overwrite .cursorrules without prompt.")
.default(false)
)
.action(async (options) => {
const spinner = Ora();
// Read agents.md
let template: string;
try {
template = fs.readFileSync(AGENTS_MD, "utf-8");
} catch (err) {
spinner.fail(`Template not found: ${AGENTS_MD}`);
return process.exit(1);
}
// Check for existing .cursorrules
const exists = fs.existsSync(CURSORRULES);
let shouldWrite = true;
if (exists && !options.force) {
shouldWrite = await confirm({
message: ".cursorrules already exists. Overwrite?",
});
if (!shouldWrite) {
spinner.info("Skipped: .cursorrules left unchanged.");
return;
}
}
try {
fs.writeFileSync(CURSORRULES, template);
spinner.succeed("✓ Created .cursorrules");
spinner.info(".cursorrules has been created with i18n-specific instructions for Cursor AI.");
if (!fs.existsSync(CURSORRULES)) {
spinner.fail(".cursorrules not found after write");
}
} catch (err) {
spinner.fail(`Failed to write .cursorrules: ${err}`);
process.exit(1);
}
});
Loading