mirror of
https://github.com/amir20/dozzle.git
synced 2025-12-21 21:33:18 +01:00
feat: enables filters and level filters when downloading (#4251)
This commit is contained in:
2
assets/auto-imports.d.ts
vendored
2
assets/auto-imports.d.ts
vendored
@@ -218,6 +218,7 @@ declare global {
|
|||||||
const useDevicesList: typeof import('@vueuse/core')['useDevicesList']
|
const useDevicesList: typeof import('@vueuse/core')['useDevicesList']
|
||||||
const useDisplayMedia: typeof import('@vueuse/core')['useDisplayMedia']
|
const useDisplayMedia: typeof import('@vueuse/core')['useDisplayMedia']
|
||||||
const useDocumentVisibility: typeof import('@vueuse/core')['useDocumentVisibility']
|
const useDocumentVisibility: typeof import('@vueuse/core')['useDocumentVisibility']
|
||||||
|
const useDownloadUrl: typeof import('./composable/downloadUrl')['useDownloadUrl']
|
||||||
const useDraggable: typeof import('@vueuse/core')['useDraggable']
|
const useDraggable: typeof import('@vueuse/core')['useDraggable']
|
||||||
const useDrawer: typeof import('./composable/drawer')['useDrawer']
|
const useDrawer: typeof import('./composable/drawer')['useDrawer']
|
||||||
const useDropZone: typeof import('@vueuse/core')['useDropZone']
|
const useDropZone: typeof import('@vueuse/core')['useDropZone']
|
||||||
@@ -616,6 +617,7 @@ declare module 'vue' {
|
|||||||
readonly useDevicesList: UnwrapRef<typeof import('@vueuse/core')['useDevicesList']>
|
readonly useDevicesList: UnwrapRef<typeof import('@vueuse/core')['useDevicesList']>
|
||||||
readonly useDisplayMedia: UnwrapRef<typeof import('@vueuse/core')['useDisplayMedia']>
|
readonly useDisplayMedia: UnwrapRef<typeof import('@vueuse/core')['useDisplayMedia']>
|
||||||
readonly useDocumentVisibility: UnwrapRef<typeof import('@vueuse/core')['useDocumentVisibility']>
|
readonly useDocumentVisibility: UnwrapRef<typeof import('@vueuse/core')['useDocumentVisibility']>
|
||||||
|
readonly useDownloadUrl: UnwrapRef<typeof import('./composable/downloadUrl')['useDownloadUrl']>
|
||||||
readonly useDraggable: UnwrapRef<typeof import('@vueuse/core')['useDraggable']>
|
readonly useDraggable: UnwrapRef<typeof import('@vueuse/core')['useDraggable']>
|
||||||
readonly useDrawer: UnwrapRef<typeof import('./composable/drawer')['useDrawer']>
|
readonly useDrawer: UnwrapRef<typeof import('./composable/drawer')['useDrawer']>
|
||||||
readonly useDropZone: UnwrapRef<typeof import('@vueuse/core')['useDropZone']>
|
readonly useDropZone: UnwrapRef<typeof import('@vueuse/core')['useDropZone']>
|
||||||
|
|||||||
@@ -16,7 +16,10 @@
|
|||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
<li v-if="enableDownload">
|
<li v-if="enableDownload">
|
||||||
<a :href="downloadUrl" download> <octicon:download-24 /> {{ $t("toolbar.download") }} </a>
|
<a :href="downloadUrl" download>
|
||||||
|
<octicon:download-24 />
|
||||||
|
{{ isFiltered ? $t("toolbar.download-filtered") : $t("toolbar.download") }}
|
||||||
|
</a>
|
||||||
</li>
|
</li>
|
||||||
<li v-if="!historical">
|
<li v-if="!historical">
|
||||||
<a @click="showSearch = true">
|
<a @click="showSearch = true">
|
||||||
@@ -199,17 +202,8 @@ if (enableShell) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const downloadParams = computed(() =>
|
const containerRef = computed(() => [container]);
|
||||||
Object.entries(toValue(streamConfig))
|
const { downloadUrl, isFiltered } = useDownloadUrl(containerRef, streamConfig, levels);
|
||||||
.filter(([, value]) => value)
|
|
||||||
.reduce((acc, [key]) => ({ ...acc, [key]: "1" }), {}),
|
|
||||||
);
|
|
||||||
|
|
||||||
const downloadUrl = computed(() =>
|
|
||||||
withBase(
|
|
||||||
`/api/containers/${container.host}~${container.id}/download?${new URLSearchParams(downloadParams.value).toString()}`,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
|
|
||||||
const disableRestart = computed(() => actionStates.stop || actionStates.start || actionStates.restart);
|
const disableRestart = computed(() => actionStates.stop || actionStates.start || actionStates.restart);
|
||||||
|
|
||||||
|
|||||||
@@ -16,7 +16,10 @@
|
|||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
<li v-if="enableDownload">
|
<li v-if="enableDownload">
|
||||||
<a :href="downloadUrl" download> <octicon:download-24 /> {{ $t("toolbar.download") }} </a>
|
<a :href="downloadUrl" download>
|
||||||
|
<octicon:download-24 />
|
||||||
|
{{ isFiltered ? $t("toolbar.download-filtered") : $t("toolbar.download") }}
|
||||||
|
</a>
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<a @click="showSearch = true">
|
<a @click="showSearch = true">
|
||||||
@@ -91,19 +94,9 @@ const { showSearch } = useSearchFilter();
|
|||||||
const { enableDownload } = config;
|
const { enableDownload } = config;
|
||||||
const clear = defineEmit();
|
const clear = defineEmit();
|
||||||
|
|
||||||
const { streamConfig, showHostname, showContainerName, containers } = useLoggingContext();
|
const { streamConfig, showHostname, showContainerName, containers, levels } = useLoggingContext();
|
||||||
|
|
||||||
const downloadParams = computed(() =>
|
const { downloadUrl, isFiltered } = useDownloadUrl(containers, streamConfig, levels);
|
||||||
Object.entries(toValue(streamConfig))
|
|
||||||
.filter(([, value]) => value)
|
|
||||||
.reduce((acc, [key]) => ({ ...acc, [key]: "1" }), {}),
|
|
||||||
);
|
|
||||||
|
|
||||||
const downloadUrl = computed(() =>
|
|
||||||
withBase(
|
|
||||||
`/api/containers/${containers.value.map((c) => c.host + "~" + c.id).join(",")}/download?${new URLSearchParams(downloadParams.value).toString()}`,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
|
|
||||||
const hideMenu = (e: MouseEvent) => {
|
const hideMenu = (e: MouseEvent) => {
|
||||||
if (e.target instanceof HTMLAnchorElement) {
|
if (e.target instanceof HTMLAnchorElement) {
|
||||||
|
|||||||
45
assets/composable/downloadUrl.ts
Normal file
45
assets/composable/downloadUrl.ts
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
import { Container } from "@/models/Container";
|
||||||
|
import { allLevels } from "@/composable/logContext";
|
||||||
|
|
||||||
|
export function useDownloadUrl(
|
||||||
|
containers: Ref<Container[]> | ComputedRef<Container[]>,
|
||||||
|
streamConfig: { stdout: boolean; stderr: boolean } | Ref<{ stdout: boolean; stderr: boolean }>,
|
||||||
|
levels: Ref<Set<string>>,
|
||||||
|
) {
|
||||||
|
const { debouncedSearchFilter } = useSearchFilter();
|
||||||
|
|
||||||
|
const downloadUrl = computed(() => {
|
||||||
|
const params = new URLSearchParams();
|
||||||
|
const config = toValue(streamConfig);
|
||||||
|
|
||||||
|
// Add stdout/stderr
|
||||||
|
if (config.stdout) params.append("stdout", "1");
|
||||||
|
if (config.stderr) params.append("stderr", "1");
|
||||||
|
|
||||||
|
// Add filter if search is active
|
||||||
|
if (debouncedSearchFilter.value) {
|
||||||
|
params.append("filter", debouncedSearchFilter.value);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add levels (multiple values) only if filtered
|
||||||
|
const selectedLevels = Array.from(levels.value);
|
||||||
|
if (selectedLevels.length > 0 && selectedLevels.length < allLevels.length) {
|
||||||
|
selectedLevels.forEach((level) => params.append("levels", level));
|
||||||
|
}
|
||||||
|
|
||||||
|
const containerIds = toValue(containers)
|
||||||
|
.map((c) => c.host + "~" + c.id)
|
||||||
|
.join(",");
|
||||||
|
|
||||||
|
return withBase(`/api/containers/${containerIds}/download?${params.toString()}`);
|
||||||
|
});
|
||||||
|
|
||||||
|
const isFiltered = computed(
|
||||||
|
() => debouncedSearchFilter.value || (levels.value.size > 0 && levels.value.size < allLevels.length),
|
||||||
|
);
|
||||||
|
|
||||||
|
return {
|
||||||
|
downloadUrl,
|
||||||
|
isFiltered,
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -5,11 +5,14 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"regexp"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/amir20/dozzle/internal/auth"
|
"github.com/amir20/dozzle/internal/auth"
|
||||||
"github.com/amir20/dozzle/internal/container"
|
"github.com/amir20/dozzle/internal/container"
|
||||||
|
container_support "github.com/amir20/dozzle/internal/support/container"
|
||||||
|
support_web "github.com/amir20/dozzle/internal/support/web"
|
||||||
"github.com/go-chi/chi/v5"
|
"github.com/go-chi/chi/v5"
|
||||||
"github.com/rs/zerolog/log"
|
"github.com/rs/zerolog/log"
|
||||||
)
|
)
|
||||||
@@ -54,15 +57,34 @@ func (h *handler) downloadLogs(w http.ResponseWriter, r *http.Request) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Set headers for zip file
|
// Parse filter regex if provided
|
||||||
w.Header().Set("Content-Disposition", fmt.Sprintf("attachment; filename=container-logs-%s.zip", nowFmt))
|
var regex *regexp.Regexp
|
||||||
w.Header().Set("Content-Type", "application/zip")
|
var err error
|
||||||
|
if r.URL.Query().Has("filter") {
|
||||||
|
regex, err = support_web.ParseRegex(r.URL.Query().Get("filter"))
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Create zip writer
|
// Parse level filters if provided
|
||||||
zw := zip.NewWriter(w)
|
levels := make(map[string]struct{})
|
||||||
defer zw.Close()
|
if r.URL.Query().Has("levels") {
|
||||||
|
for _, level := range r.URL.Query()["levels"] {
|
||||||
|
levels[level] = struct{}{}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate all containers before starting to write response
|
||||||
|
type containerInfo struct {
|
||||||
|
hostId string
|
||||||
|
host string
|
||||||
|
id string
|
||||||
|
containerService *container_support.ContainerService
|
||||||
|
}
|
||||||
|
containers := make([]containerInfo, 0, len(hostIds))
|
||||||
|
|
||||||
// Process each container
|
|
||||||
for _, hostId := range hostIds {
|
for _, hostId := range hostIds {
|
||||||
parts := strings.Split(hostId, "~")
|
parts := strings.Split(hostId, "~")
|
||||||
if len(parts) != 2 {
|
if len(parts) != 2 {
|
||||||
@@ -80,29 +102,79 @@ func (h *handler) downloadLogs(w http.ResponseWriter, r *http.Request) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
containers = append(containers, containerInfo{
|
||||||
|
hostId: hostId,
|
||||||
|
host: host,
|
||||||
|
id: id,
|
||||||
|
containerService: containerService,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set headers for zip file
|
||||||
|
w.Header().Set("Content-Disposition", fmt.Sprintf("attachment; filename=container-logs-%s.zip", nowFmt))
|
||||||
|
w.Header().Set("Content-Type", "application/zip")
|
||||||
|
|
||||||
|
// Create zip writer
|
||||||
|
zw := zip.NewWriter(w)
|
||||||
|
defer zw.Close()
|
||||||
|
|
||||||
|
// Process each container - errors after this point are logged only since response has started
|
||||||
|
for _, c := range containers {
|
||||||
// Create new file in zip for this container's logs
|
// Create new file in zip for this container's logs
|
||||||
fileName := fmt.Sprintf("%s-%s.log", containerService.Container.Name, nowFmt)
|
fileName := fmt.Sprintf("%s-%s.log", c.containerService.Container.Name, nowFmt)
|
||||||
f, err := zw.Create(fileName)
|
f, err := zw.Create(fileName)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Error().Err(err).Msgf("error creating zip entry for container %s", id)
|
log.Error().Err(err).Msgf("error creating zip entry for container %s", c.id)
|
||||||
http.Error(w, fmt.Sprintf("error creating zip entry: %v", err), http.StatusInternalServerError)
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get container logs
|
// Get container logs - use LogsBetweenDates if filtering is needed, otherwise use RawLogs
|
||||||
reader, err := containerService.RawLogs(r.Context(), time.Time{}, now, stdTypes)
|
if regex != nil || len(levels) > 0 {
|
||||||
|
// Fetch parsed log events for filtering
|
||||||
|
events, err := c.containerService.LogsBetweenDates(r.Context(), time.Time{}, now, stdTypes)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Error().Err(err).Msgf("error getting logs for container %s", id)
|
log.Error().Err(err).Msgf("error getting logs for container %s", c.id)
|
||||||
http.Error(w, fmt.Sprintf("error getting logs for container %s: %v", id, err), http.StatusInternalServerError)
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Filter and write events
|
||||||
|
for event := range events {
|
||||||
|
// Apply regex filter if provided
|
||||||
|
if regex != nil && !support_web.Search(regex, event) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// Apply level filter if provided
|
||||||
|
if len(levels) > 0 {
|
||||||
|
if _, ok := levels[event.Level]; !ok {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Format timestamp in UTC
|
||||||
|
timestamp := time.UnixMilli(event.Timestamp).UTC().Format(time.RFC3339Nano)
|
||||||
|
|
||||||
|
// Write timestamp followed by message
|
||||||
|
_, err = fmt.Fprintf(f, "%s %s\n", timestamp, event.RawMessage)
|
||||||
|
if err != nil {
|
||||||
|
log.Error().Err(err).Msgf("error writing log for container %s", c.id)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// No filtering needed, use raw logs for better performance
|
||||||
|
reader, err := c.containerService.RawLogs(r.Context(), time.Time{}, now, stdTypes)
|
||||||
|
if err != nil {
|
||||||
|
log.Error().Err(err).Msgf("error getting logs for container %s", c.id)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Copy logs to zip file
|
// Copy logs to zip file
|
||||||
_, err = io.Copy(f, reader)
|
_, err = io.Copy(f, reader)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Error().Err(err).Msgf("error copying logs for container %s", id)
|
log.Error().Err(err).Msgf("error copying logs for container %s", c.id)
|
||||||
http.Error(w, fmt.Sprintf("error copying logs for container %s: %v", id, err), http.StatusInternalServerError)
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
toolbar:
|
toolbar:
|
||||||
clear: Ryd
|
clear: Ryd
|
||||||
download: Download
|
download: Download
|
||||||
|
download-filtered: Download Filtrerede Logs
|
||||||
search: Søg
|
search: Søg
|
||||||
show: Vis kun {std}
|
show: Vis kun {std}
|
||||||
show-all: Vis alle streams
|
show-all: Vis alle streams
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
toolbar:
|
toolbar:
|
||||||
clear: Leeren
|
clear: Leeren
|
||||||
download: Herunterladen
|
download: Herunterladen
|
||||||
|
download-filtered: Gefilterte Logs Herunterladen
|
||||||
search: Suchen
|
search: Suchen
|
||||||
show: Zeige nur {std}
|
show: Zeige nur {std}
|
||||||
show-all: Zeige alle Streams
|
show-all: Zeige alle Streams
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
toolbar:
|
toolbar:
|
||||||
clear: Clear
|
clear: Clear
|
||||||
download: Download
|
download: Download
|
||||||
|
download-filtered: Download Filtered Logs
|
||||||
search: Search
|
search: Search
|
||||||
show: Show {std}
|
show: Show {std}
|
||||||
show-all: Show all
|
show-all: Show all
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
toolbar:
|
toolbar:
|
||||||
clear: Limpiar
|
clear: Limpiar
|
||||||
download: Descargar
|
download: Descargar
|
||||||
|
download-filtered: Descargar Logs Filtrados
|
||||||
search: Buscar
|
search: Buscar
|
||||||
show: Mostrar {std}
|
show: Mostrar {std}
|
||||||
show-all: Mostrar todo
|
show-all: Mostrar todo
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
toolbar:
|
toolbar:
|
||||||
clear: Effacer
|
clear: Effacer
|
||||||
download: Téléchargement
|
download: Téléchargement
|
||||||
|
download-filtered: Télécharger Logs Filtrés
|
||||||
search: Chercher
|
search: Chercher
|
||||||
show: Montrer seulement {std}
|
show: Montrer seulement {std}
|
||||||
show-all: Afficher tous les flux
|
show-all: Afficher tous les flux
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
toolbar:
|
toolbar:
|
||||||
clear: Bersihkan
|
clear: Bersihkan
|
||||||
download: Unduh
|
download: Unduh
|
||||||
|
download-filtered: Unduh Log yang Difilter
|
||||||
search: Cari
|
search: Cari
|
||||||
show: Tampilkan {std}
|
show: Tampilkan {std}
|
||||||
show-all: Tampilkan semua
|
show-all: Tampilkan semua
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
toolbar:
|
toolbar:
|
||||||
clear: Pulisci
|
clear: Pulisci
|
||||||
download: Scarica
|
download: Scarica
|
||||||
|
download-filtered: Scarica Log Filtrati
|
||||||
search: Cerca
|
search: Cerca
|
||||||
show: Mostra solo {std}
|
show: Mostra solo {std}
|
||||||
show-all: Mostra tutto
|
show-all: Mostra tutto
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
toolbar:
|
toolbar:
|
||||||
clear: 지우기
|
clear: 지우기
|
||||||
download: 다운로드
|
download: 다운로드
|
||||||
|
download-filtered: 필터링된 로그 다운로드
|
||||||
search: 검색
|
search: 검색
|
||||||
show: "{std} 보기"
|
show: "{std} 보기"
|
||||||
show-all: 전체 보기
|
show-all: 전체 보기
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
toolbar:
|
toolbar:
|
||||||
clear: Wissen
|
clear: Wissen
|
||||||
download: Downloaden
|
download: Downloaden
|
||||||
|
download-filtered: Gefilterde Logs Downloaden
|
||||||
search: Zoeken
|
search: Zoeken
|
||||||
show: Toon {std}
|
show: Toon {std}
|
||||||
show-all: Toon alles
|
show-all: Toon alles
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
toolbar:
|
toolbar:
|
||||||
clear: Wyczyść
|
clear: Wyczyść
|
||||||
download: Pobierz
|
download: Pobierz
|
||||||
|
download-filtered: Pobierz Filtrowane Logi
|
||||||
search: Szukaj
|
search: Szukaj
|
||||||
show: Pokaż tylko {std}
|
show: Pokaż tylko {std}
|
||||||
show-all: Pokaż wszystko
|
show-all: Pokaż wszystko
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
toolbar:
|
toolbar:
|
||||||
clear: Limpar
|
clear: Limpar
|
||||||
download: Descarregar
|
download: Descarregar
|
||||||
|
download-filtered: Descarregar Logs Filtrados
|
||||||
search: Pesquisa
|
search: Pesquisa
|
||||||
show: Mostrar apenas {std}
|
show: Mostrar apenas {std}
|
||||||
show-all: Mostrar todos os fluxos
|
show-all: Mostrar todos os fluxos
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
toolbar:
|
toolbar:
|
||||||
clear: Limpar
|
clear: Limpar
|
||||||
download: Baixar
|
download: Baixar
|
||||||
|
download-filtered: Baixar Logs Filtrados
|
||||||
search: Pesquisar
|
search: Pesquisar
|
||||||
show: Mostrar {std}
|
show: Mostrar {std}
|
||||||
show-all: Mostrar tudo
|
show-all: Mostrar tudo
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
toolbar:
|
toolbar:
|
||||||
clear: Очистить
|
clear: Очистить
|
||||||
download: Скачать
|
download: Скачать
|
||||||
|
download-filtered: Скачать Отфильтрованные Логи
|
||||||
search: Поиск
|
search: Поиск
|
||||||
show: Показать только {std}
|
show: Показать только {std}
|
||||||
show-all: Показать все потоки
|
show-all: Показать все потоки
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
toolbar:
|
toolbar:
|
||||||
clear: Počisti
|
clear: Počisti
|
||||||
download: Prenesi
|
download: Prenesi
|
||||||
|
download-filtered: Prenesi Filtrirane Dnevnike
|
||||||
search: Iskanje
|
search: Iskanje
|
||||||
show: Prikaži {std}
|
show: Prikaži {std}
|
||||||
show-all: Prikaži vse
|
show-all: Prikaži vse
|
||||||
@@ -121,8 +122,8 @@ releases:
|
|||||||
features: ena nova funkcija | {count} funkcij
|
features: ena nova funkcija | {count} funkcij
|
||||||
bugFixes: en popravek napake | {counr} popravkov
|
bugFixes: en popravek napake | {counr} popravkov
|
||||||
breaking: ena ključna sprememba | {count} ključnih sprememb
|
breaking: ena ključna sprememba | {count} ključnih sprememb
|
||||||
three_parts: '{first}, {second} in {third}'
|
three_parts: "{first}, {second} in {third}"
|
||||||
two_parts: '{first} s/z {second}'
|
two_parts: "{first} s/z {second}"
|
||||||
latest: Najnovejše
|
latest: Najnovejše
|
||||||
no_releases: Imate najnovejšo različico
|
no_releases: Imate najnovejšo različico
|
||||||
log_actions:
|
log_actions:
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
toolbar:
|
toolbar:
|
||||||
clear: Temizle
|
clear: Temizle
|
||||||
download: İndir
|
download: İndir
|
||||||
|
download-filtered: Filtrelenmiş Logları İndir
|
||||||
search: Ara
|
search: Ara
|
||||||
show: Sadece {std} göster
|
show: Sadece {std} göster
|
||||||
show-all: Tüm akışları göster
|
show-all: Tüm akışları göster
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
toolbar:
|
toolbar:
|
||||||
clear: 清除
|
clear: 清除
|
||||||
download: 下載
|
download: 下載
|
||||||
|
download-filtered: 下載篩選的日誌
|
||||||
search: 搜尋
|
search: 搜尋
|
||||||
show: 僅顯示 {std}
|
show: 僅顯示 {std}
|
||||||
show-all: 顯示全部
|
show-all: 顯示全部
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
toolbar:
|
toolbar:
|
||||||
clear: 清除
|
clear: 清除
|
||||||
download: 下载
|
download: 下载
|
||||||
|
download-filtered: 下载筛选的日志
|
||||||
search: 搜索
|
search: 搜索
|
||||||
show: 显示 {std}
|
show: 显示 {std}
|
||||||
show-all: 显示全部
|
show-all: 显示全部
|
||||||
|
|||||||
Reference in New Issue
Block a user