diff --git a/.env b/.env index 4392f00..82cde0e 100644 --- a/.env +++ b/.env @@ -1,3 +1,5 @@ BASE=/mink/ VITE_BACKEND_URL=https://ws.spraakbanken.gu.se/ws/mink/ +VITE_AUTH_URL=https://sp.spraakbanken.gu.se/auth/ VITE_KORP_URL=https://spraakbanken.gu.se/korp/ +VITE_STRIX_URL=https://spraakbanken.gu.se/strix/ diff --git a/src/api/api.js b/src/api/api.js index 6943308..9dbe09e 100644 --- a/src/api/api.js +++ b/src/api/api.js @@ -112,6 +112,11 @@ class MinkApi { return response.data; } + async checkStatusAll() { + const response = await this.axios.get("check-status"); + return response.data.jobs; + } + async runSparv(corpusId) { const response = await this.axios .put("run-sparv", null, { params: { corpus_id: corpusId } }) @@ -154,13 +159,20 @@ class MinkApi { return response.data; } - async installCorpus(corpusId) { + async installKorp(corpusId) { const response = await this.axios.put("install-korp", null, { params: { corpus_id: corpusId }, }); return response.data; } + async installStrix(corpusId) { + const response = await this.axios.put("install-strix", null, { + params: { corpus_id: corpusId }, + }); + return response.data; + } + async adminModeOn() { const response = await this.axios.post("admin-mode-on"); return response.data; diff --git a/src/api/backend.composable.js b/src/api/backend.composable.js index ff89d8d..d410c13 100644 --- a/src/api/backend.composable.js +++ b/src/api/backend.composable.js @@ -76,12 +76,22 @@ export default function useMinkBackend() { const loadJob = (corpusId) => spin(api.checkStatus(corpusId), t("job.loading"), `corpus/${corpusId}/job`); + const loadJobs = () => + spin(api.checkStatusAll(), t("job.loading"), `corpora`); + const runJob = (corpusId) => spin(api.runSparv(corpusId), t("job.starting"), `corpus/${corpusId}/job`); - const install = (corpusId) => + const installKorp = (corpusId) => + spin( + api.installKorp(corpusId), + t("job.installing"), + `corpus/${corpusId}/job` + ); + + const installStrix = (corpusId) => spin( - api.installCorpus(corpusId), + api.installStrix(corpusId), t("job.installing"), `corpus/${corpusId}/job` ); @@ -129,8 +139,10 @@ export default function useMinkBackend() { uploadSources, deleteSource, loadJob, + loadJobs, runJob, - install, + installKorp, + installStrix, abortJob, loadExports, downloadExports, diff --git a/src/api/corpusConfig.js b/src/api/corpusConfig.js index 3337479..a9f7164 100644 --- a/src/api/corpusConfig.js +++ b/src/api/corpusConfig.js @@ -64,6 +64,7 @@ export async function makeConfig(id, options) { ":readability.ovix", ":readability.nk", ":misc.source", + ":misc.id as _id", ], }; diff --git a/src/auth/auth.composable.js b/src/auth/auth.composable.js index 0f4c08f..d1b1e5c 100644 --- a/src/auth/auth.composable.js +++ b/src/auth/auth.composable.js @@ -31,11 +31,11 @@ export function useAuth() { const isAuthenticated = computed(() => !!jwt.value); const payload = computed(() => decodeJwt(jwt.value)?.payload); - const canUserAdmin = computed(() => - canAdmin(payload.value, "other", "mink-app") + const canUserAdmin = computed( + () => payload.value && canAdmin(payload.value, "other", "mink-app") ); - const canUserWrite = computed(() => - canWrite(payload.value, "other", "mink-app") + const canUserWrite = computed( + () => payload.value && canWrite(payload.value, "other", "mink-app") ); /** If not authenticated, redirect to the login page. */ @@ -66,7 +66,7 @@ export function useAuth() { // Schedule next request shortly before expiration time. clearTimeout(refreshTimer); - if (payload.value) { + if (payload.value && payload.value.exp) { const timeoutMs = (payload.value.exp - 10) * 1000 - Date.now(); refreshTimer = setTimeout(refreshJwt, timeoutMs); } diff --git a/src/auth/auth.js b/src/auth/auth.js index a13d250..f794fb5 100644 --- a/src/auth/auth.js +++ b/src/auth/auth.js @@ -1,6 +1,6 @@ import { pathJoin } from "@/util"; -export const AUTH_BASE = "https://sp.spraakbanken.gu.se/auth"; +export const AUTH_BASE = import.meta.env.VITE_AUTH_URL; export function getLoginUrl(redirectLocation = "") { // Prepend redirect location with Mink base url. @@ -9,11 +9,12 @@ export function getLoginUrl(redirectLocation = "") { import.meta.env.BASE_URL, redirectLocation ); - return `${AUTH_BASE}/login?redirect=${redirectLocation}`; + return AUTH_BASE + `login?redirect=${redirectLocation}`; } export async function checkLogin() { - const response = await fetch(`${AUTH_BASE}/jwt`, { credentials: "include" }); + const url = import.meta.env.VITE_JWT_URL || AUTH_BASE + "jwt"; + const response = await fetch(url, { credentials: "include" }); if (response.ok) return await response.text(); return false; } diff --git a/src/auth/jwtSb.js b/src/auth/jwtSb.js index 3383e79..6e52769 100644 --- a/src/auth/jwtSb.js +++ b/src/auth/jwtSb.js @@ -15,14 +15,31 @@ export function decodeJwt(jwt) { }; } +export function assertValidPayload(payload) { + const isValid = + payload && + payload.scope && + payload.levels && + payload.levels.ADMIN && + payload.levels.WRITE && + payload.levels.READ; + + if (!isValid) { + throw new TypeError("Malformed jwt payload: " + JSON.stringify(payload)); + } +} + export function canAdmin(payload, resourceType, resourceName) { - return payload?.scope[resourceType]?.[resourceName] >= payload?.levels.ADMIN; + assertValidPayload(payload); + return payload.scope[resourceType]?.[resourceName] >= payload.levels.ADMIN; } export function canWrite(payload, resourceType, resourceName) { - return payload?.scope[resourceType]?.[resourceName] >= payload?.levels.WRITE; + assertValidPayload(payload); + return payload.scope[resourceType]?.[resourceName] >= payload.levels.WRITE; } export function canRead(payload, resourceType, resourceName) { - return payload?.scope[resourceType]?.[resourceName] >= payload?.levels.READ; + assertValidPayload(payload); + return payload.scope[resourceType]?.[resourceName] >= payload.levels.READ; } diff --git a/src/corpora/CreateCorpus.vue b/src/corpora/CreateCorpus.vue index af2a70e..e05377f 100644 --- a/src/corpora/CreateCorpus.vue +++ b/src/corpora/CreateCorpus.vue @@ -41,6 +41,11 @@ validate="required" /> + + + {{ $t("config.format.note.pdf") }} + + result.status != "fulfilled" - ); - if (rejectedResults.length) { - // Display error message(s). - rejectedResults.forEach((result) => alertError(result.reason)); - // Discard the empty corpus. - await deleteCorpus(corpusId).catch(alertError); - return; + // Store the pending request in module scope, so simultaneous calls will await the same promise. + if (!loadPromise) { + loadPromise = mink + .loadCorpora() + .catch(alertError) + .then((corpora) => corpusStore.setCorpusIds(corpora)); + + // This request can take some time, so better not await it. + mink + .loadJobs() + .catch(alertError) + .then((jobs) => + jobs.forEach((job) => { + corpusStore.corpora[job.corpus_id].status = job; + corpusStore.corpora[job.corpus_id].sources = job.available_files; + }) + ); } - router.push(`/corpus/${corpusId}`); - } - - async function createFromConfig(name, description, format, textAnnotation) { - const config = { - name: { swe: name, eng: name }, - description: { swe: description, eng: description }, - format, - textAnnotation, - }; + await loadPromise; - // Create an empty corpus. If it fails, abort. - let corpusId; - try { - corpusId = await createCorpus().catch(alertError); - } catch (e) { - alertError(e); - return; - } - - // Upload the basic config. - try { - await uploadConfig(config, corpusId); - // Show the created corpus. - router.push(`/corpus/${corpusId}`); - return corpusId; - } catch (e) { - // If creating the config fails, there's a TypeError. - if (e.name == "TypeError") alert(e.message, "error"); - // Otherwise it's probably a backend error when saving. - else alertError(e); - // Discard the empty corpus. - await deleteCorpus(corpusId).catch(alertError); - } + loadPromise = null; + isCorporaFresh = true; } return { loadCorpora, - createFromUpload, - createFromConfig, }; } diff --git a/src/corpus/Corpus.vue b/src/corpus/Corpus.vue index 4f9886e..6030374 100644 --- a/src/corpus/Corpus.vue +++ b/src/corpus/Corpus.vue @@ -27,7 +27,6 @@ import { computed } from "vue"; import { useAuth } from "@/auth/auth.composable"; import { useCorpusStore } from "@/store/corpus.store"; import useCorpusIdParam from "./corpusIdParam.composable"; -import useCorpora from "@/corpora/corpora.composable"; import useCorpus from "./corpus.composable.js"; import useConfig from "./config/config.composable"; import PageTitle from "@/components/PageTitle.vue"; @@ -35,14 +34,12 @@ import PageTitle from "@/components/PageTitle.vue"; const corpusStore = useCorpusStore(); const { requireAuthentication, isAuthenticated } = useAuth(); const corpusId = useCorpusIdParam(); -const { loadCorpora } = useCorpora(); const { loadCorpus } = useCorpus(corpusId); const { corpusName } = useConfig(corpusId); const corpus = computed(() => corpusStore.corpora[corpusId]); requireAuthentication(async () => { - await loadCorpora(); await loadCorpus(); }); diff --git a/src/corpus/config/CorpusConfiguration.vue b/src/corpus/config/CorpusConfiguration.vue index 8b21756..3803060 100644 --- a/src/corpus/config/CorpusConfiguration.vue +++ b/src/corpus/config/CorpusConfiguration.vue @@ -27,6 +27,11 @@ :help="$t('config.format.help')" /> + + + {{ $t("config.format.note.pdf") }} + + ); const selectedFormat = computed(() => { - return extensions.value.includes(config.value.format) + return !extensions.value.length || + extensions.value.includes(config.value.format) ? config.value.format : undefined; }); diff --git a/src/corpus/corpus.composable.js b/src/corpus/corpus.composable.js index a2adad4..14daf82 100644 --- a/src/corpus/corpus.composable.js +++ b/src/corpus/corpus.composable.js @@ -2,10 +2,9 @@ import { useAuth } from "@/auth/auth.composable"; import useMinkBackend from "@/api/backend.composable"; import { useCorpusStore } from "@/store/corpus.store"; import useMessenger from "@/message/messenger.composable"; +import useCorpora from "@/corpora/corpora.composable"; import useConfig from "./config/config.composable"; import useExports from "./exports/exports.composable"; -import useJob from "./job/job.composable"; -import useSources from "./sources/sources.composable"; /** Let data be refreshed initially, but skip subsequent load calls. */ const isCorpusFresh = {}; @@ -14,22 +13,19 @@ export default function useCorpus(corpusId) { const corpusStore = useCorpusStore(); const { refreshJwt } = useAuth(); const mink = useMinkBackend(); + const { loadCorpora } = useCorpora(); const { alertError } = useMessenger(); const { loadConfig } = useConfig(corpusId); const { loadExports } = useExports(corpusId); - const { loadJob } = useJob(corpusId); - const { loadSources } = useSources(corpusId); async function loadCorpus(force = false) { - const isLoaded = Object.keys(corpusStore.corpora[corpusId]).length; - if (isLoaded && isCorpusFresh[corpusId] && !force) { + await loadCorpora(); + if (isCorpusFresh[corpusId] && !force) { return; } await Promise.all([ loadConfig(), // loadExports(), - loadJob(), - loadSources(), ]); isCorpusFresh[corpusId] = true; } diff --git a/src/corpus/corpusState.composable.js b/src/corpus/corpusState.composable.js index 45feb66..e4ad382 100644 --- a/src/corpus/corpusState.composable.js +++ b/src/corpus/corpusState.composable.js @@ -9,7 +9,7 @@ import useSources from "./sources/sources.composable"; export function useCorpusState(corpusId) { const { sources } = useSources(corpusId); const { config } = useConfig(corpusId); - const { sparvStatus, korpStatus } = useJob(corpusId); + const { sparvStatus, korpStatus, strixStatus } = useJob(corpusId); const { t } = useI18n(); const corpusState = computed(() => { @@ -19,14 +19,20 @@ export function useCorpusState(corpusId) { if (sparvStatus.value.isReady) return CorpusState.READY; if (sparvStatus.value.isError) return CorpusState.FAILED; - if (korpStatus.value.isRunning) return CorpusState.RUNNING_INSTALL; + + // TODO Revise the CorpusState concept. The workflow can now branch in two. Ifs below questionable. + if (korpStatus.value.isRunning || strixStatus.value.isRunning) + return CorpusState.RUNNING_INSTALL; if (sparvStatus.value.isRunning) return CorpusState.RUNNING; - if (korpStatus.value.isReady) return CorpusState.DONE; - if (korpStatus.value.isError) return CorpusState.FAILED_INSTALL; - if (korpStatus.value.isDone) return CorpusState.DONE_INSTALL; + if (korpStatus.value.isReady && strixStatus.value.isReady) + return CorpusState.DONE; + if (korpStatus.value.isError || strixStatus.value.isError) + return CorpusState.FAILED_INSTALL; + if (korpStatus.value.isDone || strixStatus.value.isDone) + return CorpusState.DONE_INSTALL; throw RangeError( - `Invalid state, sparv=${sparvStatus.value}, korp=${korpStatus.value}` + `Invalid state, sparv=${sparvStatus.value.state}, korp=${korpStatus.value.state}, strix=${strixStatus.value.state}` ); }); diff --git a/src/corpus/createCorpus.composable.js b/src/corpus/createCorpus.composable.js new file mode 100644 index 0000000..d4d5fb2 --- /dev/null +++ b/src/corpus/createCorpus.composable.js @@ -0,0 +1,90 @@ +import { useRouter } from "vue-router"; +import { useAuth } from "@/auth/auth.composable"; +import useMinkBackend from "@/api/backend.composable"; +import { emptyConfig } from "@/api/corpusConfig"; +import { useCorpusStore } from "@/store/corpus.store"; +import useMessenger from "@/message/messenger.composable"; +import useConfig from "./config/config.composable"; +import useSources from "./sources/sources.composable"; +import useCorpus from "./corpus.composable"; + +export default function useCorpora() { + const corpusStore = useCorpusStore(); + const router = useRouter(); + const { refreshJwt } = useAuth(); + const { deleteCorpus } = useCorpus(); + const { uploadConfig } = useConfig(); + const { uploadSources } = useSources(); + const { alert, alertError } = useMessenger(); + const mink = useMinkBackend(); + + async function createCorpus() { + const corpusId = await mink.createCorpus().catch(alertError); + // Have the new corpus included in further API calls. + await refreshJwt(); + // Adding the new id to store may trigger API calls, so do it after updating the JWT. + corpusStore.corpora[corpusId] = corpusStore.corpora[corpusId] || {}; + return corpusId; + } + + async function createFromUpload(files) { + const corpusId = await createCorpus().catch(alertError); + if (!corpusId) return; + + const results = await Promise.allSettled([ + uploadSources(files, corpusId), + uploadConfig(emptyConfig(), corpusId), + ]); + + const rejectedResults = results.filter( + (result) => result.status != "fulfilled" + ); + if (rejectedResults.length) { + // Display error message(s). + rejectedResults.forEach((result) => alertError(result.reason)); + // Discard the empty corpus. + await deleteCorpus(corpusId).catch(alertError); + return; + } + + router.push(`/corpus/${corpusId}`); + } + + async function createFromConfig(name, description, format, textAnnotation) { + const config = { + name: { swe: name, eng: name }, + description: { swe: description, eng: description }, + format, + textAnnotation, + }; + + // Create an empty corpus. If it fails, abort. + let corpusId; + try { + corpusId = await createCorpus().catch(alertError); + } catch (e) { + alertError(e); + return; + } + + // Upload the basic config. + try { + await uploadConfig(config, corpusId); + // Show the created corpus. + router.push(`/corpus/${corpusId}`); + return corpusId; + } catch (e) { + // If creating the config fails, there's a TypeError. + if (e.name == "TypeError") alert(e.message, "error"); + // Otherwise it's probably a backend error when saving. + else alertError(e); + // Discard the empty corpus. + await deleteCorpus(corpusId).catch(alertError); + } + } + + return { + createFromUpload, + createFromConfig, + }; +} diff --git a/src/corpus/exports/Exports.vue b/src/corpus/exports/Exports.vue index 6161d13..1284f93 100644 --- a/src/corpus/exports/Exports.vue +++ b/src/corpus/exports/Exports.vue @@ -4,33 +4,92 @@ class="grid grid-cols-2 gap-4" >
-

Korp

-

{{ $t("exports.korp.help") }}

-
- - {{ - !korpStatus.isDone - ? $t("exports.korp.install") - : $t("exports.korp.reinstall") - }} - - - - - {{ $t("exports.korp.view") }} +

+ {{ $t("tools") }} +

+

{{ $t("exports.tools.help") }}

+ +
+ + +
+ + {{ + $t( + !korpStatus.isDone + ? "exports.tools.install" + : "exports.tools.reinstall" + ) + }} + + + + + {{ $t("exports.tools.view") }} + + + + {{ $t("exports.tools.view") }} - - - {{ $t("exports.korp.view") }} - +
+
+ +
+
+

Strix

+

+ {{ $t("exports.tools.help.strix") }} +

+
+ +
+ + {{ + $t( + !strixStatus.isDone + ? "exports.tools.install" + : "exports.tools.reinstall" + ) + }} + + + + + {{ $t("exports.tools.view") }} + + + + {{ $t("exports.tools.view") }} + +
@@ -76,21 +135,30 @@ const corpusId = useCorpusIdParam(); const { exports, loadExports, downloadResult, getDownloadFilename } = useExports(corpusId); const { isDone } = useCorpusState(corpusId); -const { install, sparvStatus, korpStatus } = useJob(corpusId); +const { installKorp, installStrix, sparvStatus, korpStatus, strixStatus } = + useJob(corpusId); const korpUrl = ensureTrailingSlash(import.meta.env.VITE_KORP_URL); +const strixUrl = ensureTrailingSlash(import.meta.env.VITE_STRIX_URL); const isInstallPending = ref(false); const canInstall = computed( () => sparvStatus.value.isDone && !korpStatus.value.isRunning && + !strixStatus.value.isRunning && !isInstallPending.value ); async function korpInstall() { isInstallPending.value = true; - await install(); + await installKorp(); + isInstallPending.value = false; +} + +async function strixInstall() { + isInstallPending.value = true; + await installStrix(); isInstallPending.value = false; } diff --git a/src/corpus/job/job.composable.js b/src/corpus/job/job.composable.js index 1f6c8dc..d904d90 100644 --- a/src/corpus/job/job.composable.js +++ b/src/corpus/job/job.composable.js @@ -64,8 +64,12 @@ export default function useJob(corpusId) { corpus.value.status = await mink.runJob(corpusId).catch(alertError); } - async function install() { - corpus.value.status = await mink.install(corpusId).catch(alertError); + async function installKorp() { + corpus.value.status = await mink.installKorp(corpusId).catch(alertError); + } + + async function installStrix() { + corpus.value.status = await mink.installStrix(corpusId).catch(alertError); } async function abortJob() { @@ -80,18 +84,22 @@ export default function useJob(corpusId) { const korpStatus = computed( () => new JobStatus(jobStatus.value?.job_status?.korp) ); - const currentStatus = computed( - () => - ({ - sparv: sparvStatus.value, - korp: korpStatus.value, - }[jobStatus.value.current_process]) + const strixStatus = computed( + () => new JobStatus(jobStatus.value?.job_status?.strix) ); + const currentStatus = computed(() => { + const process = jobStatus.value.current_process; + return new JobStatus(jobStatus.value?.job_status?.[process]); + }); // "Running" if any job is waiting/running. - const isJobRunning = computed( - () => sparvStatus.value.isRunning || korpStatus.value.isRunning - ); + const isJobRunning = computed(() => { + const statuses = jobStatus.value?.job_status; + if (!statuses) return false; + return Object.keys(statuses).some( + (process) => new JobStatus(statuses[process]).isRunning + ); + }); // "Done" if Sparv is done, and Korp is not running/error. const isJobDone = computed( @@ -114,10 +122,12 @@ export default function useJob(corpusId) { loadJob, runJob, abortJob, - install, + installKorp, + installStrix, jobStatus, sparvStatus, korpStatus, + strixStatus, currentStatus, isJobRunning, isJobDone, diff --git a/src/i18n/locales/en.yaml b/src/i18n/locales/en.yaml index a72a6d6..085fc05 100644 --- a/src/i18n/locales/en.yaml +++ b/src/i18n/locales/en.yaml @@ -6,8 +6,8 @@ home.features.sparv.title: Configurable language technology analysis home.features.sparv.body: Your language data are analyzed by our language technology platform {sparv} according to your settings. home.features.upload.title: Deposit your text home.features.upload.body: Upload your word-processor documents, PDFs, plain text files or XML data. When using XML, the output of the analyses is added while keeping the input structure intact. -home.features.explore.title: Explore in Korp -home.features.explore.body: Export the resulting data, for search and statistical analysis, to {korp} and (soon) {strix}. +home.features.explore.title: Explore in Korp and Strix +home.features.explore.body: Export the resulting data, for search and statistical analysis, to {korp} and {strix}. home.features.share.title: Share and collaborate home.features.share.body: Invite fellow researchers to view your data in our research tools, protected by login. If you want to share your data with the research community, contact us to discuss publication. home.features.share.upcoming: These features are scheduled for upcoming versions of Mink. @@ -41,9 +41,9 @@ corpus.state.help.needing_config: This corpus needs you to change some settings corpus.state.help.needing_meta: The corpus needs a name before it can be processed. corpus.state.help.ready: Ready for annotation. You can also supply more source texts or edit the configuration. corpus.state.help.running: Annotation is running in the background. Feel free to close the website, have some coffee and check back later! -corpus.state.help.done: Annotation done! You can download the results or install it to Korp. +corpus.state.help.done: Annotation done! You can download the results or install it to Korp or Strix. corpus.state.help.running_install: Installation is running in the background. Feel free to close the website, have some coffee and check back later! -corpus.state.help.done_install: Successfully installed in Korp! +corpus.state.help.done_install: Successfully installed! corpus.state.help.failed: An error occurred while processing your data. If you need help, please contact sb-info{'@'}svenska.gu.se corpus.state.help.failed_install: An error occurred while installing your corpus. If you need help, please contact sb-info{'@'}svenska.gu.se config.metadata.help: Metadata is information about the data, meant for assessment by interested users (if you release the data), and for categorization in data repositories. @@ -64,6 +64,7 @@ job.last_run_ended: Last run ended job.time_taken: Time taken job.process.sparv: Annotation job.process.korp: Installation to Korp +job.process.strix: Installation to Strix source.deleting: Deleting text source.downloading: Downloading text source.downloading_plain: Downloading extracted text @@ -79,11 +80,16 @@ source.upload.drop.empty: No file was dropped. Drag-and-drop may not be supporte source.list.loading: Listing source texts source.content: Content jwt.refreshing: Refreshing account +tools: Explore exports.loading: Listing result files -exports.korp.install: Add to Korp -exports.korp.reinstall: Re-add to Korp -exports.korp.view: View in Korp -exports.korp.help: Use this data in Språkbanken's corpus search tool. Your data will be available to you only, in a special Mink mode. +exports.tools.help: Use this data in Språkbanken's corpus tools. Your data will be available to you only, in a special Mink mode. +exports.tools.help.korp: Korp offers concordance search, detailed queries, frequency statistics and more. +exports.tools.help.korp.manual.url: https://spraakbanken.gu.se/en/tools/korp/user-manual +exports.tools.help.korp.manual.text: User manual +exports.tools.help.strix: In Strix, the enriched texts can be examined in full. +exports.tools.install: Install +exports.tools.reinstall: Reinstall +exports.tools.view: View exports.download.help: Results can be downloaded as machine-readable files for processing in scripts or other specialized software. exports.downloading: Downloading results exports.help: The annotation process yields export files for each input file. You can download each single export file or all of them in a bundle. The exact structure of the files depends on the input files as well as the configuration. They are stored on the server indefinitely, but re-running the annotation will replace them with new ones. @@ -156,6 +162,7 @@ metadata.description.help: | to summarize the content or purpose to a potential user. fileFormat: Source format config.format.help: Select the file format of the source texts. All files must be of the selected format. +config.format.note.pdf: The PDF file format is primarily designed for display. Please note that extracting text from them can sometimes give unsatisfactory results. Performing OCR on scanned documents is (currently) outside the scope of Mink. config.text_annotation: Text element config.text_annotation.help: In your source files, what XML element is most representative of a single text? Any text-level annotations will be attached to this element. segmenter_sentence: Existing sentence segmentation diff --git a/src/i18n/locales/sv.yaml b/src/i18n/locales/sv.yaml index 5d828db..ee93014 100644 --- a/src/i18n/locales/sv.yaml +++ b/src/i18n/locales/sv.yaml @@ -6,8 +6,8 @@ home.features.sparv.title: Anpassningsbara analyser home.features.sparv.body: Dina texter analyseras av vår språkteknologiska plattform {sparv} med de inställningar du väljer. home.features.upload.title: Ladda upp din text home.features.upload.body: Ladda upp ordbehandlingsdokument, pdf:er, ren text eller XML. Om du använder XML läggs analyserna in utan att påverka strukturen från originalfilerna. -home.features.explore.title: Utforska i Korp -home.features.explore.body: Exportera den färdiga datan för sökning och statistiska undersökningar i {korp} och (snart) {strix}. +home.features.explore.title: Utforska i Korp och Strix +home.features.explore.body: Exportera den färdiga datan för sökning och statistiska undersökningar i {korp} och {strix}. home.features.share.title: Dela och samarbeta home.features.share.body: Bjud in andra forskare att ta del av dina data i våra forskningsverktyg, skyddat bakom inloggning. Om du vill göra dina data tillgängliga för alla kan du kontakta oss för att diskutera publicering. home.features.share.upcoming: De här funktionerna planeras till kommande Mink-versioner. @@ -42,9 +42,9 @@ corpus.state.help.needing_config: Den här korpusen behöver några inställning corpus.state.help.needing_meta: Korpusen behöver ha ett namn innan annoteringen kan startas. corpus.state.help.ready: Redo för annotering. Du kan också lägga till fler källtexter eller redigera konfigurationen. corpus.state.help.running: Annoteringen körs i bakgrunden. Det går bra att stänga webbsidan, ta en kaffe och kolla läget igen senare! -corpus.state.help.done: Annotering klar! Du kan ladda ner resultaten eller installera datan i Korp. +corpus.state.help.done: Annotering klar! Du kan ladda ner resultaten eller installera datan i Korp eller Strix. corpus.state.help.running_install: Installationen körs i bakgrunden. Det går bra att stänga webbsidan, ta en kaffe och kolla läget igen senare! -corpus.state.help.done_install: Nu är datan installerad i Korp! +corpus.state.help.done_install: Nu är datan installerad! corpus.state.help.failed: Ett fel uppstod under annotering. Om du behöver hjälp kan du kontakta sb-info{'@'}svenska.gu.se corpus.state.help.failed_install: Ett fel uppstod under installation. Om du behöver hjälp kan du kontakta sb-info{'@'}svenska.gu.se config.metadata.help: Metadata är information om datan, och fungerar som underlag för potentiellt intresserade användare (om du publicerar datan) samt underlättar katalogiseringen i datarepositorier. @@ -65,6 +65,7 @@ job.last_run_ended: Senast avslutad job.time_taken: Tidsåtgång job.process.sparv: Annotering job.process.korp: Installation i Korp +job.process.strix: Installation i Strix source.deleting: Raderar text source.downloading: Laddar ner text source.downloading_plain: Laddar ner extraherad text @@ -80,11 +81,16 @@ source.upload.drop.empty: Ingen fil släpptes. Dra-och-släpp kanske inte stöds source.list.loading: Hämtar textlista source.content: Innehåll jwt.refreshing: Uppdaterar konto +tools: Utforska exports.loading: Hämtar resultatlista -exports.korp.install: Installera i Korp -exports.korp.reinstall: Installera om i Korp -exports.korp.view: Visa i Korp -exports.korp.help: Använd den här datan i Språkbankens korpusverktyg. Din data visas bara för dig, i ett särskilt Mink-läge. +exports.tools.help: Använd den här datan i Språkbankens korpusverktyg. Din data visas bara för dig, i ett särskilt Mink-läge. +exports.tools.help.korp: I Korp finns konkordanssökning, detaljerade sökfrågor, frekvensstatistik m m. +exports.tools.help.korp.manual.url: https://spraakbanken.gu.se/verktyg/korp/anvandarhandledning +exports.tools.help.korp.manual.text: Användarhandledning +exports.tools.help.strix: I Strix kan de berikade texterna undersökas i sin helhet. +exports.tools.install: Installera +exports.tools.reinstall: Installera om +exports.tools.view: Visa exports.download.help: Resultatet kan laddas ner som maskinläsbara filer för bearbetning i script eller speciella program. exports.downloading: Laddar ner analysresultat exports.help: Annoteringsprocessen skapar exportfiler för varje källfil, och de kan laddas ner enskilt eller som arkiv. Filernas struktur beror både på källfilerna och konfigurationen. De lagras på servern tillsvidare, men ersätts med nya om du kör om annoteringen på nytt. @@ -157,6 +163,7 @@ metadata.description.help: | som sammanfattar innehållet eller syftet för en potentiell användare. fileFormat: Källformat config.format.help: Ange vilket filformat källtexterna har. Alla filer måste ha det valda formatet. +config.format.note.pdf: PDF-formatet är byggt främst för visuell presentation. Text som extraheras från PDF-filer kan ibland ge bristfälliga resultat. OCR-behandling av scannade dokument är (för närvarande) inget som Mink stödjer. config.text_annotation: Textelement config.text_annotation.help: Vilket XML-element i dina källfiler representerar bäst en enskild text? Eventuella annotationer på textnivå kommer att sättas på det elementet. segmenter_sentence: Existerande meningssegmentering