Merge remote-tracking branch 'origin/main' into review/pr1147

# Conflicts:
#	crates/ide-sdk/src/apply.rs
This commit is contained in:
l0ng-ai
2026-11-04 12:09:48 +08:00
29 changed files with 13055 additions and 109 deletions
+1
View File
@@ -39,6 +39,7 @@ Visual style: dark, low-contrast greys, one cool accent
icons from `src/ui/icons.tsx`, never emoji. Real `<button>`/`<input>`; `aria-label` on icon buttons.
- User-visible text goes through the text catalog: add every new string to both
`src/platform/locales/text/en.json` and `zh.json` (`textCatalog.test.ts` enforces it).
Other locales may lack entries; their UI shows the English text until a translator adds it.
- Keep comments sparse and purposeful; match surrounding style.
- For GUI verification, prefer Tauri MCP; use Computer Use only when Tauri MCP is
unavailable or cannot cover the required checks.
+55 -10
View File
@@ -82,6 +82,58 @@ pub fn save_configs(
ide_run::save_user_configs_with_removals(&project.root, &configs, removed_detected_ids)
}
/// The project `config` runs in (a workspace member, by its working directory) and its settings.
fn settings_for(
host: &Host,
project: &crate::project::Project,
config: &RunConfig,
) -> IdeResult<(std::path::PathBuf, ide_core::settings::Settings)> {
let member = ide_workspace::workspace::Workspace::load(&project.root)?.and_then(|workspace| {
let cwd = config.cwd.as_deref()?;
workspace.projects.into_iter().filter(|root| cwd.starts_with(root)).max_by_key(|root| root.components().count())
});
Ok(match member {
Some(root) => {
let settings = ide_workspace::SettingsStore::load(&host.config_dir, Some(&root)).effective();
(root, settings)
}
None => (project.root.clone(), project.settings.effective()),
})
}
/// Temporary configurations as they are kept between sessions: without what settings derived
/// (toolchain commands, Maven options, `JAVA_HOME`…), so they follow later settings changes.
pub fn without_derived(host: &Host, window: &str, configs: Vec<RunConfig>) -> IdeResult<Vec<RunConfig>> {
host.require_trust(window)?;
let project = host.project(window)?;
configs
.into_iter()
.map(|c| {
if c.kind == "gradle" {
return Ok(c);
}
let (root, settings) = settings_for(host, &project, &c)?;
Ok(ide_sdk::apply::without_derived(&c, &root, &settings))
})
.collect()
}
/// Kept temporary configurations with today's settings applied, as [`configs`] lists the others.
pub fn apply_settings(host: &Host, window: &str, configs: Vec<RunConfig>) -> IdeResult<Vec<RunConfig>> {
host.require_trust(window)?;
let project = host.project(window)?;
configs
.into_iter()
.map(|mut c| {
if c.kind != "gradle" {
let (root, settings) = settings_for(host, &project, &c)?;
ide_sdk::apply::apply_to_run_configs(std::slice::from_mut(&mut c), &root, &settings);
}
Ok(c)
})
.collect()
}
pub async fn start(host: &Host, window: &str, config: RunConfig, on_output: Emit<OutputChunk>) -> IdeResult<RunInfo> {
host.require_trust(window)?;
let project = host.project(window)?;
@@ -100,15 +152,8 @@ pub async fn start(host: &Host, window: &str, config: RunConfig, on_output: Emit
.run(&settings, &project.root, &config, project.sink.clone(), Box::new(move |c| out(c)))
.await;
}
let member = ide_workspace::workspace::Workspace::load(&project.root)?.and_then(|workspace| {
let cwd = config.cwd.as_deref()?;
workspace.projects.into_iter().filter(|root| cwd.starts_with(root)).max_by_key(|root| root.components().count())
});
let root = member.as_ref().unwrap_or(&project.root);
let settings = match &member {
Some(root) => ide_workspace::SettingsStore::load(&host.config_dir, Some(root)).effective(),
None => project.settings.effective(),
};
let (root, settings) = settings_for(host, &project, &config)?;
let root = &root;
// Configurations made in the UI name bare tools (`godot`) that may live outside PATH.
let mut config = config;
if config.kind == "maven" {
@@ -149,7 +194,7 @@ pub async fn start(host: &Host, window: &str, config: RunConfig, on_output: Emit
let encoding = settings.console.encoding;
let options = StartOptions {
encoding: Some(encoding).filter(|e| !e.trim().is_empty()),
python_paths: if member.is_some() { vec![root.clone()] } else { super::python::paths(&project) },
python_paths: if *root != project.root { vec![root.clone()] } else { super::python::paths(&project) },
};
project.require_trust()?;
let mut sink = project.sink.clone();
+2
View File
@@ -257,6 +257,8 @@ impl Dispatcher {
&c.arg::<Option<Vec<String>>>("removedDetectedIds")?.unwrap_or_default(),
)
})
.add("run_without_derived", |c| async move { ops::run::without_derived(&c.host, &c.window, c.arg("configs")?) })
.add("run_apply_settings", |c| async move { ops::run::apply_settings(&c.host, &c.window, c.arg("configs")?) })
.add("run_system_env", |_| async move { Ok(ide_run::system_env()) })
.add("run_start", |c| async move {
let on_output = c.channel("onOutput")?;
+68 -33
View File
@@ -212,17 +212,20 @@ fn generated_maven_goal(config: &RunConfig) -> bool {
config.id.starts_with("maven:goal:") && config.extras.params.is_empty()
}
fn maven_goal_prefix(config: &RunConfig) -> Option<usize> {
let (_, goals) = config.id.split_once(".xml:")?;
maven_settings_prefixes(&config.args)
.into_iter()
.find(|&start| (start + 1..=config.args.len()).any(|end| config.args[start..end].join(" ") == goals))
}
/// Applies the current Maven settings at launch, including temporary configurations
/// made before build-tool defaults stopped being stored in their command and arguments.
pub fn prepare_maven_run(config: &mut RunConfig, root: &Path, s: &Settings) {
let mut detected = config.detected;
if generated_maven_goal(config) {
// The id records the user's original goals; only the settings prefix before them is derived.
if let Some((_, goals)) = config.id.split_once(".xml:")
&& let Some(prefix) = maven_settings_prefixes(&config.args)
.into_iter()
.find(|&start| (start + 1..=config.args.len()).any(|end| config.args[start..end].join(" ") == goals))
{
if let Some(prefix) = maven_goal_prefix(config) {
config.args.drain(..prefix);
}
let name = config.command.rsplit(['/', '\\']).next().unwrap_or_default().to_ascii_lowercase();
@@ -273,26 +276,6 @@ pub fn prepare_maven_run(config: &mut RunConfig, root: &Path, s: &Settings) {
config.detected = original;
}
fn maven_launch_options(defaults: &[String], args: &[String]) -> Vec<String> {
let own_settings = args.iter().any(|a| matches!(a.as_str(), "-s" | "--settings") || a.starts_with("--settings="));
let own_repository = args.iter().any(|a| a.starts_with("-Dmaven.repo.local="))
|| args.windows(2).any(|a| a[0] == "-D" && a[1].starts_with("maven.repo.local="));
let mut out = Vec::new();
let mut defaults = defaults.iter();
while let Some(arg) = defaults.next() {
if arg == "-s" {
if let Some(value) = defaults.next()
&& !own_settings
{
out.extend([arg.clone(), value.clone()]);
}
} else if !own_repository {
out.push(arg.clone());
}
}
out
}
/// The Maven local repository from settings: the configured one, else
/// `<localRepository>` of the configured user `settings.xml`; `None` = Maven's default.
pub fn maven_local_repository(root: &Path, s: &Settings) -> Option<PathBuf> {
@@ -748,6 +731,40 @@ fn is_python_command(cmd: &str) -> bool {
const PY_RUNNERS: [&str; 3] = ["uv", "poetry", "pdm"];
/// [`maven_options`] as the groups they are passed in (`-s <file>`, `-Dmaven.repo.local=…`).
fn maven_option_groups(options: &[String]) -> Vec<&[String]> {
let mut groups = Vec::new();
let mut i = 0;
while i < options.len() {
let n = if options[i] == "-s" { 2.min(options.len() - i) } else { 1 };
groups.push(&options[i..i + n]);
i += n;
}
groups
}
/// Whether `args` set what `group` sets: a user settings file, or the local repository.
fn sets_maven_option(args: &[String], group: &[String]) -> bool {
match group.first().map(String::as_str) {
Some("-s") => args.iter().any(|a| a == "-s" || a == "--settings" || a.starts_with("--settings=")),
Some(flag) => {
let key = flag.split_once('=').map_or(flag, |(k, _)| k);
args.iter().any(|a| a.split_once('=').map_or(a.as_str(), |(k, _)| k) == key)
|| args
.windows(2)
.any(|a| a[0] == "-D" && format!("-D{}", a[1].split('=').next().unwrap_or_default()) == key)
}
None => true,
}
}
/// Settings › Maven's options ahead of the goals, each unless the configuration sets it itself.
fn add_maven_options(args: &mut Vec<String>, options: &[String]) {
let missing: Vec<String> =
maven_option_groups(options).into_iter().filter(|g| !sets_maven_option(args, g)).flatten().cloned().collect();
args.splice(0..0, missing);
}
/// Points run configurations at the selected toolchains: Python configs use the
/// project interpreter, npm scripts the chosen package manager and Node.js,
/// cargo / go configs the selected toolchain, Maven / Gradle the project JDK
@@ -884,16 +901,15 @@ pub fn apply_to_run_configs(configs: &mut [RunConfig], root: &Path, s: &Settings
if let Some(jdk) = &jdk {
c.env.entry("JAVA_HOME".into()).or_insert_with(|| jdk.home.to_string_lossy().into_owned());
}
if c.kind == "maven"
&& (c.detected
|| generated_maven_goal(c)
|| matches!(c.command.as_str(), "mvn" | "mvn.cmd" | "mvn.bat"))
{
if let Some(mvn) = &mvn {
if c.kind == "maven" {
if let Some(mvn) = &mvn
&& (c.detected
|| generated_maven_goal(c)
|| matches!(c.command.as_str(), "mvn" | "mvn.cmd" | "mvn.bat"))
{
c.command = mvn.to_string_lossy().into_owned();
}
let options = maven_launch_options(&mvn_options, &c.args);
c.args.splice(0..0, options);
add_maven_options(&mut c.args, &mvn_options);
}
}
_ => {}
@@ -936,6 +952,25 @@ pub fn without_derived(config: &RunConfig, root: &Path, s: &Settings) -> RunConf
{
out.env.remove(&key);
}
// Settings › Maven's options, which applying puts back ahead of the goals.
if out.kind == "maven" {
let options = maven_options(root, s);
let mut probe = out.clone();
let mut prefix = generated_maven_goal(&out).then(|| maven_goal_prefix(&out)).flatten();
for group in maven_option_groups(&options) {
if let Some(at) = probe.args.windows(group.len()).position(|w| w == group)
&& prefix.is_none_or(|limit| at + group.len() <= limit)
{
probe.args.drain(at..at + group.len());
if let Some(limit) = &mut prefix {
*limit -= group.len();
}
}
}
if applied(&probe).args == out.args {
out.args = probe.args;
}
}
let path = Path::new(&config.command);
if path.is_absolute()
&& let Some(name) = path.file_name().map(|n| n.to_string_lossy().into_owned())
+47 -3
View File
@@ -342,11 +342,11 @@ fn maven_launch_refreshes_legacy_defaults_but_keeps_user_overrides() {
assert_eq!(config.args, args);
}
let mut explicit = run("maven", "/custom/mvn", &["-s", "own-settings.xml", "test"]);
let mut explicit = run("maven", "/custom/mvn", &["-s", "own-settings.xml", "-Dmaven.repo.local=own-repo", "test"]);
explicit.detected = false;
prepare_maven_run(&mut explicit, tmp.path(), &s);
assert_eq!(explicit.command, "/custom/mvn");
assert_eq!(explicit.args, ["-s", "own-settings.xml", "test"]);
assert_eq!(explicit.args, ["-s", "own-settings.xml", "-Dmaven.repo.local=own-repo", "test"]);
// A gutter configuration derived from a user-edited default must keep that user's choices.
let mut saved = explicit.clone();
@@ -356,7 +356,7 @@ fn maven_launch_refreshes_legacy_defaults_but_keeps_user_overrides() {
explicit.detected = true;
prepare_maven_run(&mut explicit, tmp.path(), &s);
assert_eq!(explicit.command, "/custom/mvn");
assert_eq!(explicit.args, ["-s", "own-settings.xml", "test"]);
assert_eq!(explicit.args, ["-s", "own-settings.xml", "-Dmaven.repo.local=own-repo", "test"]);
}
#[test]
@@ -378,6 +378,24 @@ fn maven_goal_keeps_explicit_settings_and_nested_wrapper() {
assert_eq!(config.args, ["-s", "own-settings.xml", "test"]);
}
#[test]
fn persisting_a_maven_goal_keeps_explicit_options_equal_to_the_defaults() {
let tmp = tempfile::tempdir().unwrap();
let mut s = Settings::default();
s.languages.java.maven.user_settings = tmp.path().join("settings.xml").to_string_lossy().into_owned();
s.languages.java.maven.local_repository = tmp.path().join("repo").to_string_lossy().into_owned();
let options = maven_options(tmp.path(), &s);
for goals in [vec![options[2].clone(), "test".into()], [options.clone(), vec!["test".into()]].concat()] {
let mut config = run("maven", "mvn", &[]);
config.id = format!("maven:goal:pom.xml:{}", goals.join(" "));
config.detected = false;
config.args = goals.clone();
apply_to_run_configs(std::slice::from_mut(&mut config), tmp.path(), &s);
let kept = without_derived(&config, tmp.path(), &s);
assert_eq!(kept.args, goals);
}
}
#[test]
fn language_server_settings_disable_add_and_override() {
use crate::apply::{language_servers, server_launch_overrides, server_protocol_overrides, with_custom_servers};
@@ -1357,6 +1375,8 @@ fn saving_a_loaded_run_configuration_drops_what_settings_derived() {
s.languages.java.jdk = root.join("jdk17").to_string_lossy().into_owned();
touch("maven/bin/mvn");
s.languages.java.maven.home = root.join("maven").to_string_lossy().into_owned();
s.languages.java.maven.user_settings = "/m2/settings.xml".into();
s.languages.java.maven.local_repository = "/m2/repo".into();
s.languages.ruby.interpreter = touch("ruby/bin/ruby");
touch("ruby/bin/bundle");
s.tools.insert("godot".into(), ide_core::settings::ToolSettings { path: touch("Godot/godot") });
@@ -1410,4 +1430,28 @@ fn saving_a_loaded_run_configuration_drops_what_settings_derived() {
let ruby2 = root.join("ruby2/bin");
assert_eq!(next.command, ruby2.join("ruby").to_string_lossy());
assert!(next.env["PATH"].starts_with(&*ruby2.to_string_lossy()), "{}", next.env["PATH"]);
// A Maven configuration (a detected one narrowed to a test, a tool window goal) follows a
// later Maven home, settings file, local repository and JDK; options it sets itself stay.
for detected in [true, false] {
let mut goal = saved("maven", "mvn", &["test", "-Dtest=AppTest"], &[]);
goal.detected = detected;
let mut own = saved("maven", "mvn", &["-s", "/mine.xml", "test"], &[]);
own.detected = detected;
let mut loaded = [goal, own];
apply_to_run_configs(&mut loaded, root, &s);
let mut next = loaded.map(|c| without_derived(&c, root, &s));
touch("maven2/bin/mvn");
fake_jdk(&root.join("jdk21"));
let mut s2 = s.clone();
s2.languages.java.maven.home = root.join("maven2").to_string_lossy().into_owned();
s2.languages.java.maven.user_settings = "/m3/settings.xml".into();
s2.languages.java.maven.local_repository = "/m3/repo".into();
s2.languages.java.jdk = root.join("jdk21").to_string_lossy().into_owned();
apply_to_run_configs(&mut next, root, &s2);
assert_eq!(next[0].command, root.join("maven2/bin/mvn").to_string_lossy());
assert_eq!(next[0].args, ["-s", "/m3/settings.xml", "-Dmaven.repo.local=/m3/repo", "test", "-Dtest=AppTest"]);
assert!(next[0].env["JAVA_HOME"].ends_with("jdk21"), "{}", next[0].env["JAVA_HOME"]);
assert_eq!(next[1].args, ["-Dmaven.repo.local=/m3/repo", "-s", "/mine.xml", "test"]);
}
}
+2
View File
@@ -316,6 +316,8 @@ pub fn builder() -> tauri_specta::Builder<tauri::Wry> {
run::run_configs,
run::run_dotnet_resolve,
run::run_save_configs,
run::run_without_derived,
run::run_apply_settings,
run::run_templates,
run::run_save_templates,
run::run_import_sources,
+28
View File
@@ -64,6 +64,34 @@ pub fn run_save_configs(
ide_host::ops::run::save_configs(&state.host, window.label(), &configs, &removed_detected_ids.unwrap_or_default())
}
/// Temporary configurations as they are kept between sessions: without what settings derived.
#[tauri::command]
#[specta::specta]
pub async fn run_without_derived(
window: Window,
state: State<'_, AppState>,
configs: Vec<RunConfig>,
) -> IdeResult<Vec<RunConfig>> {
let (host, label) = (state.host.clone(), window.label().to_owned());
tauri::async_runtime::spawn_blocking(move || ide_host::ops::run::without_derived(&host, &label, configs))
.await
.map_err(|e| ide_core::IdeError::internal(e.to_string()))?
}
/// Kept temporary configurations with the current settings applied.
#[tauri::command]
#[specta::specta]
pub async fn run_apply_settings(
window: Window,
state: State<'_, AppState>,
configs: Vec<RunConfig>,
) -> IdeResult<Vec<RunConfig>> {
let (host, label) = (state.host.clone(), window.label().to_owned());
tauri::async_runtime::spawn_blocking(move || ide_host::ops::run::apply_settings(&host, &label, configs))
.await
.map_err(|e| ide_core::IdeError::internal(e.to_string()))?
}
#[tauri::command]
#[specta::specta]
pub async fn run_start(
+4
View File
@@ -587,6 +587,10 @@ export const commands = {
runConfigs: () => typedError<RunConfig_Serialize[], IdeError>(__TAURI_INVOKE("run_configs")),
runDotnetResolve: (config: RunConfig_Deserialize, debug: boolean) => typedError<RunConfig_Serialize, IdeError>(__TAURI_INVOKE("run_dotnet_resolve", { config, debug })),
runSaveConfigs: (configs: RunConfig_Deserialize[], removedDetectedIds: string[] | null) => typedError<null, IdeError>(__TAURI_INVOKE("run_save_configs", { configs, removedDetectedIds })),
/** Temporary configurations as they are kept between sessions: without what settings derived. */
runWithoutDerived: (configs: RunConfig_Deserialize[]) => typedError<RunConfig_Serialize[], IdeError>(__TAURI_INVOKE("run_without_derived", { configs })),
/** Kept temporary configurations with the current settings applied. */
runApplySettings: (configs: RunConfig_Deserialize[]) => typedError<RunConfig_Serialize[], IdeError>(__TAURI_INVOKE("run_apply_settings", { configs })),
/** Configuration templates: per-type defaults applied to new configurations. */
runTemplates: () => typedError<RunConfig_Serialize[], IdeError>(__TAURI_INVOKE("run_templates")),
runSaveTemplates: (templates: RunConfig_Deserialize[]) => typedError<null, IdeError>(__TAURI_INVOKE("run_save_templates", { templates })),
+15 -6
View File
@@ -23,9 +23,12 @@ export const useGhBusy = create<{ busy: { title: string; line: string; request:
busy: null,
}));
/** Runs a cancellable, streaming gh / git operation; null when it failed, was cancelled or another one runs. */
/**
* Runs a cancellable, streaming gh / git operation; null when it failed, was cancelled or another one runs.
* `title` shows in the banner while it runs, `failure` titles the error notification.
*/
export async function withBusy<T>(
title: string,
{ title, failure }: { title: string; failure: string },
run: (request: RequestId, out: Channel<string>) => Promise<T>,
): Promise<T | null> {
if (useGhBusy.getState().busy) {
@@ -48,7 +51,7 @@ export async function withBusy<T>(
notifications().notify({
group: "vcs",
level: "error",
title: `${title.replace(/…$/, "")} failed`,
title: failure,
body: errorMessage(e),
key: "github-op",
});
@@ -66,7 +69,8 @@ export function cancelBusy() {
/** "Checkout Branch": fetches and checks out the pull request's branch (gh's local name). */
export async function checkoutPullRequest(number: number): Promise<boolean> {
// The command resolves with nothing (`null`), which `withBusy` reports as a failure.
const done = await withBusy(`Checking out #${number}…`, async (r, out) => {
const op = { title: tx`Checking out #${number}…`, failure: tx`Couldn't check out #${number}` };
const done = await withBusy(op, async (r, out) => {
await call(commands.ghPrCheckout(connectedRoot(), r, number, out));
return true;
});
@@ -89,7 +93,8 @@ export async function prepareDiff(pr: GHPullRequest): Promise<PrDiffRevisions |
if (hit) return hit;
const { coordinates, root } = requireConnection();
const repo = nameWithOwner(coordinates);
const revs = await withBusy(`Fetching #${pr.number}…`, (r, out) =>
const op = { title: tx`Fetching #${pr.number}…`, failure: tx`Couldn't fetch #${pr.number}` };
const revs = await withBusy(op, (r, out) =>
call(commands.ghPrPrepareDiff(root, r, pr.number, pr.headRefOid, pr.baseRefOid, pr.baseRefName, repo, out)),
);
if (revs) diffCache.set(key, revs);
@@ -185,7 +190,11 @@ export async function openInWorktree(number: number): Promise<void> {
const revs = await prepareDiff(pr);
if (!revs) return;
const path = `${String(revs.root).replace(/[\\/]+$/, "")}_PR_${number}`;
const added = await withBusy(`Creating worktree for #${number}…`, async () => {
const op = {
title: tx`Creating worktree for #${number}…`,
failure: tx`Couldn't create the worktree for #${number}`,
};
const added = await withBusy(op, async () => {
await call(commands.vcsAddWorktree(revs.root, path, pr.headRefOid, null));
return true;
});
@@ -31,8 +31,10 @@ export async function applySuggestion(
suggestion: readonly string[],
commitMessage?: string,
): Promise<boolean> {
const title = commitMessage ? tx("Committing suggested change") : tx("Applying suggested change");
const done = await withBusy(title, async () => {
const op = commitMessage
? { title: tx("Committing suggested change"), failure: tx("Couldn't commit the suggested change") }
: { title: tx("Applying suggested change"), failure: tx("Couldn't apply the suggested change") };
const done = await withBusy(op, async () => {
const root = await repositoryRoot();
const abs = `${root}/${target.path}`;
const text = await call(commands.fsReadText(abs));
@@ -61,7 +63,10 @@ export async function applySuggestion(
return true;
});
if (!done) return false;
if (thread.viewerCanResolve) await withBusy(tx("Resolving thread"), () => resolveThread(number, thread.id));
if (thread.viewerCanResolve)
await withBusy({ title: tx("Resolving thread"), failure: tx("Couldn't resolve the thread") }, () =>
resolveThread(number, thread.id),
);
return true;
}
+8 -7
View File
@@ -10,7 +10,8 @@ import { fullDateTime, prettyDate } from "./prettyDate";
describe("prettyDate", () => {
const o = { ...DEFAULT_DATE_FORMAT, overrideSystem: true, pattern: "dd.MM.yyyy", use24Hour: true };
const now = new Date(2026, 9, 31, 9, 2).getTime();
// Between May and June, so that no date below crosses a DST change in any time zone that has one.
const now = new Date(2026, 5, 30, 9, 2).getTime();
it("formats relative dates like GitHub", () => {
expect(prettyDate(now - 20_000, now, o)).toBe("just now");
@@ -18,16 +19,16 @@ describe("prettyDate", () => {
expect(prettyDate(now - 60_000, now, o)).toBe("1 minute ago");
expect(prettyDate(now - 42 * 60_000, now, o)).toBe("42 minutes ago");
expect(prettyDate(now - 3_600_000, now, o)).toBe("1 hour ago");
expect(prettyDate(new Date(2026, 9, 31, 0, 27).getTime(), now, o)).toBe("8 hours ago");
expect(prettyDate(new Date(2026, 9, 30, 8, 0).getTime(), now, o)).toBe("yesterday");
expect(prettyDate(new Date(2026, 9, 21, 9, 5).getTime(), now, o)).toBe("9 days ago");
expect(prettyDate(new Date(2026, 8, 2, 9, 5).getTime(), now, o)).toBe("on 02.09.2026");
expect(prettyDate(new Date(2026, 5, 30, 0, 27).getTime(), now, o)).toBe("8 hours ago");
expect(prettyDate(new Date(2026, 5, 29, 8, 0).getTime(), now, o)).toBe("yesterday");
expect(prettyDate(new Date(2026, 5, 20, 9, 5).getTime(), now, o)).toBe("9 days ago");
expect(prettyDate(new Date(2026, 4, 2, 9, 5).getTime(), now, o)).toBe("on 02.05.2026");
expect(prettyDate("not a date", now, o)).toBe("");
});
it("shows the plain date and time with pretty formatting off", () => {
expect(prettyDate(now - 20_000, now, { ...o, pretty: false })).toBe("31.10.2026 09:01");
expect(fullDateTime(new Date(2026, 9, 31, 0, 27).getTime(), o)).toBe("31.10.2026 00:27");
expect(prettyDate(now - 20_000, now, { ...o, pretty: false })).toBe("30.06.2026 09:01");
expect(fullDateTime(new Date(2026, 5, 30, 0, 27).getTime(), o)).toBe("30.06.2026 00:27");
});
});
+1 -1
View File
@@ -391,7 +391,7 @@ describe("temporary configurations", () => {
expect(s.temporary).not.toContain(apps[0]!.id);
expect(s.selectedId).toBe(apps[apps.length - 1]!.id);
expect(s.configs.filter((c) => c.kind === "application")).toHaveLength(MAX_TEMPORARY);
expect(JSON.parse(localStorage.getItem("openidea.run.temporary:/p")!)).toHaveLength(MAX_TEMPORARY);
expect(JSON.parse(localStorage.getItem("openidea.run.temporary.v2:/p")!)).toHaveLength(MAX_TEMPORARY);
// Reusing an existing temporary moves it last instead of duplicating it.
adoptTemporary(apps[3]!);
expect(useRun.getState().temporary.at(-1)).toBe(apps[3]!.id);
+19 -6
View File
@@ -198,7 +198,10 @@ export async function loadConfigs(): Promise<void> {
const saved = await call(commands.runConfigs());
if (useProject.getState().project?.root !== root) return;
// Temporary configs survive reloads (and restarts) until saved or evicted; a saved one wins.
const temps = readTemporary(root).filter((t) => !saved.some((c) => c.id === t.id));
// They are kept without what settings derived: today's toolchains apply to them, as to the others.
const kept = readTemporary(root).filter((t) => !saved.some((c) => c.id === t.id));
const temps = kept.length ? await call(commands.runApplySettings(kept)).catch(() => kept) : kept;
if (useProject.getState().project?.root !== root) return;
const configs = [...saved, ...temps];
const stored = useRun.getState().selectedId ?? readSelected(root);
const selectedId =
@@ -312,7 +315,8 @@ export function selectedConfig(): RunConfig | undefined {
// ---------------------------------------------------------------------------
// Temporary configurations (IDEA: created by gutter / context runs, at most 5)
const TEMPORARY_KEY = "openidea.run.temporary:";
// v2: kept without settings-derived values (earlier entries froze the toolchain they were made with).
const TEMPORARY_KEY = "openidea.run.temporary.v2:";
export const MAX_TEMPORARY = 5;
function readTemporary(root: string): RunConfig[] {
@@ -335,14 +339,23 @@ function writeTemporary(root: string, configs: RunConfig[]) {
}
}
let persisting = 0;
function persistTemporary() {
const root = useProject.getState().project?.root;
if (!root) return;
const { configs, temporary } = useRun.getState();
writeTemporary(
root,
temporary.map((id) => configs.find((c) => c.id === id)).filter((c): c is RunConfig => !!c),
);
const list = temporary.map((id) => configs.find((c) => c.id === id)).filter((c): c is RunConfig => !!c);
writeTemporary(root, list);
if (!list.length || !isTauri()) return;
// Then rewritten without what settings derived (the project's Maven, JDK, interpreter…), so a
// later settings change reaches them as it does detected and saved configurations.
const generation = ++persisting;
void call(commands.runWithoutDerived(list))
.then((kept) => {
if (generation === persisting && useProject.getState().project?.root === root) writeTemporary(root, kept);
})
.catch(() => {});
}
/** IDEA's `isConfigurationFromContext` for an Application: the same main class of the same module. */
@@ -0,0 +1,80 @@
import { afterEach, beforeEach, expect, it, vi } from "vitest";
const ipc = vi.hoisted(() => ({
runConfigs: vi.fn(),
runWithoutDerived: vi.fn(),
runApplySettings: vi.fn(),
}));
vi.mock("@/platform/ipc", async (importOriginal) => ({
...(await importOriginal<typeof import("@/platform/ipc")>()),
isTauri: () => true,
commands: ipc,
}));
import type { RunConfig } from "@/platform/ipc";
import { useProject } from "@/platform/project";
import { adoptTemporary, loadConfigs, resetRuns, useRun } from "./store";
const KEY = "openidea.run.temporary.v2:/p";
const ok = <T>(data: T) => Promise.resolve({ status: "ok" as const, data });
/** A Maven test run made from the tool window, with the settings of the day baked in. */
const goal: RunConfig = {
id: "maven:goal:pom.xml:test",
name: "app [test]",
kind: "maven",
command: "/old/maven/bin/mvn",
args: ["-s", "/old/settings.xml", "test"],
cwd: null,
env: { JAVA_HOME: "/old/jdk" },
detected: false,
isTest: true,
debugAdapter: null,
};
let mavenHome = "/new/maven";
beforeEach(() => {
resetRuns();
localStorage.clear();
useProject.setState({ project: { root: "/p" } as never });
ipc.runConfigs.mockImplementation(() => ok([]));
// The backend drops what settings derived, and applies the current settings again.
ipc.runWithoutDerived.mockImplementation((configs: RunConfig[]) =>
ok(
configs.map((c) => ({
...c,
command: "mvn",
args: c.args.filter((a, i, all) => a !== "-s" && all[i - 1] !== "-s"),
env: {},
})),
),
);
ipc.runApplySettings.mockImplementation((configs: RunConfig[]) =>
ok(
configs.map((c) => ({
...c,
command: `${mavenHome}/bin/mvn`,
args: ["-s", `${mavenHome}/conf/settings.xml`, ...c.args],
env: { JAVA_HOME: "/new/jdk" },
})),
),
);
});
afterEach(() => useProject.setState({ project: null }));
it("keeps temporary configurations without settings-derived values and applies today's settings on load", async () => {
adoptTemporary(goal);
await vi.waitFor(() => expect(JSON.parse(localStorage.getItem(KEY)!)[0].command).toBe("mvn"));
const kept = JSON.parse(localStorage.getItem(KEY)!)[0] as RunConfig;
expect(kept.args).toEqual(["test"]);
expect(kept.env).toEqual({});
// Settings › Maven changed: the reload after it brings the temporary configuration along.
mavenHome = "/moved/maven";
await loadConfigs();
const loaded = useRun.getState().configs.find((c) => c.id === goal.id)!;
expect(useRun.getState().temporary).toEqual([goal.id]);
expect(loaded.command).toBe("/moved/maven/bin/mvn");
expect(loaded.args).toEqual(["-s", "/moved/maven/conf/settings.xml", "test"]);
expect(loaded.env).toEqual({ JAVA_HOME: "/new/jdk" });
});
@@ -1,6 +1,7 @@
import { beforeEach, describe, expect, it, vi } from "vitest";
import type { Settings } from "@/platform/ipc";
import { DEFAULT_SSH_CONFIG } from "@/platform/remoteSettings";
import { SUPPORTED_LOCALES } from "@/platform/i18n";
import { useProject } from "@/platform/project";
import { DEFAULT_SETTINGS, getAt, setAt, validateSettings } from "../model/schema";
import {
@@ -267,7 +268,7 @@ describe("validateSettings", () => {
expect(validateSettings(undefined)).toEqual(DEFAULT_SETTINGS);
expect(validateSettings("garbage")).toEqual(DEFAULT_SETTINGS);
const v = validateSettings({
appearance: { theme: "neon", accent: "red", uiFontSize: 400, locale: "fr" },
appearance: { theme: "neon", accent: "red", uiFontSize: 400, locale: "xx" },
editor: { fontSize: "big", tabSize: 2.6, lineHeight: -1, softWrap: "yes", fontFamily: " " },
files: { exclude: ["**/a", 3, "", null] },
keymap: 12,
@@ -291,6 +292,8 @@ describe("validateSettings", () => {
it("keeps valid values, including dark-hc", () => {
const v = validateSettings({ appearance: { theme: "dark-hc", accent: "#C9A080", locale: "zh" } });
expect(v.appearance).toMatchObject({ theme: "dark-hc", accent: "#c9a080", locale: "zh" });
for (const locale of SUPPORTED_LOCALES)
expect(validateSettings({ appearance: { locale } }).appearance.locale).toBe(locale);
});
it("reads and writes dotted keys immutably", () => {
@@ -54,6 +54,9 @@
.row:not(.stacked) > .label::after {
content: ":";
}
:lang(fr) .row:not(.stacked) > .label::after {
content: "\00a0:";
}
.control {
display: flex;
align-items: center;
@@ -31,6 +31,9 @@
.interpreterLabel::after {
content: ":";
}
:lang(fr) .interpreterLabel::after {
content: "\00a0:";
}
.path {
color: var(--fg-muted);
font-family: var(--font-mono);
+3 -4
View File
@@ -5,6 +5,7 @@
* back to the default (mirrors `ide-core::settings` defaults).
*/
import { validateCMake } from "@/platform/cmakeSettings";
import { SUPPORTED_LOCALES, type Locale } from "@/platform/i18n";
import { DEFAULT_IGNORED_NAMES } from "@/platform/ignoredNames";
import { validateFileTypeOverrides, validateFileTypes, type FileTypeAssociations } from "@/platform/fileTypes";
import type { AgentSettings, LanguageServerSettings, Settings } from "@/platform/ipc";
@@ -35,7 +36,6 @@ import { isThemeChoiceId, type ThemeChoiceId } from "@/platform/themes";
/** A built-in theme, a plugin's (`plugin:<id>/<theme>`) or the legacy `"system"`. */
export type ThemeId = ThemeChoiceId | "system";
export type LocaleId = "en" | "zh";
/** A user keymap (Keymap › Duplicate…): its own overrides on top of a bundled preset. */
export interface CustomKeymapSettings {
@@ -46,7 +46,7 @@ export interface CustomKeymapSettings {
export interface ValidSettings extends WorkflowSettings {
general: { reopenLastProject: boolean; confirmExit: boolean } & GeneralExtras;
appearance: { theme: ThemeId; accent: string; uiFontSize: number; locale: LocaleId } & AppearanceExtras;
appearance: { theme: ThemeId; accent: string; uiFontSize: number; locale: Locale } & AppearanceExtras;
editor: {
fontFamily: string;
fontSize: number;
@@ -175,7 +175,6 @@ export const LIMITS = {
} as const;
const theme = (v: unknown, d: ThemeId): ThemeId => (v === "system" || isThemeChoiceId(v) ? v : d);
const LOCALES: readonly LocaleId[] = ["en", "zh"];
export const HEX_COLOR = /^#(?:[0-9a-f]{3}|[0-9a-f]{6})$/i;
type Obj = Record<string, unknown>;
@@ -224,7 +223,7 @@ export function validateSettings(raw: Settings | unknown): ValidSettings {
theme: theme(a.theme, d.appearance.theme),
accent: typeof a.accent === "string" && HEX_COLOR.test(a.accent) ? a.accent.toLowerCase() : d.appearance.accent,
uiFontSize: num(a.uiFontSize, d.appearance.uiFontSize, LIMITS.uiFontSize, true),
locale: oneOf(a.locale, LOCALES, d.appearance.locale),
locale: oneOf(a.locale, SUPPORTED_LOCALES, d.appearance.locale),
},
editor: {
fontFamily: str(e.fontFamily, d.editor.fontFamily),
+3 -6
View File
@@ -31,7 +31,7 @@ import { ACCENT_PRESETS, LIMITS, type ThemeId } from "../model/schema";
import type { PageSpec } from "./spec";
import { HTTP_PROXY_PAGE } from "./httpProxy";
import styles from "./Behavior.module.css";
import { tx, useT } from "@/platform/i18n";
import { LOCALES, SUPPORTED_LOCALES, tx, useT } from "@/platform/i18n";
import { BUILTIN_THEMES, useContributedIconThemes } from "@/platform/themes";
import { originDetail, themeSelectOptions, useThemeChoices } from "@/workbench/theme";
@@ -717,7 +717,7 @@ export const BEHAVIOR_PAGES: PageSpec[] = [
id: "general.language",
title: "Language and Region",
order: 0,
keywords: ["locale", "chinese", "中文", "translation"],
keywords: ["locale", "translation", ...SUPPORTED_LOCALES.map((locale) => LOCALES[locale].name)],
sections: [
{
options: [
@@ -726,10 +726,7 @@ export const BEHAVIOR_PAGES: PageSpec[] = [
key: "appearance.locale",
label: "Language",
hint: "Language of menus and dialogs; untranslated text stays in English.",
options: [
{ value: "en", label: "English" },
{ value: "zh", label: "简体中文 (Chinese, Simplified)" },
],
options: SUPPORTED_LOCALES.map((locale) => ({ value: locale, label: LOCALES[locale].name })),
},
],
},
+2 -1
View File
@@ -8,6 +8,7 @@
* Settings written by any window (e.g. the Settings window) reach every window
* through `settingsChanged`, which re-reads the effective settings.
*/
import { LOCALES } from "@/platform/i18n";
import { isTauri, onIdeEvent } from "@/platform/ipc";
import { applyKeymapSettings } from "@/platform/keymap";
import { loadSettings, useProject } from "@/platform/project";
@@ -20,7 +21,7 @@ export function applyRuntimeSettings(raw: unknown, root: HTMLElement = document.
applyAppearanceOptions(s, root);
root.lang = s.appearance.locale === "zh" ? "zh-CN" : "en";
root.lang = LOCALES[s.appearance.locale].tag;
}
/** Subscribes to settings changes; returns the unsubscribe function. */
@@ -55,6 +55,9 @@
.label::after {
content: ":";
}
:lang(fr) .label::after {
content: "\00a0:";
}
.control {
display: flex;
align-items: flex-start;
+84 -18
View File
@@ -1,3 +1,6 @@
import { readFileSync, readdirSync } from "node:fs";
import { dirname, join } from "node:path";
import { fileURLToPath } from "node:url";
import { afterEach, describe, expect, it } from "vitest";
import {
_resetI18nForTests,
@@ -7,14 +10,13 @@ import {
registerMessages,
resolveLocale,
setLocale,
SUPPORTED_LOCALES,
t,
tx,
txTemplate,
useI18n,
} from "./i18n";
import en from "./locales/en.json";
import zh from "./locales/zh.json";
import zhText from "./locales/text/zh.json";
afterEach(() => _resetI18nForTests());
@@ -44,6 +46,15 @@ describe("formatMessage", () => {
expect(formatMessage(m, { count: 1 }, "en")).toBe("1 one");
});
it("uses CLDR rules of the locale (French zero is singular, `many` falls back to `other`)", () => {
const m = "{count, plural, one {# fichier} other {# fichiers}}";
expect(formatMessage(m, { count: 0 }, "fr")).toBe("0 fichier");
expect(formatMessage(m, { count: 2 }, "fr")).toBe("2 fichiers");
expect(formatMessage(m, { count: 1_000_000 }, "fr")).toBe(
`${new Intl.NumberFormat("fr").format(1_000_000)} fichiers`,
);
});
it("supports select and nesting", () => {
const m = "{os, select, macos {Finder} windows {Explorer} other {Files}}";
expect(formatMessage(m, { os: "macos" })).toBe("Finder");
@@ -70,8 +81,11 @@ describe("t / locales", () => {
it("resolves locale tags", () => {
expect(resolveLocale("zh-CN")).toBe("zh");
expect(resolveLocale("zh_Hans")).toBe("zh");
expect(resolveLocale("ko-KR")).toBe("ko");
expect(resolveLocale("EN-us")).toBe("en");
expect(resolveLocale("fr")).toBe("en");
expect(resolveLocale("fr")).toBe("fr");
expect(resolveLocale("fr-CA")).toBe("fr");
expect(resolveLocale("xx")).toBe("en");
expect(resolveLocale(undefined)).toBe(resolveLocale(navigator.language));
});
@@ -93,8 +107,15 @@ describe("t / locales", () => {
expect(t("app.keymap.count", { count: 1 })).toBe("1 个快捷键");
});
it("switches to Korean", async () => {
await setLocale("ko-KR");
expect(useI18n.getState().locale).toBe("ko");
expect(t("common.cancel")).toBe("취소");
expect(tx("Cancel")).toBe("취소");
});
it("falls back to English for unsupported locales", async () => {
await setLocale("de");
await setLocale("xx");
expect(useI18n.getState().locale).toBe("en");
});
@@ -154,6 +175,16 @@ describe("t / locales", () => {
expect(tx(zh)).toBe("Run 'orders'");
});
it("loads the French catalogs", async () => {
await setLocale("fr-FR");
expect(useI18n.getState().locale).toBe("fr");
expect(t("common.cancel")).toBe("Annuler");
expect(t("app.keymap.count", { count: 0 })).toBe("0 raccourci");
expect(tx("Sync with OS")).toBe("Synchroniser avec le système");
expect(tx`Run '${"orders"}'`).toBe("Exécuter 'orders'");
expect(txTemplate("ruff is not installed")).toBe("ruff n'est pas installé");
});
it("lists keys by prefix", () => {
const keys = keysWithPrefix("menu.");
expect(keys).toContain("menu.file.save");
@@ -162,23 +193,59 @@ describe("t / locales", () => {
});
describe("catalogs", () => {
const catalogs: Record<string, Record<string, string>> = { en, zh };
type Catalog = Record<string, string>;
const LOCALES_DIR = join(dirname(fileURLToPath(import.meta.url)), "locales");
/** Catalog files of a folder as `{ locale: catalog }`, read from disk so that a new file is always seen. */
const readCatalogs = (dir: string) =>
Object.fromEntries(
readdirSync(dir)
.filter((file) => file.endsWith(".json"))
.map((file) => [file.slice(0, -5), JSON.parse(readFileSync(join(dir, file), "utf8")) as Catalog]),
);
it("translate source text into non-empty, different text", () => {
for (const [source, text] of Object.entries(zhText as Record<string, string>)) {
expect(text.trim(), source).not.toBe("");
expect(text, source).not.toBe(source);
}
const { en: _en, ...translations } = readCatalogs(LOCALES_DIR);
const texts = readCatalogs(join(LOCALES_DIR, "text"));
const catalogs: Record<string, Catalog> = { en, ...translations };
/** Must have every key; the other locales show English for keys they lack. */
const COMPLETE = ["zh"];
/** Written in another script, so a text equal to its English source was not translated. */
const OWN_SCRIPT = new Set(["zh", "ko"]);
const translated = SUPPORTED_LOCALES.filter((locale) => locale !== "en").sort();
it("exist for exactly the supported locales", () => {
expect(translated.length).toBeGreaterThan(0);
expect(Object.keys(translations).sort()).toEqual(translated);
expect(Object.keys(texts).sort()).toEqual(translated);
});
it("translate source text into non-empty text", () => {
for (const [locale, catalog] of Object.entries(texts))
for (const [source, text] of Object.entries(catalog)) expect(text.trim(), `${locale}:${source}`).not.toBe("");
});
it("translate source text into text different from English, in locales with another script", () => {
for (const [locale, catalog] of Object.entries(texts))
if (OWN_SCRIPT.has(locale))
for (const [source, text] of Object.entries(catalog)) expect(text, `${locale}:${source}`).not.toBe(source);
});
it("keep the placeholders of source-text templates", () => {
const names = (m: string) => [...m.matchAll(/\{(\w+)\}/g)].map((x) => x[1]).sort();
for (const [source, text] of Object.entries(zhText as Record<string, string>))
expect(names(text), source).toEqual(names(source));
for (const [locale, catalog] of Object.entries(texts))
for (const [source, text] of Object.entries(catalog))
expect(names(text), `${locale}:${source}`).toEqual(names(source));
});
it("have the same keys", () => {
expect(Object.keys(zh).sort()).toEqual(Object.keys(en).sort());
it.each(COMPLETE)("have the same keys as English (%s)", (locale) => {
expect(Object.keys(translations[locale]!).sort()).toEqual(Object.keys(en).sort());
});
it("have no keys English lacks", () => {
for (const [locale, catalog] of Object.entries(translations))
expect(
Object.keys(catalog).filter((key) => !Object.hasOwn(en, key)),
locale,
).toEqual([]);
});
it("contain only well-formed messages", () => {
@@ -189,9 +256,8 @@ describe("catalogs", () => {
it("use the same parameters across locales", () => {
const params = (m: string) => [...m.matchAll(/\{\s*([a-z]\w*)\s*[,}]/g)].map((x) => x[1]).sort();
for (const key of Object.keys(en))
expect(params((zh as Record<string, string>)[key] ?? ""), key).toEqual(
params((en as Record<string, string>)[key]!),
);
for (const [locale, catalog] of Object.entries(translations))
for (const [key, message] of Object.entries(catalog))
expect(params(message), `${locale}:${key}`).toEqual(params((en as Record<string, string>)[key] ?? ""));
});
});
+15 -4
View File
@@ -33,8 +33,19 @@ export type MessageParams = Record<string, string | number | boolean | null | un
export type Translate = (key: string, params?: MessageParams) => string;
export const DEFAULT_LOCALE = "en";
export const SUPPORTED_LOCALES = ["en", "zh"] as const;
export type Locale = (typeof SUPPORTED_LOCALES)[number];
/**
* The UI languages: `name` is written in the language itself, `tag` is its BCP 47
* tag for `<html lang>`. Adding one here offers it in the settings. Its catalogs are
* `locales/<locale>.json` and `locales/text/<locale>.json`; text they lack shows in English.
*/
export const LOCALES = {
en: { name: "English", tag: "en" },
zh: { name: "简体中文 (Chinese, Simplified)", tag: "zh-CN" },
ko: { name: "한국어 (Korean)", tag: "ko" },
fr: { name: "Français (French)", tag: "fr" },
} as const;
export type Locale = keyof typeof LOCALES;
export const SUPPORTED_LOCALES = Object.keys(LOCALES) as Locale[];
const loaders = import.meta.glob<Catalog>(["./locales/*.json", "!./locales/en.json"], { import: "default" });
const textLoaders = import.meta.glob<Catalog>("./locales/text/*.json", { import: "default" });
@@ -54,7 +65,7 @@ interface I18nState {
export const useI18n = create<I18nState>(() => ({ locale: DEFAULT_LOCALE, messages: en as Catalog, texts: {} }));
/** Maps settings values / BCP 47 tags to a supported locale (`zh-CN` → `zh`, `system` → browser). */
/** Maps settings values / BCP 47 tags to a supported locale (`zh-CN` → `zh`, `ko-KR` → `ko`, `system` → browser). */
export function resolveLocale(input: string | null | undefined): Locale {
let tag = (input ?? "").trim();
if (!tag || tag === "system" || tag === "auto")
@@ -106,7 +117,7 @@ export async function setLocale(input: string | null | undefined): Promise<Local
}
if (token === switchToken) {
useI18n.setState({ locale, messages: merged(locale), texts: textCatalogs.get(locale) ?? {} });
if (typeof document !== "undefined") document.documentElement.lang = locale;
if (typeof document !== "undefined") document.documentElement.lang = LOCALES[locale].tag;
}
return locale;
}
+450
View File
@@ -0,0 +1,450 @@
{
"app.about.build": "Build",
"app.about.configDir": "Configuration",
"app.about.copyInfo": "Copier les informations",
"app.about.copyright": "Copyright © 2026 les contributeurs d'OpenIDEA",
"app.about.disclaimer": "Non affilié à JetBrains.",
"app.about.license": "Distribué sous licence Apache 2.0",
"app.about.platform": "Plateforme",
"app.about.runtime": "Environnement d'exécution",
"app.about.showConfig": "Ouvrir le dossier de configuration",
"app.about.tagline": "Un IDE open source inspiré d'IntelliJ IDEA",
"app.about.thirdParty": "Inclut des icônes de la plateforme IntelliJ (Apache-2.0).",
"app.about.title": "À propos d'OpenIDEA",
"app.about.version": "Version",
"app.action.about": "À propos d'OpenIDEA",
"app.action.checkForUpdates": "Rechercher des mises à jour…",
"app.action.copy": "Copier",
"app.action.cut": "Couper",
"app.action.keymapReference": "Référence des raccourcis clavier",
"app.action.lightEditOpenProject": "Ouvrir le dossier parent comme projet",
"app.action.newWindow": "Nouvelle fenêtre",
"app.action.nextWindow": "Fenêtre de projet suivante",
"app.action.openFile": "Ouvrir un fichier…",
"app.action.openInNewWindow": "Ouvrir dans une nouvelle fenêtre…",
"app.action.openRecentProject": "Ouvrir un projet récent",
"app.action.paste": "Coller",
"app.action.previousWindow": "Fenêtre de projet précédente",
"app.action.quit": "{os, select, macos {Quitter OpenIDEA} other {Quitter}}",
"app.action.redo": "Rétablir",
"app.action.selectAll": "Tout sélectionner",
"app.action.showLog": "{os, select, macos {Afficher le journal dans le Finder} windows {Afficher le journal dans l'Explorateur} other {Afficher le journal dans Fichiers}}",
"app.action.toggleFullScreen": "Activer/désactiver le plein écran",
"app.action.undo": "Annuler",
"app.category.edit": "Édition",
"app.category.file": "Fichier",
"app.category.help": "Aide",
"app.category.view": "Affichage",
"app.category.window": "Fenêtre",
"app.exit.body": "Voulez-vous vraiment quitter OpenIDEA ?",
"app.exit.confirm": "Quitter",
"app.exit.dontAsk": "Ne plus demander",
"app.exit.title": "Confirmer la fermeture",
"app.keymap.count": "{count, plural, one {# raccourci} other {# raccourcis}}",
"app.keymap.empty": "Aucun raccourci correspondant",
"app.keymap.filter": "Filtrer par action ou raccourci",
"app.keymap.other": "Autres",
"app.keymap.preset": "Raccourcis clavier : {preset}",
"app.keymap.title": "Référence des raccourcis clavier",
"app.lightEdit.openProject": "Ouvrir comme projet",
"app.lightEdit.openProjectTooltip": "Ouvrir {folder} comme projet",
"app.log.failed": "Impossible d'afficher le journal",
"app.title.default": "OpenIDEA",
"app.unsaved.body": "{count, plural, one {# fichier contient} other {# fichiers contiennent}} des modifications non enregistrées dans {project}. Enregistrer avant de fermer ?",
"app.unsaved.discard": "Ne pas enregistrer",
"app.unsaved.save": "Tout enregistrer",
"app.unsaved.saveFailed": "Impossible d'enregistrer tous les fichiers",
"app.unsaved.title": "Modifications non enregistrées",
"app.update.available": "OpenIDEA {version} est disponible",
"app.update.cancel": "Annuler",
"app.update.cancelled": "Mise à jour annulée",
"app.update.checking": "Recherche de mises à jour…",
"app.update.disabled": "Les mises à jour automatiques ne sont pas configurées",
"app.update.disabledBody": "Définissez un point de terminaison de mise à jour (OPENIDEA_UPDATE_ENDPOINT ou plugins.updater.endpoints) pour activer la recherche de mises à jour.",
"app.update.downloading": "Téléchargement d'OpenIDEA {version}",
"app.update.failed": "Échec de la recherche de mises à jour",
"app.update.install": "Installer et redémarrer",
"app.update.installFailed": "Impossible d'installer la mise à jour",
"app.update.installed": "OpenIDEA {version} est installé",
"app.update.installedBody": "Il sera utilisé après le redémarrage d'OpenIDEA.",
"app.update.installingMessage": "Installation…",
"app.update.installUncancellable": "La mise à jour est en cours d'installation ; cette étape ne peut pas être annulée",
"app.update.none": "OpenIDEA {version} est à jour",
"app.update.progress": "{done} Mo sur {total} Mo",
"app.update.progressUnknown": "{done} Mo",
"app.update.restart": "Redémarrer",
"app.update.retry": "Réessayer",
"app.window.desktopOnly": "Les fenêtres multiples nécessitent l'application de bureau",
"app.window.openFailed": "Impossible d'ouvrir {name}",
"app.window.pickFileTitle": "Ouvrir un fichier",
"app.window.pickTitle": "Ouvrir dans une nouvelle fenêtre",
"app.window.askTitle": "Ouvrir un projet",
"app.window.askBody": "Les projets peuvent s'ouvrir dans une nouvelle fenêtre, remplacer le projet actuel ou être rattachés aux projets déjà ouverts.\nComment souhaitez-vous ouvrir « {name} » ?",
"app.window.askThis": "Cette fenêtre",
"app.window.askAttach": "Rattacher",
"app.window.askNew": "Nouvelle fenêtre",
"app.window.askRemember": "Ne plus demander",
"common.cancel": "Annuler",
"common.close": "Fermer",
"common.copied": "Copié",
"common.copy": "Copier",
"menu.app": "OpenIDEA",
"menu.app.about": "À propos d'OpenIDEA",
"menu.app.checkForUpdates": "Rechercher des mises à jour…",
"menu.app.quit": "{os, select, macos {Quitter OpenIDEA} other {Quitter}}",
"menu.appearance": "Apparence",
"menu.code": "Code",
"menu.code.commentBlock": "Commenter avec un commentaire de bloc",
"menu.code.commentLine": "Commenter avec un commentaire de ligne",
"menu.code.completion": "Basique",
"menu.code.deleteLine": "Supprimer la ligne",
"menu.code.duplicateLine": "Dupliquer la ligne ou la sélection",
"menu.code.extendSelection": "Étendre la sélection",
"menu.code.foldAll": "Tout réduire",
"menu.code.foldToggle": "Développer/réduire",
"menu.code.generate": "Générer…",
"menu.code.pasteAsPlainText": "Coller en texte brut",
"menu.editor.copyReference": "Copier la référence",
"menu.code.cyclicExpandWord": "Compléter le mot par cycle",
"menu.code.cyclicExpandWordBackward": "Compléter le mot par cycle (arrière)",
"menu.code.expressionTypeInfo": "Type de l'expression",
"menu.code.overrideMethods": "Redéfinir des méthodes…",
"menu.code.implementMethods": "Implémenter des méthodes…",
"menu.code.insertLiveTemplate": "Insérer un modèle dynamique…",
"menu.code.moveLineDown": "Déplacer la ligne vers le bas",
"menu.code.moveLineUp": "Déplacer la ligne vers le haut",
"menu.code.optimizeImports": "Optimiser les imports",
"menu.code.parameterInfo": "Informations sur les paramètres",
"menu.code.quickDocumentation": "Documentation rapide",
"menu.code.reformat": "Reformater le code",
"menu.code.showIntentions": "Afficher les actions contextuelles",
"menu.code.shrinkSelection": "Réduire la sélection",
"menu.code.surroundWith": "Entourer avec…",
"menu.code.unwrap": "Déballer/supprimer…",
"menu.code.unfoldAll": "Tout développer",
"menu.debug.evaluate": "Évaluer l'expression…",
"menu.debug.pause": "Mettre le programme en pause",
"menu.debug.resume": "Reprendre le programme",
"menu.debug.runToCursor": "Exécuter jusqu'au curseur",
"menu.debug.stepInto": "Pas à pas détaillé",
"menu.debug.stepOut": "Pas à pas sortant",
"menu.debug.stepOver": "Pas à pas principal",
"menu.debug.toggleBreakpoint": "Activer/désactiver le point d'arrêt de ligne",
"menu.debug.viewBreakpoints": "Afficher les points d'arrêt…",
"menu.debug.attachToProcess": "Attacher à un processus…",
"menu.debug.forceStepOver": "Forcer le pas à pas principal",
"menu.debug.forceStepInto": "Forcer le pas à pas détaillé",
"menu.debug.smartStepInto": "Pas à pas détaillé intelligent",
"menu.debug.forceRunToCursor": "Forcer l'exécution jusqu'au curseur",
"menu.debug.showExecutionPoint": "Afficher le point d'exécution",
"menu.debug.resetFrame": "Réinitialiser le cadre de pile",
"menu.debug.toggleTemporaryBreakpoint": "Activer/désactiver le point d'arrêt de ligne temporaire",
"menu.debug.toggleMethodBreakpoint": "Activer/désactiver le point d'arrêt de méthode",
"menu.debug.toggleFieldWatchpoint": "Activer/désactiver le point de surveillance de champ",
"menu.edit": "Édition",
"menu.edit.copy": "Copier",
"menu.edit.cut": "Couper",
"menu.edit.paste": "Coller",
"menu.code.pasteFromHistory": "Coller depuis l'historique…",
"menu.edit.redo": "Rétablir",
"menu.edit.selectAll": "Tout sélectionner",
"menu.edit.undo": "Annuler",
"menu.editor.copyPath": "Copier le chemin",
"menu.code.sortLines": "Trier les lignes",
"menu.code.reverseLines": "Inverser les lignes",
"menu.editor.toggleColumnSelection": "Mode de sélection en colonne",
"menu.editor.splitDown": "Scinder vers le bas",
"menu.editor.splitRight": "Scinder vers la droite",
"menu.editor.toggleZen": "{distractionFree, select, on {Quitter le mode sans distraction} other {Passer en mode sans distraction}}",
"menu.editor.unsplit": "Annuler la scission",
"menu.file": "Fichier",
"menu.fileProperties": "Propriétés du fichier",
"menu.newProjectsSetup": "Configuration des nouveaux projets",
"menu.file.newProjectsSettings": "Paramètres des nouveaux projets…",
"menu.file.toggleReadOnly": "Rendre le fichier en lecture seule / modifiable",
"menu.file.closeProject": "Fermer le projet",
"menu.remote.reloadEnvironment": "Recharger l'environnement",
"menu.file.new": "Nouveau…",
"menu.file.newScratchFile": "Nouveau fichier brouillon…",
"menu.file.openInNewWindow": "Ouvrir dans une nouvelle fenêtre…",
"menu.file.openProject": "Ouvrir…",
"menu.file.save": "Tout enregistrer",
"menu.file.settings": "Paramètres…",
"menu.find": "Rechercher",
"menu.find.inFile": "Rechercher…",
"menu.find.inPath": "Rechercher dans les fichiers…",
"menu.find.replaceInFile": "Remplacer…",
"menu.find.replaceInPath": "Remplacer dans les fichiers…",
"menu.find.showUsages": "Afficher les utilisations",
"menu.find.usages": "Rechercher les utilisations",
"menu.completion": "Complétion de code",
"menu.folding": "Pliage",
"menu.code.foldSelection": "Plier la sélection / supprimer la région",
"menu.git": "Git",
"menu.help": "Aide",
"menu.mainMenu": "Menu principal",
"window.caption.minimize": "Réduire",
"window.caption.maximize": "Agrandir",
"window.caption.restore": "Restaurer",
"window.caption.close": "Fermer",
"menu.help.keymapReference": "Référence des raccourcis clavier",
"menu.help.showLog": "{os, select, macos {Afficher le journal dans le Finder} windows {Afficher le journal dans l'Explorateur} other {Afficher le journal dans Fichiers}}",
"menu.native.bringAllToFront": "Tout ramener au premier plan",
"menu.native.copy": "Copier",
"menu.native.cut": "Couper",
"menu.native.hide": "Masquer OpenIDEA",
"menu.native.hideOthers": "Masquer les autres",
"menu.native.minimize": "Réduire",
"menu.native.paste": "Coller",
"menu.native.redo": "Rétablir",
"menu.native.selectAll": "Tout sélectionner",
"menu.native.services": "Services",
"menu.native.showAll": "Tout afficher",
"menu.native.undo": "Annuler",
"menu.native.zoom": "Zoom",
"menu.navigate": "Naviguer",
"menu.navigate.back": "Précédent",
"menu.navigate.lastEditLocation": "Dernier emplacement modifié",
"menu.navigate.nextEditLocation": "Emplacement modifié suivant",
"menu.navigate.callHierarchy": "Hiérarchie des appels",
"menu.navigate.declaration": "Déclaration ou utilisations",
"menu.navigate.fileStructure": "Structure du fichier",
"menu.navigate.findAction": "Rechercher une action…",
"menu.navigate.forward": "Suivant",
"menu.navigate.gotoClass": "Classe…",
"menu.navigate.gotoFile": "Fichier…",
"menu.navigate.gotoLine": "Ligne/colonne…",
"menu.navigate.gotoSymbol": "Symbole…",
"menu.navigate.implementation": "Implémentation(s)",
"menu.navigate.nextError": "Erreur signalée suivante",
"menu.navigate.prevError": "Erreur signalée précédente",
"menu.navigate.recentFiles": "Fichiers récents",
"menu.navigate.recentLocations": "Emplacements récents",
"menu.navigate.searchEverywhere": "Rechercher partout",
"menu.navigate.selectIn": "Sélectionner dans…",
"menu.navigate.showBookmarks": "Afficher les signets",
"menu.navigate.toggleBookmark": "Ajouter/retirer un signet",
"menu.navigate.typeDeclaration": "Déclaration du type",
"menu.navigate.typeHierarchy": "Hiérarchie des types",
"menu.navigate.superMethod": "Méthode parente",
"menu.navigate.gotoTest": "Test",
"menu.navigate.nextMethod": "Méthode suivante",
"menu.navigate.previousMethod": "Méthode précédente",
"menu.openRecent": "Ouvrir un élément récent",
"menu.recent.none": "Aucun projet récent",
"menu.refactor": "Refactoriser",
"menu.refactor.changeSignature": "Modifier la signature…",
"menu.refactor.extractConstant": "Introduire une constante…",
"menu.refactor.extractField": "Introduire un champ…",
"menu.refactor.extractMethod": "Extraire une méthode…",
"menu.refactor.extractParameter": "Introduire un paramètre…",
"menu.refactor.extractVariable": "Introduire une variable…",
"menu.refactor.inline": "Intégrer…",
"menu.refactor.move": "Déplacer…",
"menu.refactor.rename": "Renommer…",
"menu.refactor.safeDelete": "Suppression sûre…",
"menu.refactor.thisMenu": "Refactoriser ceci…",
"menu.run": "Exécuter",
"menu.run.chooseConfig": "Exécuter…",
"menu.run.debug": "Déboguer",
"menu.run.debugContext": "Déboguer la configuration du contexte",
"menu.run.coverage": "Exécuter avec couverture",
"menu.run.profile": "Profiler",
"menu.coverage.hide": "Masquer la couverture",
"menu.debuggingActions": "Actions de débogage",
"menu.debug.reloadChangedClasses": "Recharger les classes modifiées",
"menu.toolwindow.coverage": "Couverture",
"menu.toolwindow.profiler": "Profileur",
"menu.run.editConfigs": "Modifier les configurations…",
"menu.run.run": "Exécuter",
"menu.run.runContext": "Exécuter la configuration du contexte",
"menu.run.stop": "Arrêter",
"menu.toolWindows": "Fenêtres d'outils",
"menu.toolwindow.bookmarks": "Signets",
"menu.toolwindow.commit": "Commit",
"menu.toolwindow.debug": "Débogage",
"menu.toolwindow.find": "Recherche",
"menu.toolwindow.git": "Git",
"menu.toolwindow.hideAll": "Masquer toutes les fenêtres d'outils",
"menu.toolwindow.problems": "Problèmes",
"menu.toolwindow.project": "Projet",
"menu.toolwindow.run": "Exécution",
"menu.toolwindow.structure": "Structure",
"menu.toolwindow.terminal": "Terminal",
"terminal.close.title": "Le processus '{name}' est en cours d'exécution",
"terminal.close.message": "Voulez-vous mettre fin au processus '{name}' ?",
"terminal.close.terminate": "Terminer",
"menu.vcs.annotate": "Annoter avec Git Blame",
"menu.vcs.branches": "Branches…",
"menu.vcs.commit": "Commit…",
"menu.vcs.fetch": "Fetch",
"menu.vcs.localHistory": "Historique local",
"menu.vcs.newBranch": "Nouvelle branche…",
"menu.vcs.push": "Push…",
"menu.vcs.rollback": "Annuler les modifications…",
"menu.vcs.showDiff": "Afficher le diff",
"menu.vcs.showLog": "Afficher le journal Git",
"menu.vcs.stash": "Remiser les modifications…",
"menu.vcs.unstash": "Restaurer des modifications remisées…",
"menu.vcs.update": "Mettre à jour le projet…",
"menu.vcs.pull": "Pull…",
"menu.vcs.merge": "Fusionner…",
"menu.vcs.rebase": "Rebase…",
"menu.vcs.newWorktree": "Nouveau worktree…",
"menu.vcs.worktrees": "Worktrees…",
"menu.vcs.manageRemotes": "Gérer les dépôts distants…",
"menu.vcs.clone": "Cloner…",
"menu.vcs.operations": "Menu des opérations VCS…",
"menu.view": "Affichage",
"menu.view.notifications": "Notifications",
"menu.view.toggleTheme": "Basculer entre thème clair et sombre",
"menu.welcome.recentProjects": "Gérer les projets…",
"menu.window": "Fenêtre",
"menu.tools": "Outils",
"menu.http.newRequest": "Nouvelle requête HTTP",
"menu.http.showToolWindow": "Client HTTP",
"menu.db.showToolWindow": "Base de données",
"menu.db.newDataSource": "Nouvelle source de données…",
"menu.db.openConsole": "Ouvrir la console de requêtes",
"menu.vim.toggle": "Vim",
"menu.window.new": "Nouvelle fenêtre",
"menu.window.next": "Fenêtre de projet suivante",
"menu.window.previous": "Fenêtre de projet précédente",
"menu.window.toggleFullScreen": "Activer/désactiver le plein écran",
"os.linux": "Linux",
"os.macos": "macOS",
"os.web": "Navigateur",
"os.windows": "Windows",
"menu.gitPatch": "Patch",
"menu.vcs.newTag": "Nouveau tag…",
"menu.vcs.resetHead": "Réinitialiser HEAD…",
"menu.vcs.shelve": "Mettre les modifications de côté…",
"menu.vcs.showShelf": "Afficher les modifications de côté",
"menu.vcs.createPatch": "Créer un patch…",
"menu.vcs.applyPatch": "Appliquer un patch…",
"menu.vcs.applyPatchFromClipboard": "Appliquer un patch depuis le presse-papiers…",
"menu.vcs.resolveConflicts": "Résoudre les conflits…",
"menu.vcs.rebase.continue": "Continuer le rebase",
"menu.vcs.rebase.abort": "Abandonner le rebase",
"menu.vcs.rebase.skip": "Ignorer le commit",
"menu.vcs.merge.abort": "Abandonner la fusion",
"menu.vcs.cherryPick.continue": "Continuer le cherry-pick",
"menu.vcs.cherryPick.abort": "Abandonner le cherry-pick",
"menu.vcs.revert.continue": "Continuer le revert",
"menu.vcs.revert.abort": "Abandonner le revert",
"menu.gitUncommittedChanges": "Modifications non validées",
"menu.gitCurrentFile": "Fichier actuel",
"menu.vcs.addToVcs": "Ajouter au VCS",
"menu.vcs.compareWithBranch": "Comparer avec une branche ou une révision…",
"menu.vcs.showHistory": "Afficher l'historique",
"menu.build": "Build",
"menu.build.buildProject": "Compiler le projet",
"menu.build.buildModule": "Compiler le module",
"menu.build.rebuildProject": "Recompiler le projet",
"menu.build.buildArtifacts": "Compiler les artefacts",
"menu.toolwindow.build": "Build",
"menu.find.structural": "Recherche structurelle…",
"menu.find.replaceStructural": "Remplacement structurel…",
"menu.analyze.inspectCode": "Inspecter le code…",
"menu.analyzeCode": "Analyser le code",
"menu.analyze.locateDuplicates": "Rechercher les doublons…",
"menu.analyze.stackTrace": "Analyser une trace de pile ou un dump de threads…",
"menu.toolwindow.services": "Services",
"menu.gitHub": "GitHub",
"menu.github.pullRequests": "Voir les pull requests",
"menu.github.createPullRequest": "Créer une pull request…",
"menu.github.checkoutPullRequest": "Extraire une pull request…",
"menu.github.openInBrowser": "Ouvrir sur GitHub",
"menu.file.newProject": "Nouveau projet…",
"menu.file.renameProject": "Renommer le projet…",
"menu.file.reloadAllFromDisk": "Tout recharger depuis le disque",
"menu.file.invalidateCaches": "Invalider les caches…",
"menu.manageIdeSettings": "Gérer les paramètres de l'IDE",
"menu.file.exportSettings": "Exporter les paramètres…",
"menu.file.importSettings": "Importer les paramètres…",
"menu.file.togglePowerSave": "Mode économie d'énergie",
"menu.macros": "Macros",
"menu.macro.startRecording": "Démarrer l'enregistrement de la macro",
"menu.macro.stopRecording": "Arrêter l'enregistrement de la macro",
"menu.macro.playBack": "Rejouer la dernière macro",
"menu.macro.playSaved": "Rejouer des macros enregistrées…",
"menu.macro.edit": "Modifier les macros…",
"menu.bookmarks": "Signets",
"menu.navigate.toggleBookmarkWithMnemonic": "Ajouter/retirer un signet avec mnémonique",
"menu.navigate.nextBookmark": "Signet suivant",
"menu.navigate.prevBookmark": "Signet précédent",
"menu.view.toggleCompactMode": "Mode compact",
"menu.view.zoomIn": "Zoom avant de l'IDE",
"menu.view.zoomOut": "Zoom arrière de l'IDE",
"menu.view.resetZoom": "Réinitialiser le zoom de l'IDE",
"menu.toolbar.customize": "Personnaliser la barre d'outils principale…",
"menu.code.quickDefinition": "Définition rapide",
"menu.code.quickTypeDefinition": "Définition rapide du type",
"menu.localHistory.recentChanges": "Modifications récentes",
"menu.diff.compareWithClipboard": "Comparer avec le presse-papiers",
"menu.view.quickSwitchScheme": "Changement rapide de schéma…",
"menu.editor.resetFontSize": "Réinitialiser la taille de police dans tous les éditeurs",
"menu.code.cleanup": "Nettoyage du code…",
"menu.runAnything.show": "Exécuter n'importe quoi…",
"menu.localHistory.putLabel": "Poser une étiquette…",
"menu.toolWindowLayouts": "Dispositions des fenêtres d'outils",
"menu.layout.save": "Enregistrer la disposition actuelle sous…",
"menu.layout.restoreDefault": "Restaurer la disposition par défaut",
"menu.layout.rename": "Renommer la disposition…",
"menu.layout.delete": "Supprimer la disposition…",
"menu.activeToolWindow": "Fenêtre d'outil active",
"menu.toolwindow.hideActive": "Masquer la fenêtre d'outil active",
"menu.toolwindow.maximize": "Agrandir la fenêtre d'outil",
"menu.toolwindow.closeContent": "Fermer l'onglet actif",
"menu.toolwindow.jumpToLast": "Aller à la dernière fenêtre d'outil",
"menu.toolwindow.focusEditor": "Activer l'éditeur",
"menu.editorTabs": "Onglets de l'éditeur",
"menu.editor.nextTab": "Sélectionner l'onglet suivant",
"menu.editor.prevTab": "Sélectionner l'onglet précédent",
"menu.editor.pinTab": "Épingler l'onglet",
"menu.editor.closeTab": "Fermer l'onglet",
"menu.editor.closeOtherTabs": "Fermer les autres onglets",
"menu.editor.closeAllTabs": "Fermer tous les onglets",
"menu.editor.reopenClosedTab": "Rouvrir l'onglet fermé",
"menu.editor.moveToOppositeGroup": "Déplacer vers le groupe opposé",
"menu.editor.openInOppositeGroup": "Ouvrir dans le groupe opposé",
"menu.editor.changeSplitOrientation": "Changer l'orientation de la scission",
"menu.editor.unsplitAll": "Annuler toutes les scissions",
"menu.navigate.showFilePath": "Chemin du fichier",
"menu.navigate.customRegion": "Pliage personnalisé…",
"menu.code.showErrorDescription": "Description de l'erreur",
"menu.code.surroundWithLiveTemplate": "Entourer avec un modèle dynamique…",
"menu.code.autoIndentLines": "Indenter automatiquement les lignes",
"menu.code.moveStatementUp": "Déplacer l'instruction vers le haut",
"menu.code.moveStatementDown": "Déplacer l'instruction vers le bas",
"menu.code.moveElementLeft": "Déplacer l'élément vers la gauche",
"menu.code.moveElementRight": "Déplacer l'élément vers la droite",
"menu.code.collapseBlock": "Plier le bloc de code",
"menu.analyze.inspectFile": "Inspecter le fichier actuel",
"menu.project.copyElement": "Copier…",
"menu.run.chooseDebugConfig": "Déboguer…",
"menu.debug.quickEvaluate": "Évaluation rapide de l'expression",
"menu.resizeToolWindow": "Redimensionner",
"menu.toolwindow.resizeLeft": "Étirer vers la gauche",
"menu.toolwindow.resizeRight": "Étirer vers la droite",
"menu.toolwindow.resizeUp": "Étirer vers le haut",
"menu.toolwindow.resizeDown": "Étirer vers le bas",
"menu.editor.nextSplitter": "Aller à la scission suivante",
"menu.editor.prevSplitter": "Aller à la scission précédente",
"menu.expandToLevel": "Développer jusqu'au niveau",
"menu.expandAllToLevel": "Tout développer jusqu'au niveau",
"menu.code.expandToLevel1": "1",
"menu.code.expandAllToLevel1": "1",
"menu.code.expandToLevel2": "2",
"menu.code.expandAllToLevel2": "2",
"menu.code.expandToLevel3": "3",
"menu.code.expandAllToLevel3": "3",
"menu.code.expandToLevel4": "4",
"menu.code.expandAllToLevel4": "4",
"menu.code.expandToLevel5": "5",
"menu.code.expandAllToLevel5": "5",
"menu.editor.contextInfo": "Informations de contexte",
"run.services.configurationCount": "{count} configurations",
"search.bookmarks.count": "{count, plural, one {# signet} other {# signets}}",
"settings.zoom.hint": "Modifier avec {zoomIn} ou {zoomOut}. Revenir à 100 % avec {reset}"
}
+451
View File
@@ -0,0 +1,451 @@
{
"app.about.build": "빌드",
"app.about.configDir": "구성",
"app.about.copyInfo": "정보 복사",
"app.about.copyright": "Copyright © 2026 OpenIDEA 기여자",
"app.about.disclaimer": "JetBrains와 관련이 없습니다.",
"app.about.license": "Apache License 2.0에 따라 라이선스가 부여됩니다",
"app.about.platform": "플랫폼",
"app.about.runtime": "런타임",
"app.about.showConfig": "구성 폴더 열기",
"app.about.tagline": "IntelliJ IDEA에서 영감을 받은 오픈 소스 IDE",
"app.about.thirdParty": "IntelliJ Platform의 아이콘(Apache-2.0)이 포함되어 있습니다.",
"app.about.title": "OpenIDEA 정보",
"app.about.version": "버전",
"app.action.about": "OpenIDEA 정보",
"app.action.checkForUpdates": "업데이트 확인…",
"app.action.copy": "복사",
"app.action.cut": "잘라내기",
"app.action.keymapReference": "키맵 참조",
"app.action.lightEditOpenProject": "포함 폴더를 프로젝트로 열기",
"app.action.newWindow": "새 창",
"app.action.nextWindow": "다음 프로젝트 창",
"app.action.openFile": "파일 열기…",
"app.action.openInNewWindow": "새 창에서 열기…",
"app.action.openRecentProject": "최근 프로젝트 열기",
"app.action.paste": "붙여넣기",
"app.action.previousWindow": "이전 프로젝트 창",
"app.action.quit": "{os, select, macos {OpenIDEA 종료} other {종료}}",
"app.action.redo": "다시 실행",
"app.action.selectAll": "모두 선택",
"app.action.showLog": "{os, select, macos {Finder에서 로그 표시} windows {탐색기에서 로그 표시} other {파일에서 로그 표시}}",
"app.action.toggleFullScreen": "전체 화면 전환",
"app.action.undo": "실행 취소",
"app.category.edit": "편집",
"app.category.file": "파일",
"app.category.help": "도움말",
"app.category.view": "보기",
"app.category.window": "창",
"app.exit.body": "OpenIDEA를 종료하시겠습니까?",
"app.exit.confirm": "종료",
"app.exit.dontAsk": "다시 묻지 않음",
"app.exit.title": "종료 확인",
"app.keymap.count": "{count, plural, other {단축키 #개}}",
"app.keymap.empty": "일치하는 단축키가 없습니다",
"app.keymap.filter": "액션 또는 단축키로 필터링",
"app.keymap.other": "기타",
"app.keymap.preset": "키맵: {preset}",
"app.keymap.title": "키맵 참조",
"app.lightEdit.openProject": "프로젝트로 열기",
"app.lightEdit.openProjectTooltip": "{folder}을(를) 프로젝트로 열기",
"app.log.failed": "로그를 표시할 수 없습니다",
"app.project.opening": "{name} 여는 중…",
"app.title.default": "OpenIDEA",
"app.unsaved.body": "{project}에 저장되지 않은 변경 사항이 있는 {count, plural, other {파일이 #개}} 있습니다. 닫기 전에 저장하시겠습니까?",
"app.unsaved.discard": "저장 안 함",
"app.unsaved.save": "모두 저장",
"app.unsaved.saveFailed": "모든 파일을 저장할 수 없습니다",
"app.unsaved.title": "저장되지 않은 변경 사항",
"app.update.available": "OpenIDEA {version}을(를) 사용할 수 있습니다",
"app.update.cancel": "취소",
"app.update.cancelled": "업데이트가 취소되었습니다",
"app.update.checking": "업데이트 확인 중…",
"app.update.disabled": "자동 업데이트가 구성되지 않았습니다",
"app.update.disabledBody": "업데이트 확인을 활성화하려면 업데이트 엔드포인트(OPENIDEA_UPDATE_ENDPOINT 또는 plugins.updater.endpoints)를 설정하세요.",
"app.update.downloading": "OpenIDEA {version} 다운로드 중",
"app.update.failed": "업데이트 확인에 실패했습니다",
"app.update.install": "설치 후 다시 시작",
"app.update.installFailed": "업데이트를 설치할 수 없습니다",
"app.update.installed": "OpenIDEA {version}이(가) 설치되었습니다",
"app.update.installedBody": "OpenIDEA를 다시 시작하면 적용됩니다.",
"app.update.installingMessage": "설치 중…",
"app.update.installUncancellable": "업데이트를 설치하는 중입니다. 이 단계는 취소할 수 없습니다",
"app.update.none": "OpenIDEA {version}은(는) 최신 버전입니다",
"app.update.progress": "{done}MB / {total}MB",
"app.update.progressUnknown": "{done}MB",
"app.update.restart": "다시 시작",
"app.update.retry": "다시 시도",
"app.window.desktopOnly": "여러 창을 사용하려면 데스크톱 앱이 필요합니다",
"app.window.openFailed": "{name}을(를) 열 수 없습니다",
"app.window.pickFileTitle": "파일 열기",
"app.window.pickTitle": "새 창에서 열기",
"app.window.askTitle": "프로젝트 열기",
"app.window.askBody": "프로젝트를 새 창에서 열거나, 현재 프로젝트를 대체하거나, 이미 열려 있는 프로젝트에 연결할 수 있습니다.\n\"{name}\"을(를) 어떻게 여시겠습니까?",
"app.window.askThis": "현재 창",
"app.window.askAttach": "연결",
"app.window.askNew": "새 창",
"app.window.askRemember": "다시 묻지 않음",
"common.cancel": "취소",
"common.close": "닫기",
"common.copied": "복사됨",
"common.copy": "복사",
"menu.app": "OpenIDEA",
"menu.app.about": "OpenIDEA 정보",
"menu.app.checkForUpdates": "업데이트 확인…",
"menu.app.quit": "{os, select, macos {OpenIDEA 종료} other {종료}}",
"menu.appearance": "모양",
"menu.code": "코드",
"menu.code.commentBlock": "블록 주석으로 주석 처리",
"menu.code.commentLine": "줄 주석으로 주석 처리",
"menu.code.completion": "기본",
"menu.code.deleteLine": "줄 삭제",
"menu.code.duplicateLine": "줄 또는 선택 영역 복제",
"menu.code.extendSelection": "선택 영역 확장",
"menu.code.foldAll": "모두 접기",
"menu.code.foldToggle": "펼치기/접기",
"menu.code.generate": "생성…",
"menu.code.pasteAsPlainText": "일반 텍스트로 붙여넣기",
"menu.editor.copyReference": "참조 복사",
"menu.code.cyclicExpandWord": "단어 순환 확장",
"menu.code.cyclicExpandWordBackward": "단어 순환 확장(역방향)",
"menu.code.expressionTypeInfo": "표현식 타입 정보",
"menu.code.overrideMethods": "메서드 재정의…",
"menu.code.implementMethods": "메서드 구현…",
"menu.code.insertLiveTemplate": "라이브 템플릿 삽입…",
"menu.code.moveLineDown": "줄 아래로 이동",
"menu.code.moveLineUp": "줄 위로 이동",
"menu.code.optimizeImports": "import 최적화",
"menu.code.parameterInfo": "매개변수 정보",
"menu.code.quickDocumentation": "빠른 문서",
"menu.code.reformat": "코드 서식 다시 지정",
"menu.code.showIntentions": "컨텍스트 액션 표시",
"menu.code.shrinkSelection": "선택 영역 축소",
"menu.code.surroundWith": "다음으로 감싸기…",
"menu.code.unwrap": "감싸기 해제/제거…",
"menu.code.unfoldAll": "모두 펼치기",
"menu.debug.evaluate": "표현식 평가…",
"menu.debug.pause": "프로그램 일시 중지",
"menu.debug.resume": "프로그램 재개",
"menu.debug.runToCursor": "커서까지 실행",
"menu.debug.stepInto": "스텝 인",
"menu.debug.stepOut": "스텝 아웃",
"menu.debug.stepOver": "스텝 오버",
"menu.debug.toggleBreakpoint": "줄 중단점 전환",
"menu.debug.viewBreakpoints": "중단점 보기…",
"menu.debug.attachToProcess": "프로세스에 연결…",
"menu.debug.forceStepOver": "강제 스텝 오버",
"menu.debug.forceStepInto": "강제 스텝 인",
"menu.debug.smartStepInto": "스마트 스텝 인",
"menu.debug.forceRunToCursor": "커서까지 강제 실행",
"menu.debug.showExecutionPoint": "실행 지점 표시",
"menu.debug.resetFrame": "프레임 리셋",
"menu.debug.toggleTemporaryBreakpoint": "임시 줄 중단점 전환",
"menu.debug.toggleMethodBreakpoint": "메서드 중단점 전환",
"menu.debug.toggleFieldWatchpoint": "필드 감시점 전환",
"menu.edit": "편집",
"menu.edit.copy": "복사",
"menu.edit.cut": "잘라내기",
"menu.edit.paste": "붙여넣기",
"menu.code.pasteFromHistory": "기록에서 붙여넣기…",
"menu.edit.redo": "다시 실행",
"menu.edit.selectAll": "모두 선택",
"menu.edit.undo": "실행 취소",
"menu.editor.copyPath": "경로 복사",
"menu.code.sortLines": "줄 정렬",
"menu.code.reverseLines": "줄 순서 반전",
"menu.editor.toggleColumnSelection": "열 선택 모드",
"menu.editor.splitDown": "아래로 분할",
"menu.editor.splitRight": "오른쪽으로 분할",
"menu.editor.toggleZen": "{distractionFree, select, on {집중 모드 종료} other {집중 모드 시작}}",
"menu.editor.unsplit": "분할 해제",
"menu.file": "파일",
"menu.fileProperties": "파일 속성",
"menu.newProjectsSetup": "새 프로젝트 설정",
"menu.file.newProjectsSettings": "새 프로젝트용 설정…",
"menu.file.toggleReadOnly": "파일을 읽기 전용/쓰기 가능으로 설정",
"menu.file.closeProject": "프로젝트 닫기",
"menu.remote.reloadEnvironment": "환경 다시 로드",
"menu.file.new": "새로 만들기…",
"menu.file.newScratchFile": "새 스크래치 파일…",
"menu.file.openInNewWindow": "새 창에서 열기…",
"menu.file.openProject": "열기…",
"menu.file.save": "모두 저장",
"menu.file.settings": "설정…",
"menu.find": "찾기",
"menu.find.inFile": "찾기…",
"menu.find.inPath": "파일에서 찾기…",
"menu.find.replaceInFile": "바꾸기…",
"menu.find.replaceInPath": "파일에서 바꾸기…",
"menu.find.showUsages": "사용 위치 표시",
"menu.find.usages": "사용 위치 찾기",
"menu.completion": "코드 완성",
"menu.folding": "접기",
"menu.code.foldSelection": "선택 영역 접기/영역 제거",
"menu.git": "Git",
"menu.help": "도움말",
"menu.mainMenu": "메인 메뉴",
"window.caption.minimize": "최소화",
"window.caption.maximize": "최대화",
"window.caption.restore": "이전 크기로 복원",
"window.caption.close": "닫기",
"menu.help.keymapReference": "키맵 참조",
"menu.help.showLog": "{os, select, macos {Finder에서 로그 표시} windows {탐색기에서 로그 표시} other {파일에서 로그 표시}}",
"menu.native.bringAllToFront": "모두 앞으로 가져오기",
"menu.native.copy": "복사",
"menu.native.cut": "잘라내기",
"menu.native.hide": "OpenIDEA 숨기기",
"menu.native.hideOthers": "기타 숨기기",
"menu.native.minimize": "최소화",
"menu.native.paste": "붙여넣기",
"menu.native.redo": "다시 실행",
"menu.native.selectAll": "모두 선택",
"menu.native.services": "서비스",
"menu.native.showAll": "모두 표시",
"menu.native.undo": "실행 취소",
"menu.native.zoom": "확대/축소",
"menu.navigate": "탐색",
"menu.navigate.back": "뒤로",
"menu.navigate.lastEditLocation": "마지막 편집 위치",
"menu.navigate.nextEditLocation": "다음 편집 위치",
"menu.navigate.callHierarchy": "호출 계층 구조",
"menu.navigate.declaration": "선언 또는 사용 위치",
"menu.navigate.fileStructure": "파일 구조",
"menu.navigate.findAction": "액션 찾기…",
"menu.navigate.forward": "앞으로",
"menu.navigate.gotoClass": "클래스…",
"menu.navigate.gotoFile": "파일…",
"menu.navigate.gotoLine": "줄/열…",
"menu.navigate.gotoSymbol": "심볼…",
"menu.navigate.implementation": "구현",
"menu.navigate.nextError": "다음 강조 표시된 오류",
"menu.navigate.prevError": "이전 강조 표시된 오류",
"menu.navigate.recentFiles": "최근 파일",
"menu.navigate.recentLocations": "최근 위치",
"menu.navigate.searchEverywhere": "어디서나 검색",
"menu.navigate.selectIn": "다음에서 선택…",
"menu.navigate.showBookmarks": "북마크 표시",
"menu.navigate.toggleBookmark": "북마크 전환",
"menu.navigate.typeDeclaration": "타입 선언",
"menu.navigate.typeHierarchy": "타입 계층 구조",
"menu.navigate.superMethod": "슈퍼 메서드",
"menu.navigate.gotoTest": "테스트",
"menu.navigate.nextMethod": "다음 메서드",
"menu.navigate.previousMethod": "이전 메서드",
"menu.openRecent": "최근 항목 열기",
"menu.recent.none": "최근 프로젝트 없음",
"menu.refactor": "리팩터링",
"menu.refactor.changeSignature": "시그니처 변경…",
"menu.refactor.extractConstant": "상수 도입…",
"menu.refactor.extractField": "필드 도입…",
"menu.refactor.extractMethod": "메서드 추출…",
"menu.refactor.extractParameter": "매개변수 도입…",
"menu.refactor.extractVariable": "변수 도입…",
"menu.refactor.inline": "인라인…",
"menu.refactor.move": "이동…",
"menu.refactor.rename": "이름 변경…",
"menu.refactor.safeDelete": "안전 삭제…",
"menu.refactor.thisMenu": "이 항목 리팩터링…",
"menu.run": "실행",
"menu.run.chooseConfig": "실행…",
"menu.run.debug": "디버그",
"menu.run.debugContext": "컨텍스트 구성 디버그",
"menu.run.coverage": "커버리지와 함께 실행",
"menu.run.profile": "프로파일",
"menu.coverage.hide": "커버리지 숨기기",
"menu.debuggingActions": "디버깅 액션",
"menu.debug.reloadChangedClasses": "변경된 클래스 다시 로드",
"menu.toolwindow.coverage": "커버리지",
"menu.toolwindow.profiler": "프로파일러",
"menu.run.editConfigs": "구성 편집…",
"menu.run.run": "실행",
"menu.run.runContext": "컨텍스트 구성 실행",
"menu.run.stop": "중지",
"menu.toolWindows": "도구 창",
"menu.toolwindow.bookmarks": "북마크",
"menu.toolwindow.commit": "커밋",
"menu.toolwindow.debug": "디버그",
"menu.toolwindow.find": "찾기",
"menu.toolwindow.git": "Git",
"menu.toolwindow.hideAll": "모든 도구 창 숨기기",
"menu.toolwindow.problems": "문제",
"menu.toolwindow.project": "프로젝트",
"menu.toolwindow.run": "실행",
"menu.toolwindow.structure": "구조",
"menu.toolwindow.terminal": "터미널",
"terminal.close.title": "'{name}' 프로세스가 실행 중입니다",
"terminal.close.message": "'{name}' 프로세스를 종료하시겠습니까?",
"terminal.close.terminate": "종료",
"menu.vcs.annotate": "Git Blame으로 주석 달기",
"menu.vcs.branches": "브랜치…",
"menu.vcs.commit": "커밋…",
"menu.vcs.fetch": "페치",
"menu.vcs.localHistory": "로컬 기록",
"menu.vcs.newBranch": "새 브랜치…",
"menu.vcs.push": "푸시…",
"menu.vcs.rollback": "롤백…",
"menu.vcs.showDiff": "차이 표시",
"menu.vcs.showLog": "Git 로그 표시",
"menu.vcs.stash": "변경 사항 스태시…",
"menu.vcs.unstash": "변경 사항 스태시 적용…",
"menu.vcs.update": "프로젝트 업데이트…",
"menu.vcs.pull": "풀…",
"menu.vcs.merge": "병합…",
"menu.vcs.rebase": "리베이스…",
"menu.vcs.newWorktree": "새 워크트리…",
"menu.vcs.worktrees": "워크트리…",
"menu.vcs.manageRemotes": "원격 관리…",
"menu.vcs.clone": "복제…",
"menu.vcs.operations": "VCS 작업 팝업…",
"menu.view": "보기",
"menu.view.notifications": "알림",
"menu.view.toggleTheme": "밝은/어두운 테마 전환",
"menu.welcome.recentProjects": "프로젝트 관리…",
"menu.window": "창",
"menu.tools": "도구",
"menu.http.newRequest": "새 HTTP 요청",
"menu.http.showToolWindow": "HTTP 클라이언트",
"menu.db.showToolWindow": "데이터베이스",
"menu.db.newDataSource": "새 데이터 소스…",
"menu.db.openConsole": "쿼리 콘솔 열기",
"menu.vim.toggle": "Vim",
"menu.window.new": "새 창",
"menu.window.next": "다음 프로젝트 창",
"menu.window.previous": "이전 프로젝트 창",
"menu.window.toggleFullScreen": "전체 화면 전환",
"os.linux": "Linux",
"os.macos": "macOS",
"os.web": "브라우저",
"os.windows": "Windows",
"menu.gitPatch": "패치",
"menu.vcs.newTag": "새 태그…",
"menu.vcs.resetHead": "HEAD 리셋…",
"menu.vcs.shelve": "변경 사항 셸브…",
"menu.vcs.showShelf": "셸프 표시",
"menu.vcs.createPatch": "패치 생성…",
"menu.vcs.applyPatch": "패치 적용…",
"menu.vcs.applyPatchFromClipboard": "클립보드에서 패치 적용…",
"menu.vcs.resolveConflicts": "충돌 해결…",
"menu.vcs.rebase.continue": "리베이스 계속",
"menu.vcs.rebase.abort": "리베이스 중단",
"menu.vcs.rebase.skip": "커밋 건너뛰기",
"menu.vcs.merge.abort": "병합 중단",
"menu.vcs.cherryPick.continue": "체리픽 계속",
"menu.vcs.cherryPick.abort": "체리픽 중단",
"menu.vcs.revert.continue": "되돌리기 계속",
"menu.vcs.revert.abort": "되돌리기 중단",
"menu.gitUncommittedChanges": "커밋되지 않은 변경 사항",
"menu.gitCurrentFile": "현재 파일",
"menu.vcs.addToVcs": "VCS에 추가",
"menu.vcs.compareWithBranch": "브랜치 또는 리비전과 비교…",
"menu.vcs.showHistory": "기록 표시",
"menu.build": "빌드",
"menu.build.buildProject": "프로젝트 빌드",
"menu.build.buildModule": "모듈 빌드",
"menu.build.rebuildProject": "프로젝트 다시 빌드",
"menu.build.buildArtifacts": "아티팩트 빌드",
"menu.toolwindow.build": "빌드",
"menu.find.structural": "구조적으로 검색…",
"menu.find.replaceStructural": "구조적으로 바꾸기…",
"menu.analyze.inspectCode": "코드 검사…",
"menu.analyzeCode": "코드 분석",
"menu.analyze.locateDuplicates": "중복 찾기…",
"menu.analyze.stackTrace": "스택 추적 또는 스레드 덤프 분석…",
"menu.toolwindow.services": "서비스",
"menu.gitHub": "GitHub",
"menu.github.pullRequests": "풀 리퀘스트 보기",
"menu.github.createPullRequest": "풀 리퀘스트 생성…",
"menu.github.checkoutPullRequest": "풀 리퀘스트 체크아웃…",
"menu.github.openInBrowser": "GitHub에서 열기",
"menu.file.newProject": "새 프로젝트…",
"menu.file.renameProject": "프로젝트 이름 변경…",
"menu.file.reloadAllFromDisk": "디스크에서 모두 다시 로드",
"menu.file.invalidateCaches": "캐시 무효화…",
"menu.manageIdeSettings": "IDE 설정 관리",
"menu.file.exportSettings": "설정 내보내기…",
"menu.file.importSettings": "설정 가져오기…",
"menu.file.togglePowerSave": "절전 모드",
"menu.macros": "매크로",
"menu.macro.startRecording": "매크로 기록 시작",
"menu.macro.stopRecording": "매크로 기록 중지",
"menu.macro.playBack": "마지막 매크로 재생",
"menu.macro.playSaved": "저장된 매크로 재생…",
"menu.macro.edit": "매크로 편집…",
"menu.bookmarks": "북마크",
"menu.navigate.toggleBookmarkWithMnemonic": "니모닉으로 북마크 전환",
"menu.navigate.nextBookmark": "다음 북마크",
"menu.navigate.prevBookmark": "이전 북마크",
"menu.view.toggleCompactMode": "컴팩트 모드",
"menu.view.zoomIn": "IDE 확대",
"menu.view.zoomOut": "IDE 축소",
"menu.view.resetZoom": "IDE 확대/축소 초기화",
"menu.toolbar.customize": "메인 도구 모음 사용자 지정…",
"menu.code.quickDefinition": "빠른 정의",
"menu.code.quickTypeDefinition": "빠른 타입 정의",
"menu.localHistory.recentChanges": "최근 변경 사항",
"menu.diff.compareWithClipboard": "클립보드와 비교",
"menu.view.quickSwitchScheme": "구성표 빠른 전환…",
"menu.editor.resetFontSize": "모든 편집기의 글꼴 크기 초기화",
"menu.code.cleanup": "코드 정리…",
"menu.runAnything.show": "무엇이든 실행…",
"menu.localHistory.putLabel": "레이블 지정…",
"menu.toolWindowLayouts": "도구 창 레이아웃",
"menu.layout.save": "현재 레이아웃을 다른 이름으로 저장…",
"menu.layout.restoreDefault": "기본 레이아웃 복원",
"menu.layout.rename": "레이아웃 이름 변경…",
"menu.layout.delete": "레이아웃 삭제…",
"menu.activeToolWindow": "활성 도구 창",
"menu.toolwindow.hideActive": "활성 도구 창 숨기기",
"menu.toolwindow.maximize": "도구 창 최대화",
"menu.toolwindow.closeContent": "활성 탭 닫기",
"menu.toolwindow.jumpToLast": "마지막 도구 창으로 이동",
"menu.toolwindow.focusEditor": "편집기에 포커스",
"menu.editorTabs": "편집기 탭",
"menu.editor.nextTab": "다음 탭 선택",
"menu.editor.prevTab": "이전 탭 선택",
"menu.editor.pinTab": "탭 고정",
"menu.editor.closeTab": "탭 닫기",
"menu.editor.closeOtherTabs": "다른 탭 닫기",
"menu.editor.closeAllTabs": "모든 탭 닫기",
"menu.editor.reopenClosedTab": "닫은 탭 다시 열기",
"menu.editor.moveToOppositeGroup": "반대쪽 그룹으로 이동",
"menu.editor.openInOppositeGroup": "반대쪽 그룹에서 열기",
"menu.editor.changeSplitOrientation": "분할 방향 변경",
"menu.editor.unsplitAll": "모든 분할 해제",
"menu.navigate.showFilePath": "파일 경로",
"menu.navigate.customRegion": "사용자 지정 접기…",
"menu.code.showErrorDescription": "오류 설명",
"menu.code.surroundWithLiveTemplate": "라이브 템플릿으로 감싸기…",
"menu.code.autoIndentLines": "줄 자동 들여쓰기",
"menu.code.moveStatementUp": "문 위로 이동",
"menu.code.moveStatementDown": "문 아래로 이동",
"menu.code.moveElementLeft": "요소 왼쪽으로 이동",
"menu.code.moveElementRight": "요소 오른쪽으로 이동",
"menu.code.collapseBlock": "코드 블록 접기",
"menu.analyze.inspectFile": "현재 파일 검사",
"menu.project.copyElement": "복사…",
"menu.run.chooseDebugConfig": "디버그…",
"menu.debug.quickEvaluate": "빠른 표현식 평가",
"menu.resizeToolWindow": "크기 조정",
"menu.toolwindow.resizeLeft": "왼쪽으로 늘이기",
"menu.toolwindow.resizeRight": "오른쪽으로 늘이기",
"menu.toolwindow.resizeUp": "위쪽으로 늘이기",
"menu.toolwindow.resizeDown": "아래쪽으로 늘이기",
"menu.editor.nextSplitter": "다음 분할기로 이동",
"menu.editor.prevSplitter": "이전 분할기로 이동",
"menu.expandToLevel": "수준까지 펼치기",
"menu.expandAllToLevel": "모두 수준까지 펼치기",
"menu.code.expandToLevel1": "1",
"menu.code.expandAllToLevel1": "1",
"menu.code.expandToLevel2": "2",
"menu.code.expandAllToLevel2": "2",
"menu.code.expandToLevel3": "3",
"menu.code.expandAllToLevel3": "3",
"menu.code.expandToLevel4": "4",
"menu.code.expandAllToLevel4": "4",
"menu.code.expandToLevel5": "5",
"menu.code.expandAllToLevel5": "5",
"menu.editor.contextInfo": "컨텍스트 정보",
"run.services.configurationCount": "구성 {count}개",
"search.bookmarks.count": "{count, plural, other {북마크 #개}}",
"settings.zoom.hint": "{zoomIn} 또는 {zoomOut}(으)로 변경합니다. {reset}(으)로 100%로 설정합니다"
}
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+10 -1
View File
@@ -5912,5 +5912,14 @@
"Delete shelved changes “{name}”": "删除搁置的更改“{name}”",
"Rename shelved changes": "重命名搁置的更改",
"Passed to the agent process, replacing variables of the same name. Changes apply to new sessions.": "传给智能体进程,覆盖同名变量。对新会话生效。",
"Expected NAME=value: {pair}": "应为 NAME=value:{pair}"
"Expected NAME=value: {pair}": "应为 NAME=value:{pair}",
"Checking out #{number}…": "正在检出 #{number}…",
"Couldn't check out #{number}": "无法检出 #{number}",
"Fetching #{number}…": "正在获取 #{number}…",
"Couldn't fetch #{number}": "无法获取 #{number}",
"Creating worktree for #{number}…": "正在为 #{number} 创建工作树…",
"Couldn't create the worktree for #{number}": "无法为 #{number} 创建工作树",
"Couldn't commit the suggested change": "无法提交建议的更改",
"Couldn't apply the suggested change": "无法应用建议的更改",
"Couldn't resolve the thread": "无法解决讨论串"
}
+46 -5
View File
@@ -26,11 +26,16 @@
* Not seen: text kept in a variable or built by a helper before it reaches `tx`,
* and text sent by the backend. Their entries count as used (not stale) as long as
* the text appears as a literal anywhere in `src/`, the Rust crates or `languages/`.
*
* Message keys are checked too: a literal key given to `t`, or to a function bound
* from `useT()` in the same file, must exist in `en.json`; otherwise the UI shows the
* raw key. Keys built at runtime or passed through a helper are not seen.
*/
import { readFileSync, readdirSync } from "node:fs";
import { fileURLToPath } from "node:url";
import ts from "typescript";
import { describe, expect, it } from "vitest";
import en from "./en.json";
const ROOT = fileURLToPath(new URL("../../../", import.meta.url));
const TEXT_DIR = new URL("./text/", import.meta.url);
@@ -177,9 +182,6 @@ const UNTRANSLATED = new Set<string>([
"IDE",
"mod tests",
"Windows (\\r\\n)",
// Language names, written in their own language
"English",
"简体中文 (Chinese, Simplified)",
// Terms, keywords and acronyms kept in English
"DDL",
"GOPATH",
@@ -245,6 +247,9 @@ const UNTRANSLATED = new Set<string>([
"text",
]);
/** Locales that must translate every UI text; the others show English for text they lack. */
const COMPLETE_LOCALES = new Set(["zh"]);
/**
* Template literals that reach an exact lookup on purpose: the text around the
* values is code or a format that reads the same in every locale.
@@ -435,12 +440,27 @@ interface Scan {
untranslatable: Use[];
/** Translated templates that splice English words into the sentence. */
spliced: Use[];
/** Literal message keys given to `t`. */
keys: Use[];
}
/** Local names of `t` and of the functions bound from `useT()` in a file. */
function messageFunctions(source: string): Set<string> {
const names = new Set<string>();
for (const m of source.matchAll(/import\s*\{([^}]*)\}\s*from\s*"(?:@\/platform|\.)\/i18n"/g))
for (const part of m[1]!.split(",")) {
const named = /^\s*t(?:\s+as\s+(\w+))?\s*$/.exec(part);
if (named) names.add(named[1] ?? "t");
}
for (const m of source.matchAll(/(?:const|let)\s+(\w+)\s*=\s*useT\(\)/g)) names.add(m[1]!);
return names;
}
function scanTypeScript(): Scan {
const translated: Use[] = [];
const untranslatable: Use[] = [];
const spliced: Use[] = [];
const keys: Use[] = [];
const literals = new Set<string>();
const files = walk(`${ROOT}src`, (p) => /\.tsx?$/.test(p) && !isTest(p) && !p.endsWith("/bindings.ts"));
for (const file of files) {
@@ -458,8 +478,14 @@ function scanTypeScript(): Scan {
for (const m of source.matchAll(/(?:const|let)\s+(\w+)\s*=\s*useTx(Template)?\(\)/g))
(m[2] ? template : exact).add(m[1]!);
const checked = !UNCHECKED_FILES.has(path);
const messages = messageFunctions(source);
const where = (node: ts.Node) => `${path}:${sf.getLineAndCharacterOfPosition(node.getStart()).line + 1}`;
const visit = (node: ts.Node) => {
if (ts.isCallExpression(node) && ts.isIdentifier(node.expression) && messages.has(node.expression.text)) {
const arg = node.arguments[0] && unwrap(node.arguments[0]);
const literal = arg && ts.isStringLiteralLike(arg) ? [arg.text] : arg && stringBranches(arg);
for (const text of literal ?? []) keys.push({ text, at: where(node), match: "exact" });
}
if (checked && ts.isTaggedTemplateExpression(node) && ts.isIdentifier(node.tag) && exact.has(node.tag.text)) {
// Without `${}` the tag looks its text up as written.
const match = ts.isNoSubstitutionTemplateLiteral(node.template) ? "exact" : "shape";
@@ -497,7 +523,7 @@ function scanTypeScript(): Scan {
};
visit(sf);
}
return { translated, literals, untranslatable, spliced };
return { translated, literals, untranslatable, spliced, keys };
}
/** Read strings without letting a Rust character literal open a string match. */
@@ -566,6 +592,21 @@ it("translates built text as whole sentences, one per variant", () => {
expect(report(scan.spliced)).toEqual([]);
});
it("reads the imported and bound names of the message function", () => {
expect(messageFunctions(`import { t, tx } from "@/platform/i18n";`)).toEqual(new Set(["t"]));
expect(messageFunctions(`import { t as translate, useT } from "./i18n"; const tr = useT();`)).toEqual(
new Set(["translate", "tr"]),
);
// `t` bound from `useTx()` takes English text, not a key.
expect(messageFunctions(`import { useTx } from "@/platform/i18n"; const t = useTx();`)).toEqual(new Set());
});
it("uses only message keys of the English catalog", () => {
// Ensure the check cannot pass because the scanner stopped finding keys.
expect(scan.keys.some((use) => use.text === "app.unsaved.title")).toBe(true);
expect(report(scan.keys.filter(({ text }) => !Object.hasOwn(en, text)))).toEqual([]);
});
describe.each(Object.keys(catalogs))("source-text catalog %s", (locale) => {
const catalog = catalogs[locale]!;
const patterns = templatePatterns(catalog);
@@ -576,7 +617,7 @@ describe.each(Object.keys(catalogs))("source-text catalog %s", (locale) => {
const found = ({ text, match }: Use) =>
has(text) || (match === "pattern" && matchesTemplate(text)) || (match === "shape" && shapes.has(shape(text)));
it("translates every literal UI text", () => {
it.runIf(COMPLETE_LOCALES.has(locale))("translates every literal UI text", () => {
const missing = new Map<string, string>();
for (const use of scan.translated)
if (!found(use) && needsTranslation(use.text) && !missing.has(use.text)) missing.set(use.text, use.at);