mirror of
https://github.com/paperless-ngx/paperless-ngx.git
synced 2026-07-28 14:54:55 +00:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b57dbf7c32 | ||
|
|
de9f520143 | ||
|
|
a68c487d60 | ||
|
|
217421bceb |
@@ -1,5 +1,51 @@
|
|||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
|
## paperless-ngx 3.0.4
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
- Fix: prevent pdfjs highlight scrolling from affecting the entire page [@shamoon](https://github.com/shamoon) ([#13355](https://github.com/paperless-ngx/paperless-ngx/pull/13355))
|
||||||
|
- Fix: don't skip OCR/archive for tagged PDFs with no actual text [@stumpylog](https://github.com/stumpylog) ([#13351](https://github.com/paperless-ngx/paperless-ngx/pull/13351))
|
||||||
|
- Performance: more efficient diacritic normalization in dropdown filtering [@shamoon](https://github.com/shamoon) ([#13347](https://github.com/paperless-ngx/paperless-ngx/pull/13347))
|
||||||
|
- Fix: dedupe permission-visible documents when combined with multi-tag filters [@stumpylog](https://github.com/stumpylog) ([#13345](https://github.com/paperless-ngx/paperless-ngx/pull/13345))
|
||||||
|
- Fixhancement: pass LLM output language to chat if specified [@shamoon](https://github.com/shamoon) ([#13340](https://github.com/paperless-ngx/paperless-ngx/pull/13340))
|
||||||
|
- Fix: ensure preview reload on live changes [@shamoon](https://github.com/shamoon) ([#13321](https://github.com/paperless-ngx/paperless-ngx/pull/13321))
|
||||||
|
- Fix: clamp all out out-of-range possible fields [@stumpylog](https://github.com/stumpylog) ([#13316](https://github.com/paperless-ngx/paperless-ngx/pull/13316))
|
||||||
|
- Fix: add docstring to DocumentClassifierSchema for cleaner LLM tool description [@stumpylog](https://github.com/stumpylog) ([#13315](https://github.com/paperless-ngx/paperless-ngx/pull/13315))
|
||||||
|
- Fix: guard build\_document\_node against stale FK on deleted correspondent/doc type [@stumpylog](https://github.com/stumpylog) ([#13318](https://github.com/paperless-ngx/paperless-ngx/pull/13318))
|
||||||
|
- Fix: prevent search filter loss when closing document with Escape key [@shamoon](https://github.com/shamoon) ([#13317](https://github.com/paperless-ngx/paperless-ngx/pull/13317))
|
||||||
|
- Fix: fix frontend permissions display for stats/system perms [@shamoon](https://github.com/shamoon) ([#13305](https://github.com/paperless-ngx/paperless-ngx/pull/13305))
|
||||||
|
|
||||||
|
### Documentation
|
||||||
|
|
||||||
|
- Documentation: fix PAPERLESS\_AI\_LLM\_OUTPUT\_LANGUAGE heading level [@SuperSandro2000](https://github.com/SuperSandro2000) ([#13341](https://github.com/paperless-ngx/paperless-ngx/pull/13341))
|
||||||
|
|
||||||
|
### Dependencies
|
||||||
|
|
||||||
|
- Chore: resolve npm provenance issues with chokidar and semver [@shamoon](https://github.com/shamoon) ([#13323](https://github.com/paperless-ngx/paperless-ngx/pull/13323))
|
||||||
|
|
||||||
|
### All App Changes
|
||||||
|
|
||||||
|
<details>
|
||||||
|
<summary>14 changes</summary>
|
||||||
|
|
||||||
|
- Fix: prevent pdfjs highlight scrolling from affecting the entire page [@shamoon](https://github.com/shamoon) ([#13355](https://github.com/paperless-ngx/paperless-ngx/pull/13355))
|
||||||
|
- Fix: don't skip OCR/archive for tagged PDFs with no actual text [@stumpylog](https://github.com/stumpylog) ([#13351](https://github.com/paperless-ngx/paperless-ngx/pull/13351))
|
||||||
|
- Performance: more efficient diacritic normalization in dropdown filtering [@shamoon](https://github.com/shamoon) ([#13347](https://github.com/paperless-ngx/paperless-ngx/pull/13347))
|
||||||
|
- Performance: prefetch notes and custom fields for LLM index text building [@stumpylog](https://github.com/stumpylog) ([#13350](https://github.com/paperless-ngx/paperless-ngx/pull/13350))
|
||||||
|
- Fix: dedupe permission-visible documents when combined with multi-tag filters [@stumpylog](https://github.com/stumpylog) ([#13345](https://github.com/paperless-ngx/paperless-ngx/pull/13345))
|
||||||
|
- Performance: Scope llm index updates to actually modified documents [@stumpylog](https://github.com/stumpylog) ([#13322](https://github.com/paperless-ngx/paperless-ngx/pull/13322))
|
||||||
|
- Fixhancement: pass LLM output language to chat if specified [@shamoon](https://github.com/shamoon) ([#13340](https://github.com/paperless-ngx/paperless-ngx/pull/13340))
|
||||||
|
- Chore: resolve npm provenance issues with chokidar and semver [@shamoon](https://github.com/shamoon) ([#13323](https://github.com/paperless-ngx/paperless-ngx/pull/13323))
|
||||||
|
- Fix: ensure preview reload on live changes [@shamoon](https://github.com/shamoon) ([#13321](https://github.com/paperless-ngx/paperless-ngx/pull/13321))
|
||||||
|
- Fix: clamp all out out-of-range possible fields [@stumpylog](https://github.com/stumpylog) ([#13316](https://github.com/paperless-ngx/paperless-ngx/pull/13316))
|
||||||
|
- Fix: add docstring to DocumentClassifierSchema for cleaner LLM tool description [@stumpylog](https://github.com/stumpylog) ([#13315](https://github.com/paperless-ngx/paperless-ngx/pull/13315))
|
||||||
|
- Fix: guard build\_document\_node against stale FK on deleted correspondent/doc type [@stumpylog](https://github.com/stumpylog) ([#13318](https://github.com/paperless-ngx/paperless-ngx/pull/13318))
|
||||||
|
- Fix: prevent search filter loss when closing document with Escape key [@shamoon](https://github.com/shamoon) ([#13317](https://github.com/paperless-ngx/paperless-ngx/pull/13317))
|
||||||
|
- Fix: fix frontend permissions display for stats/system perms [@shamoon](https://github.com/shamoon) ([#13305](https://github.com/paperless-ngx/paperless-ngx/pull/13305))
|
||||||
|
|
||||||
|
</details>
|
||||||
|
|
||||||
## paperless-ngx 3.0.3
|
## paperless-ngx 3.0.3
|
||||||
|
|
||||||
### Bug Fixes
|
### Bug Fixes
|
||||||
|
|||||||
@@ -21,7 +21,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</pngx-page-header>
|
</pngx-page-header>
|
||||||
|
|
||||||
@if (!tasksService.completedFileTasks && tasksService.loading) {
|
@if (loading() && pagedTasks().length === 0) {
|
||||||
<div class="spinner-border spinner-border-sm fw-normal ms-2 me-auto" role="status"></div>
|
<div class="spinner-border spinner-border-sm fw-normal ms-2 me-auto" role="status"></div>
|
||||||
<div class="visually-hidden" i18n>Loading...</div>
|
<div class="visually-hidden" i18n>Loading...</div>
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2269,4 +2269,34 @@ describe('FilterEditorComponent', () => {
|
|||||||
component.itemSelected({ item: 'world', preventDefault: () => true })
|
component.itemSelected({ item: 'world', preventDefault: () => true })
|
||||||
expect(component.textFilter).toEqual('hello world ')
|
expect(component.textFilter).toEqual('hello world ')
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('should choose the active autocomplete item with Enter', () => {
|
||||||
|
component.textFilterTarget = 'fulltext-query'
|
||||||
|
jest
|
||||||
|
.spyOn(searchService, 'autocomplete')
|
||||||
|
.mockReturnValue(of(['hello', 'help']))
|
||||||
|
|
||||||
|
const input = component.textFilterInput.nativeElement as HTMLInputElement
|
||||||
|
input.value = 'he'
|
||||||
|
input.dispatchEvent(new Event('input'))
|
||||||
|
tick(250)
|
||||||
|
|
||||||
|
input.dispatchEvent(
|
||||||
|
new KeyboardEvent('keydown', {
|
||||||
|
key: 'ArrowDown',
|
||||||
|
bubbles: true,
|
||||||
|
cancelable: true,
|
||||||
|
})
|
||||||
|
)
|
||||||
|
input.dispatchEvent(
|
||||||
|
new KeyboardEvent('keydown', {
|
||||||
|
key: 'Enter',
|
||||||
|
bubbles: true,
|
||||||
|
cancelable: true,
|
||||||
|
})
|
||||||
|
)
|
||||||
|
fixture.detectChanges()
|
||||||
|
|
||||||
|
expect(component.textFilter).toEqual('help ')
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1314,6 +1314,10 @@ export class FilterEditorComponent
|
|||||||
|
|
||||||
textFilterKeydown(event: KeyboardEvent) {
|
textFilterKeydown(event: KeyboardEvent) {
|
||||||
if (event.key == 'Enter') {
|
if (event.key == 'Enter') {
|
||||||
|
if (event.defaultPrevented) {
|
||||||
|
// NgbTypeahead calls preventDefault, so use that to detect if the Enter key was for the dropdown
|
||||||
|
return
|
||||||
|
}
|
||||||
const filterString = (
|
const filterString = (
|
||||||
this.textFilterInput.nativeElement as HTMLInputElement
|
this.textFilterInput.nativeElement as HTMLInputElement
|
||||||
).value
|
).value
|
||||||
|
|||||||
@@ -50,13 +50,66 @@ describe('TasksService', () => {
|
|||||||
req.flush({ count: 0, results: [] })
|
req.flush({ count: 0, results: [] })
|
||||||
})
|
})
|
||||||
|
|
||||||
it('does not call tasks api endpoint on reload if already loading', () => {
|
it('cancels an in-progress reload when reloading again', () => {
|
||||||
tasksService.loading = true
|
|
||||||
tasksService.reload()
|
tasksService.reload()
|
||||||
httpTestingController.expectNone(
|
const staleReload = httpTestingController.expectOne(
|
||||||
(req: HttpRequest<unknown>) =>
|
(req: HttpRequest<unknown>) =>
|
||||||
req.url === `${environment.apiBaseUrl}tasks/`
|
req.url === `${environment.apiBaseUrl}tasks/`
|
||||||
)
|
)
|
||||||
|
tasksService.reload()
|
||||||
|
|
||||||
|
expect(staleReload.cancelled).toBe(true)
|
||||||
|
httpTestingController
|
||||||
|
.expectOne(
|
||||||
|
(req: HttpRequest<unknown>) =>
|
||||||
|
req.url === `${environment.apiBaseUrl}tasks/`
|
||||||
|
)
|
||||||
|
.flush({ count: 0, results: [] })
|
||||||
|
})
|
||||||
|
|
||||||
|
it('continues reloading after a reload request fails', () => {
|
||||||
|
tasksService.reload()
|
||||||
|
httpTestingController
|
||||||
|
.expectOne(
|
||||||
|
(req: HttpRequest<unknown>) =>
|
||||||
|
req.url === `${environment.apiBaseUrl}tasks/`
|
||||||
|
)
|
||||||
|
.flush('error', { status: 500, statusText: 'error' })
|
||||||
|
|
||||||
|
expect(tasksService.loading).toBe(false)
|
||||||
|
|
||||||
|
tasksService.reload()
|
||||||
|
httpTestingController
|
||||||
|
.expectOne(
|
||||||
|
(req: HttpRequest<unknown>) =>
|
||||||
|
req.url === `${environment.apiBaseUrl}tasks/`
|
||||||
|
)
|
||||||
|
.flush({ count: 0, results: [] })
|
||||||
|
})
|
||||||
|
|
||||||
|
it('reloads after dismissing a task while a reload is already in progress', () => {
|
||||||
|
tasksService.reload()
|
||||||
|
const staleReload = httpTestingController.expectOne(
|
||||||
|
(req: HttpRequest<unknown>) =>
|
||||||
|
req.url === `${environment.apiBaseUrl}tasks/` &&
|
||||||
|
req.params.get('acknowledged') === 'false'
|
||||||
|
)
|
||||||
|
|
||||||
|
tasksService.dismissTasks(new Set([1])).subscribe()
|
||||||
|
httpTestingController
|
||||||
|
.expectOne(`${environment.apiBaseUrl}tasks/acknowledge/`)
|
||||||
|
.flush([])
|
||||||
|
|
||||||
|
expect(staleReload.cancelled).toBe(true)
|
||||||
|
httpTestingController
|
||||||
|
.expectOne(
|
||||||
|
(req: HttpRequest<unknown>) =>
|
||||||
|
req.url === `${environment.apiBaseUrl}tasks/` &&
|
||||||
|
req.params.get('acknowledged') === 'false'
|
||||||
|
)
|
||||||
|
.flush({ count: 0, results: [] })
|
||||||
|
|
||||||
|
expect(tasksService.needsAttentionTasks).toHaveLength(0)
|
||||||
})
|
})
|
||||||
|
|
||||||
it('calls acknowledge_tasks api endpoint on dismiss and reloads', () => {
|
it('calls acknowledge_tasks api endpoint on dismiss and reloads', () => {
|
||||||
@@ -101,79 +154,6 @@ describe('TasksService', () => {
|
|||||||
.flush({ count: 0, results: [] })
|
.flush({ count: 0, results: [] })
|
||||||
})
|
})
|
||||||
|
|
||||||
it('groups mixed task types by status when reloading', () => {
|
|
||||||
expect(tasksService.total).toEqual(0)
|
|
||||||
const mockTasks = [
|
|
||||||
{
|
|
||||||
task_type: PaperlessTaskType.ConsumeFile,
|
|
||||||
trigger_source: PaperlessTaskTriggerSource.FolderConsume,
|
|
||||||
status: PaperlessTaskStatus.Success,
|
|
||||||
acknowledged: false,
|
|
||||||
task_id: '1234',
|
|
||||||
input_data: { filename: 'file1.pdf' },
|
|
||||||
date_created: new Date(),
|
|
||||||
related_document_ids: [],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
task_type: PaperlessTaskType.SanityCheck,
|
|
||||||
trigger_source: PaperlessTaskTriggerSource.System,
|
|
||||||
status: PaperlessTaskStatus.Failure,
|
|
||||||
acknowledged: false,
|
|
||||||
task_id: '1235',
|
|
||||||
input_data: {},
|
|
||||||
date_created: new Date(),
|
|
||||||
related_document_ids: [],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
task_type: PaperlessTaskType.MailFetch,
|
|
||||||
trigger_source: PaperlessTaskTriggerSource.Scheduled,
|
|
||||||
status: PaperlessTaskStatus.Pending,
|
|
||||||
acknowledged: false,
|
|
||||||
task_id: '1236',
|
|
||||||
input_data: {},
|
|
||||||
date_created: new Date(),
|
|
||||||
related_document_ids: [],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
task_type: PaperlessTaskType.LlmIndex,
|
|
||||||
trigger_source: PaperlessTaskTriggerSource.WebUI,
|
|
||||||
status: PaperlessTaskStatus.Started,
|
|
||||||
acknowledged: false,
|
|
||||||
task_id: '1237',
|
|
||||||
input_data: {},
|
|
||||||
date_created: new Date(),
|
|
||||||
related_document_ids: [],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
task_type: PaperlessTaskType.EmptyTrash,
|
|
||||||
trigger_source: PaperlessTaskTriggerSource.Manual,
|
|
||||||
status: PaperlessTaskStatus.Success,
|
|
||||||
acknowledged: false,
|
|
||||||
task_id: '1238',
|
|
||||||
input_data: {},
|
|
||||||
date_created: new Date(),
|
|
||||||
related_document_ids: [],
|
|
||||||
},
|
|
||||||
]
|
|
||||||
|
|
||||||
tasksService.reload()
|
|
||||||
|
|
||||||
const req = httpTestingController.expectOne(
|
|
||||||
(req: HttpRequest<unknown>) =>
|
|
||||||
req.url === `${environment.apiBaseUrl}tasks/` &&
|
|
||||||
req.params.get('acknowledged') === 'false' &&
|
|
||||||
req.params.get('page_size') === '1000'
|
|
||||||
)
|
|
||||||
|
|
||||||
req.flush({ count: mockTasks.length, results: mockTasks })
|
|
||||||
|
|
||||||
expect(tasksService.allFileTasks).toHaveLength(5)
|
|
||||||
expect(tasksService.completedFileTasks).toHaveLength(2)
|
|
||||||
expect(tasksService.failedFileTasks).toHaveLength(1)
|
|
||||||
expect(tasksService.queuedFileTasks).toHaveLength(1)
|
|
||||||
expect(tasksService.startedFileTasks).toHaveLength(1)
|
|
||||||
})
|
|
||||||
|
|
||||||
it('includes revoked tasks in needs attention', () => {
|
it('includes revoked tasks in needs attention', () => {
|
||||||
const mockTasks = [
|
const mockTasks = [
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1,7 +1,15 @@
|
|||||||
import { HttpClient } from '@angular/common/http'
|
import { HttpClient } from '@angular/common/http'
|
||||||
import { Injectable, inject, signal } from '@angular/core'
|
import { Injectable, inject, signal } from '@angular/core'
|
||||||
import { Observable, Subject } from 'rxjs'
|
import { EMPTY, Observable, Subject } from 'rxjs'
|
||||||
import { first, map, takeUntil, tap } from 'rxjs/operators'
|
import {
|
||||||
|
catchError,
|
||||||
|
finalize,
|
||||||
|
first,
|
||||||
|
map,
|
||||||
|
switchMap,
|
||||||
|
takeUntil,
|
||||||
|
tap,
|
||||||
|
} from 'rxjs/operators'
|
||||||
import {
|
import {
|
||||||
PaperlessTask,
|
PaperlessTask,
|
||||||
PaperlessTaskStatus,
|
PaperlessTaskStatus,
|
||||||
@@ -23,44 +31,40 @@ export class TasksService {
|
|||||||
|
|
||||||
public loading: boolean = false
|
public loading: boolean = false
|
||||||
|
|
||||||
private readonly fileTasks = signal<PaperlessTask[]>([])
|
private readonly tasks = signal<PaperlessTask[]>([])
|
||||||
|
private readonly reloadNotifier = new Subject<void>()
|
||||||
|
|
||||||
private unsubscribeNotifer: Subject<any> = new Subject()
|
private unsubscribeNotifer: Subject<any> = new Subject()
|
||||||
|
|
||||||
public get total(): number {
|
constructor() {
|
||||||
return this.fileTasks().length
|
this.reloadNotifier
|
||||||
}
|
.pipe(
|
||||||
|
switchMap(() => {
|
||||||
public get allFileTasks(): PaperlessTask[] {
|
this.loading = true
|
||||||
return this.fileTasks().slice(0)
|
return this.http
|
||||||
}
|
.get<Results<PaperlessTask>>(`${this.baseUrl}${this.endpoint}/`, {
|
||||||
|
params: {
|
||||||
public get queuedFileTasks(): PaperlessTask[] {
|
acknowledged: 'false',
|
||||||
return this.fileTasks().filter(
|
page_size: this.defaultReloadPageSize,
|
||||||
(t) => t.status === PaperlessTaskStatus.Pending
|
},
|
||||||
)
|
})
|
||||||
}
|
.pipe(
|
||||||
|
map((response) => response.results),
|
||||||
public get startedFileTasks(): PaperlessTask[] {
|
takeUntil(this.unsubscribeNotifer),
|
||||||
return this.fileTasks().filter(
|
catchError(() => EMPTY),
|
||||||
(t) => t.status === PaperlessTaskStatus.Started
|
finalize(() => {
|
||||||
)
|
this.loading = false
|
||||||
}
|
})
|
||||||
|
)
|
||||||
public get completedFileTasks(): PaperlessTask[] {
|
})
|
||||||
return this.fileTasks().filter(
|
)
|
||||||
(t) => t.status === PaperlessTaskStatus.Success
|
.subscribe((tasks) => {
|
||||||
)
|
this.tasks.set(tasks)
|
||||||
}
|
})
|
||||||
|
|
||||||
public get failedFileTasks(): PaperlessTask[] {
|
|
||||||
return this.fileTasks().filter(
|
|
||||||
(t) => t.status === PaperlessTaskStatus.Failure
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public get needsAttentionTasks(): PaperlessTask[] {
|
public get needsAttentionTasks(): PaperlessTask[] {
|
||||||
return this.fileTasks().filter((t) =>
|
return this.tasks().filter((t) =>
|
||||||
[PaperlessTaskStatus.Failure, PaperlessTaskStatus.Revoked].includes(
|
[PaperlessTaskStatus.Failure, PaperlessTaskStatus.Revoked].includes(
|
||||||
t.status
|
t.status
|
||||||
)
|
)
|
||||||
@@ -68,22 +72,7 @@ export class TasksService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public reload() {
|
public reload() {
|
||||||
if (this.loading) return
|
this.reloadNotifier.next()
|
||||||
this.loading = true
|
|
||||||
|
|
||||||
this.http
|
|
||||||
.get<Results<PaperlessTask>>(`${this.baseUrl}${this.endpoint}/`, {
|
|
||||||
params: {
|
|
||||||
acknowledged: 'false',
|
|
||||||
page_size: this.defaultReloadPageSize,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
.pipe(map((r) => r.results))
|
|
||||||
.pipe(takeUntil(this.unsubscribeNotifer), first())
|
|
||||||
.subscribe((r) => {
|
|
||||||
this.fileTasks.set(r)
|
|
||||||
this.loading = false
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public list(
|
public list(
|
||||||
|
|||||||
@@ -1528,7 +1528,7 @@
|
|||||||
<context context-type="sourcefile">src/app/components/admin/settings/settings.component.html</context>
|
<context context-type="sourcefile">src/app/components/admin/settings/settings.component.html</context>
|
||||||
<context context-type="linenumber">306,308</context>
|
<context context-type="linenumber">306,308</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
<target state="needs-translation"> Settings apply to this user account for objects (Tags, Mail Rules, etc.) created via the web UI. These settings do not apply to documents. </target>
|
<target state="translated"> 这些设置将应用于通过 Web 界面创建的对象(标签、邮件规则等)。这些设置不适用于文档。 </target>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="4292903881380648974" datatype="html">
|
<trans-unit id="4292903881380648974" datatype="html">
|
||||||
<source>Default Owner</source>
|
<source>Default Owner</source>
|
||||||
@@ -9719,7 +9719,7 @@
|
|||||||
<context context-type="sourcefile">src/app/components/document-list/document-card-small/document-card-small.component.html</context>
|
<context context-type="sourcefile">src/app/components/document-list/document-card-small/document-card-small.component.html</context>
|
||||||
<context context-type="linenumber">8</context>
|
<context context-type="linenumber">8</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
<target state="needs-translation"><x id="INTERPOLATION" equiv-text="document().title | documentTitle }}"/> thumbnail</target>
|
<target state="translated"><x id="INTERPOLATION" equiv-text="document().title | documentTitle }}"/> 缩略图</target>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="2784168796433474565" datatype="html">
|
<trans-unit id="2784168796433474565" datatype="html">
|
||||||
<source>Filter by tag</source>
|
<source>Filter by tag</source>
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ msgstr ""
|
|||||||
"Project-Id-Version: paperless-ngx\n"
|
"Project-Id-Version: paperless-ngx\n"
|
||||||
"Report-Msgid-Bugs-To: \n"
|
"Report-Msgid-Bugs-To: \n"
|
||||||
"POT-Creation-Date: 2026-07-27 19:34+0000\n"
|
"POT-Creation-Date: 2026-07-27 19:34+0000\n"
|
||||||
"PO-Revision-Date: 2026-07-27 19:35\n"
|
"PO-Revision-Date: 2026-07-28 12:46\n"
|
||||||
"Last-Translator: \n"
|
"Last-Translator: \n"
|
||||||
"Language-Team: Chinese Simplified\n"
|
"Language-Team: Chinese Simplified\n"
|
||||||
"Language: zh_CN\n"
|
"Language: zh_CN\n"
|
||||||
|
|||||||
Reference in New Issue
Block a user