mirror of
https://github.com/l0ng-ai/openidea.git
synced 2026-11-06 08:07:49 +00:00
Merge remote-tracking branch 'origin/main' into review/pr1147
# Conflicts: # crates/ide-sdk/src/apply.rs
This commit is contained in:
@@ -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.
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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
@@ -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())
|
||||
|
||||
@@ -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"]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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 })),
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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");
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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 })),
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
@@ -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
@@ -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
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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}"
|
||||
}
|
||||
@@ -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
@@ -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": "无法解决讨论串"
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user