дзеркало
https://github.com/l0ng-ai/openidea.git
синхронізовано 2026-11-09 03:05:40 +02:00
feat(i18n): add French (#730)
Adds French as a UI language, with French catalogs for message keys and source text, and the French space before a colon in settings labels. UI languages are now registered in one place, `LOCALES` in `src/platform/i18n.ts` (name in its own language and `<html lang>` tag); the Language and Region options, settings validation and `<html lang>` all come from it. The catalog tests read every catalog in `locales/` and `locales/text/`, so a new file is always checked, and literal message keys given to `t` must exist in `en.json`. Only Chinese must cover every UI string; French and Korean show English for text they lack. Also makes two date tests independent of DST changes. Co-authored-by: J-Dudek <[email protected]>
Цей коміт міститься в:
1 джерело
7f0c78893e
коміт
59567f1af0
13 files changed
+6409
-56
No files matched your search
@@ -10,7 +10,8 @@ import { fullDateTime, prettyDate } from "./prettyDate";
|
||||
|
||||
describe("prettyDate", () => {
|
||||
const o = { ...DEFAULT_DATE_FORMAT, overrideSystem: true, pattern: "dd.MM.yyyy", use24Hour: true };
|
||||
const now = new Date(2026, 9, 31, 9, 2).getTime();
|
||||
// Between May and June, so that no date below crosses a DST change in any time zone that has one.
|
||||
const now = new Date(2026, 5, 30, 9, 2).getTime();
|
||||
|
||||
it("formats relative dates like GitHub", () => {
|
||||
expect(prettyDate(now - 20_000, now, o)).toBe("just now");
|
||||
@@ -18,16 +19,16 @@ describe("prettyDate", () => {
|
||||
expect(prettyDate(now - 60_000, now, o)).toBe("1 minute ago");
|
||||
expect(prettyDate(now - 42 * 60_000, now, o)).toBe("42 minutes ago");
|
||||
expect(prettyDate(now - 3_600_000, now, o)).toBe("1 hour ago");
|
||||
expect(prettyDate(new Date(2026, 9, 31, 0, 27).getTime(), now, o)).toBe("8 hours ago");
|
||||
expect(prettyDate(new Date(2026, 9, 30, 8, 0).getTime(), now, o)).toBe("yesterday");
|
||||
expect(prettyDate(new Date(2026, 9, 21, 9, 5).getTime(), now, o)).toBe("9 days ago");
|
||||
expect(prettyDate(new Date(2026, 8, 2, 9, 5).getTime(), now, o)).toBe("on 02.09.2026");
|
||||
expect(prettyDate(new Date(2026, 5, 30, 0, 27).getTime(), now, o)).toBe("8 hours ago");
|
||||
expect(prettyDate(new Date(2026, 5, 29, 8, 0).getTime(), now, o)).toBe("yesterday");
|
||||
expect(prettyDate(new Date(2026, 5, 20, 9, 5).getTime(), now, o)).toBe("9 days ago");
|
||||
expect(prettyDate(new Date(2026, 4, 2, 9, 5).getTime(), now, o)).toBe("on 02.05.2026");
|
||||
expect(prettyDate("not a date", now, o)).toBe("");
|
||||
});
|
||||
|
||||
it("shows the plain date and time with pretty formatting off", () => {
|
||||
expect(prettyDate(now - 20_000, now, { ...o, pretty: false })).toBe("31.10.2026 09:01");
|
||||
expect(fullDateTime(new Date(2026, 9, 31, 0, 27).getTime(), o)).toBe("31.10.2026 00:27");
|
||||
expect(prettyDate(now - 20_000, now, { ...o, pretty: false })).toBe("30.06.2026 09:01");
|
||||
expect(fullDateTime(new Date(2026, 5, 30, 0, 27).getTime(), o)).toBe("30.06.2026 00:27");
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -1,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,7 +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" });
|
||||
expect(validateSettings({ appearance: { locale: "ko" } }).appearance.locale).toBe("ko");
|
||||
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" | "ko";
|
||||
|
||||
/** 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", "ko"];
|
||||
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", "中文", "korean", "한국어", "translation"],
|
||||
keywords: ["locale", "translation", ...SUPPORTED_LOCALES.map((locale) => LOCALES[locale].name)],
|
||||
sections: [
|
||||
{
|
||||
options: [
|
||||
@@ -726,11 +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)" },
|
||||
{ value: "ko", label: "한국어 (Korean)" },
|
||||
],
|
||||
options: SUPPORTED_LOCALES.map((locale) => ({ value: locale, label: LOCALES[locale].name })),
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
@@ -8,14 +8,12 @@
|
||||
* 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";
|
||||
import { applyAppearanceOptions, installAltDragGuard, installIdleSave } from "./appearanceRuntime";
|
||||
import { type LocaleId, validateSettings } from "./model/schema";
|
||||
|
||||
/** `<html lang>` of each UI locale. */
|
||||
const HTML_LANG: Record<LocaleId, string> = { en: "en", zh: "zh-CN", ko: "ko" };
|
||||
import { validateSettings } from "./model/schema";
|
||||
|
||||
export function applyRuntimeSettings(raw: unknown, root: HTMLElement = document.documentElement): void {
|
||||
const s = validateSettings(raw);
|
||||
@@ -23,7 +21,7 @@ export function applyRuntimeSettings(raw: unknown, root: HTMLElement = document.
|
||||
|
||||
applyAppearanceOptions(s, root);
|
||||
|
||||
root.lang = HTML_LANG[s.appearance.locale];
|
||||
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;
|
||||
|
||||
@@ -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,16 +10,13 @@ import {
|
||||
registerMessages,
|
||||
resolveLocale,
|
||||
setLocale,
|
||||
SUPPORTED_LOCALES,
|
||||
t,
|
||||
tx,
|
||||
txTemplate,
|
||||
useI18n,
|
||||
} from "./i18n";
|
||||
import en from "./locales/en.json";
|
||||
import ko from "./locales/ko.json";
|
||||
import koText from "./locales/text/ko.json";
|
||||
import zh from "./locales/zh.json";
|
||||
import zhText from "./locales/text/zh.json";
|
||||
|
||||
afterEach(() => _resetI18nForTests());
|
||||
|
||||
@@ -46,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");
|
||||
@@ -74,7 +83,9 @@ describe("t / locales", () => {
|
||||
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));
|
||||
});
|
||||
|
||||
@@ -104,7 +115,7 @@ describe("t / locales", () => {
|
||||
});
|
||||
|
||||
it("falls back to English for unsupported locales", async () => {
|
||||
await setLocale("de");
|
||||
await setLocale("xx");
|
||||
expect(useI18n.getState().locale).toBe("en");
|
||||
});
|
||||
|
||||
@@ -164,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");
|
||||
@@ -172,28 +193,59 @@ describe("t / locales", () => {
|
||||
});
|
||||
|
||||
describe("catalogs", () => {
|
||||
const catalogs: Record<string, Record<string, string>> = { en, zh, ko };
|
||||
const textCatalogs: Record<string, Record<string, string>> = { zh: zhText, ko: koText };
|
||||
const translations = Object.entries(catalogs).filter(([locale]) => locale !== "en");
|
||||
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.each(Object.entries(textCatalogs))("translate source text into non-empty, different text (%s)", (_, texts) => {
|
||||
for (const [source, text] of Object.entries(texts)) {
|
||||
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.each(Object.entries(textCatalogs))("keep the placeholders of source-text templates (%s)", (_, texts) => {
|
||||
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(texts)) 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 (zh)", () => {
|
||||
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.each(translations)("have no keys English lacks (%s)", (_, catalog) => {
|
||||
expect(Object.keys(catalog).filter((key) => !Object.hasOwn(en, key))).toEqual([]);
|
||||
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", () => {
|
||||
@@ -202,9 +254,10 @@ describe("catalogs", () => {
|
||||
expect(isValidMessage(message), `${locale}:${key}`).toBe(true);
|
||||
});
|
||||
|
||||
it.each(translations)("use the same parameters across locales (%s)", (_, catalog) => {
|
||||
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, message] of Object.entries(catalog))
|
||||
expect(params(message), 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] ?? ""));
|
||||
});
|
||||
});
|
||||
@@ -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", "ko"] 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" });
|
||||
@@ -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}"
|
||||
}
|
||||
File diff suppressed because it is too large.
Load diff
@@ -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,10 +182,6 @@ const UNTRANSLATED = new Set<string>([
|
||||
"IDE",
|
||||
"mod tests",
|
||||
"Windows (\\r\\n)",
|
||||
// Language names, written in their own language
|
||||
"English",
|
||||
"简体中文 (Chinese, Simplified)",
|
||||
"한국어 (Korean)",
|
||||
// Terms, keywords and acronyms kept in English
|
||||
"DDL",
|
||||
"GOPATH",
|
||||
@@ -439,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) {
|
||||
@@ -462,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";
|
||||
@@ -501,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. */
|
||||
@@ -570,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);
|
||||
|
||||
Reference in new issue
Заблокувати користувача