mirror of
https://github.com/zadam/trilium.git
synced 2026-02-18 19:54:26 +01:00
Compare commits
11 Commits
f902fcd997
...
8d83b4e084
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8d83b4e084 | ||
|
|
25667e84b7 | ||
|
|
72e0d77be5 | ||
|
|
9ac4b9ed4f | ||
|
|
b3b89ba05c | ||
|
|
00dc04df25 | ||
|
|
21d47c3fef | ||
|
|
66de94f050 | ||
|
|
1917adb322 | ||
|
|
715f42b6c3 | ||
|
|
c372ba03dd |
@ -42,7 +42,7 @@
|
||||
"color": "5.0.3",
|
||||
"debounce": "3.0.0",
|
||||
"draggabilly": "3.0.0",
|
||||
"force-graph": "1.51.0",
|
||||
"force-graph": "1.51.1",
|
||||
"globals": "17.3.0",
|
||||
"i18next": "25.8.0",
|
||||
"i18next-http-backend": "3.0.2",
|
||||
|
||||
@ -111,6 +111,9 @@
|
||||
"opml_version_1": "OPML v1.0 - plain text only",
|
||||
"opml_version_2": "OPML v2.0 - allows also HTML",
|
||||
"export_type_single": "Only this note without its descendants",
|
||||
"export_to_clipboard": "Export to clipboard",
|
||||
"export_to_clipboard_on_tooltip": "Export the note content to clipboard.",
|
||||
"export_to_clipboard_off_tooltip": "Download the note as a file.",
|
||||
"export": "Export",
|
||||
"choose_export_type": "Choose export type first please",
|
||||
"export_status": "Export status",
|
||||
|
||||
@ -79,6 +79,7 @@ export const LOCALE_MAPPINGS: Record<DISPLAYABLE_LOCALE_IDS, (() => Promise<{ de
|
||||
es: () => import("@fullcalendar/core/locales/es"),
|
||||
fr: () => import("@fullcalendar/core/locales/fr"),
|
||||
it: () => import("@fullcalendar/core/locales/it"),
|
||||
ga: null,
|
||||
cn: () => import("@fullcalendar/core/locales/zh-cn"),
|
||||
tw: () => import("@fullcalendar/core/locales/zh-tw"),
|
||||
ro: () => import("@fullcalendar/core/locales/ro"),
|
||||
|
||||
@ -13,4 +13,8 @@
|
||||
|
||||
.export-dialog form .form-check-label {
|
||||
padding: 2px;
|
||||
}
|
||||
}
|
||||
|
||||
.export-dialog form .export-single-formats .switch-widget {
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
@ -1,8 +1,10 @@
|
||||
import { useState } from "preact/hooks";
|
||||
import { t } from "../../services/i18n";
|
||||
import tree from "../../services/tree";
|
||||
import { copyTextWithToast } from "../../services/clipboard_ext.js";
|
||||
import Button from "../react/Button";
|
||||
import FormRadioGroup from "../react/FormRadioGroup";
|
||||
import FormToggle from "../react/FormToggle";
|
||||
import Modal from "../react/Modal";
|
||||
import "./export.css";
|
||||
import ws from "../../services/ws";
|
||||
@ -21,10 +23,12 @@ interface ExportDialogProps {
|
||||
export default function ExportDialog() {
|
||||
const [ opts, setOpts ] = useState<ExportDialogProps>();
|
||||
const [ exportType, setExportType ] = useState<string>(opts?.defaultType ?? "subtree");
|
||||
const [ exportToClipboard, setExportToClipboard ] = useState(false);
|
||||
const [ subtreeFormat, setSubtreeFormat ] = useState("html");
|
||||
const [ singleFormat, setSingleFormat ] = useState("html");
|
||||
const [ opmlVersion, setOpmlVersion ] = useState("2.0");
|
||||
const [ shown, setShown ] = useState(false);
|
||||
const [ exporting, setExporting ] = useState(false);
|
||||
|
||||
useTriliumEvent("showExportDialog", async ({ notePath, defaultType }) => {
|
||||
const { noteId, parentNoteId } = tree.getNoteIdAndParentIdFromUrl(notePath);
|
||||
@ -47,18 +51,20 @@ export default function ExportDialog() {
|
||||
className="export-dialog"
|
||||
title={`${t("export.export_note_title")} ${opts?.noteTitle ?? ""}`}
|
||||
size="lg"
|
||||
onSubmit={() => {
|
||||
onSubmit={async () => {
|
||||
if (!opts || !opts.branchId) {
|
||||
return;
|
||||
}
|
||||
|
||||
const format = (exportType === "subtree" ? subtreeFormat : singleFormat);
|
||||
const version = (format === "opml" ? opmlVersion : "1.0");
|
||||
exportBranch(opts.branchId, exportType, format, version);
|
||||
setExporting(true);
|
||||
await exportBranch(opts.branchId, exportType, format, version, exportToClipboard);
|
||||
setExporting(false);
|
||||
setShown(false);
|
||||
}}
|
||||
onHidden={() => setShown(false)}
|
||||
footer={<Button className="export-button" text={t("export.export")} primary />}
|
||||
footer={<Button className="export-button" text={t("export.export")} primary disabled={exporting} />}
|
||||
show={shown}
|
||||
>
|
||||
|
||||
@ -118,6 +124,12 @@ export default function ExportDialog() {
|
||||
{ value: "markdown", label: t("export.format_markdown") }
|
||||
]}
|
||||
/>
|
||||
|
||||
<FormToggle
|
||||
switchOnName={t("export.export_to_clipboard")} switchOnTooltip={t("export.export_to_clipboard_on_tooltip")}
|
||||
switchOffName={t("export.export_to_clipboard")} switchOffTooltip={t("export.export_to_clipboard_off_tooltip")}
|
||||
currentValue={exportToClipboard} onChange={setExportToClipboard}
|
||||
/>
|
||||
</div>
|
||||
}
|
||||
|
||||
@ -125,10 +137,37 @@ export default function ExportDialog() {
|
||||
);
|
||||
}
|
||||
|
||||
function exportBranch(branchId: string, type: string, format: string, version: string) {
|
||||
async function exportBranch(branchId: string, type: string, format: string, version: string, exportToClipboard: boolean) {
|
||||
const taskId = utils.randomString(10);
|
||||
const url = open.getUrlForDownload(`api/branches/${branchId}/export/${type}/${format}/${version}/${taskId}`);
|
||||
open.download(url);
|
||||
if (type === "single" && exportToClipboard) {
|
||||
await exportSingleToClipboard(url);
|
||||
} else {
|
||||
open.download(url);
|
||||
}
|
||||
}
|
||||
|
||||
async function exportSingleToClipboard(url: string) {
|
||||
try {
|
||||
const res = await fetch(url);
|
||||
if (!res.ok) {
|
||||
throw new Error(`${res.status} ${res.statusText}`);
|
||||
}
|
||||
const blob = await res.blob();
|
||||
|
||||
// Try reading as text (HTML/Markdown are text); if that fails, fall back to ArrayBuffer->UTF-8
|
||||
let text: string;
|
||||
try {
|
||||
text = await blob.text();
|
||||
} catch {
|
||||
const ab = await blob.arrayBuffer();
|
||||
text = new TextDecoder("utf-8").decode(new Uint8Array(ab));
|
||||
}
|
||||
|
||||
await copyTextWithToast(text);
|
||||
} catch (error) {
|
||||
console.error("Failed to copy exported note to clipboard:", error);
|
||||
}
|
||||
}
|
||||
|
||||
ws.subscribeToMessages(async (message) => {
|
||||
|
||||
@ -34,6 +34,7 @@ const LOCALE_MAPPINGS: Record<DISPLAYABLE_LOCALE_IDS, Options["locale"] | null>
|
||||
"en-GB": "en",
|
||||
es: "es",
|
||||
fr: "fr",
|
||||
ga: null,
|
||||
it: "it",
|
||||
ja: "ja",
|
||||
pt: "pt",
|
||||
|
||||
@ -2,6 +2,7 @@ import { LOCALES } from "@triliumnext/commons";
|
||||
import { readdirSync } from "fs";
|
||||
import { join } from "path";
|
||||
import { describe, expect, it } from "vitest";
|
||||
|
||||
import { LANGUAGE_MAPPINGS } from "./i18n.js";
|
||||
|
||||
const localeDir = join(__dirname, "../../../../../../node_modules/@excalidraw/excalidraw/dist/prod/locales");
|
||||
@ -21,9 +22,9 @@ describe("Canvas i18n", () => {
|
||||
for (const locale of LOCALES) {
|
||||
if (locale.contentOnly || locale.devOnly) continue;
|
||||
const languageCode = LANGUAGE_MAPPINGS[locale.id];
|
||||
if (!supportedLanguageCodes.has(languageCode)) {
|
||||
if (languageCode && !supportedLanguageCodes.has(languageCode)) {
|
||||
console.log("Supported locales:", Array.from(supportedLanguageCodes.values()).join(", "));
|
||||
expect.fail(`Unable to find locale for ${locale.id} -> ${languageCode}.`)
|
||||
expect.fail(`Unable to find locale for ${locale.id} -> ${languageCode}.`);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
@ -10,6 +10,7 @@ export const LANGUAGE_MAPPINGS: Record<DISPLAYABLE_LOCALE_IDS, Language["code"]
|
||||
en_rtl: "en",
|
||||
es: "es-ES",
|
||||
fr: "fr-FR",
|
||||
ga: null,
|
||||
it: "it-IT",
|
||||
ja: "ja-JP",
|
||||
pt: "pt-PT",
|
||||
|
||||
@ -29,7 +29,7 @@ describe("CK config", () => {
|
||||
if (expectedLocale === "cn") expectedLocale = "zh";
|
||||
if (expectedLocale === "tw") expectedLocale = "zh-tw";
|
||||
|
||||
if (locale.id !== "en") {
|
||||
if (locale.id !== "en" && locale.id !== "ga") {
|
||||
expect((config.language as any).ui).toMatch(new RegExp(`^${expectedLocale}`));
|
||||
expect(config.translations, locale.id).toBeDefined();
|
||||
expect(config.translations, locale.id).toHaveLength(2);
|
||||
|
||||
@ -4,8 +4,9 @@ Once the Weblate translations for a single language have reached ~50% in coverag
|
||||
To do so:
|
||||
|
||||
1. In `packages/commons` look for `i18n.ts` and add a new entry to `UNSORTED_LOCALES` for the language.
|
||||
2. In `apps/server` look for `services/i18n.ts` and add a mapping for the new language in `DAYJS_LOADER`. Sort the entire list.
|
||||
2. In `packages/commons` look for `dayjs.ts` and add a mapping for the new language in `DAYJS_LOADER`. Sort the entire list.
|
||||
3. In `apps/client`, look for `collections/calendar/index.tsx` and modify `LOCALE_MAPPINGS` to add support to the new language.
|
||||
4. In `apps/client`, look for `widgets/type_widgets/canvas/i18n.ts` and modify `LANGUAGE_MAPPINGS`. A unit test ensures that the language is actually loadable.
|
||||
5. In `apps/client`, look for `widgets/type_widgets/MindMap.tsx` and modify `LOCALE_MAPPINGS`. The type definitions should already validate if the new value is supported by Mind Elixir.
|
||||
6. In `packages/ckeditor5`, look for `i18n.ts` and modify `LOCALE_MAPPINGS`. The import validation should already check if the new value is supported by CKEditor, and there's also a test to ensure it.
|
||||
6. In `packages/ckeditor5`, look for `i18n.ts` and modify `LOCALE_MAPPINGS`. The import validation should already check if the new value is supported by CKEditor, and there's also a test to ensure it.
|
||||
7. Locale mappings for PDF.js might need adjustment. To do so, in `packages/pdfjs-viewer/scripts/build.ts` there is `LOCALE_MAPPINGS`.
|
||||
@ -40,6 +40,7 @@ const LOCALE_MAPPINGS: Record<DISPLAYABLE_LOCALE_IDS, LocaleMapping | null> = {
|
||||
coreTranslation: () => import("ckeditor5/translations/fr.js"),
|
||||
premiumFeaturesTranslation: () => import("ckeditor5-premium-features/translations/fr.js"),
|
||||
},
|
||||
ga: null,
|
||||
it: {
|
||||
languageCode: "it",
|
||||
coreTranslation: () => import("ckeditor5/translations/it.js"),
|
||||
|
||||
@ -41,6 +41,7 @@ export const DAYJS_LOADER: Record<LOCALE_IDS, () => Promise<typeof import("dayjs
|
||||
"es": () => import("dayjs/locale/es.js"),
|
||||
"fa": () => import("dayjs/locale/fa.js"),
|
||||
"fr": () => import("dayjs/locale/fr.js"),
|
||||
"ga": () => import("dayjs/locale/ga.js"),
|
||||
"it": () => import("dayjs/locale/it.js"),
|
||||
"he": () => import("dayjs/locale/he.js"),
|
||||
"ja": () => import("dayjs/locale/ja.js"),
|
||||
|
||||
@ -19,6 +19,7 @@ const UNSORTED_LOCALES = [
|
||||
{ id: "en-GB", name: "English (United Kingdom)", electronLocale: "en_GB" },
|
||||
{ id: "es", name: "Español", electronLocale: "es" },
|
||||
{ id: "fr", name: "Français", electronLocale: "fr" },
|
||||
{ id: "ga", name: "Gaeilge", electronLocale: "en" },
|
||||
{ id: "it", name: "Italiano", electronLocale: "it" },
|
||||
{ id: "ja", name: "日本語", electronLocale: "ja" },
|
||||
{ id: "pt_br", name: "Português (Brasil)", electronLocale: "pt_BR" },
|
||||
|
||||
@ -10,7 +10,8 @@ const build = new BuildHelper("packages/pdfjs-viewer");
|
||||
const watchMode = process.argv.includes("--watch");
|
||||
|
||||
const LOCALE_MAPPINGS: Record<string, string> = {
|
||||
"es": "es-ES"
|
||||
"es": "es-ES",
|
||||
"ga": "ga-IE"
|
||||
};
|
||||
|
||||
async function main() {
|
||||
@ -28,8 +29,9 @@ async function main() {
|
||||
// Copy locales.
|
||||
const localeMappings = {};
|
||||
for (const locale of LOCALES) {
|
||||
if (locale.id === "en" || locale.contentOnly || locale.devOnly) continue;
|
||||
const mappedLocale = LOCALE_MAPPINGS[locale.electronLocale] || locale.electronLocale.replace("_", "-");
|
||||
if (locale.contentOnly || locale.devOnly) continue;
|
||||
const mappedLocale = LOCALE_MAPPINGS[locale.id] || locale.electronLocale.replace("_", "-");
|
||||
if (mappedLocale === "en") continue;
|
||||
const localePath = `${locale.id}/viewer.ftl`;
|
||||
build.copy(`viewer/locale/${mappedLocale}/viewer.ftl`, `web/locale/${localePath}`);
|
||||
localeMappings[locale.id] = localePath;
|
||||
|
||||
14
pnpm-lock.yaml
generated
14
pnpm-lock.yaml
generated
@ -258,8 +258,8 @@ importers:
|
||||
specifier: 3.0.0
|
||||
version: 3.0.0
|
||||
force-graph:
|
||||
specifier: 1.51.0
|
||||
version: 1.51.0
|
||||
specifier: 1.51.1
|
||||
version: 1.51.1
|
||||
globals:
|
||||
specifier: 17.3.0
|
||||
version: 17.3.0
|
||||
@ -8799,8 +8799,8 @@ packages:
|
||||
resolution: {integrity: sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg==}
|
||||
engines: {node: '>= 0.4'}
|
||||
|
||||
force-graph@1.51.0:
|
||||
resolution: {integrity: sha512-aTnihCmiMA0ItLJLCbrQYS9mzriopW24goFPgUnKAAmAlPogTSmFWqoBPMXzIfPb7bs04Hur5zEI4WYgLW3Sig==}
|
||||
force-graph@1.51.1:
|
||||
resolution: {integrity: sha512-uEEX8iRzgq1IKRISOw6RrB2RLMhcI25xznQYrCTVvxZHZZ+A2jH6qIolYuwavVxAMi64pFp2yZm4KFVdD993cg==}
|
||||
engines: {node: '>=12'}
|
||||
|
||||
foreach@2.0.6:
|
||||
@ -15999,6 +15999,8 @@ snapshots:
|
||||
'@ckeditor/ckeditor5-core': 47.4.0
|
||||
'@ckeditor/ckeditor5-utils': 47.4.0
|
||||
ckeditor5: 47.4.0
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
'@ckeditor/ckeditor5-code-block@47.4.0(patch_hash=2361d8caad7d6b5bddacc3a3b4aa37dbfba260b1c1b22a450413a79c1bb1ce95)':
|
||||
dependencies:
|
||||
@ -16729,6 +16731,8 @@ snapshots:
|
||||
'@ckeditor/ckeditor5-ui': 47.4.0
|
||||
'@ckeditor/ckeditor5-utils': 47.4.0
|
||||
ckeditor5: 47.4.0
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
'@ckeditor/ckeditor5-restricted-editing@47.4.0':
|
||||
dependencies:
|
||||
@ -25101,7 +25105,7 @@ snapshots:
|
||||
dependencies:
|
||||
is-callable: 1.2.7
|
||||
|
||||
force-graph@1.51.0:
|
||||
force-graph@1.51.1:
|
||||
dependencies:
|
||||
'@tweenjs/tween.js': 25.0.0
|
||||
accessor-fn: 1.5.3
|
||||
|
||||
@ -2,20 +2,21 @@ import { LOCALES } from "../../packages/commons/src/lib/i18n";
|
||||
import { getLanguageStats } from "./utils";
|
||||
|
||||
async function main() {
|
||||
const languageStats = await getLanguageStats("client");
|
||||
const localeIdsWithCoverage = languageStats.results
|
||||
const project = "client";
|
||||
const languageStats = await getLanguageStats(project);
|
||||
const localesWithCoverage = languageStats.results
|
||||
.filter(language => language.translated_percent > 50)
|
||||
.map(language => language.language_code);
|
||||
|
||||
for (const localeId of localeIdsWithCoverage) {
|
||||
for (const localeData of localesWithCoverage) {
|
||||
const { language_code: localeId, translated_percent: percentage, language } = localeData;
|
||||
const locale = LOCALES.find(l => l.id === localeId);
|
||||
if (!locale) {
|
||||
console.error(`Locale not found for id: ${localeId}`);
|
||||
console.error(`❌ Language ${language.name} (${localeId}) has a coverage of ${percentage}% in '${project}', but it is not supported by the application.`);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
console.log("Translation coverage check passed.");
|
||||
console.log("✅ Translation coverage check passed.");
|
||||
}
|
||||
|
||||
main();
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user