1
0
Fork 0
mirror of https://github.com/miniflux/v2.git synced 2025-08-26 18:21:01 +00:00

feat(integration): add LinkTaco service for saving articles

This commit is contained in:
Peter Sanchez 2025-08-20 21:35:33 -06:00 committed by GitHub
parent 983291c78b
commit 4d656d2739
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
31 changed files with 939 additions and 5 deletions

View file

@ -72,7 +72,7 @@ Features
### Integrations
- 25+ integrations with third-party services: [Apprise](https://github.com/caronc/apprise), [Betula](https://sr.ht/~bouncepaw/betula/), [Cubox](https://cubox.cc/), [Discord](https://discord.com/), [Espial](https://github.com/jonschoning/espial), [Instapaper](https://www.instapaper.com/), [LinkAce](https://www.linkace.org/), [Linkding](https://github.com/sissbruecker/linkding), [LinkWarden](https://linkwarden.app/), [Matrix](https://matrix.org), [Notion](https://www.notion.com/), [Ntfy](https://ntfy.sh/), [Nunux Keeper](https://keeper.nunux.org/), [Pinboard](https://pinboard.in/), [Pushover](https://pushover.net), [RainDrop](https://raindrop.io/), [Readeck](https://readeck.org/en/), [Readwise Reader](https://readwise.io/read), [RssBridge](https://rss-bridge.org/), [Shaarli](https://github.com/shaarli/Shaarli), [Shiori](https://github.com/go-shiori/shiori), [Slack](https://slack.com/), [Telegram](https://telegram.org), [Wallabag](https://www.wallabag.org/), etc.
- 25+ integrations with third-party services: [Apprise](https://github.com/caronc/apprise), [Betula](https://sr.ht/~bouncepaw/betula/), [Cubox](https://cubox.cc/), [Discord](https://discord.com/), [Espial](https://github.com/jonschoning/espial), [Instapaper](https://www.instapaper.com/), [LinkAce](https://www.linkace.org/), [Linkding](https://github.com/sissbruecker/linkding), [LinkTaco](https://linktaco.com), [LinkWarden](https://linkwarden.app/), [Matrix](https://matrix.org), [Notion](https://www.notion.com/), [Ntfy](https://ntfy.sh/), [Nunux Keeper](https://keeper.nunux.org/), [Pinboard](https://pinboard.in/), [Pushover](https://pushover.net), [RainDrop](https://raindrop.io/), [Readeck](https://readeck.org/en/), [Readwise Reader](https://readwise.io/read), [RssBridge](https://rss-bridge.org/), [Shaarli](https://github.com/shaarli/Shaarli), [Shiori](https://github.com/go-shiori/shiori), [Slack](https://slack.com/), [Telegram](https://telegram.org), [Wallabag](https://www.wallabag.org/), etc.
- Bookmarklet for subscribing to websites directly from any web browser.
- Webhooks for real-time notifications or custom integrations.
- Compatibility with existing mobile applications using the Fever or Google Reader API.

View file

@ -1135,4 +1135,20 @@ var migrations = []func(tx *sql.Tx) error{
_, err = tx.Exec(sql)
return err
},
func(tx *sql.Tx) (err error) {
sql := `
CREATE TYPE linktaco_link_visibility AS ENUM (
'PUBLIC',
'PRIVATE'
);
ALTER TABLE integrations
ADD COLUMN linktaco_enabled bool default 'f',
ADD COLUMN linktaco_api_token text default '',
ADD COLUMN linktaco_org_slug text default '',
ADD COLUMN linktaco_tags text default '',
ADD COLUMN linktaco_visibility linktaco_link_visibility default 'PUBLIC';
`
_, err = tx.Exec(sql)
return err
},
}

View file

@ -15,6 +15,7 @@ import (
"miniflux.app/v2/internal/integration/karakeep"
"miniflux.app/v2/internal/integration/linkace"
"miniflux.app/v2/internal/integration/linkding"
"miniflux.app/v2/internal/integration/linktaco"
"miniflux.app/v2/internal/integration/linkwarden"
"miniflux.app/v2/internal/integration/matrixbot"
"miniflux.app/v2/internal/integration/notion"
@ -242,6 +243,29 @@ func SendEntry(entry *model.Entry, userIntegrations *model.Integration) {
}
}
if userIntegrations.LinktacoEnabled {
slog.Debug("Sending entry to LinkTaco",
slog.Int64("user_id", userIntegrations.UserID),
slog.Int64("entry_id", entry.ID),
slog.String("entry_url", entry.URL),
)
client := linktaco.NewClient(
userIntegrations.LinktacoAPIToken,
userIntegrations.LinktacoOrgSlug,
userIntegrations.LinktacoTags,
userIntegrations.LinktacoVisibility,
)
if err := client.CreateBookmark(entry.URL, entry.Title, entry.Content); err != nil {
slog.Error("Unable to send entry to LinkTaco",
slog.Int64("user_id", userIntegrations.UserID),
slog.Int64("entry_id", entry.ID),
slog.String("entry_url", entry.URL),
slog.Any("error", err),
)
}
}
if userIntegrations.LinkwardenEnabled {
slog.Debug("Sending entry to linkwarden",
slog.Int64("user_id", userIntegrations.UserID),

View file

@ -0,0 +1,146 @@
// SPDX-FileCopyrightText: Copyright The Miniflux Authors. All rights reserved.
// SPDX-License-Identifier: Apache-2.0
package linktaco // import "miniflux.app/v2/internal/integration/linktaco"
import (
"bytes"
"encoding/json"
"fmt"
"net/http"
"strings"
"time"
"miniflux.app/v2/internal/version"
)
const (
defaultClientTimeout = 10 * time.Second
defaultGraphQLURL = "https://api.linktaco.com/query"
maxTags = 10
maxDescriptionLength = 500
)
type Client struct {
graphqlURL string
apiToken string
orgSlug string
tags string
visibility string
}
func NewClient(apiToken, orgSlug, tags, visibility string) *Client {
if visibility == "" {
visibility = "PUBLIC"
}
return &Client{
graphqlURL: defaultGraphQLURL,
apiToken: apiToken,
orgSlug: orgSlug,
tags: tags,
visibility: visibility,
}
}
func (c *Client) CreateBookmark(entryURL, entryTitle, entryContent string) error {
if c.apiToken == "" || c.orgSlug == "" {
return fmt.Errorf("linktaco: missing API token or organization slug")
}
description := entryContent
if len(description) > maxDescriptionLength {
description = description[:maxDescriptionLength]
}
// tags (limit to 10)
tags := strings.FieldsFunc(c.tags, func(c rune) bool {
return c == ',' || c == ' '
})
if len(tags) > maxTags {
tags = tags[:maxTags]
}
// tagsStr is used in GraphQL query to pass comma separated tags
tagsStr := strings.Join(tags, ",")
mutation := `
mutation AddLink($input: LinkInput!) {
addLink(input: $input) {
id
url
title
}
}
`
variables := map[string]any{
"input": map[string]any{
"url": entryURL,
"title": entryTitle,
"description": description,
"orgSlug": c.orgSlug,
"visibility": c.visibility,
"unread": true,
"starred": false,
"archive": false,
"tags": tagsStr,
},
}
requestBody, err := json.Marshal(map[string]any{
"query": mutation,
"variables": variables,
})
if err != nil {
return fmt.Errorf("linktaco: unable to encode request body: %v", err)
}
request, err := http.NewRequest(http.MethodPost, c.graphqlURL, bytes.NewReader(requestBody))
if err != nil {
return fmt.Errorf("linktaco: unable to create request: %v", err)
}
request.Header.Set("Content-Type", "application/json")
request.Header.Set("User-Agent", "Miniflux/"+version.Version)
request.Header.Set("Authorization", "Bearer "+c.apiToken)
httpClient := &http.Client{Timeout: defaultClientTimeout}
response, err := httpClient.Do(request)
if err != nil {
return fmt.Errorf("linktaco: unable to send request: %v", err)
}
defer response.Body.Close()
if response.StatusCode >= 400 {
return fmt.Errorf("linktaco: unable to create bookmark: status=%d", response.StatusCode)
}
var graphqlResponse struct {
Data json.RawMessage `json:"data"`
Errors []json.RawMessage `json:"errors"`
}
if err := json.NewDecoder(response.Body).Decode(&graphqlResponse); err != nil {
return fmt.Errorf("linktaco: unable to decode response: %v", err)
}
if len(graphqlResponse.Errors) > 0 {
// Try to extract error message
var errorMsg string
for _, errJSON := range graphqlResponse.Errors {
var errObj struct {
Message string `json:"message"`
}
if json.Unmarshal(errJSON, &errObj) == nil && errObj.Message != "" {
errorMsg = errObj.Message
break
}
}
if errorMsg == "" {
// Fallback. Should never be reached.
errorMsg = "GraphQL error occurred (fallback message)"
}
return fmt.Errorf("linktaco: %s", errorMsg)
}
return nil
}

View file

@ -0,0 +1,440 @@
// SPDX-FileCopyrightText: Copyright The Miniflux Authors. All rights reserved.
// SPDX-License-Identifier: Apache-2.0
package linktaco
import (
"encoding/json"
"io"
"net/http"
"net/http/httptest"
"strings"
"testing"
)
func TestCreateBookmark(t *testing.T) {
tests := []struct {
name string
apiToken string
orgSlug string
tags string
visibility string
entryURL string
entryTitle string
entryContent string
serverResponse func(w http.ResponseWriter, r *http.Request)
wantErr bool
errContains string
}{
{
name: "successful bookmark creation",
apiToken: "test-token",
orgSlug: "test-org",
tags: "tag1, tag2",
visibility: "PUBLIC",
entryURL: "https://example.com",
entryTitle: "Test Article",
entryContent: "Test content",
serverResponse: func(w http.ResponseWriter, r *http.Request) {
// Verify authorization header
auth := r.Header.Get("Authorization")
if auth != "Bearer test-token" {
t.Errorf("Expected Authorization header 'Bearer test-token', got %s", auth)
}
// Verify content type
contentType := r.Header.Get("Content-Type")
if contentType != "application/json" {
t.Errorf("Expected Content-Type 'application/json', got %s", contentType)
}
// Parse and verify request
body, _ := io.ReadAll(r.Body)
var req map[string]interface{}
if err := json.Unmarshal(body, &req); err != nil {
t.Errorf("Failed to parse request body: %v", err)
}
// Verify mutation exists
if _, ok := req["query"]; !ok {
t.Error("Missing 'query' field in request")
}
// Return success response
w.WriteHeader(http.StatusOK)
json.NewEncoder(w).Encode(map[string]interface{}{
"data": map[string]interface{}{
"addLink": map[string]interface{}{
"id": "123",
"url": "https://example.com",
"title": "Test Article",
},
},
})
},
wantErr: false,
},
{
name: "missing API token",
apiToken: "",
orgSlug: "test-org",
entryURL: "https://example.com",
entryTitle: "Test",
entryContent: "Content",
serverResponse: func(w http.ResponseWriter, r *http.Request) {
// Should not be called
t.Error("Server should not be called when API token is missing")
},
wantErr: true,
errContains: "missing API token or organization slug",
},
{
name: "missing organization slug",
apiToken: "test-token",
orgSlug: "",
entryURL: "https://example.com",
entryTitle: "Test",
entryContent: "Content",
serverResponse: func(w http.ResponseWriter, r *http.Request) {
// Should not be called
t.Error("Server should not be called when org slug is missing")
},
wantErr: true,
errContains: "missing API token or organization slug",
},
{
name: "GraphQL error response",
apiToken: "test-token",
orgSlug: "test-org",
entryURL: "https://example.com",
entryTitle: "Test",
entryContent: "Content",
serverResponse: func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
json.NewEncoder(w).Encode(map[string]interface{}{
"errors": []interface{}{
map[string]interface{}{
"message": "Invalid input",
},
},
})
},
wantErr: true,
errContains: "Invalid input",
},
{
name: "HTTP error status",
apiToken: "test-token",
orgSlug: "test-org",
entryURL: "https://example.com",
entryTitle: "Test",
entryContent: "Content",
serverResponse: func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusUnauthorized)
},
wantErr: true,
errContains: "status=401",
},
{
name: "private visibility permission error",
apiToken: "test-token",
orgSlug: "test-org",
visibility: "PRIVATE",
entryURL: "https://example.com",
entryTitle: "Test",
entryContent: "Content",
serverResponse: func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
json.NewEncoder(w).Encode(map[string]interface{}{
"errors": []interface{}{
map[string]interface{}{
"message": "PRIVATE visibility requires a paid LinkTaco account",
},
},
})
},
wantErr: true,
errContains: "PRIVATE visibility requires a paid LinkTaco account",
},
{
name: "content truncation",
apiToken: "test-token",
orgSlug: "test-org",
entryURL: "https://example.com",
entryTitle: "Test",
entryContent: strings.Repeat("a", 600), // Content longer than 500 chars
serverResponse: func(w http.ResponseWriter, r *http.Request) {
body, _ := io.ReadAll(r.Body)
var req map[string]interface{}
json.Unmarshal(body, &req)
// Check that description was truncated
variables := req["variables"].(map[string]interface{})
input := variables["input"].(map[string]interface{})
description := input["description"].(string)
if len(description) != maxDescriptionLength {
t.Errorf("Expected description length %d, got %d", maxDescriptionLength, len(description))
}
w.WriteHeader(http.StatusOK)
json.NewEncoder(w).Encode(map[string]interface{}{
"data": map[string]interface{}{
"addLink": map[string]interface{}{"id": "123"},
},
})
},
wantErr: false,
},
{
name: "tag limiting",
apiToken: "test-token",
orgSlug: "test-org",
tags: "tag1,tag2,tag3,tag4,tag5,tag6,tag7,tag8,tag9,tag10,tag11,tag12",
entryURL: "https://example.com",
entryTitle: "Test",
entryContent: "Content",
serverResponse: func(w http.ResponseWriter, r *http.Request) {
body, _ := io.ReadAll(r.Body)
var req map[string]interface{}
json.Unmarshal(body, &req)
// Check that only 10 tags were sent
variables := req["variables"].(map[string]interface{})
input := variables["input"].(map[string]interface{})
tags := input["tags"].(string)
tagCount := len(strings.Split(tags, ","))
if tagCount != maxTags {
t.Errorf("Expected %d tags, got %d", maxTags, tagCount)
}
w.WriteHeader(http.StatusOK)
json.NewEncoder(w).Encode(map[string]interface{}{
"data": map[string]interface{}{
"addLink": map[string]interface{}{"id": "123"},
},
})
},
wantErr: false,
},
{
name: "invalid JSON response",
apiToken: "test-token",
orgSlug: "test-org",
entryURL: "https://example.com",
entryTitle: "Test",
entryContent: "Content",
serverResponse: func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
w.Write([]byte("invalid json"))
},
wantErr: true,
errContains: "unable to decode response",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
// Create test server if we have a server response function
var serverURL string
if tt.serverResponse != nil {
server := httptest.NewServer(http.HandlerFunc(tt.serverResponse))
defer server.Close()
serverURL = server.URL
}
// Create client with test server URL
client := &Client{
graphqlURL: serverURL,
apiToken: tt.apiToken,
orgSlug: tt.orgSlug,
tags: tt.tags,
visibility: tt.visibility,
}
// Call CreateBookmark
err := client.CreateBookmark(tt.entryURL, tt.entryTitle, tt.entryContent)
// Check error expectations
if tt.wantErr {
if err == nil {
t.Errorf("Expected error but got none")
} else if tt.errContains != "" && !strings.Contains(err.Error(), tt.errContains) {
t.Errorf("Expected error containing '%s', got '%s'", tt.errContains, err.Error())
}
} else {
if err != nil {
t.Errorf("Unexpected error: %v", err)
}
}
})
}
}
func TestNewClient(t *testing.T) {
tests := []struct {
name string
apiToken string
orgSlug string
tags string
visibility string
expectedVisibility string
}{
{
name: "with all parameters",
apiToken: "token",
orgSlug: "org",
tags: "tag1,tag2",
visibility: "PRIVATE",
expectedVisibility: "PRIVATE",
},
{
name: "empty visibility defaults to PUBLIC",
apiToken: "token",
orgSlug: "org",
tags: "tag1",
visibility: "",
expectedVisibility: "PUBLIC",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
client := NewClient(tt.apiToken, tt.orgSlug, tt.tags, tt.visibility)
if client.apiToken != tt.apiToken {
t.Errorf("Expected apiToken %s, got %s", tt.apiToken, client.apiToken)
}
if client.orgSlug != tt.orgSlug {
t.Errorf("Expected orgSlug %s, got %s", tt.orgSlug, client.orgSlug)
}
if client.tags != tt.tags {
t.Errorf("Expected tags %s, got %s", tt.tags, client.tags)
}
if client.visibility != tt.expectedVisibility {
t.Errorf("Expected visibility %s, got %s", tt.expectedVisibility, client.visibility)
}
if client.graphqlURL != defaultGraphQLURL {
t.Errorf("Expected graphqlURL %s, got %s", defaultGraphQLURL, client.graphqlURL)
}
})
}
}
func TestGraphQLMutation(t *testing.T) {
// Test that the GraphQL mutation is properly formatted
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
body, _ := io.ReadAll(r.Body)
var req map[string]interface{}
if err := json.Unmarshal(body, &req); err != nil {
t.Fatalf("Failed to parse request: %v", err)
}
// Verify mutation structure
query, ok := req["query"].(string)
if !ok {
t.Fatal("Missing query field")
}
// Check that mutation contains expected parts
if !strings.Contains(query, "mutation AddLink") {
t.Error("Mutation should contain 'mutation AddLink'")
}
if !strings.Contains(query, "$input: LinkInput!") {
t.Error("Mutation should contain input parameter")
}
if !strings.Contains(query, "addLink(input: $input)") {
t.Error("Mutation should contain addLink call")
}
// Verify variables structure
variables, ok := req["variables"].(map[string]interface{})
if !ok {
t.Fatal("Missing variables field")
}
input, ok := variables["input"].(map[string]interface{})
if !ok {
t.Fatal("Missing input in variables")
}
// Check all required fields
requiredFields := []string{"url", "title", "description", "orgSlug", "visibility", "unread", "starred", "archive", "tags"}
for _, field := range requiredFields {
if _, ok := input[field]; !ok {
t.Errorf("Missing required field: %s", field)
}
}
// Return success
w.WriteHeader(http.StatusOK)
json.NewEncoder(w).Encode(map[string]interface{}{
"data": map[string]interface{}{
"addLink": map[string]interface{}{
"id": "123",
},
},
})
}))
defer server.Close()
client := &Client{
graphqlURL: server.URL,
apiToken: "test-token",
orgSlug: "test-org",
tags: "test",
visibility: "PUBLIC",
}
err := client.CreateBookmark("https://example.com", "Test Title", "Test Content")
if err != nil {
t.Errorf("Unexpected error: %v", err)
}
}
func BenchmarkCreateBookmark(b *testing.B) {
// Create a mock server that always returns success
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
json.NewEncoder(w).Encode(map[string]interface{}{
"data": map[string]interface{}{
"addLink": map[string]interface{}{
"id": "123",
},
},
})
}))
defer server.Close()
client := &Client{
graphqlURL: server.URL,
apiToken: "test-token",
orgSlug: "test-org",
tags: "tag1,tag2,tag3",
visibility: "PUBLIC",
}
// Run benchmark
b.ResetTimer()
for i := 0; i < b.N; i++ {
_ = client.CreateBookmark("https://example.com", "Test Title", "Test Content")
}
}
func BenchmarkTagProcessing(b *testing.B) {
// Benchmark tag splitting and limiting
tags := "tag1,tag2,tag3,tag4,tag5,tag6,tag7,tag8,tag9,tag10,tag11,tag12,tag13,tag14,tag15"
b.ResetTimer()
for i := 0; i < b.N; i++ {
tagsSplitFn := func(c rune) bool {
return c == ',' || c == ' '
}
splitTags := strings.FieldsFunc(tags, tagsSplitFn)
if len(splitTags) > maxTags {
splitTags = splitTags[:maxTags]
}
_ = strings.Join(splitTags, ",")
}
}

View file

@ -160,6 +160,7 @@
"error.unlink_account_without_password": "Sie müssen ein Passwort festlegen, sonst können Sie sich nicht erneut anmelden.",
"error.user_already_exists": "Dieser Benutzer existiert bereits.",
"error.user_mandatory_fields": "Der Benutzername ist obligatorisch.",
"error.linktaco_missing_required_fields": "LinkTaco API Token und Organization Slug sind erforderlich.",
"form.api_key.label.description": "API-Schlüsselbezeichnung",
"form.category.hide_globally": "Einträge in der globalen Ungelesen-Liste ausblenden",
"form.category.label.title": "Titel",
@ -250,6 +251,16 @@
"form.integration.linkding_bookmark": "Lesezeichen als ungelesen markieren",
"form.integration.linkding_endpoint": "Linkding-API-Endpunkt",
"form.integration.linkding_tags": "Linkding-Tags",
"form.integration.linktaco_activate": "Einträge in LinkTaco speichern",
"form.integration.linktaco_api_token": "LinkTaco API Token",
"form.integration.linktaco_api_token_hint": "Holen Sie sich Ihr persönliches Zugriffstoken unter",
"form.integration.linktaco_org_slug": "Organization Slug",
"form.integration.linktaco_tags": "Tags (max. 10, kommagetrennt)",
"form.integration.linktaco_tags_hint": "Maximal 10 Tags, kommagetrennt",
"form.integration.linktaco_visibility": "Sichtbarkeit",
"form.integration.linktaco_visibility_public": "Öffentlich",
"form.integration.linktaco_visibility_private": "Privat",
"form.integration.linktaco_visibility_hint": "PRIVATE Sichtbarkeit erfordert ein kostenpflichtiges LinkTaco-Konto",
"form.integration.linkwarden_activate": "Artikel in Linkwarden speichern",
"form.integration.linkwarden_api_key": "Linkwarden-API-Schlüssel",
"form.integration.linkwarden_endpoint": "Linkwarden-Base-URL",

View file

@ -160,6 +160,7 @@
"error.unlink_account_without_password": "Πρέπει να ορίσετε έναν κωδικό πρόσβασης διαφορετικά δεν θα μπορείτε να συνδεθείτε ξανά.",
"error.user_already_exists": "Αυτός ο χρήστης υπάρχει ήδη.",
"error.user_mandatory_fields": "Το όνομα χρήστη είναι υποχρεωτικό.",
"error.linktaco_missing_required_fields": "Το LinkTaco API Token και το Organization Slug είναι απαραίτητα",
"form.api_key.label.description": "Ετικέτα κλειδιού API",
"form.category.hide_globally": "Απόκρυψη καταχωρήσεων σε γενική λίστα μη αναγνωσμένων",
"form.category.label.title": "Τίτλος",
@ -250,6 +251,16 @@
"form.integration.linkding_bookmark": "Σημείωση του σελιδοδείκτη ως μη αναγνωσμένου",
"form.integration.linkding_endpoint": "Τελικό σημείο Linkding API",
"form.integration.linkding_tags": "Ετικέτες Linkding",
"form.integration.linktaco_activate": "Αποθήκευση καταχωρήσεων στο LinkTaco",
"form.integration.linktaco_api_token": "LinkTaco API Token",
"form.integration.linktaco_api_token_hint": "Λάβετε το προσωπικό σας διακριτικό πρόσβασης στο",
"form.integration.linktaco_org_slug": "Organization Slug",
"form.integration.linktaco_tags": "Ετικέτες (μέγιστο 10, διαχωρισμένες με κόμμα)",
"form.integration.linktaco_tags_hint": "Μέγιστο 10 ετικέτες, διαχωρισμένες με κόμμα",
"form.integration.linktaco_visibility": "Ορατότητα",
"form.integration.linktaco_visibility_public": "Δημόσια",
"form.integration.linktaco_visibility_private": "Ιδιωτική",
"form.integration.linktaco_visibility_hint": "Η ΙΔΙΩΤΙΚΗ ορατότητα απαιτεί επί πληρωμή λογαριασμό LinkTaco",
"form.integration.linkwarden_activate": "Αποθήκευση άρθρων στο Linkwarden",
"form.integration.linkwarden_api_key": "Κλειδί API Linkwarden",
"form.integration.linkwarden_endpoint": "URL βάσης Linkwarden",

View file

@ -89,6 +89,7 @@
"error.different_passwords": "Passwords are not the same.",
"error.duplicate_fever_username": "There is already someone else with the same Fever username!",
"error.duplicate_googlereader_username": "There is already someone else with the same Google Reader username!",
"error.linktaco_missing_required_fields": "LinkTaco API Token and Organization Slug are required",
"error.duplicate_linked_account": "There is already someone associated with this provider!",
"error.duplicated_feed": "This feed already exists.",
"error.empty_file": "This file is empty.",
@ -250,6 +251,16 @@
"form.integration.linkding_bookmark": "Mark bookmark as unread",
"form.integration.linkding_endpoint": "Linkding API Endpoint",
"form.integration.linkding_tags": "Linkding Tags",
"form.integration.linktaco_activate": "Save entries to LinkTaco",
"form.integration.linktaco_api_token": "LinkTaco API Token",
"form.integration.linktaco_api_token_hint": "Get your personal access token at",
"form.integration.linktaco_org_slug": "Organization Slug",
"form.integration.linktaco_tags": "Tags (max 10, comma-separated)",
"form.integration.linktaco_tags_hint": "Maximum 10 tags, comma-separated",
"form.integration.linktaco_visibility": "Visibility",
"form.integration.linktaco_visibility_public": "Public",
"form.integration.linktaco_visibility_private": "Private",
"form.integration.linktaco_visibility_hint": "PRIVATE visibility requires a paid LinkTaco account",
"form.integration.linkwarden_activate": "Save entries to Linkwarden",
"form.integration.linkwarden_api_key": "Linkwarden API key",
"form.integration.linkwarden_endpoint": "Linkwarden Base URL",

View file

@ -160,6 +160,7 @@
"error.unlink_account_without_password": "Debe definir una contraseña, de lo contrario no podrá volver a iniciar sesión.",
"error.user_already_exists": "Este usuario ya existe.",
"error.user_mandatory_fields": "El nombre de usuario es obligatorio.",
"error.linktaco_missing_required_fields": "LinkTaco API Token y Organization Slug son obligatorios.",
"form.api_key.label.description": "Etiqueta de clave API",
"form.category.hide_globally": "Ocultar artículos en la lista global de no leídos",
"form.category.label.title": "Título",
@ -250,6 +251,16 @@
"form.integration.linkding_bookmark": "Marcar marcador como no leído",
"form.integration.linkding_endpoint": "Acceso API de Linkding",
"form.integration.linkding_tags": "Etiquetas de Linkding",
"form.integration.linktaco_activate": "Guardar entradas en LinkTaco",
"form.integration.linktaco_api_token": "LinkTaco API Token",
"form.integration.linktaco_api_token_hint": "Obtenga su token de acceso personal en",
"form.integration.linktaco_org_slug": "Organization Slug",
"form.integration.linktaco_tags": "Etiquetas (máx. 10, separadas por comas)",
"form.integration.linktaco_tags_hint": "Máximo 10 etiquetas, separadas por comas",
"form.integration.linktaco_visibility": "Visibilidad",
"form.integration.linktaco_visibility_public": "Público",
"form.integration.linktaco_visibility_private": "Privado",
"form.integration.linktaco_visibility_hint": "La visibilidad PRIVADA requiere una cuenta de pago de LinkTaco",
"form.integration.linkwarden_activate": "Enviar artículos a Linkwarden",
"form.integration.linkwarden_api_key": "Clave de API de Linkwarden",
"form.integration.linkwarden_endpoint": "URL base de Linkwarden",

View file

@ -160,6 +160,7 @@
"error.unlink_account_without_password": "Sinun on määritettävä salasana, muuten et voi kirjautua uudelleen.",
"error.user_already_exists": "Käyttäjä on jo olemassa.",
"error.user_mandatory_fields": "Käyttäjätunnus on pakollinen.",
"error.linktaco_missing_required_fields": "LinkTaco API Token ja Organization Slug vaaditaan",
"form.api_key.label.description": "API Key Label",
"form.category.hide_globally": "Piilota artikkelit lukemattomien listassa",
"form.category.label.title": "Otsikko",
@ -250,6 +251,16 @@
"form.integration.linkding_bookmark": "Merkitse kirjanmerkki lukemattomaksi",
"form.integration.linkding_endpoint": "Linkding API-päätepiste",
"form.integration.linkding_tags": "Linkding Tags",
"form.integration.linktaco_activate": "Tallenna kirjoituksia LinkTacoon",
"form.integration.linktaco_api_token": "LinkTaco API Token",
"form.integration.linktaco_api_token_hint": "Hanki henkilökohtainen pääsytunnistesi osoitteesta",
"form.integration.linktaco_org_slug": "Organization Slug",
"form.integration.linktaco_tags": "Tagit (enintään 10, pilkuilla erotettu)",
"form.integration.linktaco_tags_hint": "Enintään 10 tagia, pilkuilla erotettu",
"form.integration.linktaco_visibility": "Näkyvyys",
"form.integration.linktaco_visibility_public": "Julkinen",
"form.integration.linktaco_visibility_private": "Yksityinen",
"form.integration.linktaco_visibility_hint": "YKSITYINEN näkyvyys vaatii maksullisen LinkTaco-tilin",
"form.integration.linkwarden_activate": "Tallenna artikkelit Linkkiin",
"form.integration.linkwarden_api_key": "Linkwarden API-avain",
"form.integration.linkwarden_endpoint": "Linkwarden Base URL",

View file

@ -160,6 +160,7 @@
"error.unlink_account_without_password": "Vous devez définir un mot de passe sinon vous ne pourrez plus vous connecter par la suite.",
"error.user_already_exists": "Cet utilisateur existe déjà.",
"error.user_mandatory_fields": "Le nom d'utilisateur est obligatoire.",
"error.linktaco_missing_required_fields": "Le token API LinkTaco et le slug de l'organisation sont requis.",
"form.api_key.label.description": "Libellé de la clé d'API",
"form.category.hide_globally": "Masquer les entrées dans la liste globale non lue",
"form.category.label.title": "Titre",
@ -250,6 +251,16 @@
"form.integration.linkding_bookmark": "Marquer le lien comme non lu",
"form.integration.linkding_endpoint": "URL de l'API de Linkding",
"form.integration.linkding_tags": "Libellés",
"form.integration.linktaco_activate": "Sauvegarder les entrées vers LinkTaco",
"form.integration.linktaco_api_token": "Token API LinkTaco",
"form.integration.linktaco_api_token_hint": "Obtenez votre token d'accès personnel sur",
"form.integration.linktaco_org_slug": "Slug de l'organisation",
"form.integration.linktaco_tags": "Tags (max. 10, séparés par des virgules)",
"form.integration.linktaco_tags_hint": "Maximum 10 tags, séparés par des virgules",
"form.integration.linktaco_visibility": "Visibilité",
"form.integration.linktaco_visibility_public": "Public",
"form.integration.linktaco_visibility_private": "Privé",
"form.integration.linktaco_visibility_hint": "La visibilité PRIVÉE nécessite un compte LinkTaco payant",
"form.integration.linkwarden_activate": "Sauvegarder les articles vers Linkwarden",
"form.integration.linkwarden_api_key": "Clé d'API de Linkwarden",
"form.integration.linkwarden_endpoint": "URL de base de Linkwarden",

View file

@ -160,6 +160,7 @@
"error.unlink_account_without_password": "आपको एक पासवर्ड परिभाषित करना होगा अन्यथा आप फिर से लॉगिन नहीं कर पाएंगे।",
"error.user_already_exists": "यह उपयोगकर्ता पहले से ही मौजूद है।",
"error.user_mandatory_fields": "उपयोगकर्ता नाम अनिवार्य है।",
"error.linktaco_missing_required_fields": "LinkTaco API Token और Organization Slug आवश्यक हैं",
"form.api_key.label.description": "एपीआई कुंजी लेबल",
"form.category.hide_globally": "वैश्विक अपठित सूची में प्रविष्टियां छिपाएं",
"form.category.label.title": "शीर्षक",
@ -250,6 +251,16 @@
"form.integration.linkding_bookmark": "बुकमार्क को अपठित के रूप में चिह्नित करें",
"form.integration.linkding_endpoint": "लिंकिंग एपीआई समापन बिंदु",
"form.integration.linkding_tags": "Linkding Tags",
"form.integration.linktaco_activate": "LinkTaco में प्रविष्टियाँ सहेजें",
"form.integration.linktaco_api_token": "LinkTaco API Token",
"form.integration.linktaco_api_token_hint": "अपना व्यक्तिगत पहुँच टोकन प्राप्त करें",
"form.integration.linktaco_org_slug": "Organization Slug",
"form.integration.linktaco_tags": "टैग (अधिकतम 10, कॉमा से अलग किए गए)",
"form.integration.linktaco_tags_hint": "अधिकतम 10 टैग, कॉमा से अलग किए गए",
"form.integration.linktaco_visibility": "दृश्यता",
"form.integration.linktaco_visibility_public": "सार्वजनिक",
"form.integration.linktaco_visibility_private": "निजी",
"form.integration.linktaco_visibility_hint": "निजी दृश्यता के लिए भुगतान LinkTaco खाता आवश्यक है",
"form.integration.linkwarden_activate": "Save entries to Linkwarden",
"form.integration.linkwarden_api_key": "Linkwarden API key",
"form.integration.linkwarden_endpoint": "लिंकवर्डन बेस यूआरएलL",

View file

@ -157,6 +157,7 @@
"error.unlink_account_without_password": "Anda harus mengatur kata sandi atau Anda tidak bisa masuk kembali.",
"error.user_already_exists": "Pengguna ini sudah ada.",
"error.user_mandatory_fields": "Harus ada nama pengguna.",
"error.linktaco_missing_required_fields": "LinkTaco API Token dan Organization Slug diperlukan",
"form.api_key.label.description": "Label Kunci API",
"form.category.hide_globally": "Sembunyikan entri di daftar belum dibaca global",
"form.category.label.title": "Judul",
@ -247,6 +248,16 @@
"form.integration.linkding_bookmark": "Tandai markah sebagai belum dibaca",
"form.integration.linkding_endpoint": "Titik URL API Linkding",
"form.integration.linkding_tags": "Tanda Linkding",
"form.integration.linktaco_activate": "Simpan entri ke LinkTaco",
"form.integration.linktaco_api_token": "LinkTaco API Token",
"form.integration.linktaco_api_token_hint": "Dapatkan token akses pribadi Anda di",
"form.integration.linktaco_org_slug": "Organization Slug",
"form.integration.linktaco_tags": "Tag (maksimal 10, dipisahkan koma)",
"form.integration.linktaco_tags_hint": "Maksimal 10 tag, dipisahkan koma",
"form.integration.linktaco_visibility": "Visibilitas",
"form.integration.linktaco_visibility_public": "Publik",
"form.integration.linktaco_visibility_private": "Pribadi",
"form.integration.linktaco_visibility_hint": "Visibilitas PRIBADI membutuhkan akun LinkTaco berbayar",
"form.integration.linkwarden_activate": "Simpan artikel ke Linkwarden",
"form.integration.linkwarden_api_key": "Kunci API Linkwarden",
"form.integration.linkwarden_endpoint": "URL Dasar Linkwarden",

View file

@ -160,6 +160,7 @@
"error.unlink_account_without_password": "Devi scegliere una password altrimenti la prossima volta non riuscirai ad accedere.",
"error.user_already_exists": "Questo utente esiste già.",
"error.user_mandatory_fields": "Il nome utente è obbligatorio.",
"error.linktaco_missing_required_fields": "LinkTaco API Token e Organization Slug sono richiesti",
"form.api_key.label.description": "Etichetta chiave API",
"form.category.hide_globally": "Nascondere le voci nella lista globale dei non letti",
"form.category.label.title": "Titolo",
@ -250,6 +251,16 @@
"form.integration.linkding_bookmark": "Segna i preferiti come non letti",
"form.integration.linkding_endpoint": "Endpoint dell'API di Linkding",
"form.integration.linkding_tags": "Linkding Tags",
"form.integration.linktaco_activate": "Salva le voci in LinkTaco",
"form.integration.linktaco_api_token": "LinkTaco API Token",
"form.integration.linktaco_api_token_hint": "Ottieni il tuo token di accesso personale su",
"form.integration.linktaco_org_slug": "Organization Slug",
"form.integration.linktaco_tags": "Tag (massimo 10, separati da virgola)",
"form.integration.linktaco_tags_hint": "Massimo 10 tag, separati da virgola",
"form.integration.linktaco_visibility": "Visibilità",
"form.integration.linktaco_visibility_public": "Pubblico",
"form.integration.linktaco_visibility_private": "Privato",
"form.integration.linktaco_visibility_hint": "La visibilità PRIVATA richiede un account LinkTaco a pagamento",
"form.integration.linkwarden_activate": "Salva gli articoli su Linkwarden",
"form.integration.linkwarden_api_key": "API key dell'account Linkwarden",
"form.integration.linkwarden_endpoint": "URL di base di Linkwarden",

View file

@ -157,6 +157,7 @@
"error.unlink_account_without_password": "パスワードを設定しなければ再びログインすることはできません。",
"error.user_already_exists": "このユーザーは既に存在します。",
"error.user_mandatory_fields": "ユーザー名が必要です。",
"error.linktaco_missing_required_fields": "LinkTaco API TokenとOrganization Slugが必要です",
"form.api_key.label.description": "API キーラベル",
"form.category.hide_globally": "未読一覧に記事を表示しない",
"form.category.label.title": "タイトル",
@ -247,6 +248,16 @@
"form.integration.linkding_bookmark": "ブックマークを未読にする",
"form.integration.linkding_endpoint": "Linkding の API Endpoint",
"form.integration.linkding_tags": "Linkding Tags",
"form.integration.linktaco_activate": "LinkTacoでエントリを保存する",
"form.integration.linktaco_api_token": "LinkTaco API Token",
"form.integration.linktaco_api_token_hint": "パーソナルアクセストークンを取得する",
"form.integration.linktaco_org_slug": "Organization Slug",
"form.integration.linktaco_tags": "タグ (最大10件、カンマ区切り)",
"form.integration.linktaco_tags_hint": "最大10件のタグ、カンマ区切り",
"form.integration.linktaco_visibility": "公開設定",
"form.integration.linktaco_visibility_public": "公開",
"form.integration.linktaco_visibility_private": "非公開",
"form.integration.linktaco_visibility_hint": "非公開設定には有料のLinkTacoアカウントが必要です",
"form.integration.linkwarden_activate": "Linkwarden に記事を保存する",
"form.integration.linkwarden_api_key": "Linkwarden の API key",
"form.integration.linkwarden_endpoint": "リンクワーデン ベース URL",

View file

@ -157,6 +157,7 @@
"error.unlink_account_without_password": "Lí it-tēng ài siat-tēng bi̍t-bé, bô lí ē bô-hoat-tō͘ koh teng-lo̍k.",
"error.user_already_exists": "Chit ê sú-iōng-lâng í-keng chûn-chāi.",
"error.user_mandatory_fields": "Tio̍h-ài su-li̍p kháu-chō miâ",
"error.linktaco_missing_required_fields": "LinkTaco API Token kâh Organization Slug sio̍kêi",
"form.api_key.label.description": "API só-sîkhan-á",
"form.category.hide_globally": "Mài hián-sī siau-sit tī choân-he̍k ah-bōe tha̍k lia̍t-pió lāi",
"form.category.label.title": "Piau-tôe",
@ -247,6 +248,16 @@
"form.integration.linkding_bookmark": "Chù chòe ah-bōe tha̍k",
"form.integration.linkding_endpoint": "Linkding API thâu",
"form.integration.linkding_tags": "Linkding khan-á",
"form.integration.linktaco_activate": "Pó-chûn siau-sit kàu LinkTaco",
"form.integration.linktaco_api_token": "LinkTaco API Token",
"form.integration.linktaco_api_token_hint": "Chhú-tek lí ê kò-jîn chún-chhú token tī",
"form.integration.linktaco_org_slug": "Organization Slug",
"form.integration.linktaco_tags": "khan-á (siōn-koân 10, iōng tō͘-tiám keh khui)",
"form.integration.linktaco_tags_hint": "Siōn-koân 10 khan-á, iōng tō͘-tiám keh khui",
"form.integration.linktaco_visibility": "Kò-chhiah-kì sìa?",
"form.integration.linktaco_visibility_public": "Kò-chhiah-kì",
"form.integration.linktaco_visibility_private": "Su-lîn",
"form.integration.linktaco_visibility_hint": "Su-lîn sìa tík tio̍h-ài chù-hêng LinkTaco kháu-chō",
"form.integration.linkwarden_activate": "Pó-chûn siau-sit kàu Linkwarden",
"form.integration.linkwarden_api_key": "Linkwarden API só-sî",
"form.integration.linkwarden_endpoint": "Linkwarden Base URL",

View file

@ -160,6 +160,7 @@
"error.unlink_account_without_password": "Je moet een wachtwoord opgeven anders kun je niet meer inloggen.",
"error.user_already_exists": "Deze gebruiker bestaat al.",
"error.user_mandatory_fields": "Gebruikersnaam is verplicht",
"error.linktaco_missing_required_fields": "LinkTaco API Token en Organization Slug zijn verplicht",
"form.api_key.label.description": "API-sleutel omschrijving",
"form.category.hide_globally": "Verberg artikelen in de globale ongelezen lijst",
"form.category.label.title": "Titel",
@ -250,6 +251,16 @@
"form.integration.linkding_bookmark": "Markeer favoriet als ongelezen",
"form.integration.linkding_endpoint": "Linkding URL",
"form.integration.linkding_tags": "Linkding tags",
"form.integration.linktaco_activate": "Artikelen opslaan in LinkTaco",
"form.integration.linktaco_api_token": "LinkTaco API Token",
"form.integration.linktaco_api_token_hint": "Verkrijg uw persoonlijke toegangstoken op",
"form.integration.linktaco_org_slug": "Organization Slug",
"form.integration.linktaco_tags": "Tags (max 10, kommagescheiden)",
"form.integration.linktaco_tags_hint": "Maximaal 10 tags, kommagescheiden",
"form.integration.linktaco_visibility": "Zichtbaarheid",
"form.integration.linktaco_visibility_public": "Openbaar",
"form.integration.linktaco_visibility_private": "Privé",
"form.integration.linktaco_visibility_hint": "PRIVÉ zichtbaarheid vereist een betaald LinkTaco account",
"form.integration.linkwarden_activate": "Artikelen opslaan in Linkwarden",
"form.integration.linkwarden_api_key": "Linkwarden API-sleutel",
"form.integration.linkwarden_endpoint": "Linkwarden Basis URL",

View file

@ -163,6 +163,7 @@
"error.unlink_account_without_password": "Musisz zdefiniować hasło, inaczej nie będziesz mógł się ponownie zalogować.",
"error.user_already_exists": "Ten użytkownik już istnieje.",
"error.user_mandatory_fields": "Nazwa użytkownika jest obowiązkowa.",
"error.linktaco_missing_required_fields": "LinkTaco API Token i Organization Slug są wymagane",
"form.api_key.label.description": "Etykieta klucza API",
"form.category.hide_globally": "Ukryj wpisy na globalnej liście nieprzeczytanych",
"form.category.label.title": "Tytuł",
@ -253,6 +254,16 @@
"form.integration.linkding_bookmark": "Oznacz zakładkę jako nieprzeczytaną",
"form.integration.linkding_endpoint": "Punkt końcowy API Linkding",
"form.integration.linkding_tags": "Znaczniki Linkding",
"form.integration.linktaco_activate": "Zapisuj wpisy w LinkTaco",
"form.integration.linktaco_api_token": "LinkTaco API Token",
"form.integration.linktaco_api_token_hint": "Uzyskaj swój osobisty token dostępu w",
"form.integration.linktaco_org_slug": "Organization Slug",
"form.integration.linktaco_tags": "Znaczniki (max 10, oddzielone przecinkami)",
"form.integration.linktaco_tags_hint": "Maksymalnie 10 znaczników, oddzielone przecinkami",
"form.integration.linktaco_visibility": "Widoczność",
"form.integration.linktaco_visibility_public": "Publiczne",
"form.integration.linktaco_visibility_private": "Prywatne",
"form.integration.linktaco_visibility_hint": "Widoczność PRYWATNA wymaga płatnego konta LinkTaco",
"form.integration.linkwarden_activate": "Zapisuj wpisy w Linkwarden",
"form.integration.linkwarden_api_key": "Klucz API do Linkwarden",
"form.integration.linkwarden_endpoint": "Podstawowy adres URL Linkwardena",

View file

@ -160,6 +160,7 @@
"error.unlink_account_without_password": "Você deve definir uma senha, senão não será possível efetuar a sessão novamente.",
"error.user_already_exists": "Esse usuário já existe.",
"error.user_mandatory_fields": "O nome de usuário é obrigatório.",
"error.linktaco_missing_required_fields": "LinkTaco API Token e Organization Slug são obrigatórios",
"form.api_key.label.description": "Etiqueta da chave de API",
"form.category.hide_globally": "Ocultar entradas na lista global não lida",
"form.category.label.title": "Título",
@ -250,6 +251,16 @@
"form.integration.linkding_bookmark": "Salvar marcador como não lido",
"form.integration.linkding_endpoint": "Endpoint de API do Linkding",
"form.integration.linkding_tags": "Linkding Tags",
"form.integration.linktaco_activate": "Salvar itens no LinkTaco",
"form.integration.linktaco_api_token": "LinkTaco API Token",
"form.integration.linktaco_api_token_hint": "Obtenha seu token de acesso pessoal em",
"form.integration.linktaco_org_slug": "Organization Slug",
"form.integration.linktaco_tags": "Tags (máx 10, separadas por vírgula)",
"form.integration.linktaco_tags_hint": "Máximo 10 tags, separadas por vírgula",
"form.integration.linktaco_visibility": "Visibilidade",
"form.integration.linktaco_visibility_public": "Público",
"form.integration.linktaco_visibility_private": "Privado",
"form.integration.linktaco_visibility_hint": "Visibilidade PRIVADA requer uma conta LinkTaco paga",
"form.integration.linkwarden_activate": "Salvar itens no Linkwarden",
"form.integration.linkwarden_api_key": "Chave de API do Linkwarden",
"form.integration.linkwarden_endpoint": "URL base do Linkwarden",

View file

@ -163,6 +163,7 @@
"error.unlink_account_without_password": "Trebuie să definiți o parolă, altfel nu vă veți mai putea conecta.",
"error.user_already_exists": "Acest utilizator există deja.",
"error.user_mandatory_fields": "Numele utilizatorului este obligatoriu.",
"error.linktaco_missing_required_fields": "LinkTaco API Token și Organization Slug sunt necesare",
"form.api_key.label.description": "Etichetă Cheie API",
"form.category.hide_globally": "Ascunde intrările în lista globală de articole necitite",
"form.category.label.title": "Titlu",
@ -253,6 +254,16 @@
"form.integration.linkding_bookmark": "Marchează semnele de carte ca necitite",
"form.integration.linkding_endpoint": "Endpoint API Linkding",
"form.integration.linkding_tags": "TAG-uri Linkding",
"form.integration.linktaco_activate": "Salvează înregistrările în LinkTaco",
"form.integration.linktaco_api_token": "LinkTaco API Token",
"form.integration.linktaco_api_token_hint": "Obțineți jetonul de acces personal la",
"form.integration.linktaco_org_slug": "Organization Slug",
"form.integration.linktaco_tags": "Tag-uri (maxim 10, separate prin virgule)",
"form.integration.linktaco_tags_hint": "Maxim 10 tag-uri, separate prin virgule",
"form.integration.linktaco_visibility": "Vizibilitate",
"form.integration.linktaco_visibility_public": "Public",
"form.integration.linktaco_visibility_private": "Privat",
"form.integration.linktaco_visibility_hint": "Vizibilitatea PRIVATĂ necesită un cont LinkTaco plătit",
"form.integration.linkwarden_activate": "Salvează intrările în Linkwarden",
"form.integration.linkwarden_api_key": "Cheie API Linkwarden",
"form.integration.linkwarden_endpoint": "URL-ul de bază Linkwarden",

View file

@ -163,6 +163,7 @@
"error.unlink_account_without_password": "Вы должны установить пароль, иначе вы не сможете войти снова.",
"error.user_already_exists": "Этот пользователь уже существует.",
"error.user_mandatory_fields": "Имя пользователя обязательно.",
"error.linktaco_missing_required_fields": "LinkTaco API Token и Organization Slug обязательны",
"form.api_key.label.description": "Описание API-ключа",
"form.category.hide_globally": "Скрыть записи в глобальном списке непрочитанных",
"form.category.label.title": "Название",
@ -253,6 +254,16 @@
"form.integration.linkding_bookmark": "Помечать закладки как непрочитанное",
"form.integration.linkding_endpoint": "Конечная точка Linkding API",
"form.integration.linkding_tags": "Теги Linkding",
"form.integration.linktaco_activate": "Сохранять статьи в LinkTaco",
"form.integration.linktaco_api_token": "LinkTaco API Token",
"form.integration.linktaco_api_token_hint": "Получить ваш персональный токен доступа на",
"form.integration.linktaco_org_slug": "Organization Slug",
"form.integration.linktaco_tags": "Теги (макс. 10, через запятую)",
"form.integration.linktaco_tags_hint": "Максимум 10 тегов, через запятую",
"form.integration.linktaco_visibility": "Видимость",
"form.integration.linktaco_visibility_public": "Публично",
"form.integration.linktaco_visibility_private": "Приватно",
"form.integration.linktaco_visibility_hint": "ПРИВАТНАЯ видимость требует платного аккаунта LinkTaco",
"form.integration.linkwarden_activate": "Сохранять статьи в Linkwarden",
"form.integration.linkwarden_api_key": "API-ключ Linkwarden",
"form.integration.linkwarden_endpoint": "Базовый URL-адрес Linkwarden",

View file

@ -160,6 +160,7 @@
"error.unlink_account_without_password": "Bir şifre belirlemelisiniz, aksi takdirde tekrar oturum açamazsınız.",
"error.user_already_exists": "Bu kullanıcı zaten mevcut.",
"error.user_mandatory_fields": "Kullanıcı adı zorunlu.",
"error.linktaco_missing_required_fields": "LinkTaco API Token ve Organization Slug gereklidir",
"form.api_key.label.description": "API Anahtar Etiketi",
"form.category.hide_globally": "Genel okunmamış listesindeki girişleri gizle",
"form.category.label.title": "Başlık",
@ -250,6 +251,16 @@
"form.integration.linkding_bookmark": "Yer imini okunmadı olarak işaretle",
"form.integration.linkding_endpoint": "Linkding API Uç Noktası",
"form.integration.linkding_tags": "Linkding Etiketleri",
"form.integration.linktaco_activate": "Makaleleri LinkTaco'ya kaydet",
"form.integration.linktaco_api_token": "LinkTaco API Token",
"form.integration.linktaco_api_token_hint": "Kişisel erişim tokenınızı edinin",
"form.integration.linktaco_org_slug": "Organization Slug",
"form.integration.linktaco_tags": "Etiketler (maks 10, virgülle ayrılmış)",
"form.integration.linktaco_tags_hint": "Maksimum 10 etiket, virgülle ayrılmış",
"form.integration.linktaco_visibility": "Görünürlük",
"form.integration.linktaco_visibility_public": "Genel",
"form.integration.linktaco_visibility_private": "Özel",
"form.integration.linktaco_visibility_hint": "ÖZEL görünürlük ücretli bir LinkTaco hesabı gerektirir",
"form.integration.linkwarden_activate": "Makaleleri Linkwarden'e kaydet",
"form.integration.linkwarden_api_key": "Linkwarden API Anahtarı",
"form.integration.linkwarden_endpoint": "Linkwarden Temel URL'si",

View file

@ -162,7 +162,8 @@
"error.unable_to_update_user": "Не вдається оновити користувача.",
"error.unlink_account_without_password": "Ви маєте встановити пароль, щоб мати можливість увійти наступного разу",
"error.user_already_exists": "Такий користувач вже існує.",
"error.user_mandatory_fields": "Ім’я користувача є обов’язковим.",
"error.user_mandatory_fields": "Ім'я користувача є обов'язковим.",
"error.linktaco_missing_required_fields": "LinkTaco API Token і Organization Slug є обов'язковими",
"form.api_key.label.description": "Назва ключа API",
"form.category.hide_globally": "Приховати записи в глобальному списку непрочитаного",
"form.category.label.title": "Назва",
@ -253,6 +254,16 @@
"form.integration.linkding_bookmark": "Відмічати закладку як непрочитану",
"form.integration.linkding_endpoint": "Linkding API Endpoint",
"form.integration.linkding_tags": "Linkding Tags",
"form.integration.linktaco_activate": "Зберігати статті в LinkTaco",
"form.integration.linktaco_api_token": "LinkTaco API Token",
"form.integration.linktaco_api_token_hint": "Отримайте ваш персональний токен доступу на",
"form.integration.linktaco_org_slug": "Organization Slug",
"form.integration.linktaco_tags": "Теги (макс. 10, через кому)",
"form.integration.linktaco_tags_hint": "Максимум 10 тегів, через кому",
"form.integration.linktaco_visibility": "Видимість",
"form.integration.linktaco_visibility_public": "Публічно",
"form.integration.linktaco_visibility_private": "Приватно",
"form.integration.linktaco_visibility_hint": "ПРИВАТНА видимість потребує платного акаунта LinkTaco",
"form.integration.linkwarden_activate": "Зберігати статті до Linkwarden",
"form.integration.linkwarden_api_key": "Ключ API Linkwarden",
"form.integration.linkwarden_endpoint": "Базова URL-адреса Linkwarden",

View file

@ -157,6 +157,7 @@
"error.unlink_account_without_password": "您必须设置密码,否则您将无法再次登录。",
"error.user_already_exists": "此用户已存在。",
"error.user_mandatory_fields": "必须填写用户名。",
"error.linktaco_missing_required_fields": "LinkTaco API Token 和 Organization Slug 是必需的",
"form.api_key.label.description": "API 密钥标签",
"form.category.hide_globally": "在全局未读列表中隐藏条目",
"form.category.label.title": "标题",
@ -247,6 +248,16 @@
"form.integration.linkding_bookmark": "将书签标记为未读",
"form.integration.linkding_endpoint": "Linkding API 端点",
"form.integration.linkding_tags": "Linkding 标签",
"form.integration.linktaco_activate": "保存条目到 LinkTaco",
"form.integration.linktaco_api_token": "LinkTaco API Token",
"form.integration.linktaco_api_token_hint": "在此获取您的个人访问令牌",
"form.integration.linktaco_org_slug": "Organization Slug",
"form.integration.linktaco_tags": "标签最多10个逗号分隔",
"form.integration.linktaco_tags_hint": "最多10个标签逗号分隔",
"form.integration.linktaco_visibility": "可见性",
"form.integration.linktaco_visibility_public": "公开",
"form.integration.linktaco_visibility_private": "私人",
"form.integration.linktaco_visibility_hint": "私人可见性需要付费的 LinkTaco 帐户",
"form.integration.linkwarden_activate": "保存条目到 Linkwarden",
"form.integration.linkwarden_api_key": "Linkwarden API 密钥",
"form.integration.linkwarden_endpoint": "Linkwarden 基本 URL",

View file

@ -157,6 +157,7 @@
"error.unlink_account_without_password": "您必須設定密碼,否則您將無法再次登入。",
"error.user_already_exists": "使用者已存在",
"error.user_mandatory_fields": "必須填寫使用者名稱",
"error.linktaco_missing_required_fields": "LinkTaco API Token 和 Organization Slug 是必需的",
"form.api_key.label.description": "API 金鑰標籤",
"form.category.hide_globally": "在全域未讀列表中隱藏文章",
"form.category.label.title": "標題",
@ -247,6 +248,16 @@
"form.integration.linkding_bookmark": "標記為未讀",
"form.integration.linkding_endpoint": "Linkding API 端點",
"form.integration.linkding_tags": "Linkding 標籤",
"form.integration.linktaco_activate": "儲存文章到 LinkTaco",
"form.integration.linktaco_api_token": "LinkTaco API Token",
"form.integration.linktaco_api_token_hint": "在此取得您的個人存取權杖",
"form.integration.linktaco_org_slug": "Organization Slug",
"form.integration.linktaco_tags": "標籤最多10個逗號分隔",
"form.integration.linktaco_tags_hint": "最多10個標籤逗號分隔",
"form.integration.linktaco_visibility": "可見性",
"form.integration.linktaco_visibility_public": "公開",
"form.integration.linktaco_visibility_private": "私人",
"form.integration.linktaco_visibility_hint": "私人可見性需要付費的 LinkTaco 帳戶",
"form.integration.linkwarden_activate": "儲存文章到 Linkwarden",
"form.integration.linkwarden_api_key": "Linkwarden API 金鑰",
"form.integration.linkwarden_endpoint": "Linkwarden 基本 URL",

View file

@ -59,6 +59,11 @@ type Integration struct {
LinkdingAPIKey string
LinkdingTags string
LinkdingMarkAsUnread bool
LinktacoEnabled bool
LinktacoAPIToken string
LinktacoOrgSlug string
LinktacoTags string
LinktacoVisibility string
LinkwardenEnabled bool
LinkwardenURL string
LinkwardenAPIKey string

View file

@ -220,7 +220,12 @@ func (s *Storage) Integration(userID int64) (*model.Integration, error) {
rssbridge_token,
karakeep_enabled,
karakeep_api_key,
karakeep_url
karakeep_url,
linktaco_enabled,
linktaco_api_token,
linktaco_org_slug,
linktaco_tags,
linktaco_visibility
FROM
integrations
WHERE
@ -340,6 +345,11 @@ func (s *Storage) Integration(userID int64) (*model.Integration, error) {
&integration.KarakeepEnabled,
&integration.KarakeepAPIKey,
&integration.KarakeepURL,
&integration.LinktacoEnabled,
&integration.LinktacoAPIToken,
&integration.LinktacoOrgSlug,
&integration.LinktacoTags,
&integration.LinktacoVisibility,
)
switch {
case err == sql.ErrNoRows:
@ -467,9 +477,14 @@ func (s *Storage) UpdateIntegration(integration *model.Integration) error {
rssbridge_token=$108,
karakeep_enabled=$109,
karakeep_api_key=$110,
karakeep_url=$111
karakeep_url=$111,
linktaco_enabled=$112,
linktaco_api_token=$113,
linktaco_org_slug=$114,
linktaco_tags=$115,
linktaco_visibility=$116
WHERE
user_id=$112
user_id=$117
`
_, err := s.db.Exec(
query,
@ -584,6 +599,11 @@ func (s *Storage) UpdateIntegration(integration *model.Integration) error {
integration.KarakeepEnabled,
integration.KarakeepAPIKey,
integration.KarakeepURL,
integration.LinktacoEnabled,
integration.LinktacoAPIToken,
integration.LinktacoOrgSlug,
integration.LinktacoTags,
integration.LinktacoVisibility,
integration.UserID,
)
@ -614,6 +634,7 @@ func (s *Storage) HasSaveEntry(userID int64) (result bool) {
readwise_enabled='t' OR
linkace_enabled='t' OR
linkding_enabled='t' OR
linktaco_enabled='t' OR
linkwarden_enabled='t' OR
apprise_enabled='t' OR
shiori_enabled='t' OR

View file

@ -228,6 +228,37 @@
</div>
</details>
<details {{ if .form.LinktacoEnabled }}open{{ end }}>
<summary>LinkTaco</summary>
<div class="form-section">
<label>
<input type="checkbox" name="linktaco_enabled" value="1" {{ if .form.LinktacoEnabled }}checked{{ end }}> {{ t "form.integration.linktaco_activate" }}
</label>
<label for="form-linktaco-api-token">{{ t "form.integration.linktaco_api_token" }}</label>
<input type="password" name="linktaco_api_token" id="form-linktaco-api-token" value="{{ .form.LinktacoAPIToken }}" spellcheck="false">
<p class="hint">{{ t "form.integration.linktaco_api_token_hint" }} <a href="https://linktaco.com/oauth2/personal" target="_blank" rel="noopener noreferrer">https://linktaco.com/oauth2/personal</a></p>
<label for="form-linktaco-org-slug">{{ t "form.integration.linktaco_org_slug" }}</label>
<input type="text" name="linktaco_org_slug" id="form-linktaco-org-slug" value="{{ .form.LinktacoOrgSlug }}" placeholder="my-organization" spellcheck="false">
<label for="form-linktaco-tags">{{ t "form.integration.linktaco_tags" }}</label>
<input type="text" name="linktaco_tags" id="form-linktaco-tags" value="{{ .form.LinktacoTags }}" placeholder="miniflux, bookmarks" spellcheck="false">
<p class="hint">{{ t "form.integration.linktaco_tags_hint" }}</p>
<label for="form-linktaco-visibility">{{ t "form.integration.linktaco_visibility" }}</label>
<select name="linktaco_visibility" id="form-linktaco-visibility">
<option value="PUBLIC" {{ if eq .form.LinktacoVisibility "PUBLIC" }}selected{{ end }}>{{ t "form.integration.linktaco_visibility_public" }}</option>
<option value="PRIVATE" {{ if eq .form.LinktacoVisibility "PRIVATE" }}selected{{ end }}>{{ t "form.integration.linktaco_visibility_private" }}</option>
</select>
<p class="hint">{{ t "form.integration.linktaco_visibility_hint" }}</p>
<div class="buttons">
<button type="submit" class="button button-primary" data-label-loading="{{ t "form.submit.saving" }}">{{ t "action.update" }}</button>
</div>
</div>
</details>
<details {{ if .form.LinkwardenEnabled }}open{{ end }}>
<summary>Linkwarden</summary>
<div class="form-section">

View file

@ -62,6 +62,11 @@ type IntegrationForm struct {
LinkdingAPIKey string
LinkdingTags string
LinkdingMarkAsUnread bool
LinktacoEnabled bool
LinktacoAPIToken string
LinktacoOrgSlug string
LinktacoTags string
LinktacoVisibility string
LinkwardenEnabled bool
LinkwardenURL string
LinkwardenAPIKey string
@ -175,6 +180,11 @@ func (i IntegrationForm) Merge(integration *model.Integration) {
integration.LinkdingAPIKey = i.LinkdingAPIKey
integration.LinkdingTags = i.LinkdingTags
integration.LinkdingMarkAsUnread = i.LinkdingMarkAsUnread
integration.LinktacoEnabled = i.LinktacoEnabled
integration.LinktacoAPIToken = i.LinktacoAPIToken
integration.LinktacoOrgSlug = i.LinktacoOrgSlug
integration.LinktacoTags = i.LinktacoTags
integration.LinktacoVisibility = i.LinktacoVisibility
integration.LinkwardenEnabled = i.LinkwardenEnabled
integration.LinkwardenURL = i.LinkwardenURL
integration.LinkwardenAPIKey = i.LinkwardenAPIKey
@ -290,6 +300,11 @@ func NewIntegrationForm(r *http.Request) *IntegrationForm {
LinkdingAPIKey: r.FormValue("linkding_api_key"),
LinkdingTags: r.FormValue("linkding_tags"),
LinkdingMarkAsUnread: r.FormValue("linkding_mark_as_unread") == "1",
LinktacoEnabled: r.FormValue("linktaco_enabled") == "1",
LinktacoAPIToken: r.FormValue("linktaco_api_token"),
LinktacoOrgSlug: r.FormValue("linktaco_org_slug"),
LinktacoTags: r.FormValue("linktaco_tags"),
LinktacoVisibility: r.FormValue("linktaco_visibility"),
LinkwardenEnabled: r.FormValue("linkwarden_enabled") == "1",
LinkwardenURL: r.FormValue("linkwarden_url"),
LinkwardenAPIKey: r.FormValue("linkwarden_api_key"),

View file

@ -75,6 +75,11 @@ func (h *handler) showIntegrationPage(w http.ResponseWriter, r *http.Request) {
LinkdingAPIKey: integration.LinkdingAPIKey,
LinkdingTags: integration.LinkdingTags,
LinkdingMarkAsUnread: integration.LinkdingMarkAsUnread,
LinktacoEnabled: integration.LinktacoEnabled,
LinktacoAPIToken: integration.LinktacoAPIToken,
LinktacoOrgSlug: integration.LinktacoOrgSlug,
LinktacoTags: integration.LinktacoTags,
LinktacoVisibility: integration.LinktacoVisibility,
LinkwardenEnabled: integration.LinkwardenEnabled,
LinkwardenURL: integration.LinkwardenURL,
LinkwardenAPIKey: integration.LinkwardenAPIKey,

View file

@ -75,6 +75,17 @@ func (h *handler) updateIntegration(w http.ResponseWriter, r *http.Request) {
integration.WebhookSecret = ""
}
if integrationForm.LinktacoEnabled {
if integrationForm.LinktacoAPIToken == "" || integrationForm.LinktacoOrgSlug == "" {
sess.NewFlashErrorMessage(printer.Print("error.linktaco_missing_required_fields"))
html.Redirect(w, r, route.Path(h.router, "integrations"))
return
}
if integration.LinktacoVisibility == "" {
integration.LinktacoVisibility = "PUBLIC"
}
}
err = h.store.UpdateIntegration(integration)
if err != nil {
html.ServerError(w, r, err)