mirror of
https://github.com/Athou/commafeed.git
synced 2026-03-21 21:37:29 +00:00
migrate filtering expressions to safer CEL and add a query builder
This commit is contained in:
110
commafeed-client/package-lock.json
generated
110
commafeed-client/package-lock.json
generated
@@ -19,6 +19,7 @@
|
||||
"@mantine/notifications": "^8.3.14",
|
||||
"@mantine/spotlight": "^8.3.14",
|
||||
"@monaco-editor/react": "^4.7.0",
|
||||
"@react-querybuilder/mantine": "^8.14.0",
|
||||
"@reduxjs/toolkit": "^2.11.2",
|
||||
"axios": "^1.13.5",
|
||||
"dayjs": "^1.11.19",
|
||||
@@ -33,6 +34,7 @@
|
||||
"react-draggable": "^4.5.0",
|
||||
"react-icons": "^5.5.0",
|
||||
"react-infinite-scroller": "^1.2.6",
|
||||
"react-querybuilder": "^8.14.0",
|
||||
"react-redux": "^9.2.0",
|
||||
"react-router-dom": "^7.13.0",
|
||||
"react-swipeable": "^7.0.2",
|
||||
@@ -1707,6 +1709,23 @@
|
||||
"react-dom": "^18.x || ^19.x"
|
||||
}
|
||||
},
|
||||
"node_modules/@mantine/dates": {
|
||||
"version": "8.3.14",
|
||||
"resolved": "https://registry.npmjs.org/@mantine/dates/-/dates-8.3.14.tgz",
|
||||
"integrity": "sha512-NdStRo2ZQ55MoMF5B9vjhpBpHRDHF1XA9Dkb1kKSdNuLlaFXKlvoaZxj/3LfNPpn7Nqlns78nWt4X8/cgC2YIg==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"clsx": "^2.1.1"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@mantine/core": "8.3.14",
|
||||
"@mantine/hooks": "8.3.14",
|
||||
"dayjs": ">=1.0.0",
|
||||
"react": "^18.x || ^19.x",
|
||||
"react-dom": "^18.x || ^19.x"
|
||||
}
|
||||
},
|
||||
"node_modules/@mantine/form": {
|
||||
"version": "8.3.14",
|
||||
"resolved": "https://registry.npmjs.org/@mantine/form/-/form-8.3.14.tgz",
|
||||
@@ -1813,6 +1832,48 @@
|
||||
"react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@react-querybuilder/core": {
|
||||
"version": "8.14.0",
|
||||
"resolved": "https://registry.npmjs.org/@react-querybuilder/core/-/core-8.14.0.tgz",
|
||||
"integrity": "sha512-j1pIY0Yyn/dXu9ZST/DVY7TqRmIO1hY/mZ8653DaeHaDzUF37tOdkm/NQDU9RfM0KXIWsJY5zlvYAR1DypZ+7g==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@ts-jison/lexer": "0.4.1-alpha.1",
|
||||
"@ts-jison/parser": "0.4.1-alpha.1",
|
||||
"immer": "^11.1.3",
|
||||
"numeric-quantity": "^2.1.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"drizzle-orm": ">=0.38.0",
|
||||
"json-logic-js": ">=2",
|
||||
"sequelize": ">=6"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"drizzle-orm": {
|
||||
"optional": true
|
||||
},
|
||||
"json-logic-js": {
|
||||
"optional": true
|
||||
},
|
||||
"sequelize": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@react-querybuilder/mantine": {
|
||||
"version": "8.14.0",
|
||||
"resolved": "https://registry.npmjs.org/@react-querybuilder/mantine/-/mantine-8.14.0.tgz",
|
||||
"integrity": "sha512-bfoLRBI4x4PbgdlM25f0kPmxz3SjASTGKCE5mZWc5UmfI6P0lbhCXe5t30LJHXvGG+tUeTfloeacaESB9TD9MA==",
|
||||
"license": "MIT",
|
||||
"peerDependencies": {
|
||||
"@mantine/core": ">=7",
|
||||
"@mantine/dates": ">=7",
|
||||
"@mantine/hooks": ">=7",
|
||||
"dayjs": ">=1",
|
||||
"react": ">=18",
|
||||
"react-querybuilder": "8.14.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@reduxjs/toolkit": {
|
||||
"version": "2.11.2",
|
||||
"resolved": "https://registry.npmjs.org/@reduxjs/toolkit/-/toolkit-2.11.2.tgz",
|
||||
@@ -2305,6 +2366,31 @@
|
||||
"@testing-library/dom": ">=7.21.4"
|
||||
}
|
||||
},
|
||||
"node_modules/@ts-jison/common": {
|
||||
"version": "0.4.1-alpha.1",
|
||||
"resolved": "https://registry.npmjs.org/@ts-jison/common/-/common-0.4.1-alpha.1.tgz",
|
||||
"integrity": "sha512-SDbHzq+UMD+V3ciKVBHwCEgVqSeyQPTCjOsd/ZNTGySUVg4x3EauR9ZcEfdVFAsYRR38XWgDI+spq5LDY46KvQ==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@ts-jison/lexer": {
|
||||
"version": "0.4.1-alpha.1",
|
||||
"resolved": "https://registry.npmjs.org/@ts-jison/lexer/-/lexer-0.4.1-alpha.1.tgz",
|
||||
"integrity": "sha512-5C1Wr+wixAzn2MOFtgy7KbT6N6j9mhmbjAtyvOqZKsikKtNOQj22MM5HxT+ooRexG2NbtxnDSXYdhHR1Lg58ow==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@ts-jison/common": "^0.4.1-alpha.1"
|
||||
}
|
||||
},
|
||||
"node_modules/@ts-jison/parser": {
|
||||
"version": "0.4.1-alpha.1",
|
||||
"resolved": "https://registry.npmjs.org/@ts-jison/parser/-/parser-0.4.1-alpha.1.tgz",
|
||||
"integrity": "sha512-xNj+qOez/7dju44LlYiTlCjxMzW5oek9EckUAElfln/GBK9vgMSk0swWcnacMr0TYbGjUQuXvL2wEgmDf5WajQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@ts-jison/common": "^0.4.1-alpha.1",
|
||||
"@ts-jison/lexer": "^0.4.1-alpha.1"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/aria-query": {
|
||||
"version": "5.0.4",
|
||||
"resolved": "https://registry.npmjs.org/@types/aria-query/-/aria-query-5.0.4.tgz",
|
||||
@@ -4843,6 +4929,15 @@
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/numeric-quantity": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/numeric-quantity/-/numeric-quantity-2.1.0.tgz",
|
||||
"integrity": "sha512-oDkQ8nFuNVA+unEg1jd6dAS+O7eLXWWzsa4ViI0S0yFi6654GK0s74o8bF8uLRQdWIz/qFF1GABNFPfwAGQUsg==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=16"
|
||||
}
|
||||
},
|
||||
"node_modules/object-assign": {
|
||||
"version": "4.1.1",
|
||||
"resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
|
||||
@@ -5275,6 +5370,21 @@
|
||||
"react-dom": "^0.14 || ^15.0.0 || ^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/react-querybuilder": {
|
||||
"version": "8.14.0",
|
||||
"resolved": "https://registry.npmjs.org/react-querybuilder/-/react-querybuilder-8.14.0.tgz",
|
||||
"integrity": "sha512-uwJn1XT4A6reuxjPmRLUnvewhC4PZksZU4XSxCJgqwR37r2A1/REvxEgv+zVQGVFcd4dUIZs1E++WDabOWVlmA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@react-querybuilder/core": "^8.14.0",
|
||||
"@reduxjs/toolkit": "^2.11.2",
|
||||
"immer": "^11.1.3",
|
||||
"react-redux": "^9.2.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/react-redux": {
|
||||
"version": "9.2.0",
|
||||
"resolved": "https://registry.npmjs.org/react-redux/-/react-redux-9.2.0.tgz",
|
||||
|
||||
@@ -27,6 +27,7 @@
|
||||
"@mantine/notifications": "^8.3.14",
|
||||
"@mantine/spotlight": "^8.3.14",
|
||||
"@monaco-editor/react": "^4.7.0",
|
||||
"@react-querybuilder/mantine": "^8.14.0",
|
||||
"@reduxjs/toolkit": "^2.11.2",
|
||||
"axios": "^1.13.5",
|
||||
"dayjs": "^1.11.19",
|
||||
@@ -41,6 +42,7 @@
|
||||
"react-draggable": "^4.5.0",
|
||||
"react-icons": "^5.5.0",
|
||||
"react-infinite-scroller": "^1.2.6",
|
||||
"react-querybuilder": "^8.14.0",
|
||||
"react-redux": "^9.2.0",
|
||||
"react-router-dom": "^7.13.0",
|
||||
"react-swipeable": "^7.0.2",
|
||||
|
||||
@@ -28,6 +28,7 @@ export interface Subscription {
|
||||
position: number
|
||||
newestItemTime?: number
|
||||
filter?: string
|
||||
filterLegacy?: string
|
||||
}
|
||||
|
||||
export interface Category {
|
||||
|
||||
@@ -0,0 +1,118 @@
|
||||
import { Stack } from "@mantine/core"
|
||||
import { MantineValueSelector, QueryBuilderMantine } from "@react-querybuilder/mantine"
|
||||
import {
|
||||
type CombinatorSelectorProps,
|
||||
defaultOperators,
|
||||
defaultRuleProcessorCEL,
|
||||
type Field,
|
||||
type FormatQueryOptions,
|
||||
formatQuery,
|
||||
QueryBuilder,
|
||||
type RuleGroupType,
|
||||
} from "react-querybuilder"
|
||||
import { isCELIdentifier, isCELMember, isCELStringLiteral, parseCEL } from "react-querybuilder/parseCEL"
|
||||
import "react-querybuilder/dist/query-builder.css"
|
||||
|
||||
const fields: Field[] = [
|
||||
{ name: "title", label: "Title" },
|
||||
{ name: "content", label: "Content" },
|
||||
{ name: "url", label: "URL" },
|
||||
{ name: "author", label: "Author" },
|
||||
{ name: "categories", label: "Categories" },
|
||||
{ name: "titleLower", label: "Title (lower case)" },
|
||||
{ name: "contentLower", label: "Content (lower case)" },
|
||||
{ name: "urlLower", label: "URL (lower case)" },
|
||||
{ name: "authorLower", label: "Author (lower case)" },
|
||||
{ name: "categoriesLower", label: "Categories (lower case)" },
|
||||
]
|
||||
|
||||
const textOperators = new Set(["=", "!=", "contains", "beginsWith", "endsWith", "doesNotContain", "doesNotBeginWith", "doesNotEndWith"])
|
||||
|
||||
function toCelString(query: RuleGroupType): string {
|
||||
if (query.rules.length === 0) {
|
||||
return ""
|
||||
}
|
||||
|
||||
const celFormatOptions: FormatQueryOptions = {
|
||||
format: "cel",
|
||||
ruleProcessor: (rule, options, meta) => {
|
||||
if (rule.operator === "matches") {
|
||||
const escapedValue = String(rule.value).replaceAll("\\", "\\\\").replaceAll('"', String.raw`\"`)
|
||||
return `${rule.field}.matches("${escapedValue}")`
|
||||
}
|
||||
|
||||
return defaultRuleProcessorCEL(rule, options, meta)
|
||||
},
|
||||
}
|
||||
|
||||
return formatQuery(query, celFormatOptions)
|
||||
}
|
||||
|
||||
function fromCelString(celString: string): RuleGroupType {
|
||||
return parseCEL(celString ?? "", {
|
||||
customExpressionHandler: expr => {
|
||||
if (
|
||||
isCELMember(expr) &&
|
||||
expr.right?.value === "matches" &&
|
||||
expr.left &&
|
||||
isCELIdentifier(expr.left) &&
|
||||
expr.list &&
|
||||
isCELStringLiteral(expr.list.value[0])
|
||||
) {
|
||||
return {
|
||||
field: expr.left.value,
|
||||
operator: "matches",
|
||||
value: JSON.parse(expr.list.value[0].value),
|
||||
}
|
||||
}
|
||||
|
||||
return null
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
const getOperators = () => {
|
||||
const filteredDefault = defaultOperators.filter(op => textOperators.has(op.name))
|
||||
return [
|
||||
...filteredDefault,
|
||||
{
|
||||
name: "matches",
|
||||
label: "matches pattern",
|
||||
},
|
||||
]
|
||||
}
|
||||
|
||||
function CombinatorSelector(props: Readonly<CombinatorSelectorProps>) {
|
||||
if (props.rules.length === 0) {
|
||||
return null
|
||||
}
|
||||
return <MantineValueSelector {...props} />
|
||||
}
|
||||
|
||||
interface FilteringExpressionEditorProps {
|
||||
initialValue: string | undefined
|
||||
onChange: (value: string) => void
|
||||
}
|
||||
|
||||
export function FilteringExpressionEditor({ initialValue, onChange }: Readonly<FilteringExpressionEditorProps>) {
|
||||
const handleQueryChange = (newQuery: RuleGroupType) => {
|
||||
onChange(toCelString(newQuery))
|
||||
}
|
||||
|
||||
return (
|
||||
<Stack gap="sm">
|
||||
<QueryBuilderMantine>
|
||||
<QueryBuilder
|
||||
fields={fields}
|
||||
defaultQuery={fromCelString(initialValue ?? "")}
|
||||
onQueryChange={handleQueryChange}
|
||||
getOperators={getOperators}
|
||||
addRuleToNewGroups
|
||||
resetOnFieldChange={false}
|
||||
controlClassnames={{ queryBuilder: "queryBuilder-branches" }}
|
||||
controlElements={{ combinatorSelector: CombinatorSelector }}
|
||||
/>
|
||||
</QueryBuilderMantine>
|
||||
</Stack>
|
||||
)
|
||||
}
|
||||
@@ -95,6 +95,7 @@ export function Tree() {
|
||||
expanded={false}
|
||||
level={0}
|
||||
hasError={false}
|
||||
hasWarning={false}
|
||||
onClick={categoryClicked}
|
||||
/>
|
||||
)
|
||||
@@ -110,6 +111,7 @@ export function Tree() {
|
||||
expanded={false}
|
||||
level={0}
|
||||
hasError={false}
|
||||
hasWarning={false}
|
||||
onClick={categoryClicked}
|
||||
/>
|
||||
)
|
||||
@@ -118,6 +120,7 @@ export function Tree() {
|
||||
if (!isCategoryDisplayed(category)) return null
|
||||
|
||||
const hasError = !category.expanded && flattenCategoryTree(category).some(c => c.feeds.some(f => f.errorCount > errorThreshold))
|
||||
const hasWarning = !category.expanded && flattenCategoryTree(category).some(c => c.feeds.some(f => !!f.filterLegacy))
|
||||
return (
|
||||
<TreeNode
|
||||
id={category.id}
|
||||
@@ -130,6 +133,7 @@ export function Tree() {
|
||||
expanded={category.expanded}
|
||||
level={level}
|
||||
hasError={hasError}
|
||||
hasWarning={hasWarning}
|
||||
onClick={categoryClicked}
|
||||
onIconClick={e => categoryIconClicked(e, category)}
|
||||
key={category.id}
|
||||
@@ -151,6 +155,7 @@ export function Tree() {
|
||||
selected={source.type === "feed" && source.id === String(feed.id)}
|
||||
level={level}
|
||||
hasError={feed.errorCount > errorThreshold}
|
||||
hasWarning={!!feed.filterLegacy}
|
||||
onClick={feedClicked}
|
||||
key={feed.id}
|
||||
/>
|
||||
@@ -168,6 +173,7 @@ export function Tree() {
|
||||
selected={source.type === "tag" && source.id === tag}
|
||||
level={0}
|
||||
hasError={false}
|
||||
hasWarning={false}
|
||||
onClick={tagClicked}
|
||||
key={tag}
|
||||
/>
|
||||
|
||||
@@ -15,6 +15,7 @@ interface TreeNodeProps {
|
||||
expanded?: boolean
|
||||
level: number
|
||||
hasError: boolean
|
||||
hasWarning: boolean
|
||||
hasNewEntries: boolean
|
||||
onClick: (e: React.MouseEvent, id: string) => void
|
||||
onIconClick?: (e: React.MouseEvent, id: string) => void
|
||||
@@ -24,15 +25,18 @@ const useStyles = tss
|
||||
.withParams<{
|
||||
selected: boolean
|
||||
hasError: boolean
|
||||
hasWarning: boolean
|
||||
hasUnread: boolean
|
||||
}>()
|
||||
.create(({ theme, colorScheme, selected, hasError, hasUnread }) => {
|
||||
.create(({ theme, colorScheme, selected, hasError, hasWarning, hasUnread }) => {
|
||||
let backgroundColor = "inherit"
|
||||
if (selected) backgroundColor = colorScheme === "dark" ? theme.colors.dark[4] : theme.colors.gray[1]
|
||||
|
||||
let color: string
|
||||
if (hasError) {
|
||||
color = theme.colors.red[6]
|
||||
} else if (hasWarning) {
|
||||
color = theme.colors.yellow[6]
|
||||
} else if (colorScheme === "dark") {
|
||||
color = hasUnread ? theme.colors.dark[0] : theme.colors.dark[3]
|
||||
} else {
|
||||
@@ -63,6 +67,7 @@ export function TreeNode(props: Readonly<TreeNodeProps>) {
|
||||
const { classes } = useStyles({
|
||||
selected: props.selected,
|
||||
hasError: props.hasError,
|
||||
hasWarning: props.hasWarning,
|
||||
hasUnread: props.unread > 0,
|
||||
})
|
||||
return (
|
||||
|
||||
@@ -17,10 +17,6 @@ msgstr ""
|
||||
msgid "<0>CommaFeed is an open-source project. Sources are hosted on </0><1>GitHub</1>."
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/app/FeedDetailsPage.tsx
|
||||
msgid "<0>Complete syntax is available </0><1>here</1><2>.</2>"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/auth/RegistrationPage.tsx
|
||||
msgid "<0>Have an account?</0><1>Log in!</1>"
|
||||
msgstr "<0> هل لديك حساب؟ </0> <1> تسجيل الدخول! </ 1>"
|
||||
@@ -126,10 +122,6 @@ msgstr "هل أنت متأكد أنك تريد إلغاء الاشتراك من
|
||||
msgid "Asc"
|
||||
msgstr "تصاعدي"
|
||||
|
||||
#: src/pages/app/FeedDetailsPage.tsx
|
||||
msgid "Available variables are 'title', 'content', 'url' 'author' and 'categories' and their content is converted to lower case to ease string comparison."
|
||||
msgstr "المتغيرات المتاحة هي \"العنوان\" و \"المحتوى\" و \"url\" و \"المؤلف\" و \"الفئات\" ويتم تحويل محتواها إلى أحرف صغيرة لتسهيل مقارنة السلسلة."
|
||||
|
||||
#: src/components/content/add/Subscribe.tsx
|
||||
msgid "Back"
|
||||
msgstr "العودة"
|
||||
@@ -155,6 +147,10 @@ msgstr ""
|
||||
msgid "Browser tab"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/app/FeedDetailsPage.tsx
|
||||
msgid "Build a filter expression to indicate what you want to read. Entries that don't match will be marked as read automatically."
|
||||
msgstr ""
|
||||
|
||||
#: src/components/admin/UserEdit.tsx
|
||||
#: src/components/content/add/AddCategory.tsx
|
||||
#: src/components/content/add/ImportOpml.tsx
|
||||
@@ -365,10 +361,6 @@ msgstr ""
|
||||
msgid "Error"
|
||||
msgstr "خطأ"
|
||||
|
||||
#: src/pages/app/FeedDetailsPage.tsx
|
||||
msgid "Example: {example}."
|
||||
msgstr "مثال: {مثال}."
|
||||
|
||||
#: src/components/header/ProfileMenu.tsx
|
||||
msgid "Expanded"
|
||||
msgstr "موسع"
|
||||
@@ -469,10 +461,6 @@ msgstr ""
|
||||
msgid "Id"
|
||||
msgstr "المرجع نفسه"
|
||||
|
||||
#: src/pages/app/FeedDetailsPage.tsx
|
||||
msgid "If not empty, an expression evaluating to 'true' or 'false'. If false, new entries for this feed will be marked as read automatically."
|
||||
msgstr "إذا لم يكن فارغًا ، فسيتم تقييم التعبير إلى \"صواب\" أو \"خطأ\". "
|
||||
|
||||
#: src/components/settings/DisplaySettings.tsx
|
||||
msgid "If the entry doesn't entirely fit on the screen"
|
||||
msgstr ""
|
||||
@@ -1037,6 +1025,11 @@ msgstr "عنوان URL للتغذية التي تريد الاشتراك فيه
|
||||
msgid "Theme"
|
||||
msgstr "الموضوع"
|
||||
|
||||
#. placeholder {0}: feed.filterLegacy
|
||||
#: src/pages/app/FeedDetailsPage.tsx
|
||||
msgid "This feed has a legacy filter that cannot be edited and is not applied. Please recreate the filter using the new expression editor. The legacy filter expression was: <0>{0}</0>"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/settings/ProfileSettings.tsx
|
||||
msgid "This is your API key. It can be used for some read-only API operations and grants access to the Fever API. Use the form at the bottom of the page to generate a new API key"
|
||||
msgstr ""
|
||||
|
||||
@@ -17,10 +17,6 @@ msgstr ""
|
||||
msgid "<0>CommaFeed is an open-source project. Sources are hosted on </0><1>GitHub</1>."
|
||||
msgstr "<0>CommaFeed és un projecte de codi obert. El codi font està allotjat a </0><1>GitHub</1>."
|
||||
|
||||
#: src/pages/app/FeedDetailsPage.tsx
|
||||
msgid "<0>Complete syntax is available </0><1>here</1><2>.</2>"
|
||||
msgstr "<0>La sintaxi completa està disponible </0><1>aquí</1><2>.</2>"
|
||||
|
||||
#: src/pages/auth/RegistrationPage.tsx
|
||||
msgid "<0>Have an account?</0><1>Log in!</1>"
|
||||
msgstr "<0>Teniu un compte?</0><1>Inicieu la sessió!</1>"
|
||||
@@ -126,10 +122,6 @@ msgstr "Esteu segur que voleu cancel·lar la subscripció a <0>{feedName}</0>?"
|
||||
msgid "Asc"
|
||||
msgstr "Asc"
|
||||
|
||||
#: src/pages/app/FeedDetailsPage.tsx
|
||||
msgid "Available variables are 'title', 'content', 'url' 'author' and 'categories' and their content is converted to lower case to ease string comparison."
|
||||
msgstr "Les variables disponibles són \"títol\", \"contingut\", \"url\" \"autor\" i \"categories\" i el seu contingut es converteix en minúscules per facilitar la comparació de cadenes."
|
||||
|
||||
#: src/components/content/add/Subscribe.tsx
|
||||
msgid "Back"
|
||||
msgstr "Enrere"
|
||||
@@ -155,6 +147,10 @@ msgstr "Extensió del navegador necessària per a Chrome"
|
||||
msgid "Browser tab"
|
||||
msgstr "Pestanya del navegador"
|
||||
|
||||
#: src/pages/app/FeedDetailsPage.tsx
|
||||
msgid "Build a filter expression to indicate what you want to read. Entries that don't match will be marked as read automatically."
|
||||
msgstr ""
|
||||
|
||||
#: src/components/admin/UserEdit.tsx
|
||||
#: src/components/content/add/AddCategory.tsx
|
||||
#: src/components/content/add/ImportOpml.tsx
|
||||
@@ -365,10 +361,6 @@ msgstr "Encapçalaments d'entrada"
|
||||
msgid "Error"
|
||||
msgstr "Error"
|
||||
|
||||
#: src/pages/app/FeedDetailsPage.tsx
|
||||
msgid "Example: {example}."
|
||||
msgstr "Exemple: {exemple}."
|
||||
|
||||
#: src/components/header/ProfileMenu.tsx
|
||||
msgid "Expanded"
|
||||
msgstr "Ampliat"
|
||||
@@ -469,10 +461,6 @@ msgstr "Verd"
|
||||
msgid "Id"
|
||||
msgstr "Id"
|
||||
|
||||
#: src/pages/app/FeedDetailsPage.tsx
|
||||
msgid "If not empty, an expression evaluating to 'true' or 'false'. If false, new entries for this feed will be marked as read automatically."
|
||||
msgstr "Si no està buida, una expressió que s'avalua com a \"vertader\" o \"fals\". "
|
||||
|
||||
#: src/components/settings/DisplaySettings.tsx
|
||||
msgid "If the entry doesn't entirely fit on the screen"
|
||||
msgstr "Si l'entrada no encaixa del tot a la pantalla"
|
||||
@@ -1037,6 +1025,11 @@ msgstr "l'URL del canal al qual us voleu subscriure. "
|
||||
msgid "Theme"
|
||||
msgstr "Tema"
|
||||
|
||||
#. placeholder {0}: feed.filterLegacy
|
||||
#: src/pages/app/FeedDetailsPage.tsx
|
||||
msgid "This feed has a legacy filter that cannot be edited and is not applied. Please recreate the filter using the new expression editor. The legacy filter expression was: <0>{0}</0>"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/settings/ProfileSettings.tsx
|
||||
msgid "This is your API key. It can be used for some read-only API operations and grants access to the Fever API. Use the form at the bottom of the page to generate a new API key"
|
||||
msgstr "Aquesta és la vostra clau de l'API. Es pot utilitzar per a algunes operacions de l'API de només lectura i permet accedir a l'API Fever. Utilitzeu el formulari de la part inferior de la pàgina per generar una nova clau d'API."
|
||||
|
||||
@@ -17,10 +17,6 @@ msgstr ""
|
||||
msgid "<0>CommaFeed is an open-source project. Sources are hosted on </0><1>GitHub</1>."
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/app/FeedDetailsPage.tsx
|
||||
msgid "<0>Complete syntax is available </0><1>here</1><2>.</2>"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/auth/RegistrationPage.tsx
|
||||
msgid "<0>Have an account?</0><1>Log in!</1>"
|
||||
msgstr "<0>Máte účet?</0><1>Přihlaste se!</1>"
|
||||
@@ -126,10 +122,6 @@ msgstr "Opravdu se chcete odhlásit z odběru <0>{feedName}</0>?"
|
||||
msgid "Asc"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/app/FeedDetailsPage.tsx
|
||||
msgid "Available variables are 'title', 'content', 'url' 'author' and 'categories' and their content is converted to lower case to ease string comparison."
|
||||
msgstr "Dostupné proměnné jsou 'název', 'obsah', 'url', 'autor' a 'kategorie' a jejich obsah je převeden na malá písmena, aby se usnadnilo porovnávání řetězců."
|
||||
|
||||
#: src/components/content/add/Subscribe.tsx
|
||||
msgid "Back"
|
||||
msgstr "Zpět"
|
||||
@@ -155,6 +147,10 @@ msgstr ""
|
||||
msgid "Browser tab"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/app/FeedDetailsPage.tsx
|
||||
msgid "Build a filter expression to indicate what you want to read. Entries that don't match will be marked as read automatically."
|
||||
msgstr ""
|
||||
|
||||
#: src/components/admin/UserEdit.tsx
|
||||
#: src/components/content/add/AddCategory.tsx
|
||||
#: src/components/content/add/ImportOpml.tsx
|
||||
@@ -365,10 +361,6 @@ msgstr ""
|
||||
msgid "Error"
|
||||
msgstr "Chyba"
|
||||
|
||||
#: src/pages/app/FeedDetailsPage.tsx
|
||||
msgid "Example: {example}."
|
||||
msgstr "Příklad: {příklad}."
|
||||
|
||||
#: src/components/header/ProfileMenu.tsx
|
||||
msgid "Expanded"
|
||||
msgstr "Rozbaleno"
|
||||
@@ -469,10 +461,6 @@ msgstr ""
|
||||
msgid "Id"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/app/FeedDetailsPage.tsx
|
||||
msgid "If not empty, an expression evaluating to 'true' or 'false'. If false, new entries for this feed will be marked as read automatically."
|
||||
msgstr "Pokud není prázdný, výraz vyhodnocený jako 'true' nebo 'false'. "
|
||||
|
||||
#: src/components/settings/DisplaySettings.tsx
|
||||
msgid "If the entry doesn't entirely fit on the screen"
|
||||
msgstr ""
|
||||
@@ -1037,6 +1025,11 @@ msgstr "Adresa URL kanálu, k jehož odběru se chcete přihlásit. "
|
||||
msgid "Theme"
|
||||
msgstr "Téma"
|
||||
|
||||
#. placeholder {0}: feed.filterLegacy
|
||||
#: src/pages/app/FeedDetailsPage.tsx
|
||||
msgid "This feed has a legacy filter that cannot be edited and is not applied. Please recreate the filter using the new expression editor. The legacy filter expression was: <0>{0}</0>"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/settings/ProfileSettings.tsx
|
||||
msgid "This is your API key. It can be used for some read-only API operations and grants access to the Fever API. Use the form at the bottom of the page to generate a new API key"
|
||||
msgstr ""
|
||||
|
||||
@@ -17,10 +17,6 @@ msgstr ""
|
||||
msgid "<0>CommaFeed is an open-source project. Sources are hosted on </0><1>GitHub</1>."
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/app/FeedDetailsPage.tsx
|
||||
msgid "<0>Complete syntax is available </0><1>here</1><2>.</2>"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/auth/RegistrationPage.tsx
|
||||
msgid "<0>Have an account?</0><1>Log in!</1>"
|
||||
msgstr "<0>A oes gennych gyfrif?</0><1>Mewngofnodi!</1>"
|
||||
@@ -126,10 +122,6 @@ msgstr "Ydych chi'n siŵr eich bod am ddad-danysgrifio o <0>{feedName}</0>?"
|
||||
msgid "Asc"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/app/FeedDetailsPage.tsx
|
||||
msgid "Available variables are 'title', 'content', 'url' 'author' and 'categories' and their content is converted to lower case to ease string comparison."
|
||||
msgstr "Y newidynnau sydd ar gael yw 'teitl', 'cynnwys', 'url' 'awdur' a 'chategorïau' ac mae eu cynnwys yn cael ei drosi i lythrennau bach er mwyn hwyluso cymhariaeth llinynnol."
|
||||
|
||||
#: src/components/content/add/Subscribe.tsx
|
||||
msgid "Back"
|
||||
msgstr "Yn ôl"
|
||||
@@ -155,6 +147,10 @@ msgstr ""
|
||||
msgid "Browser tab"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/app/FeedDetailsPage.tsx
|
||||
msgid "Build a filter expression to indicate what you want to read. Entries that don't match will be marked as read automatically."
|
||||
msgstr ""
|
||||
|
||||
#: src/components/admin/UserEdit.tsx
|
||||
#: src/components/content/add/AddCategory.tsx
|
||||
#: src/components/content/add/ImportOpml.tsx
|
||||
@@ -365,10 +361,6 @@ msgstr ""
|
||||
msgid "Error"
|
||||
msgstr "Gwall"
|
||||
|
||||
#: src/pages/app/FeedDetailsPage.tsx
|
||||
msgid "Example: {example}."
|
||||
msgstr "enghraifft: {example}."
|
||||
|
||||
#: src/components/header/ProfileMenu.tsx
|
||||
msgid "Expanded"
|
||||
msgstr "Ehangu"
|
||||
@@ -469,10 +461,6 @@ msgstr ""
|
||||
msgid "Id"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/app/FeedDetailsPage.tsx
|
||||
msgid "If not empty, an expression evaluating to 'true' or 'false'. If false, new entries for this feed will be marked as read automatically."
|
||||
msgstr "Os nad yw'n wag, mynegiad sy'n gwerthuso i 'gwir' neu 'anghywir'. "
|
||||
|
||||
#: src/components/settings/DisplaySettings.tsx
|
||||
msgid "If the entry doesn't entirely fit on the screen"
|
||||
msgstr ""
|
||||
@@ -1037,6 +1025,11 @@ msgstr "Y URL ar gyfer y porthwr rydych chi am danysgrifio iddo. "
|
||||
msgid "Theme"
|
||||
msgstr "Thema"
|
||||
|
||||
#. placeholder {0}: feed.filterLegacy
|
||||
#: src/pages/app/FeedDetailsPage.tsx
|
||||
msgid "This feed has a legacy filter that cannot be edited and is not applied. Please recreate the filter using the new expression editor. The legacy filter expression was: <0>{0}</0>"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/settings/ProfileSettings.tsx
|
||||
msgid "This is your API key. It can be used for some read-only API operations and grants access to the Fever API. Use the form at the bottom of the page to generate a new API key"
|
||||
msgstr ""
|
||||
|
||||
@@ -17,10 +17,6 @@ msgstr ""
|
||||
msgid "<0>CommaFeed is an open-source project. Sources are hosted on </0><1>GitHub</1>."
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/app/FeedDetailsPage.tsx
|
||||
msgid "<0>Complete syntax is available </0><1>here</1><2>.</2>"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/auth/RegistrationPage.tsx
|
||||
msgid "<0>Have an account?</0><1>Log in!</1>"
|
||||
msgstr "<0>Har du en konto?</0><1>Log ind!</1>"
|
||||
@@ -126,10 +122,6 @@ msgstr "Er du sikker på, at du vil afmelde <0>{feedName}</0>?"
|
||||
msgid "Asc"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/app/FeedDetailsPage.tsx
|
||||
msgid "Available variables are 'title', 'content', 'url' 'author' and 'categories' and their content is converted to lower case to ease string comparison."
|
||||
msgstr "Tilgængelige variabler er 'title', 'content', 'url' 'author' og 'category', og deres indhold konverteres til små bogstaver for at lette strengsammenligning."
|
||||
|
||||
#: src/components/content/add/Subscribe.tsx
|
||||
msgid "Back"
|
||||
msgstr "Tilbage"
|
||||
@@ -155,6 +147,10 @@ msgstr ""
|
||||
msgid "Browser tab"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/app/FeedDetailsPage.tsx
|
||||
msgid "Build a filter expression to indicate what you want to read. Entries that don't match will be marked as read automatically."
|
||||
msgstr ""
|
||||
|
||||
#: src/components/admin/UserEdit.tsx
|
||||
#: src/components/content/add/AddCategory.tsx
|
||||
#: src/components/content/add/ImportOpml.tsx
|
||||
@@ -365,10 +361,6 @@ msgstr ""
|
||||
msgid "Error"
|
||||
msgstr "Fejl"
|
||||
|
||||
#: src/pages/app/FeedDetailsPage.tsx
|
||||
msgid "Example: {example}."
|
||||
msgstr "Eksempel: {eksempel}."
|
||||
|
||||
#: src/components/header/ProfileMenu.tsx
|
||||
msgid "Expanded"
|
||||
msgstr "Udvidet"
|
||||
@@ -469,10 +461,6 @@ msgstr ""
|
||||
msgid "Id"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/app/FeedDetailsPage.tsx
|
||||
msgid "If not empty, an expression evaluating to 'true' or 'false'. If false, new entries for this feed will be marked as read automatically."
|
||||
msgstr "Hvis det ikke er tomt, et udtryk, der vurderes til 'sand' eller 'falsk'. "
|
||||
|
||||
#: src/components/settings/DisplaySettings.tsx
|
||||
msgid "If the entry doesn't entirely fit on the screen"
|
||||
msgstr ""
|
||||
@@ -1037,6 +1025,11 @@ msgstr "URL'en til det feed, du vil abonnere på. "
|
||||
msgid "Theme"
|
||||
msgstr "Tema"
|
||||
|
||||
#. placeholder {0}: feed.filterLegacy
|
||||
#: src/pages/app/FeedDetailsPage.tsx
|
||||
msgid "This feed has a legacy filter that cannot be edited and is not applied. Please recreate the filter using the new expression editor. The legacy filter expression was: <0>{0}</0>"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/settings/ProfileSettings.tsx
|
||||
msgid "This is your API key. It can be used for some read-only API operations and grants access to the Fever API. Use the form at the bottom of the page to generate a new API key"
|
||||
msgstr ""
|
||||
|
||||
@@ -17,10 +17,6 @@ msgstr ""
|
||||
msgid "<0>CommaFeed is an open-source project. Sources are hosted on </0><1>GitHub</1>."
|
||||
msgstr "<0>CommaFeed ist ein Open Source Projekt. Der Quellcode wird auf auf </0><1>GitHub</1> gehostet."
|
||||
|
||||
#: src/pages/app/FeedDetailsPage.tsx
|
||||
msgid "<0>Complete syntax is available </0><1>here</1><2>.</2>"
|
||||
msgstr "<0>Die vollständige Syntax ist </0><1>hier</1> verfügbar<2>.</2>"
|
||||
|
||||
#: src/pages/auth/RegistrationPage.tsx
|
||||
msgid "<0>Have an account?</0><1>Log in!</1>"
|
||||
msgstr "<0>Haben Sie ein Konto?</0><1>Melden Sie sich an!</1>"
|
||||
@@ -126,10 +122,6 @@ msgstr "Sind Sie sicher, dass Sie <0>{feedName}</0> abbestellen möchten?"
|
||||
msgid "Asc"
|
||||
msgstr "Aufsteigend"
|
||||
|
||||
#: src/pages/app/FeedDetailsPage.tsx
|
||||
msgid "Available variables are 'title', 'content', 'url' 'author' and 'categories' and their content is converted to lower case to ease string comparison."
|
||||
msgstr "Verfügbare Variablen sind „Titel“, „Inhalt“, „URL“, „Autor“ und „Kategorien“, und ihr Inhalt wird in Kleinbuchstaben umgewandelt, um den String-Vergleich zu erleichtern."
|
||||
|
||||
#: src/components/content/add/Subscribe.tsx
|
||||
msgid "Back"
|
||||
msgstr "Zurück"
|
||||
@@ -155,6 +147,10 @@ msgstr "Browser-Erweiterung für Chrome benötigt"
|
||||
msgid "Browser tab"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/app/FeedDetailsPage.tsx
|
||||
msgid "Build a filter expression to indicate what you want to read. Entries that don't match will be marked as read automatically."
|
||||
msgstr ""
|
||||
|
||||
#: src/components/admin/UserEdit.tsx
|
||||
#: src/components/content/add/AddCategory.tsx
|
||||
#: src/components/content/add/ImportOpml.tsx
|
||||
@@ -365,10 +361,6 @@ msgstr ""
|
||||
msgid "Error"
|
||||
msgstr "Fehler"
|
||||
|
||||
#: src/pages/app/FeedDetailsPage.tsx
|
||||
msgid "Example: {example}."
|
||||
msgstr "Beispiel: {example}."
|
||||
|
||||
#: src/components/header/ProfileMenu.tsx
|
||||
msgid "Expanded"
|
||||
msgstr "Erweitert"
|
||||
@@ -469,10 +461,6 @@ msgstr ""
|
||||
msgid "Id"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/app/FeedDetailsPage.tsx
|
||||
msgid "If not empty, an expression evaluating to 'true' or 'false'. If false, new entries for this feed will be marked as read automatically."
|
||||
msgstr "Wenn nicht leer, ein Ausdruck, der als „wahr“ oder „falsch“ ausgewertet wird."
|
||||
|
||||
#: src/components/settings/DisplaySettings.tsx
|
||||
msgid "If the entry doesn't entirely fit on the screen"
|
||||
msgstr "Wenn der Eintrag nicht ganz auf den Bildschirm passt"
|
||||
@@ -1037,6 +1025,11 @@ msgstr "Die URL für den Feed, den Sie abonnieren möchten. "
|
||||
msgid "Theme"
|
||||
msgstr "Thema"
|
||||
|
||||
#. placeholder {0}: feed.filterLegacy
|
||||
#: src/pages/app/FeedDetailsPage.tsx
|
||||
msgid "This feed has a legacy filter that cannot be edited and is not applied. Please recreate the filter using the new expression editor. The legacy filter expression was: <0>{0}</0>"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/settings/ProfileSettings.tsx
|
||||
msgid "This is your API key. It can be used for some read-only API operations and grants access to the Fever API. Use the form at the bottom of the page to generate a new API key"
|
||||
msgstr "Dies ist Ihr API-Schlüssel. Er kann für einige schreibgeschützte API-Vorgänge verwendet werden und ermöglicht den Zugriff auf die Fever-API. Verwenden Sie das Formular unten auf der Seite, um einen neuen API-Schlüssel zu generieren"
|
||||
|
||||
@@ -17,10 +17,6 @@ msgstr ""
|
||||
msgid "<0>CommaFeed is an open-source project. Sources are hosted on </0><1>GitHub</1>."
|
||||
msgstr "<0>CommaFeed is an open-source project. Sources are hosted on </0><1>GitHub</1>."
|
||||
|
||||
#: src/pages/app/FeedDetailsPage.tsx
|
||||
msgid "<0>Complete syntax is available </0><1>here</1><2>.</2>"
|
||||
msgstr "<0>Complete syntax is available </0><1>here</1><2>.</2>"
|
||||
|
||||
#: src/pages/auth/RegistrationPage.tsx
|
||||
msgid "<0>Have an account?</0><1>Log in!</1>"
|
||||
msgstr "<0>Have an account?</0><1>Log in!</1>"
|
||||
@@ -126,10 +122,6 @@ msgstr "Are you sure you want to unsubscribe from <0>{feedName}</0>?"
|
||||
msgid "Asc"
|
||||
msgstr "Asc"
|
||||
|
||||
#: src/pages/app/FeedDetailsPage.tsx
|
||||
msgid "Available variables are 'title', 'content', 'url' 'author' and 'categories' and their content is converted to lower case to ease string comparison."
|
||||
msgstr "Available variables are 'title', 'content', 'url' 'author' and 'categories' and their content is converted to lower case to ease string comparison."
|
||||
|
||||
#: src/components/content/add/Subscribe.tsx
|
||||
msgid "Back"
|
||||
msgstr "Back"
|
||||
@@ -155,6 +147,10 @@ msgstr "Browser extension required for Chrome"
|
||||
msgid "Browser tab"
|
||||
msgstr "Browser tab"
|
||||
|
||||
#: src/pages/app/FeedDetailsPage.tsx
|
||||
msgid "Build a filter expression to indicate what you want to read. Entries that don't match will be marked as read automatically."
|
||||
msgstr "Build a filter expression to indicate what you want to read. Entries that don't match will be marked as read automatically."
|
||||
|
||||
#: src/components/admin/UserEdit.tsx
|
||||
#: src/components/content/add/AddCategory.tsx
|
||||
#: src/components/content/add/ImportOpml.tsx
|
||||
@@ -365,10 +361,6 @@ msgstr "Entry headers"
|
||||
msgid "Error"
|
||||
msgstr "Error"
|
||||
|
||||
#: src/pages/app/FeedDetailsPage.tsx
|
||||
msgid "Example: {example}."
|
||||
msgstr "Example: {example}."
|
||||
|
||||
#: src/components/header/ProfileMenu.tsx
|
||||
msgid "Expanded"
|
||||
msgstr "Expanded"
|
||||
@@ -469,10 +461,6 @@ msgstr "Green"
|
||||
msgid "Id"
|
||||
msgstr "Id"
|
||||
|
||||
#: src/pages/app/FeedDetailsPage.tsx
|
||||
msgid "If not empty, an expression evaluating to 'true' or 'false'. If false, new entries for this feed will be marked as read automatically."
|
||||
msgstr "If not empty, an expression evaluating to 'true' or 'false'. If false, new entries for this feed will be marked as read automatically."
|
||||
|
||||
#: src/components/settings/DisplaySettings.tsx
|
||||
msgid "If the entry doesn't entirely fit on the screen"
|
||||
msgstr "If the entry doesn't entirely fit on the screen"
|
||||
@@ -1037,6 +1025,11 @@ msgstr "The URL for the feed you want to subscribe to. You can also use the webs
|
||||
msgid "Theme"
|
||||
msgstr "Theme"
|
||||
|
||||
#. placeholder {0}: feed.filterLegacy
|
||||
#: src/pages/app/FeedDetailsPage.tsx
|
||||
msgid "This feed has a legacy filter that cannot be edited and is not applied. Please recreate the filter using the new expression editor. The legacy filter expression was: <0>{0}</0>"
|
||||
msgstr "This feed has a legacy filter that cannot be edited and is not applied. Please recreate the filter using the new expression editor. The legacy filter expression was: <0>{0}</0>"
|
||||
|
||||
#: src/components/settings/ProfileSettings.tsx
|
||||
msgid "This is your API key. It can be used for some read-only API operations and grants access to the Fever API. Use the form at the bottom of the page to generate a new API key"
|
||||
msgstr "This is your API key. It can be used for some read-only API operations and grants access to the Fever API. Use the form at the bottom of the page to generate a new API key"
|
||||
|
||||
@@ -18,10 +18,6 @@ msgstr ""
|
||||
msgid "<0>CommaFeed is an open-source project. Sources are hosted on </0><1>GitHub</1>."
|
||||
msgstr "<0>CommaFeed es un proyecto de código abierto. El código fuente está hospedado en </0><1>GitHub</1>."
|
||||
|
||||
#: src/pages/app/FeedDetailsPage.tsx
|
||||
msgid "<0>Complete syntax is available </0><1>here</1><2>.</2>"
|
||||
msgstr "<0>La sintaxis completa está disponible </0><1>aquí</1><2>.</2>"
|
||||
|
||||
#: src/pages/auth/RegistrationPage.tsx
|
||||
msgid "<0>Have an account?</0><1>Log in!</1>"
|
||||
msgstr "<0>¿Tienes una cuenta?</0><1>¡Inicia sesión!</1>"
|
||||
@@ -127,10 +123,6 @@ msgstr "¿Estás seguro de que deseas darte de baja de <0>{feedName}</0>?"
|
||||
msgid "Asc"
|
||||
msgstr "Asc"
|
||||
|
||||
#: src/pages/app/FeedDetailsPage.tsx
|
||||
msgid "Available variables are 'title', 'content', 'url' 'author' and 'categories' and their content is converted to lower case to ease string comparison."
|
||||
msgstr "Las variables disponibles son 'título', 'contenido', 'url', 'autor' y 'categorías' y su contenido se convierte a minúsculas para facilitar la comparación de cadenas."
|
||||
|
||||
#: src/components/content/add/Subscribe.tsx
|
||||
msgid "Back"
|
||||
msgstr "Atrás"
|
||||
@@ -156,6 +148,10 @@ msgstr "Se requiere extensión de navegador para Chrome"
|
||||
msgid "Browser tab"
|
||||
msgstr "Pestaña del navegador"
|
||||
|
||||
#: src/pages/app/FeedDetailsPage.tsx
|
||||
msgid "Build a filter expression to indicate what you want to read. Entries that don't match will be marked as read automatically."
|
||||
msgstr ""
|
||||
|
||||
#: src/components/admin/UserEdit.tsx
|
||||
#: src/components/content/add/AddCategory.tsx
|
||||
#: src/components/content/add/ImportOpml.tsx
|
||||
@@ -366,10 +362,6 @@ msgstr "Encabezados de las entradas"
|
||||
msgid "Error"
|
||||
msgstr "Error"
|
||||
|
||||
#: src/pages/app/FeedDetailsPage.tsx
|
||||
msgid "Example: {example}."
|
||||
msgstr "Ejemplo: {ejemplo}."
|
||||
|
||||
#: src/components/header/ProfileMenu.tsx
|
||||
msgid "Expanded"
|
||||
msgstr "Expandido"
|
||||
@@ -470,10 +462,6 @@ msgstr ""
|
||||
msgid "Id"
|
||||
msgstr "Identificación"
|
||||
|
||||
#: src/pages/app/FeedDetailsPage.tsx
|
||||
msgid "If not empty, an expression evaluating to 'true' or 'false'. If false, new entries for this feed will be marked as read automatically."
|
||||
msgstr "Si no está vacía, una expresión que se evalúa como \"verdadera\" o \"falso\". Si es falso, las nuevas entradas de este feed se marcarán como leídas automáticamente."
|
||||
|
||||
#: src/components/settings/DisplaySettings.tsx
|
||||
msgid "If the entry doesn't entirely fit on the screen"
|
||||
msgstr "Si la entrada no cabe completamente en la pantalla"
|
||||
@@ -1038,6 +1026,11 @@ msgstr "La URL del feed al que desea suscribirse. También puede utilizar la URL
|
||||
msgid "Theme"
|
||||
msgstr "Tema"
|
||||
|
||||
#. placeholder {0}: feed.filterLegacy
|
||||
#: src/pages/app/FeedDetailsPage.tsx
|
||||
msgid "This feed has a legacy filter that cannot be edited and is not applied. Please recreate the filter using the new expression editor. The legacy filter expression was: <0>{0}</0>"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/settings/ProfileSettings.tsx
|
||||
msgid "This is your API key. It can be used for some read-only API operations and grants access to the Fever API. Use the form at the bottom of the page to generate a new API key"
|
||||
msgstr "Esta es su clave API. Se puede utilizar para algunas operaciones API de solo lectura y otorga acceso a Fever API. Utilice el formulario en la parte inferior de la página para generar una nueva clave API"
|
||||
|
||||
@@ -17,10 +17,6 @@ msgstr ""
|
||||
msgid "<0>CommaFeed is an open-source project. Sources are hosted on </0><1>GitHub</1>."
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/app/FeedDetailsPage.tsx
|
||||
msgid "<0>Complete syntax is available </0><1>here</1><2>.</2>"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/auth/RegistrationPage.tsx
|
||||
msgid "<0>Have an account?</0><1>Log in!</1>"
|
||||
msgstr "<0>حساب دارید؟</0><1>وارد سیستم شوید!</1>"
|
||||
@@ -126,10 +122,6 @@ msgstr "آیا مطمئن هستید که می خواهید اشتراک <0>{fee
|
||||
msgid "Asc"
|
||||
msgstr "صعودی"
|
||||
|
||||
#: src/pages/app/FeedDetailsPage.tsx
|
||||
msgid "Available variables are 'title', 'content', 'url' 'author' and 'categories' and their content is converted to lower case to ease string comparison."
|
||||
msgstr "متغیرهای موجود عبارتند از «عنوان»، «محتوا»، «url» «نویسنده» و «دستهها» و محتوای آنها برای سهولت مقایسه رشتهها به حروف کوچک تبدیل میشود."
|
||||
|
||||
#: src/components/content/add/Subscribe.tsx
|
||||
msgid "Back"
|
||||
msgstr "برگشت"
|
||||
@@ -155,6 +147,10 @@ msgstr ""
|
||||
msgid "Browser tab"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/app/FeedDetailsPage.tsx
|
||||
msgid "Build a filter expression to indicate what you want to read. Entries that don't match will be marked as read automatically."
|
||||
msgstr ""
|
||||
|
||||
#: src/components/admin/UserEdit.tsx
|
||||
#: src/components/content/add/AddCategory.tsx
|
||||
#: src/components/content/add/ImportOpml.tsx
|
||||
@@ -365,10 +361,6 @@ msgstr ""
|
||||
msgid "Error"
|
||||
msgstr "خطا"
|
||||
|
||||
#: src/pages/app/FeedDetailsPage.tsx
|
||||
msgid "Example: {example}."
|
||||
msgstr "مثال: {مثال}."
|
||||
|
||||
#: src/components/header/ProfileMenu.tsx
|
||||
msgid "Expanded"
|
||||
msgstr "گسترش یافت"
|
||||
@@ -469,10 +461,6 @@ msgstr ""
|
||||
msgid "Id"
|
||||
msgstr "شناسه"
|
||||
|
||||
#: src/pages/app/FeedDetailsPage.tsx
|
||||
msgid "If not empty, an expression evaluating to 'true' or 'false'. If false, new entries for this feed will be marked as read automatically."
|
||||
msgstr "اگر خالی نباشد، عبارتی به \"درست\" یا \"نادرست\" ارزیابی می شود. "
|
||||
|
||||
#: src/components/settings/DisplaySettings.tsx
|
||||
msgid "If the entry doesn't entirely fit on the screen"
|
||||
msgstr ""
|
||||
@@ -1037,6 +1025,11 @@ msgstr "URL فیدی که می خواهید در آن مشترک شوید. "
|
||||
msgid "Theme"
|
||||
msgstr "تم"
|
||||
|
||||
#. placeholder {0}: feed.filterLegacy
|
||||
#: src/pages/app/FeedDetailsPage.tsx
|
||||
msgid "This feed has a legacy filter that cannot be edited and is not applied. Please recreate the filter using the new expression editor. The legacy filter expression was: <0>{0}</0>"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/settings/ProfileSettings.tsx
|
||||
msgid "This is your API key. It can be used for some read-only API operations and grants access to the Fever API. Use the form at the bottom of the page to generate a new API key"
|
||||
msgstr ""
|
||||
|
||||
@@ -17,10 +17,6 @@ msgstr ""
|
||||
msgid "<0>CommaFeed is an open-source project. Sources are hosted on </0><1>GitHub</1>."
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/app/FeedDetailsPage.tsx
|
||||
msgid "<0>Complete syntax is available </0><1>here</1><2>.</2>"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/auth/RegistrationPage.tsx
|
||||
msgid "<0>Have an account?</0><1>Log in!</1>"
|
||||
msgstr "<0>Onko sinulla tili?</0><1>Kirjaudu sisään!</1>"
|
||||
@@ -126,10 +122,6 @@ msgstr "Haluatko varmasti peruuttaa kohteen <0>{feedName}</0> tilauksen?"
|
||||
msgid "Asc"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/app/FeedDetailsPage.tsx
|
||||
msgid "Available variables are 'title', 'content', 'url' 'author' and 'categories' and their content is converted to lower case to ease string comparison."
|
||||
msgstr "Käytettävissä olevat muuttujat ovat 'title', 'content', 'url' 'author' ja 'categories', ja niiden sisältö muunnetaan pienillä kirjaimilla merkkijonojen vertailun helpottamiseksi."
|
||||
|
||||
#: src/components/content/add/Subscribe.tsx
|
||||
msgid "Back"
|
||||
msgstr "Takaisin"
|
||||
@@ -155,6 +147,10 @@ msgstr ""
|
||||
msgid "Browser tab"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/app/FeedDetailsPage.tsx
|
||||
msgid "Build a filter expression to indicate what you want to read. Entries that don't match will be marked as read automatically."
|
||||
msgstr ""
|
||||
|
||||
#: src/components/admin/UserEdit.tsx
|
||||
#: src/components/content/add/AddCategory.tsx
|
||||
#: src/components/content/add/ImportOpml.tsx
|
||||
@@ -365,10 +361,6 @@ msgstr ""
|
||||
msgid "Error"
|
||||
msgstr "Virhe"
|
||||
|
||||
#: src/pages/app/FeedDetailsPage.tsx
|
||||
msgid "Example: {example}."
|
||||
msgstr "Esimerkki: {example}."
|
||||
|
||||
#: src/components/header/ProfileMenu.tsx
|
||||
msgid "Expanded"
|
||||
msgstr "Laajennettu"
|
||||
@@ -469,10 +461,6 @@ msgstr ""
|
||||
msgid "Id"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/app/FeedDetailsPage.tsx
|
||||
msgid "If not empty, an expression evaluating to 'true' or 'false'. If false, new entries for this feed will be marked as read automatically."
|
||||
msgstr "Jos ei tyhjä, lauseke, jonka arvo on \"tosi\" tai \"epätosi\". "
|
||||
|
||||
#: src/components/settings/DisplaySettings.tsx
|
||||
msgid "If the entry doesn't entirely fit on the screen"
|
||||
msgstr ""
|
||||
@@ -1037,6 +1025,11 @@ msgstr "Sen syötteen URL-osoite, jonka haluat tilata. "
|
||||
msgid "Theme"
|
||||
msgstr "Teema"
|
||||
|
||||
#. placeholder {0}: feed.filterLegacy
|
||||
#: src/pages/app/FeedDetailsPage.tsx
|
||||
msgid "This feed has a legacy filter that cannot be edited and is not applied. Please recreate the filter using the new expression editor. The legacy filter expression was: <0>{0}</0>"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/settings/ProfileSettings.tsx
|
||||
msgid "This is your API key. It can be used for some read-only API operations and grants access to the Fever API. Use the form at the bottom of the page to generate a new API key"
|
||||
msgstr ""
|
||||
|
||||
@@ -17,10 +17,6 @@ msgstr ""
|
||||
msgid "<0>CommaFeed is an open-source project. Sources are hosted on </0><1>GitHub</1>."
|
||||
msgstr "<0>CommaFeed est un projet open-source. Les sources sont hébergées sur </0><1>GitHub</1>."
|
||||
|
||||
#: src/pages/app/FeedDetailsPage.tsx
|
||||
msgid "<0>Complete syntax is available </0><1>here</1><2>.</2>"
|
||||
msgstr "<0>La syntaxe complète est disponible </0><1>ici</1><2>.</2>"
|
||||
|
||||
#: src/pages/auth/RegistrationPage.tsx
|
||||
msgid "<0>Have an account?</0><1>Log in!</1>"
|
||||
msgstr "<0>Déjà un compte ?</0><1>Connectez-vous !</1>"
|
||||
@@ -126,10 +122,6 @@ msgstr "Êtes-vous sûr de vouloir vous désabonner de <0>{feedName}</0> ?"
|
||||
msgid "Asc"
|
||||
msgstr "Ascendant"
|
||||
|
||||
#: src/pages/app/FeedDetailsPage.tsx
|
||||
msgid "Available variables are 'title', 'content', 'url' 'author' and 'categories' and their content is converted to lower case to ease string comparison."
|
||||
msgstr "Les variables disponibles sont 'title', 'content', 'url' 'author' et 'categories' et leur contenu est converti en minuscules pour faciliter la comparaison de chaînes."
|
||||
|
||||
#: src/components/content/add/Subscribe.tsx
|
||||
msgid "Back"
|
||||
msgstr "Retour"
|
||||
@@ -155,6 +147,10 @@ msgstr "L'extension navigateur est nécessaire sur Chrome"
|
||||
msgid "Browser tab"
|
||||
msgstr "Onglet navigateur"
|
||||
|
||||
#: src/pages/app/FeedDetailsPage.tsx
|
||||
msgid "Build a filter expression to indicate what you want to read. Entries that don't match will be marked as read automatically."
|
||||
msgstr ""
|
||||
|
||||
#: src/components/admin/UserEdit.tsx
|
||||
#: src/components/content/add/AddCategory.tsx
|
||||
#: src/components/content/add/ImportOpml.tsx
|
||||
@@ -365,10 +361,6 @@ msgstr "En-têtes de l'entrée"
|
||||
msgid "Error"
|
||||
msgstr "Erreur"
|
||||
|
||||
#: src/pages/app/FeedDetailsPage.tsx
|
||||
msgid "Example: {example}."
|
||||
msgstr "Exemple : {example}."
|
||||
|
||||
#: src/components/header/ProfileMenu.tsx
|
||||
msgid "Expanded"
|
||||
msgstr "Vue étendue"
|
||||
@@ -469,10 +461,6 @@ msgstr "Vert"
|
||||
msgid "Id"
|
||||
msgstr "Identifiant"
|
||||
|
||||
#: src/pages/app/FeedDetailsPage.tsx
|
||||
msgid "If not empty, an expression evaluating to 'true' or 'false'. If false, new entries for this feed will be marked as read automatically."
|
||||
msgstr "Si non vide, une expression évaluant à 'vrai' ou 'faux'. Si faux, les nouvelles entrées de ce flux seront marquées comme lues automatiquement."
|
||||
|
||||
#: src/components/settings/DisplaySettings.tsx
|
||||
msgid "If the entry doesn't entirely fit on the screen"
|
||||
msgstr "Si l'entrée ne tient pas entièrement sur l'écran"
|
||||
@@ -1037,6 +1025,11 @@ msgstr "L'URL du flux auquel vous souhaitez vous abonner. Vous pouvez aussi util
|
||||
msgid "Theme"
|
||||
msgstr "Thème"
|
||||
|
||||
#. placeholder {0}: feed.filterLegacy
|
||||
#: src/pages/app/FeedDetailsPage.tsx
|
||||
msgid "This feed has a legacy filter that cannot be edited and is not applied. Please recreate the filter using the new expression editor. The legacy filter expression was: <0>{0}</0>"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/settings/ProfileSettings.tsx
|
||||
msgid "This is your API key. It can be used for some read-only API operations and grants access to the Fever API. Use the form at the bottom of the page to generate a new API key"
|
||||
msgstr "Ceci est votre clef API. Elle peut être utilisée pour certaines opérations en lecture seule et donne accès à l'API Fever. Utilisez le formulaire en bas de la page pour générer une nouvelle clef API"
|
||||
|
||||
@@ -18,10 +18,6 @@ msgstr ""
|
||||
msgid "<0>CommaFeed is an open-source project. Sources are hosted on </0><1>GitHub</1>."
|
||||
msgstr "<0>CommaFeed é un proxecto de código aberto. O código está en </0><1>GitHub</1>."
|
||||
|
||||
#: src/pages/app/FeedDetailsPage.tsx
|
||||
msgid "<0>Complete syntax is available </0><1>here</1><2>.</2>"
|
||||
msgstr "<0>A sintaxe completa está dispoñible </0><1>aquí</1><2>.</2>"
|
||||
|
||||
#: src/pages/auth/RegistrationPage.tsx
|
||||
msgid "<0>Have an account?</0><1>Log in!</1>"
|
||||
msgstr "<0>Tes unha conta?</0><1>Inicia sesión!</1>"
|
||||
@@ -127,10 +123,6 @@ msgstr "Tes certeza de querer cancelar a subscrición a <0>{feedName}</0>?"
|
||||
msgid "Asc"
|
||||
msgstr "Asc"
|
||||
|
||||
#: src/pages/app/FeedDetailsPage.tsx
|
||||
msgid "Available variables are 'title', 'content', 'url' 'author' and 'categories' and their content is converted to lower case to ease string comparison."
|
||||
msgstr "As variables dispoñibles son 'título', 'contido', 'url' 'autoría' e 'categorías' e o seu contido convértese a minúsculas para facilitar a comparación de cadeas."
|
||||
|
||||
#: src/components/content/add/Subscribe.tsx
|
||||
msgid "Back"
|
||||
msgstr "Volver"
|
||||
@@ -156,6 +148,10 @@ msgstr "Complemento para o navegador requerido para Chrome"
|
||||
msgid "Browser tab"
|
||||
msgstr "Pestana do navegador"
|
||||
|
||||
#: src/pages/app/FeedDetailsPage.tsx
|
||||
msgid "Build a filter expression to indicate what you want to read. Entries that don't match will be marked as read automatically."
|
||||
msgstr ""
|
||||
|
||||
#: src/components/admin/UserEdit.tsx
|
||||
#: src/components/content/add/AddCategory.tsx
|
||||
#: src/components/content/add/ImportOpml.tsx
|
||||
@@ -366,10 +362,6 @@ msgstr "Cabeceira dos artigos"
|
||||
msgid "Error"
|
||||
msgstr "Erro"
|
||||
|
||||
#: src/pages/app/FeedDetailsPage.tsx
|
||||
msgid "Example: {example}."
|
||||
msgstr "Exemplo: {exemplo}."
|
||||
|
||||
#: src/components/header/ProfileMenu.tsx
|
||||
msgid "Expanded"
|
||||
msgstr "Ampliado"
|
||||
@@ -470,10 +462,6 @@ msgstr "Verde"
|
||||
msgid "Id"
|
||||
msgstr "Id"
|
||||
|
||||
#: src/pages/app/FeedDetailsPage.tsx
|
||||
msgid "If not empty, an expression evaluating to 'true' or 'false'. If false, new entries for this feed will be marked as read automatically."
|
||||
msgstr "Se non está baleira, unha expresión que se avalía como «true» ou «false». Se é falsa, os novos artigos desta canle marcaranse automaticamente como lidos."
|
||||
|
||||
#: src/components/settings/DisplaySettings.tsx
|
||||
msgid "If the entry doesn't entirely fit on the screen"
|
||||
msgstr "Se o artigo non se axusta por completo na pantalla"
|
||||
@@ -1038,6 +1026,11 @@ msgstr "URL da canle á que queres subscribirte. Podes usar a url do sitio web d
|
||||
msgid "Theme"
|
||||
msgstr "Decorado"
|
||||
|
||||
#. placeholder {0}: feed.filterLegacy
|
||||
#: src/pages/app/FeedDetailsPage.tsx
|
||||
msgid "This feed has a legacy filter that cannot be edited and is not applied. Please recreate the filter using the new expression editor. The legacy filter expression was: <0>{0}</0>"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/settings/ProfileSettings.tsx
|
||||
msgid "This is your API key. It can be used for some read-only API operations and grants access to the Fever API. Use the form at the bottom of the page to generate a new API key"
|
||||
msgstr "Esta é a túa clave da API. Pode usarse para algunhas operacións da API de só-lectura e concede acceso á API Fever. Usa o formulario da parte inferior da páxina para crear unha nova clave da API"
|
||||
|
||||
@@ -17,10 +17,6 @@ msgstr ""
|
||||
msgid "<0>CommaFeed is an open-source project. Sources are hosted on </0><1>GitHub</1>."
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/app/FeedDetailsPage.tsx
|
||||
msgid "<0>Complete syntax is available </0><1>here</1><2>.</2>"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/auth/RegistrationPage.tsx
|
||||
msgid "<0>Have an account?</0><1>Log in!</1>"
|
||||
msgstr "<0>Van fiókja?</0><1>Jelentkezzen be!</1>"
|
||||
@@ -126,10 +122,6 @@ msgstr "Biztosan le szeretne iratkozni a következőről: <0>{feedName}</0>?"
|
||||
msgid "Asc"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/app/FeedDetailsPage.tsx
|
||||
msgid "Available variables are 'title', 'content', 'url' 'author' and 'categories' and their content is converted to lower case to ease string comparison."
|
||||
msgstr "A rendelkezésre álló változók a következők: 'cím', 'tartalom', 'url' 'szerző' és 'kategóriák', és tartalmukat a rendszer kisbetűssé alakítja a karakterlánc-összehasonlítás megkönnyítése érdekében."
|
||||
|
||||
#: src/components/content/add/Subscribe.tsx
|
||||
msgid "Back"
|
||||
msgstr "Vissza"
|
||||
@@ -155,6 +147,10 @@ msgstr ""
|
||||
msgid "Browser tab"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/app/FeedDetailsPage.tsx
|
||||
msgid "Build a filter expression to indicate what you want to read. Entries that don't match will be marked as read automatically."
|
||||
msgstr ""
|
||||
|
||||
#: src/components/admin/UserEdit.tsx
|
||||
#: src/components/content/add/AddCategory.tsx
|
||||
#: src/components/content/add/ImportOpml.tsx
|
||||
@@ -365,10 +361,6 @@ msgstr ""
|
||||
msgid "Error"
|
||||
msgstr "Hiba"
|
||||
|
||||
#: src/pages/app/FeedDetailsPage.tsx
|
||||
msgid "Example: {example}."
|
||||
msgstr "Példa: {example}."
|
||||
|
||||
#: src/components/header/ProfileMenu.tsx
|
||||
msgid "Expanded"
|
||||
msgstr "Kiterjesztve"
|
||||
@@ -469,10 +461,6 @@ msgstr ""
|
||||
msgid "Id"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/app/FeedDetailsPage.tsx
|
||||
msgid "If not empty, an expression evaluating to 'true' or 'false'. If false, new entries for this feed will be marked as read automatically."
|
||||
msgstr "Ha nem üres, akkor 'igaz' vagy 'hamis' értékre kiértékelő kifejezés. "
|
||||
|
||||
#: src/components/settings/DisplaySettings.tsx
|
||||
msgid "If the entry doesn't entirely fit on the screen"
|
||||
msgstr ""
|
||||
@@ -1037,6 +1025,11 @@ msgstr "Az előfizetni kívánt hírcsatorna URL-je. "
|
||||
msgid "Theme"
|
||||
msgstr "Téma"
|
||||
|
||||
#. placeholder {0}: feed.filterLegacy
|
||||
#: src/pages/app/FeedDetailsPage.tsx
|
||||
msgid "This feed has a legacy filter that cannot be edited and is not applied. Please recreate the filter using the new expression editor. The legacy filter expression was: <0>{0}</0>"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/settings/ProfileSettings.tsx
|
||||
msgid "This is your API key. It can be used for some read-only API operations and grants access to the Fever API. Use the form at the bottom of the page to generate a new API key"
|
||||
msgstr ""
|
||||
|
||||
@@ -17,10 +17,6 @@ msgstr ""
|
||||
msgid "<0>CommaFeed is an open-source project. Sources are hosted on </0><1>GitHub</1>."
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/app/FeedDetailsPage.tsx
|
||||
msgid "<0>Complete syntax is available </0><1>here</1><2>.</2>"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/auth/RegistrationPage.tsx
|
||||
msgid "<0>Have an account?</0><1>Log in!</1>"
|
||||
msgstr "<0>Punya akun?</0><1>Masuk!</1>"
|
||||
@@ -126,10 +122,6 @@ msgstr "Yakin ingin berhenti berlangganan <0>{feedName}</0>?"
|
||||
msgid "Asc"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/app/FeedDetailsPage.tsx
|
||||
msgid "Available variables are 'title', 'content', 'url' 'author' and 'categories' and their content is converted to lower case to ease string comparison."
|
||||
msgstr "Variabel yang tersedia adalah 'judul', 'konten', 'url' 'penulis' dan 'kategori' dan kontennya diubah menjadi huruf kecil untuk memudahkan perbandingan string."
|
||||
|
||||
#: src/components/content/add/Subscribe.tsx
|
||||
msgid "Back"
|
||||
msgstr "Kembali"
|
||||
@@ -155,6 +147,10 @@ msgstr ""
|
||||
msgid "Browser tab"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/app/FeedDetailsPage.tsx
|
||||
msgid "Build a filter expression to indicate what you want to read. Entries that don't match will be marked as read automatically."
|
||||
msgstr ""
|
||||
|
||||
#: src/components/admin/UserEdit.tsx
|
||||
#: src/components/content/add/AddCategory.tsx
|
||||
#: src/components/content/add/ImportOpml.tsx
|
||||
@@ -365,10 +361,6 @@ msgstr ""
|
||||
msgid "Error"
|
||||
msgstr "Kesalahan"
|
||||
|
||||
#: src/pages/app/FeedDetailsPage.tsx
|
||||
msgid "Example: {example}."
|
||||
msgstr "Contoh: {contoh}."
|
||||
|
||||
#: src/components/header/ProfileMenu.tsx
|
||||
msgid "Expanded"
|
||||
msgstr "Diperluas"
|
||||
@@ -469,10 +461,6 @@ msgstr ""
|
||||
msgid "Id"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/app/FeedDetailsPage.tsx
|
||||
msgid "If not empty, an expression evaluating to 'true' or 'false'. If false, new entries for this feed will be marked as read automatically."
|
||||
msgstr "Jika tidak kosong, ekspresi mengevaluasi ke 'benar' atau 'salah'. "
|
||||
|
||||
#: src/components/settings/DisplaySettings.tsx
|
||||
msgid "If the entry doesn't entirely fit on the screen"
|
||||
msgstr ""
|
||||
@@ -1037,6 +1025,11 @@ msgstr "URL untuk umpan yang ingin Anda langgani. "
|
||||
msgid "Theme"
|
||||
msgstr "Tema"
|
||||
|
||||
#. placeholder {0}: feed.filterLegacy
|
||||
#: src/pages/app/FeedDetailsPage.tsx
|
||||
msgid "This feed has a legacy filter that cannot be edited and is not applied. Please recreate the filter using the new expression editor. The legacy filter expression was: <0>{0}</0>"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/settings/ProfileSettings.tsx
|
||||
msgid "This is your API key. It can be used for some read-only API operations and grants access to the Fever API. Use the form at the bottom of the page to generate a new API key"
|
||||
msgstr ""
|
||||
|
||||
@@ -17,10 +17,6 @@ msgstr ""
|
||||
msgid "<0>CommaFeed is an open-source project. Sources are hosted on </0><1>GitHub</1>."
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/app/FeedDetailsPage.tsx
|
||||
msgid "<0>Complete syntax is available </0><1>here</1><2>.</2>"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/auth/RegistrationPage.tsx
|
||||
msgid "<0>Have an account?</0><1>Log in!</1>"
|
||||
msgstr "<0>Hai un account?</0><1>Accedi!</1>"
|
||||
@@ -126,10 +122,6 @@ msgstr "Sei sicuro di voler annullare l'iscrizione a <0>{feedName}</0>?"
|
||||
msgid "Asc"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/app/FeedDetailsPage.tsx
|
||||
msgid "Available variables are 'title', 'content', 'url' 'author' and 'categories' and their content is converted to lower case to ease string comparison."
|
||||
msgstr "Le variabili disponibili sono 'titolo', 'contenuto', 'url' 'autore' e 'categorie' e il loro contenuto viene convertito in minuscolo per facilitare il confronto delle stringhe."
|
||||
|
||||
#: src/components/content/add/Subscribe.tsx
|
||||
msgid "Back"
|
||||
msgstr "Indietro"
|
||||
@@ -155,6 +147,10 @@ msgstr ""
|
||||
msgid "Browser tab"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/app/FeedDetailsPage.tsx
|
||||
msgid "Build a filter expression to indicate what you want to read. Entries that don't match will be marked as read automatically."
|
||||
msgstr ""
|
||||
|
||||
#: src/components/admin/UserEdit.tsx
|
||||
#: src/components/content/add/AddCategory.tsx
|
||||
#: src/components/content/add/ImportOpml.tsx
|
||||
@@ -365,10 +361,6 @@ msgstr ""
|
||||
msgid "Error"
|
||||
msgstr "Errore"
|
||||
|
||||
#: src/pages/app/FeedDetailsPage.tsx
|
||||
msgid "Example: {example}."
|
||||
msgstr "Esempio: {esempio}."
|
||||
|
||||
#: src/components/header/ProfileMenu.tsx
|
||||
msgid "Expanded"
|
||||
msgstr "Espanso"
|
||||
@@ -469,10 +461,6 @@ msgstr ""
|
||||
msgid "Id"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/app/FeedDetailsPage.tsx
|
||||
msgid "If not empty, an expression evaluating to 'true' or 'false'. If false, new entries for this feed will be marked as read automatically."
|
||||
msgstr "Se non è vuota, un'espressione valutata come 'vero' o 'falso'. "
|
||||
|
||||
#: src/components/settings/DisplaySettings.tsx
|
||||
msgid "If the entry doesn't entirely fit on the screen"
|
||||
msgstr ""
|
||||
@@ -1037,6 +1025,11 @@ msgstr "L'URL del feed a cui vuoi iscriverti. "
|
||||
msgid "Theme"
|
||||
msgstr "Tema"
|
||||
|
||||
#. placeholder {0}: feed.filterLegacy
|
||||
#: src/pages/app/FeedDetailsPage.tsx
|
||||
msgid "This feed has a legacy filter that cannot be edited and is not applied. Please recreate the filter using the new expression editor. The legacy filter expression was: <0>{0}</0>"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/settings/ProfileSettings.tsx
|
||||
msgid "This is your API key. It can be used for some read-only API operations and grants access to the Fever API. Use the form at the bottom of the page to generate a new API key"
|
||||
msgstr ""
|
||||
|
||||
@@ -17,10 +17,6 @@ msgstr ""
|
||||
msgid "<0>CommaFeed is an open-source project. Sources are hosted on </0><1>GitHub</1>."
|
||||
msgstr "<0>CommaFeed はオープンソースのプロジェクトです。 ソースは以下でホストされています </0><1>GitHub</1>。"
|
||||
|
||||
#: src/pages/app/FeedDetailsPage.tsx
|
||||
msgid "<0>Complete syntax is available </0><1>here</1><2>.</2>"
|
||||
msgstr "<0>完全な syntax </0><1>こちら</1>で利用可能です<2>。</2>"
|
||||
|
||||
#: src/pages/auth/RegistrationPage.tsx
|
||||
msgid "<0>Have an account?</0><1>Log in!</1>"
|
||||
msgstr "<0>アカウントをお持ちですか?</0><1>ログインしてください!</1>"
|
||||
@@ -126,10 +122,6 @@ msgstr "<0>{feedName}</0> の登録を解除してもよろしいですか?"
|
||||
msgid "Asc"
|
||||
msgstr "昇順"
|
||||
|
||||
#: src/pages/app/FeedDetailsPage.tsx
|
||||
msgid "Available variables are 'title', 'content', 'url' 'author' and 'categories' and their content is converted to lower case to ease string comparison."
|
||||
msgstr "使用可能な変数は「title」、「content」、「url」、「author」、および「categories」であり、それらのコンテンツは文字列の比較を容易にするために小文字に変換されます。"
|
||||
|
||||
#: src/components/content/add/Subscribe.tsx
|
||||
msgid "Back"
|
||||
msgstr "戻る"
|
||||
@@ -155,6 +147,10 @@ msgstr "Chromeのブラウザー拡張が必要です"
|
||||
msgid "Browser tab"
|
||||
msgstr "ブラウザータブ"
|
||||
|
||||
#: src/pages/app/FeedDetailsPage.tsx
|
||||
msgid "Build a filter expression to indicate what you want to read. Entries that don't match will be marked as read automatically."
|
||||
msgstr ""
|
||||
|
||||
#: src/components/admin/UserEdit.tsx
|
||||
#: src/components/content/add/AddCategory.tsx
|
||||
#: src/components/content/add/ImportOpml.tsx
|
||||
@@ -365,10 +361,6 @@ msgstr "エントリーヘッダー"
|
||||
msgid "Error"
|
||||
msgstr "エラー"
|
||||
|
||||
#: src/pages/app/FeedDetailsPage.tsx
|
||||
msgid "Example: {example}."
|
||||
msgstr "例: {example}."
|
||||
|
||||
#: src/components/header/ProfileMenu.tsx
|
||||
msgid "Expanded"
|
||||
msgstr "拡張"
|
||||
@@ -469,10 +461,6 @@ msgstr ""
|
||||
msgid "Id"
|
||||
msgstr "ID"
|
||||
|
||||
#: src/pages/app/FeedDetailsPage.tsx
|
||||
msgid "If not empty, an expression evaluating to 'true' or 'false'. If false, new entries for this feed will be marked as read automatically."
|
||||
msgstr "空でない場合は、'true' または 'false' に評価される式。 'false' の場合、このフィードの新しいエントリーは自動的に既読としてマークされます。"
|
||||
|
||||
#: src/components/settings/DisplaySettings.tsx
|
||||
msgid "If the entry doesn't entirely fit on the screen"
|
||||
msgstr "エントリーが画面に完全に収まらない場合"
|
||||
@@ -1037,6 +1025,11 @@ msgstr "購読したいフィードのURL。ウェブサイトのURLを直接使
|
||||
msgid "Theme"
|
||||
msgstr "テーマ"
|
||||
|
||||
#. placeholder {0}: feed.filterLegacy
|
||||
#: src/pages/app/FeedDetailsPage.tsx
|
||||
msgid "This feed has a legacy filter that cannot be edited and is not applied. Please recreate the filter using the new expression editor. The legacy filter expression was: <0>{0}</0>"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/settings/ProfileSettings.tsx
|
||||
msgid "This is your API key. It can be used for some read-only API operations and grants access to the Fever API. Use the form at the bottom of the page to generate a new API key"
|
||||
msgstr "これはあなたのAPIキーです。いくつかの読み取り専用API操作に使用できます。これにより、Fever APIへのアクセスが可能になります。ページの下部のフォームを使用して新しいAPIキーを生成します。"
|
||||
|
||||
@@ -17,10 +17,6 @@ msgstr ""
|
||||
msgid "<0>CommaFeed is an open-source project. Sources are hosted on </0><1>GitHub</1>."
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/app/FeedDetailsPage.tsx
|
||||
msgid "<0>Complete syntax is available </0><1>here</1><2>.</2>"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/auth/RegistrationPage.tsx
|
||||
msgid "<0>Have an account?</0><1>Log in!</1>"
|
||||
msgstr "<0>계정이 있습니까?</0><1>로그인하세요!</1>"
|
||||
@@ -126,10 +122,6 @@ msgstr "<0>{feedName}</0> 구독을 취소하시겠습니까?"
|
||||
msgid "Asc"
|
||||
msgstr "오름차순"
|
||||
|
||||
#: src/pages/app/FeedDetailsPage.tsx
|
||||
msgid "Available variables are 'title', 'content', 'url' 'author' and 'categories' and their content is converted to lower case to ease string comparison."
|
||||
msgstr "사용 가능한 변수는 'title', 'content', 'url' 'author' 및 'categories'이며 해당 내용은 문자열 비교를 쉽게 하기 위해 소문자로 변환됩니다."
|
||||
|
||||
#: src/components/content/add/Subscribe.tsx
|
||||
msgid "Back"
|
||||
msgstr "뒤로"
|
||||
@@ -155,6 +147,10 @@ msgstr ""
|
||||
msgid "Browser tab"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/app/FeedDetailsPage.tsx
|
||||
msgid "Build a filter expression to indicate what you want to read. Entries that don't match will be marked as read automatically."
|
||||
msgstr ""
|
||||
|
||||
#: src/components/admin/UserEdit.tsx
|
||||
#: src/components/content/add/AddCategory.tsx
|
||||
#: src/components/content/add/ImportOpml.tsx
|
||||
@@ -365,10 +361,6 @@ msgstr ""
|
||||
msgid "Error"
|
||||
msgstr "오류"
|
||||
|
||||
#: src/pages/app/FeedDetailsPage.tsx
|
||||
msgid "Example: {example}."
|
||||
msgstr "예: {예}."
|
||||
|
||||
#: src/components/header/ProfileMenu.tsx
|
||||
msgid "Expanded"
|
||||
msgstr "확장"
|
||||
@@ -469,10 +461,6 @@ msgstr ""
|
||||
msgid "Id"
|
||||
msgstr "아이디"
|
||||
|
||||
#: src/pages/app/FeedDetailsPage.tsx
|
||||
msgid "If not empty, an expression evaluating to 'true' or 'false'. If false, new entries for this feed will be marked as read automatically."
|
||||
msgstr "비어 있지 않은 경우 'true' 또는 'false'로 평가되는 표현식입니다. "
|
||||
|
||||
#: src/components/settings/DisplaySettings.tsx
|
||||
msgid "If the entry doesn't entirely fit on the screen"
|
||||
msgstr ""
|
||||
@@ -1037,6 +1025,11 @@ msgstr "구독하려는 피드의 URL입니다. "
|
||||
msgid "Theme"
|
||||
msgstr "테마"
|
||||
|
||||
#. placeholder {0}: feed.filterLegacy
|
||||
#: src/pages/app/FeedDetailsPage.tsx
|
||||
msgid "This feed has a legacy filter that cannot be edited and is not applied. Please recreate the filter using the new expression editor. The legacy filter expression was: <0>{0}</0>"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/settings/ProfileSettings.tsx
|
||||
msgid "This is your API key. It can be used for some read-only API operations and grants access to the Fever API. Use the form at the bottom of the page to generate a new API key"
|
||||
msgstr ""
|
||||
|
||||
@@ -17,10 +17,6 @@ msgstr ""
|
||||
msgid "<0>CommaFeed is an open-source project. Sources are hosted on </0><1>GitHub</1>."
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/app/FeedDetailsPage.tsx
|
||||
msgid "<0>Complete syntax is available </0><1>here</1><2>.</2>"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/auth/RegistrationPage.tsx
|
||||
msgid "<0>Have an account?</0><1>Log in!</1>"
|
||||
msgstr "<0>Ada akaun?</0><1>Log masuk!</1>"
|
||||
@@ -126,10 +122,6 @@ msgstr "Adakah anda pasti mahu berhenti melanggan <0>{feedName}</0>?"
|
||||
msgid "Asc"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/app/FeedDetailsPage.tsx
|
||||
msgid "Available variables are 'title', 'content', 'url' 'author' and 'categories' and their content is converted to lower case to ease string comparison."
|
||||
msgstr "Pembolehubah yang tersedia ialah 'tajuk', 'kandungan', 'url' 'pengarang' dan 'kategori' dan kandungannya ditukar kepada huruf kecil untuk memudahkan perbandingan rentetan."
|
||||
|
||||
#: src/components/content/add/Subscribe.tsx
|
||||
msgid "Back"
|
||||
msgstr "Kembali"
|
||||
@@ -155,6 +147,10 @@ msgstr ""
|
||||
msgid "Browser tab"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/app/FeedDetailsPage.tsx
|
||||
msgid "Build a filter expression to indicate what you want to read. Entries that don't match will be marked as read automatically."
|
||||
msgstr ""
|
||||
|
||||
#: src/components/admin/UserEdit.tsx
|
||||
#: src/components/content/add/AddCategory.tsx
|
||||
#: src/components/content/add/ImportOpml.tsx
|
||||
@@ -365,10 +361,6 @@ msgstr ""
|
||||
msgid "Error"
|
||||
msgstr "Ralat"
|
||||
|
||||
#: src/pages/app/FeedDetailsPage.tsx
|
||||
msgid "Example: {example}."
|
||||
msgstr "Contoh: {example}."
|
||||
|
||||
#: src/components/header/ProfileMenu.tsx
|
||||
msgid "Expanded"
|
||||
msgstr "Dikembangkan"
|
||||
@@ -469,10 +461,6 @@ msgstr ""
|
||||
msgid "Id"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/app/FeedDetailsPage.tsx
|
||||
msgid "If not empty, an expression evaluating to 'true' or 'false'. If false, new entries for this feed will be marked as read automatically."
|
||||
msgstr "Jika tidak kosong, ungkapan yang menilai kepada 'benar' atau 'palsu'. "
|
||||
|
||||
#: src/components/settings/DisplaySettings.tsx
|
||||
msgid "If the entry doesn't entirely fit on the screen"
|
||||
msgstr ""
|
||||
@@ -1037,6 +1025,11 @@ msgstr "URL untuk suapan yang anda ingin langgan. "
|
||||
msgid "Theme"
|
||||
msgstr "Tema"
|
||||
|
||||
#. placeholder {0}: feed.filterLegacy
|
||||
#: src/pages/app/FeedDetailsPage.tsx
|
||||
msgid "This feed has a legacy filter that cannot be edited and is not applied. Please recreate the filter using the new expression editor. The legacy filter expression was: <0>{0}</0>"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/settings/ProfileSettings.tsx
|
||||
msgid "This is your API key. It can be used for some read-only API operations and grants access to the Fever API. Use the form at the bottom of the page to generate a new API key"
|
||||
msgstr ""
|
||||
|
||||
@@ -17,10 +17,6 @@ msgstr ""
|
||||
msgid "<0>CommaFeed is an open-source project. Sources are hosted on </0><1>GitHub</1>."
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/app/FeedDetailsPage.tsx
|
||||
msgid "<0>Complete syntax is available </0><1>here</1><2>.</2>"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/auth/RegistrationPage.tsx
|
||||
msgid "<0>Have an account?</0><1>Log in!</1>"
|
||||
msgstr "<0>Har du en konto?</0><1>Logg på!</1>"
|
||||
@@ -126,10 +122,6 @@ msgstr "Er du sikker på at du vil avslutte abonnementet på <0>{feedName}</0>?"
|
||||
msgid "Asc"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/app/FeedDetailsPage.tsx
|
||||
msgid "Available variables are 'title', 'content', 'url' 'author' and 'categories' and their content is converted to lower case to ease string comparison."
|
||||
msgstr "Tilgjengelige variabler er 'tittel', 'innhold', 'url' 'forfatter' og 'kategorier', og innholdet deres konverteres til små bokstaver for å lette strengsammenligning."
|
||||
|
||||
#: src/components/content/add/Subscribe.tsx
|
||||
msgid "Back"
|
||||
msgstr "Tilbake"
|
||||
@@ -155,6 +147,10 @@ msgstr ""
|
||||
msgid "Browser tab"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/app/FeedDetailsPage.tsx
|
||||
msgid "Build a filter expression to indicate what you want to read. Entries that don't match will be marked as read automatically."
|
||||
msgstr ""
|
||||
|
||||
#: src/components/admin/UserEdit.tsx
|
||||
#: src/components/content/add/AddCategory.tsx
|
||||
#: src/components/content/add/ImportOpml.tsx
|
||||
@@ -365,10 +361,6 @@ msgstr ""
|
||||
msgid "Error"
|
||||
msgstr "Feil"
|
||||
|
||||
#: src/pages/app/FeedDetailsPage.tsx
|
||||
msgid "Example: {example}."
|
||||
msgstr "Eksempel: {eksempel}."
|
||||
|
||||
#: src/components/header/ProfileMenu.tsx
|
||||
msgid "Expanded"
|
||||
msgstr "Utvidet"
|
||||
@@ -469,10 +461,6 @@ msgstr ""
|
||||
msgid "Id"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/app/FeedDetailsPage.tsx
|
||||
msgid "If not empty, an expression evaluating to 'true' or 'false'. If false, new entries for this feed will be marked as read automatically."
|
||||
msgstr "Hvis det ikke er tomt, et uttrykk som vurderes til 'sant' eller 'usant'. "
|
||||
|
||||
#: src/components/settings/DisplaySettings.tsx
|
||||
msgid "If the entry doesn't entirely fit on the screen"
|
||||
msgstr ""
|
||||
@@ -1037,6 +1025,11 @@ msgstr "URL-en til feeden du vil abonnere på. "
|
||||
msgid "Theme"
|
||||
msgstr "Tema"
|
||||
|
||||
#. placeholder {0}: feed.filterLegacy
|
||||
#: src/pages/app/FeedDetailsPage.tsx
|
||||
msgid "This feed has a legacy filter that cannot be edited and is not applied. Please recreate the filter using the new expression editor. The legacy filter expression was: <0>{0}</0>"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/settings/ProfileSettings.tsx
|
||||
msgid "This is your API key. It can be used for some read-only API operations and grants access to the Fever API. Use the form at the bottom of the page to generate a new API key"
|
||||
msgstr ""
|
||||
|
||||
@@ -17,10 +17,6 @@ msgstr ""
|
||||
msgid "<0>CommaFeed is an open-source project. Sources are hosted on </0><1>GitHub</1>."
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/app/FeedDetailsPage.tsx
|
||||
msgid "<0>Complete syntax is available </0><1>here</1><2>.</2>"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/auth/RegistrationPage.tsx
|
||||
msgid "<0>Have an account?</0><1>Log in!</1>"
|
||||
msgstr "<0>Heb je een account?</0><1>Log in!</1>"
|
||||
@@ -126,10 +122,6 @@ msgstr "Weet je zeker dat je je wilt afmelden voor <0>{feedName}</0>?"
|
||||
msgid "Asc"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/app/FeedDetailsPage.tsx
|
||||
msgid "Available variables are 'title', 'content', 'url' 'author' and 'categories' and their content is converted to lower case to ease string comparison."
|
||||
msgstr "Beschikbare variabelen zijn 'titel', 'inhoud', 'url', 'auteur' en 'categorieën' en hun inhoud wordt geconverteerd naar kleine letters om het vergelijken van tekenreeksen te vergemakkelijken."
|
||||
|
||||
#: src/components/content/add/Subscribe.tsx
|
||||
msgid "Back"
|
||||
msgstr "Terug"
|
||||
@@ -155,6 +147,10 @@ msgstr ""
|
||||
msgid "Browser tab"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/app/FeedDetailsPage.tsx
|
||||
msgid "Build a filter expression to indicate what you want to read. Entries that don't match will be marked as read automatically."
|
||||
msgstr ""
|
||||
|
||||
#: src/components/admin/UserEdit.tsx
|
||||
#: src/components/content/add/AddCategory.tsx
|
||||
#: src/components/content/add/ImportOpml.tsx
|
||||
@@ -365,10 +361,6 @@ msgstr ""
|
||||
msgid "Error"
|
||||
msgstr "Fout"
|
||||
|
||||
#: src/pages/app/FeedDetailsPage.tsx
|
||||
msgid "Example: {example}."
|
||||
msgstr "Voorbeeld: {voorbeeld}."
|
||||
|
||||
#: src/components/header/ProfileMenu.tsx
|
||||
msgid "Expanded"
|
||||
msgstr "Uitgebreid"
|
||||
@@ -469,10 +461,6 @@ msgstr ""
|
||||
msgid "Id"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/app/FeedDetailsPage.tsx
|
||||
msgid "If not empty, an expression evaluating to 'true' or 'false'. If false, new entries for this feed will be marked as read automatically."
|
||||
msgstr "Indien niet leeg, een uitdrukking die evalueert naar 'true' of 'false'. "
|
||||
|
||||
#: src/components/settings/DisplaySettings.tsx
|
||||
msgid "If the entry doesn't entirely fit on the screen"
|
||||
msgstr ""
|
||||
@@ -1037,6 +1025,11 @@ msgstr "De URL voor de feed waarop u zich wilt abonneren. "
|
||||
msgid "Theme"
|
||||
msgstr "Thema"
|
||||
|
||||
#. placeholder {0}: feed.filterLegacy
|
||||
#: src/pages/app/FeedDetailsPage.tsx
|
||||
msgid "This feed has a legacy filter that cannot be edited and is not applied. Please recreate the filter using the new expression editor. The legacy filter expression was: <0>{0}</0>"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/settings/ProfileSettings.tsx
|
||||
msgid "This is your API key. It can be used for some read-only API operations and grants access to the Fever API. Use the form at the bottom of the page to generate a new API key"
|
||||
msgstr ""
|
||||
|
||||
@@ -17,10 +17,6 @@ msgstr ""
|
||||
msgid "<0>CommaFeed is an open-source project. Sources are hosted on </0><1>GitHub</1>."
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/app/FeedDetailsPage.tsx
|
||||
msgid "<0>Complete syntax is available </0><1>here</1><2>.</2>"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/auth/RegistrationPage.tsx
|
||||
msgid "<0>Have an account?</0><1>Log in!</1>"
|
||||
msgstr "<0>Har du en konto?</0><1>Logg på!</1>"
|
||||
@@ -126,10 +122,6 @@ msgstr "Er du sikker på at du vil avslutte abonnementet på <0>{feedName}</0>?"
|
||||
msgid "Asc"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/app/FeedDetailsPage.tsx
|
||||
msgid "Available variables are 'title', 'content', 'url' 'author' and 'categories' and their content is converted to lower case to ease string comparison."
|
||||
msgstr "Tilgjengelige variabler er 'tittel', 'innhold', 'url' 'forfatter' og 'kategorier', og innholdet deres konverteres til små bokstaver for å lette strengsammenligning."
|
||||
|
||||
#: src/components/content/add/Subscribe.tsx
|
||||
msgid "Back"
|
||||
msgstr "Tilbake"
|
||||
@@ -155,6 +147,10 @@ msgstr ""
|
||||
msgid "Browser tab"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/app/FeedDetailsPage.tsx
|
||||
msgid "Build a filter expression to indicate what you want to read. Entries that don't match will be marked as read automatically."
|
||||
msgstr ""
|
||||
|
||||
#: src/components/admin/UserEdit.tsx
|
||||
#: src/components/content/add/AddCategory.tsx
|
||||
#: src/components/content/add/ImportOpml.tsx
|
||||
@@ -365,10 +361,6 @@ msgstr ""
|
||||
msgid "Error"
|
||||
msgstr "Feil"
|
||||
|
||||
#: src/pages/app/FeedDetailsPage.tsx
|
||||
msgid "Example: {example}."
|
||||
msgstr "Eksempel: {eksempel}."
|
||||
|
||||
#: src/components/header/ProfileMenu.tsx
|
||||
msgid "Expanded"
|
||||
msgstr "Utvidet"
|
||||
@@ -469,10 +461,6 @@ msgstr ""
|
||||
msgid "Id"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/app/FeedDetailsPage.tsx
|
||||
msgid "If not empty, an expression evaluating to 'true' or 'false'. If false, new entries for this feed will be marked as read automatically."
|
||||
msgstr "Hvis det ikke er tomt, et uttrykk som vurderes til 'sant' eller 'usant'. "
|
||||
|
||||
#: src/components/settings/DisplaySettings.tsx
|
||||
msgid "If the entry doesn't entirely fit on the screen"
|
||||
msgstr ""
|
||||
@@ -1037,6 +1025,11 @@ msgstr "URL-en til feeden du vil abonnere på. "
|
||||
msgid "Theme"
|
||||
msgstr "Tema"
|
||||
|
||||
#. placeholder {0}: feed.filterLegacy
|
||||
#: src/pages/app/FeedDetailsPage.tsx
|
||||
msgid "This feed has a legacy filter that cannot be edited and is not applied. Please recreate the filter using the new expression editor. The legacy filter expression was: <0>{0}</0>"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/settings/ProfileSettings.tsx
|
||||
msgid "This is your API key. It can be used for some read-only API operations and grants access to the Fever API. Use the form at the bottom of the page to generate a new API key"
|
||||
msgstr ""
|
||||
|
||||
@@ -17,10 +17,6 @@ msgstr ""
|
||||
msgid "<0>CommaFeed is an open-source project. Sources are hosted on </0><1>GitHub</1>."
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/app/FeedDetailsPage.tsx
|
||||
msgid "<0>Complete syntax is available </0><1>here</1><2>.</2>"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/auth/RegistrationPage.tsx
|
||||
msgid "<0>Have an account?</0><1>Log in!</1>"
|
||||
msgstr "<0>Masz konto?</0><1>Zaloguj się!<//1>"
|
||||
@@ -126,10 +122,6 @@ msgstr "Czy na pewno chcesz zrezygnować z subskrypcji <0>{feedName}</0>?"
|
||||
msgid "Asc"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/app/FeedDetailsPage.tsx
|
||||
msgid "Available variables are 'title', 'content', 'url' 'author' and 'categories' and their content is converted to lower case to ease string comparison."
|
||||
msgstr "Dostępne zmienne to „tytuł”, „treść”, „adres URL”, „autor” i „kategorie”, a ich zawartość jest konwertowana na małe litery, aby ułatwić porównanie ciągów."
|
||||
|
||||
#: src/components/content/add/Subscribe.tsx
|
||||
msgid "Back"
|
||||
msgstr "Powrót"
|
||||
@@ -155,6 +147,10 @@ msgstr ""
|
||||
msgid "Browser tab"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/app/FeedDetailsPage.tsx
|
||||
msgid "Build a filter expression to indicate what you want to read. Entries that don't match will be marked as read automatically."
|
||||
msgstr ""
|
||||
|
||||
#: src/components/admin/UserEdit.tsx
|
||||
#: src/components/content/add/AddCategory.tsx
|
||||
#: src/components/content/add/ImportOpml.tsx
|
||||
@@ -365,10 +361,6 @@ msgstr ""
|
||||
msgid "Error"
|
||||
msgstr "Błąd"
|
||||
|
||||
#: src/pages/app/FeedDetailsPage.tsx
|
||||
msgid "Example: {example}."
|
||||
msgstr "Przykład: {przykład}."
|
||||
|
||||
#: src/components/header/ProfileMenu.tsx
|
||||
msgid "Expanded"
|
||||
msgstr "Rozszerzony"
|
||||
@@ -469,10 +461,6 @@ msgstr ""
|
||||
msgid "Id"
|
||||
msgstr "Identyfikator"
|
||||
|
||||
#: src/pages/app/FeedDetailsPage.tsx
|
||||
msgid "If not empty, an expression evaluating to 'true' or 'false'. If false, new entries for this feed will be marked as read automatically."
|
||||
msgstr "Jeśli nie jest puste, wyrażenie oceniające jako „prawda” lub „fałsz”. "
|
||||
|
||||
#: src/components/settings/DisplaySettings.tsx
|
||||
msgid "If the entry doesn't entirely fit on the screen"
|
||||
msgstr ""
|
||||
@@ -1037,6 +1025,11 @@ msgstr "URL kanału, który chcesz subskrybować. "
|
||||
msgid "Theme"
|
||||
msgstr "Motyw"
|
||||
|
||||
#. placeholder {0}: feed.filterLegacy
|
||||
#: src/pages/app/FeedDetailsPage.tsx
|
||||
msgid "This feed has a legacy filter that cannot be edited and is not applied. Please recreate the filter using the new expression editor. The legacy filter expression was: <0>{0}</0>"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/settings/ProfileSettings.tsx
|
||||
msgid "This is your API key. It can be used for some read-only API operations and grants access to the Fever API. Use the form at the bottom of the page to generate a new API key"
|
||||
msgstr ""
|
||||
|
||||
@@ -17,10 +17,6 @@ msgstr ""
|
||||
msgid "<0>CommaFeed is an open-source project. Sources are hosted on </0><1>GitHub</1>."
|
||||
msgstr "<0>CommaFeed é um projeto de código abrto. O código está hospedado no </0><1>GitHub</1>."
|
||||
|
||||
#: src/pages/app/FeedDetailsPage.tsx
|
||||
msgid "<0>Complete syntax is available </0><1>here</1><2>.</2>"
|
||||
msgstr "<0>Sintaxe completa disponível </0><1>aqui</1><2>.</2>"
|
||||
|
||||
#: src/pages/auth/RegistrationPage.tsx
|
||||
msgid "<0>Have an account?</0><1>Log in!</1>"
|
||||
msgstr "<0>Tem uma conta?</0><1>Faça login!</1>"
|
||||
@@ -126,10 +122,6 @@ msgstr "Tem certeza de que deseja cancelar a inscrição em <0>{feedName}</0>?"
|
||||
msgid "Asc"
|
||||
msgstr "Asc"
|
||||
|
||||
#: src/pages/app/FeedDetailsPage.tsx
|
||||
msgid "Available variables are 'title', 'content', 'url' 'author' and 'categories' and their content is converted to lower case to ease string comparison."
|
||||
msgstr "As variáveis disponíveis são 'título', 'conteúdo', 'url' 'autor' e 'categorias' e seu conteúdo é convertido em letras minúsculas para facilitar a comparação de strings."
|
||||
|
||||
#: src/components/content/add/Subscribe.tsx
|
||||
msgid "Back"
|
||||
msgstr "Voltar"
|
||||
@@ -155,6 +147,10 @@ msgstr "Extensão para o Chrome necessária"
|
||||
msgid "Browser tab"
|
||||
msgstr "Aba do navegador"
|
||||
|
||||
#: src/pages/app/FeedDetailsPage.tsx
|
||||
msgid "Build a filter expression to indicate what you want to read. Entries that don't match will be marked as read automatically."
|
||||
msgstr ""
|
||||
|
||||
#: src/components/admin/UserEdit.tsx
|
||||
#: src/components/content/add/AddCategory.tsx
|
||||
#: src/components/content/add/ImportOpml.tsx
|
||||
@@ -365,10 +361,6 @@ msgstr "Cabeçalho das entredas"
|
||||
msgid "Error"
|
||||
msgstr "Erro"
|
||||
|
||||
#: src/pages/app/FeedDetailsPage.tsx
|
||||
msgid "Example: {example}."
|
||||
msgstr "Exemplo: {exemplo}."
|
||||
|
||||
#: src/components/header/ProfileMenu.tsx
|
||||
msgid "Expanded"
|
||||
msgstr "Expandido"
|
||||
@@ -469,10 +461,6 @@ msgstr "Verde"
|
||||
msgid "Id"
|
||||
msgstr "ID"
|
||||
|
||||
#: src/pages/app/FeedDetailsPage.tsx
|
||||
msgid "If not empty, an expression evaluating to 'true' or 'false'. If false, new entries for this feed will be marked as read automatically."
|
||||
msgstr "Se não estiver vazio, uma expressão avaliada como 'true' ou 'false'. "
|
||||
|
||||
#: src/components/settings/DisplaySettings.tsx
|
||||
msgid "If the entry doesn't entirely fit on the screen"
|
||||
msgstr "Se a entrafa não couber por completo na tela"
|
||||
@@ -1037,6 +1025,11 @@ msgstr "A URL do feed que você deseja assinar. "
|
||||
msgid "Theme"
|
||||
msgstr "Tema"
|
||||
|
||||
#. placeholder {0}: feed.filterLegacy
|
||||
#: src/pages/app/FeedDetailsPage.tsx
|
||||
msgid "This feed has a legacy filter that cannot be edited and is not applied. Please recreate the filter using the new expression editor. The legacy filter expression was: <0>{0}</0>"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/settings/ProfileSettings.tsx
|
||||
msgid "This is your API key. It can be used for some read-only API operations and grants access to the Fever API. Use the form at the bottom of the page to generate a new API key"
|
||||
msgstr "Esta é sua chave de API. Ela pode ser usada para algumas operações somente leitura da API e concede acesso à API do Fever. Use o formulário abaixo para gerar uma nova chave de API"
|
||||
|
||||
@@ -17,10 +17,6 @@ msgstr ""
|
||||
msgid "<0>CommaFeed is an open-source project. Sources are hosted on </0><1>GitHub</1>."
|
||||
msgstr "<0>CommaFeed - это проект с открытым исходным кодом. Исходный код доступен по адресу </0><1>GitHub</1>."
|
||||
|
||||
#: src/pages/app/FeedDetailsPage.tsx
|
||||
msgid "<0>Complete syntax is available </0><1>here</1><2>.</2>"
|
||||
msgstr "<0>Полный синтаксис доступен </0><1>здесь</1><2>.</2>"
|
||||
|
||||
#: src/pages/auth/RegistrationPage.tsx
|
||||
msgid "<0>Have an account?</0><1>Log in!</1>"
|
||||
msgstr "<0>Есть аккаунт?</0><1>Войти!</1>"
|
||||
@@ -126,10 +122,6 @@ msgstr "Вы уверены, что хотите отказаться от по
|
||||
msgid "Asc"
|
||||
msgstr "По возрастанию"
|
||||
|
||||
#: src/pages/app/FeedDetailsPage.tsx
|
||||
msgid "Available variables are 'title', 'content', 'url' 'author' and 'categories' and their content is converted to lower case to ease string comparison."
|
||||
msgstr "Доступными переменными являются «заголовок», «контент», «url», «автор» и «категории», и их содержимое преобразуется в нижний регистр для облегчения сравнения строк."
|
||||
|
||||
#: src/components/content/add/Subscribe.tsx
|
||||
msgid "Back"
|
||||
msgstr "Назад"
|
||||
@@ -155,6 +147,10 @@ msgstr "Для браузера Chrome требуется расширение"
|
||||
msgid "Browser tab"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/app/FeedDetailsPage.tsx
|
||||
msgid "Build a filter expression to indicate what you want to read. Entries that don't match will be marked as read automatically."
|
||||
msgstr ""
|
||||
|
||||
#: src/components/admin/UserEdit.tsx
|
||||
#: src/components/content/add/AddCategory.tsx
|
||||
#: src/components/content/add/ImportOpml.tsx
|
||||
@@ -365,10 +361,6 @@ msgstr ""
|
||||
msgid "Error"
|
||||
msgstr "Ошибка"
|
||||
|
||||
#: src/pages/app/FeedDetailsPage.tsx
|
||||
msgid "Example: {example}."
|
||||
msgstr "Пример: {пример}."
|
||||
|
||||
#: src/components/header/ProfileMenu.tsx
|
||||
msgid "Expanded"
|
||||
msgstr "Расширенный"
|
||||
@@ -469,10 +461,6 @@ msgstr ""
|
||||
msgid "Id"
|
||||
msgstr "Идентификатор"
|
||||
|
||||
#: src/pages/app/FeedDetailsPage.tsx
|
||||
msgid "If not empty, an expression evaluating to 'true' or 'false'. If false, new entries for this feed will be marked as read automatically."
|
||||
msgstr "Если не пусто, то выражение, оценивающееся как 'true' или 'false'. Если false, то новые записи для этой ленты будут автоматически помечаться как прочитанные."
|
||||
|
||||
#: src/components/settings/DisplaySettings.tsx
|
||||
msgid "If the entry doesn't entirely fit on the screen"
|
||||
msgstr "Если запись не помещается на экране полностью"
|
||||
@@ -1037,6 +1025,11 @@ msgstr "URL канала, на который вы хотите подписат
|
||||
msgid "Theme"
|
||||
msgstr "Тема"
|
||||
|
||||
#. placeholder {0}: feed.filterLegacy
|
||||
#: src/pages/app/FeedDetailsPage.tsx
|
||||
msgid "This feed has a legacy filter that cannot be edited and is not applied. Please recreate the filter using the new expression editor. The legacy filter expression was: <0>{0}</0>"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/settings/ProfileSettings.tsx
|
||||
msgid "This is your API key. It can be used for some read-only API operations and grants access to the Fever API. Use the form at the bottom of the page to generate a new API key"
|
||||
msgstr "Это ваш ключ API. Он может использоваться для некоторых операций API только для чтения и предоставляет доступ к API Fever. Чтобы сгенерировать новый ключ API, воспользуйтесь формой в нижней части страницы"
|
||||
|
||||
@@ -17,10 +17,6 @@ msgstr ""
|
||||
msgid "<0>CommaFeed is an open-source project. Sources are hosted on </0><1>GitHub</1>."
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/app/FeedDetailsPage.tsx
|
||||
msgid "<0>Complete syntax is available </0><1>here</1><2>.</2>"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/auth/RegistrationPage.tsx
|
||||
msgid "<0>Have an account?</0><1>Log in!</1>"
|
||||
msgstr "<0>Máte účet?</0><1>Prihláste sa!</1>"
|
||||
@@ -126,10 +122,6 @@ msgstr "Naozaj chcete zrušiť odber kanála <0>{feedName}</0>?"
|
||||
msgid "Asc"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/app/FeedDetailsPage.tsx
|
||||
msgid "Available variables are 'title', 'content', 'url' 'author' and 'categories' and their content is converted to lower case to ease string comparison."
|
||||
msgstr "Dostupné premenné sú 'názov', 'obsah', 'url', 'autor' a 'kategórie' a ich obsah je skonvertovaný na malé písmená, aby sa uľahčilo porovnávanie reťazcov."
|
||||
|
||||
#: src/components/content/add/Subscribe.tsx
|
||||
msgid "Back"
|
||||
msgstr "Späť"
|
||||
@@ -155,6 +147,10 @@ msgstr ""
|
||||
msgid "Browser tab"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/app/FeedDetailsPage.tsx
|
||||
msgid "Build a filter expression to indicate what you want to read. Entries that don't match will be marked as read automatically."
|
||||
msgstr ""
|
||||
|
||||
#: src/components/admin/UserEdit.tsx
|
||||
#: src/components/content/add/AddCategory.tsx
|
||||
#: src/components/content/add/ImportOpml.tsx
|
||||
@@ -365,10 +361,6 @@ msgstr ""
|
||||
msgid "Error"
|
||||
msgstr "Chyba"
|
||||
|
||||
#: src/pages/app/FeedDetailsPage.tsx
|
||||
msgid "Example: {example}."
|
||||
msgstr "Príklad: {príklad}."
|
||||
|
||||
#: src/components/header/ProfileMenu.tsx
|
||||
msgid "Expanded"
|
||||
msgstr "Rozšírené"
|
||||
@@ -469,10 +461,6 @@ msgstr ""
|
||||
msgid "Id"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/app/FeedDetailsPage.tsx
|
||||
msgid "If not empty, an expression evaluating to 'true' or 'false'. If false, new entries for this feed will be marked as read automatically."
|
||||
msgstr "Ak nie je prázdny, výraz vyhodnotený ako 'pravda' alebo 'nepravda'. "
|
||||
|
||||
#: src/components/settings/DisplaySettings.tsx
|
||||
msgid "If the entry doesn't entirely fit on the screen"
|
||||
msgstr ""
|
||||
@@ -1037,6 +1025,11 @@ msgstr "URL zdroja, na odber ktorého sa chcete prihlásiť. "
|
||||
msgid "Theme"
|
||||
msgstr "Téma"
|
||||
|
||||
#. placeholder {0}: feed.filterLegacy
|
||||
#: src/pages/app/FeedDetailsPage.tsx
|
||||
msgid "This feed has a legacy filter that cannot be edited and is not applied. Please recreate the filter using the new expression editor. The legacy filter expression was: <0>{0}</0>"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/settings/ProfileSettings.tsx
|
||||
msgid "This is your API key. It can be used for some read-only API operations and grants access to the Fever API. Use the form at the bottom of the page to generate a new API key"
|
||||
msgstr ""
|
||||
|
||||
@@ -17,10 +17,6 @@ msgstr ""
|
||||
msgid "<0>CommaFeed is an open-source project. Sources are hosted on </0><1>GitHub</1>."
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/app/FeedDetailsPage.tsx
|
||||
msgid "<0>Complete syntax is available </0><1>here</1><2>.</2>"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/auth/RegistrationPage.tsx
|
||||
msgid "<0>Have an account?</0><1>Log in!</1>"
|
||||
msgstr "<0>Har du ett konto?</0><1>Logga in!</1>"
|
||||
@@ -126,10 +122,6 @@ msgstr "Är du säker på att du vill avsluta prenumerationen på <0>{feedName}<
|
||||
msgid "Asc"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/app/FeedDetailsPage.tsx
|
||||
msgid "Available variables are 'title', 'content', 'url' 'author' and 'categories' and their content is converted to lower case to ease string comparison."
|
||||
msgstr "Tillgängliga variabler är 'titel', 'innehåll', 'url' 'författare' och 'kategorier' och deras innehåll konverteras till gemener för att underlätta strängjämförelse."
|
||||
|
||||
#: src/components/content/add/Subscribe.tsx
|
||||
msgid "Back"
|
||||
msgstr "Tillbaka"
|
||||
@@ -155,6 +147,10 @@ msgstr ""
|
||||
msgid "Browser tab"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/app/FeedDetailsPage.tsx
|
||||
msgid "Build a filter expression to indicate what you want to read. Entries that don't match will be marked as read automatically."
|
||||
msgstr ""
|
||||
|
||||
#: src/components/admin/UserEdit.tsx
|
||||
#: src/components/content/add/AddCategory.tsx
|
||||
#: src/components/content/add/ImportOpml.tsx
|
||||
@@ -365,10 +361,6 @@ msgstr ""
|
||||
msgid "Error"
|
||||
msgstr "Fel"
|
||||
|
||||
#: src/pages/app/FeedDetailsPage.tsx
|
||||
msgid "Example: {example}."
|
||||
msgstr "Exempel: {exempel}."
|
||||
|
||||
#: src/components/header/ProfileMenu.tsx
|
||||
msgid "Expanded"
|
||||
msgstr "Utökad"
|
||||
@@ -469,10 +461,6 @@ msgstr ""
|
||||
msgid "Id"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/app/FeedDetailsPage.tsx
|
||||
msgid "If not empty, an expression evaluating to 'true' or 'false'. If false, new entries for this feed will be marked as read automatically."
|
||||
msgstr "Om det inte är tomt, ett uttryck som utvärderas till 'sant' eller 'falskt'. "
|
||||
|
||||
#: src/components/settings/DisplaySettings.tsx
|
||||
msgid "If the entry doesn't entirely fit on the screen"
|
||||
msgstr ""
|
||||
@@ -1037,6 +1025,11 @@ msgstr "URL:en för flödet du vill prenumerera på. "
|
||||
msgid "Theme"
|
||||
msgstr "Tema"
|
||||
|
||||
#. placeholder {0}: feed.filterLegacy
|
||||
#: src/pages/app/FeedDetailsPage.tsx
|
||||
msgid "This feed has a legacy filter that cannot be edited and is not applied. Please recreate the filter using the new expression editor. The legacy filter expression was: <0>{0}</0>"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/settings/ProfileSettings.tsx
|
||||
msgid "This is your API key. It can be used for some read-only API operations and grants access to the Fever API. Use the form at the bottom of the page to generate a new API key"
|
||||
msgstr ""
|
||||
|
||||
@@ -17,10 +17,6 @@ msgstr ""
|
||||
msgid "<0>CommaFeed is an open-source project. Sources are hosted on </0><1>GitHub</1>."
|
||||
msgstr "<0>CommaFeed açık kaynak kodlu bir proje. Kaynak kodları </0><1>GitHub</1>'da."
|
||||
|
||||
#: src/pages/app/FeedDetailsPage.tsx
|
||||
msgid "<0>Complete syntax is available </0><1>here</1><2>.</2>"
|
||||
msgstr "<0>Tüm sözdizimi </0><1>burada</1><2>.</2>"
|
||||
|
||||
#: src/pages/auth/RegistrationPage.tsx
|
||||
msgid "<0>Have an account?</0><1>Log in!</1>"
|
||||
msgstr "<0>Hesabınız var mı?</0><1>Giriş yapın!</1>"
|
||||
@@ -126,10 +122,6 @@ msgstr "<0>{feedName}</0> aboneliğinden çıkmak istediğinizden emin misiniz?"
|
||||
msgid "Asc"
|
||||
msgstr "Artan"
|
||||
|
||||
#: src/pages/app/FeedDetailsPage.tsx
|
||||
msgid "Available variables are 'title', 'content', 'url' 'author' and 'categories' and their content is converted to lower case to ease string comparison."
|
||||
msgstr "Mevcut değişkenler 'title', 'content', 'url' 'yazar' ve 'kategoriler'dir ve dize karşılaştırmasını kolaylaştırmak için içerikleri küçük harfe dönüştürülür."
|
||||
|
||||
#: src/components/content/add/Subscribe.tsx
|
||||
msgid "Back"
|
||||
msgstr "Geri"
|
||||
@@ -155,6 +147,10 @@ msgstr ""
|
||||
msgid "Browser tab"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/app/FeedDetailsPage.tsx
|
||||
msgid "Build a filter expression to indicate what you want to read. Entries that don't match will be marked as read automatically."
|
||||
msgstr ""
|
||||
|
||||
#: src/components/admin/UserEdit.tsx
|
||||
#: src/components/content/add/AddCategory.tsx
|
||||
#: src/components/content/add/ImportOpml.tsx
|
||||
@@ -365,10 +361,6 @@ msgstr ""
|
||||
msgid "Error"
|
||||
msgstr "Hata"
|
||||
|
||||
#: src/pages/app/FeedDetailsPage.tsx
|
||||
msgid "Example: {example}."
|
||||
msgstr "Örnek: {örnek}."
|
||||
|
||||
#: src/components/header/ProfileMenu.tsx
|
||||
msgid "Expanded"
|
||||
msgstr "Genişletilmiş"
|
||||
@@ -469,10 +461,6 @@ msgstr ""
|
||||
msgid "Id"
|
||||
msgstr "Kimlik"
|
||||
|
||||
#: src/pages/app/FeedDetailsPage.tsx
|
||||
msgid "If not empty, an expression evaluating to 'true' or 'false'. If false, new entries for this feed will be marked as read automatically."
|
||||
msgstr "Boş değilse, 'doğru' veya 'yanlış' olarak değerlendirilen bir ifade. "
|
||||
|
||||
#: src/components/settings/DisplaySettings.tsx
|
||||
msgid "If the entry doesn't entirely fit on the screen"
|
||||
msgstr ""
|
||||
@@ -1037,6 +1025,11 @@ msgstr "Abone olmak istediğiniz beslemenin URL'si. "
|
||||
msgid "Theme"
|
||||
msgstr "Tema"
|
||||
|
||||
#. placeholder {0}: feed.filterLegacy
|
||||
#: src/pages/app/FeedDetailsPage.tsx
|
||||
msgid "This feed has a legacy filter that cannot be edited and is not applied. Please recreate the filter using the new expression editor. The legacy filter expression was: <0>{0}</0>"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/settings/ProfileSettings.tsx
|
||||
msgid "This is your API key. It can be used for some read-only API operations and grants access to the Fever API. Use the form at the bottom of the page to generate a new API key"
|
||||
msgstr ""
|
||||
|
||||
@@ -17,10 +17,6 @@ msgstr ""
|
||||
msgid "<0>CommaFeed is an open-source project. Sources are hosted on </0><1>GitHub</1>."
|
||||
msgstr "<0>CommaFeed是一个开源项目,源码托管在 </0><1>GitHub</1>。"
|
||||
|
||||
#: src/pages/app/FeedDetailsPage.tsx
|
||||
msgid "<0>Complete syntax is available </0><1>here</1><2>.</2>"
|
||||
msgstr "<0>可以使用完整的语法 </0><1>详情</1><2>.</2>"
|
||||
|
||||
#: src/pages/auth/RegistrationPage.tsx
|
||||
msgid "<0>Have an account?</0><1>Log in!</1>"
|
||||
msgstr "<0>有帐号吗?</0><1>登录!</1>"
|
||||
@@ -126,10 +122,6 @@ msgstr "您确定要退订 <0>{feedName}</0> 吗?"
|
||||
msgid "Asc"
|
||||
msgstr "升序"
|
||||
|
||||
#: src/pages/app/FeedDetailsPage.tsx
|
||||
msgid "Available variables are 'title', 'content', 'url' 'author' and 'categories' and their content is converted to lower case to ease string comparison."
|
||||
msgstr "可用变量为'title'、'content'、'url'、'author'和'categories',它们的内容被转换为小写以方便字符串比较。"
|
||||
|
||||
#: src/components/content/add/Subscribe.tsx
|
||||
msgid "Back"
|
||||
msgstr "返回"
|
||||
@@ -155,6 +147,10 @@ msgstr "浏览器扩展"
|
||||
msgid "Browser tab"
|
||||
msgstr "浏览器标签页"
|
||||
|
||||
#: src/pages/app/FeedDetailsPage.tsx
|
||||
msgid "Build a filter expression to indicate what you want to read. Entries that don't match will be marked as read automatically."
|
||||
msgstr ""
|
||||
|
||||
#: src/components/admin/UserEdit.tsx
|
||||
#: src/components/content/add/AddCategory.tsx
|
||||
#: src/components/content/add/ImportOpml.tsx
|
||||
@@ -365,10 +361,6 @@ msgstr "条目头部"
|
||||
msgid "Error"
|
||||
msgstr "错误"
|
||||
|
||||
#: src/pages/app/FeedDetailsPage.tsx
|
||||
msgid "Example: {example}."
|
||||
msgstr "示例:{示例}。"
|
||||
|
||||
#: src/components/header/ProfileMenu.tsx
|
||||
msgid "Expanded"
|
||||
msgstr "展开"
|
||||
@@ -469,10 +461,6 @@ msgstr "绿"
|
||||
msgid "Id"
|
||||
msgstr "序号"
|
||||
|
||||
#: src/pages/app/FeedDetailsPage.tsx
|
||||
msgid "If not empty, an expression evaluating to 'true' or 'false'. If false, new entries for this feed will be marked as read automatically."
|
||||
msgstr "如果不为空,则表达式的计算结果为“真”或“假”。如果为“假”,则此信息流的新条目将自动标记为已读。"
|
||||
|
||||
#: src/components/settings/DisplaySettings.tsx
|
||||
msgid "If the entry doesn't entirely fit on the screen"
|
||||
msgstr "如果条目不能完全显示在屏幕上"
|
||||
@@ -1037,6 +1025,11 @@ msgstr "您要订阅的信息流的网址。您也可以直接使用网站的网
|
||||
msgid "Theme"
|
||||
msgstr "主题"
|
||||
|
||||
#. placeholder {0}: feed.filterLegacy
|
||||
#: src/pages/app/FeedDetailsPage.tsx
|
||||
msgid "This feed has a legacy filter that cannot be edited and is not applied. Please recreate the filter using the new expression editor. The legacy filter expression was: <0>{0}</0>"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/settings/ProfileSettings.tsx
|
||||
msgid "This is your API key. It can be used for some read-only API operations and grants access to the Fever API. Use the form at the bottom of the page to generate a new API key"
|
||||
msgstr "这是您的API 密钥,它可以被用于Fever API的只读操作及访问授权。使用页面底部的表单生成一个新的API密钥。"
|
||||
|
||||
@@ -1,10 +1,25 @@
|
||||
import { Trans } from "@lingui/react/macro"
|
||||
import { Anchor, Box, Button, Code, Container, Divider, Group, Input, NumberInput, Stack, Text, TextInput, Title } from "@mantine/core"
|
||||
import {
|
||||
Anchor,
|
||||
Box,
|
||||
Button,
|
||||
Code,
|
||||
Container,
|
||||
Divider,
|
||||
Group,
|
||||
Input,
|
||||
Alert as MantineAlert,
|
||||
NumberInput,
|
||||
Stack,
|
||||
Text,
|
||||
TextInput,
|
||||
Title,
|
||||
} from "@mantine/core"
|
||||
import { useForm } from "@mantine/form"
|
||||
import { openConfirmModal } from "@mantine/modals"
|
||||
import { useEffect } from "react"
|
||||
import { useAsync, useAsyncCallback } from "react-async-hook"
|
||||
import { TbDeviceFloppy, TbTrash } from "react-icons/tb"
|
||||
import { TbAlertTriangle, TbDeviceFloppy, TbTrash } from "react-icons/tb"
|
||||
import { useParams } from "react-router-dom"
|
||||
import { client, errorToStrings } from "@/app/client"
|
||||
import { redirectToRootCategory, redirectToSelectedSource } from "@/app/redirect/thunks"
|
||||
@@ -13,41 +28,10 @@ import { reloadTree } from "@/app/tree/thunks"
|
||||
import type { FeedModificationRequest } from "@/app/types"
|
||||
import { Alert } from "@/components/Alert"
|
||||
import { CategorySelect } from "@/components/content/add/CategorySelect"
|
||||
import { FilteringExpressionEditor } from "@/components/content/edit/FilteringExpressionEditor"
|
||||
import { Loader } from "@/components/Loader"
|
||||
import { RelativeDate } from "@/components/RelativeDate"
|
||||
|
||||
function FilteringExpressionDescription() {
|
||||
const example = <Code>url.contains('youtube') or (author eq 'athou' and title.contains('github'))</Code>
|
||||
return (
|
||||
<div>
|
||||
<div>
|
||||
<Trans>
|
||||
If not empty, an expression evaluating to 'true' or 'false'. If false, new entries for this feed will be marked as read
|
||||
automatically.
|
||||
</Trans>
|
||||
</div>
|
||||
<div>
|
||||
<Trans>
|
||||
Available variables are 'title', 'content', 'url' 'author' and 'categories' and their content is converted to lower case
|
||||
to ease string comparison.
|
||||
</Trans>
|
||||
</div>
|
||||
<div>
|
||||
<Trans>Example: {example}.</Trans>
|
||||
</div>
|
||||
<div>
|
||||
<Trans>
|
||||
<span>Complete syntax is available </span>
|
||||
<a href="https://commons.apache.org/proper/commons-jexl/reference/syntax.html" target="_blank" rel="noreferrer">
|
||||
here
|
||||
</a>
|
||||
<span>.</span>
|
||||
</Trans>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export function FeedDetailsPage() {
|
||||
const { id } = useParams()
|
||||
if (!id) throw new Error("id required")
|
||||
@@ -158,11 +142,27 @@ export function FeedDetailsPage() {
|
||||
<TextInput label={<Trans>Name</Trans>} {...form.getInputProps("name")} required />
|
||||
<CategorySelect label={<Trans>Category</Trans>} {...form.getInputProps("categoryId")} clearable />
|
||||
<NumberInput label={<Trans>Position</Trans>} {...form.getInputProps("position")} required min={0} />
|
||||
<TextInput
|
||||
<Input.Wrapper
|
||||
label={<Trans>Filtering expression</Trans>}
|
||||
description={<FilteringExpressionDescription />}
|
||||
{...form.getInputProps("filter")}
|
||||
/>
|
||||
description={
|
||||
<Trans>
|
||||
Build a filter expression to indicate what you want to read. Entries that don't match will be marked as read
|
||||
automatically.
|
||||
</Trans>
|
||||
}
|
||||
>
|
||||
{feed.filterLegacy && (
|
||||
<MantineAlert color="yellow" icon={<TbAlertTriangle />}>
|
||||
<Trans>
|
||||
This feed has a legacy filter that cannot be edited and is not applied. Please recreate the filter using
|
||||
the new expression editor. The legacy filter expression was: <Code>{feed.filterLegacy}</Code>
|
||||
</Trans>
|
||||
</MantineAlert>
|
||||
)}
|
||||
<Box mt="xs">
|
||||
<FilteringExpressionEditor initialValue={feed.filter} onChange={value => form.setFieldValue("filter", value)} />
|
||||
</Box>
|
||||
</Input.Wrapper>
|
||||
|
||||
<Group>
|
||||
<Button variant="default" onClick={async () => await dispatch(redirectToSelectedSource())}>
|
||||
|
||||
@@ -392,15 +392,9 @@
|
||||
<version>3.6.1</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.apache.commons</groupId>
|
||||
<artifactId>commons-jexl</artifactId>
|
||||
<version>2.1.1</version>
|
||||
<exclusions>
|
||||
<exclusion>
|
||||
<artifactId>commons-logging</artifactId>
|
||||
<groupId>commons-logging</groupId>
|
||||
</exclusion>
|
||||
</exclusions>
|
||||
<groupId>dev.cel</groupId>
|
||||
<artifactId>cel</artifactId>
|
||||
<version>0.11.1</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.passay</groupId>
|
||||
|
||||
@@ -43,4 +43,7 @@ public class FeedSubscription extends AbstractModel {
|
||||
@Column(name = "filtering_expression", length = 4096)
|
||||
private String filter;
|
||||
|
||||
@Column(name = "filtering_expression_legacy", length = 4096)
|
||||
private String filterLegacy;
|
||||
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
package com.commafeed.backend.service;
|
||||
|
||||
import java.time.Year;
|
||||
import java.util.concurrent.Callable;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.ExecutionException;
|
||||
import java.util.concurrent.ExecutorService;
|
||||
import java.util.concurrent.Executors;
|
||||
@@ -11,92 +11,71 @@ import java.util.concurrent.TimeoutException;
|
||||
|
||||
import jakarta.inject.Singleton;
|
||||
|
||||
import org.apache.commons.jexl2.JexlContext;
|
||||
import org.apache.commons.jexl2.JexlEngine;
|
||||
import org.apache.commons.jexl2.JexlException;
|
||||
import org.apache.commons.jexl2.JexlInfo;
|
||||
import org.apache.commons.jexl2.MapContext;
|
||||
import org.apache.commons.jexl2.Script;
|
||||
import org.apache.commons.jexl2.introspection.JexlMethod;
|
||||
import org.apache.commons.jexl2.introspection.JexlPropertyGet;
|
||||
import org.apache.commons.jexl2.introspection.Uberspect;
|
||||
import org.apache.commons.jexl2.introspection.UberspectImpl;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.apache.commons.logging.LogFactory;
|
||||
import org.jsoup.Jsoup;
|
||||
|
||||
import com.commafeed.CommaFeedConfiguration;
|
||||
import com.commafeed.backend.model.FeedEntry;
|
||||
|
||||
import dev.cel.common.CelAbstractSyntaxTree;
|
||||
import dev.cel.common.CelValidationException;
|
||||
import dev.cel.common.types.SimpleType;
|
||||
import dev.cel.compiler.CelCompiler;
|
||||
import dev.cel.compiler.CelCompilerFactory;
|
||||
import dev.cel.runtime.CelEvaluationException;
|
||||
import dev.cel.runtime.CelRuntime;
|
||||
import dev.cel.runtime.CelRuntimeFactory;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
|
||||
@RequiredArgsConstructor
|
||||
@Singleton
|
||||
public class FeedEntryFilteringService {
|
||||
|
||||
private static final JexlEngine ENGINE = initEngine();
|
||||
private static final CelCompiler CEL_COMPILER = CelCompilerFactory.standardCelCompilerBuilder()
|
||||
.addVar("title", SimpleType.STRING)
|
||||
.addVar("titleLower", SimpleType.STRING)
|
||||
.addVar("author", SimpleType.STRING)
|
||||
.addVar("authorLower", SimpleType.STRING)
|
||||
.addVar("content", SimpleType.STRING)
|
||||
.addVar("contentLower", SimpleType.STRING)
|
||||
.addVar("url", SimpleType.STRING)
|
||||
.addVar("urlLower", SimpleType.STRING)
|
||||
.addVar("categories", SimpleType.STRING)
|
||||
.addVar("categoriesLower", SimpleType.STRING)
|
||||
.build();
|
||||
private static final CelRuntime CEL_RUNTIME = CelRuntimeFactory.standardCelRuntimeBuilder().build();
|
||||
|
||||
private final ExecutorService executor = Executors.newCachedThreadPool();
|
||||
private final CommaFeedConfiguration config;
|
||||
|
||||
private static JexlEngine initEngine() {
|
||||
// classloader that prevents object creation
|
||||
ClassLoader cl = new ClassLoader() {
|
||||
@Override
|
||||
protected Class<?> loadClass(String name, boolean resolve) {
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
// uberspect that prevents access to .class and .getClass()
|
||||
Uberspect uberspect = new UberspectImpl(LogFactory.getLog(JexlEngine.class)) {
|
||||
@Override
|
||||
public JexlPropertyGet getPropertyGet(Object obj, Object identifier, JexlInfo info) {
|
||||
if ("class".equals(identifier)) {
|
||||
return null;
|
||||
}
|
||||
return super.getPropertyGet(obj, identifier, info);
|
||||
}
|
||||
|
||||
@Override
|
||||
public JexlMethod getMethod(Object obj, String method, Object[] args, JexlInfo info) {
|
||||
if ("getClass".equals(method)) {
|
||||
return null;
|
||||
}
|
||||
return super.getMethod(obj, method, args, info);
|
||||
}
|
||||
};
|
||||
|
||||
JexlEngine engine = new JexlEngine(uberspect, null, null, null);
|
||||
engine.setStrict(true);
|
||||
engine.setClassLoader(cl);
|
||||
return engine;
|
||||
}
|
||||
|
||||
public boolean filterMatchesEntry(String filter, FeedEntry entry) throws FeedEntryFilterException {
|
||||
if (StringUtils.isBlank(filter)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
Script script;
|
||||
try {
|
||||
script = ENGINE.createScript(filter);
|
||||
} catch (JexlException e) {
|
||||
throw new FeedEntryFilterException("Exception while parsing expression " + filter, e);
|
||||
}
|
||||
String title = entry.getContent().getTitle() == null ? "" : Jsoup.parse(entry.getContent().getTitle()).text();
|
||||
String author = entry.getContent().getAuthor() == null ? "" : entry.getContent().getAuthor();
|
||||
String content = entry.getContent().getContent() == null ? "" : Jsoup.parse(entry.getContent().getContent()).text();
|
||||
String url = entry.getUrl() == null ? "" : entry.getUrl();
|
||||
String categories = entry.getContent().getCategories() == null ? "" : entry.getContent().getCategories();
|
||||
|
||||
JexlContext context = new MapContext();
|
||||
context.set("title", entry.getContent().getTitle() == null ? "" : Jsoup.parse(entry.getContent().getTitle()).text().toLowerCase());
|
||||
context.set("author", entry.getContent().getAuthor() == null ? "" : entry.getContent().getAuthor().toLowerCase());
|
||||
context.set("content",
|
||||
entry.getContent().getContent() == null ? "" : Jsoup.parse(entry.getContent().getContent()).text().toLowerCase());
|
||||
context.set("url", entry.getUrl() == null ? "" : entry.getUrl().toLowerCase());
|
||||
context.set("categories", entry.getContent().getCategories() == null ? "" : entry.getContent().getCategories().toLowerCase());
|
||||
Map<String, Object> data = new HashMap<>();
|
||||
data.put("title", title);
|
||||
data.put("titleLower", title.toLowerCase());
|
||||
|
||||
context.set("year", Year.now().getValue());
|
||||
data.put("author", author);
|
||||
data.put("authorLower", author.toLowerCase());
|
||||
|
||||
Callable<Object> callable = script.callable(context);
|
||||
Future<Object> future = executor.submit(callable);
|
||||
data.put("content", content);
|
||||
data.put("contentLower", content.toLowerCase());
|
||||
|
||||
data.put("url", url);
|
||||
data.put("urlLower", url.toLowerCase());
|
||||
|
||||
data.put("categories", categories);
|
||||
data.put("categoriesLower", categories.toLowerCase());
|
||||
|
||||
Future<Object> future = executor.submit(() -> evaluateCelExpression(filter, data));
|
||||
Object result;
|
||||
try {
|
||||
result = future.get(config.feedRefresh().filteringExpressionEvaluationTimeout().toMillis(), TimeUnit.MILLISECONDS);
|
||||
@@ -108,11 +87,15 @@ public class FeedEntryFilteringService {
|
||||
} catch (TimeoutException e) {
|
||||
throw new FeedEntryFilterException("Took too long evaluating expression " + filter, e);
|
||||
}
|
||||
try {
|
||||
return (boolean) result;
|
||||
} catch (ClassCastException e) {
|
||||
throw new FeedEntryFilterException(e.getMessage(), e);
|
||||
}
|
||||
|
||||
return Boolean.TRUE.equals(result);
|
||||
}
|
||||
|
||||
private Object evaluateCelExpression(String expression, Map<String, Object> data)
|
||||
throws CelValidationException, CelEvaluationException {
|
||||
CelAbstractSyntaxTree ast = CEL_COMPILER.compile(expression).getAst();
|
||||
CelRuntime.Program program = CEL_RUNTIME.createProgram(ast);
|
||||
return program.eval(data);
|
||||
}
|
||||
|
||||
@SuppressWarnings("serial")
|
||||
|
||||
@@ -59,9 +59,12 @@ public class Subscription implements Serializable {
|
||||
@Schema(description = "date of the newest item", type = SchemaType.INTEGER)
|
||||
private Instant newestItemTime;
|
||||
|
||||
@Schema(description = "JEXL string evaluated on new entries to mark them as read if they do not match")
|
||||
@Schema(description = "CEL string evaluated on new entries to mark them as read if they do not match")
|
||||
private String filter;
|
||||
|
||||
@Schema(description = "JEXL legacy filter")
|
||||
private String filterLegacy;
|
||||
|
||||
public static Subscription build(FeedSubscription subscription, UnreadCount unreadCount) {
|
||||
FeedCategory category = subscription.getCategory();
|
||||
Feed feed = subscription.getFeed();
|
||||
@@ -81,6 +84,7 @@ public class Subscription implements Serializable {
|
||||
sub.setNewestItemTime(unreadCount.getNewestItemTime());
|
||||
sub.setCategoryId(category == null ? null : String.valueOf(category.getId()));
|
||||
sub.setFilter(subscription.getFilter());
|
||||
sub.setFilterLegacy(subscription.getFilterLegacy());
|
||||
return sub;
|
||||
}
|
||||
|
||||
|
||||
@@ -27,7 +27,7 @@ public class FeedModificationRequest implements Serializable {
|
||||
@Schema(description = "new display position, null if not changed")
|
||||
private Integer position;
|
||||
|
||||
@Schema(description = "JEXL string evaluated on new entries to mark them as read if they do not match")
|
||||
@Schema(description = "CEL string evaluated on new entries to mark them as read if they do not match")
|
||||
@Size(max = 4096)
|
||||
private String filter;
|
||||
|
||||
|
||||
@@ -431,7 +431,12 @@ public class FeedREST {
|
||||
|
||||
User user = authenticationContext.getCurrentUser();
|
||||
FeedSubscription subscription = feedSubscriptionDAO.findById(user, req.getId());
|
||||
|
||||
subscription.setFilter(req.getFilter());
|
||||
if (StringUtils.isNotBlank(subscription.getFilter())) {
|
||||
// if the new filter is filled, remove the legacy filter
|
||||
subscription.setFilterLegacy(null);
|
||||
}
|
||||
|
||||
if (StringUtils.isNotBlank(req.getName())) {
|
||||
subscription.setTitle(req.getName());
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<databaseChangeLog xmlns="http://www.liquibase.org/xml/ns/dbchangelog"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog https://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-latest.xsd">
|
||||
|
||||
<changeSet id="cel-filtering-expressions" author="athou">
|
||||
<addColumn tableName="FEEDSUBSCRIPTIONS">
|
||||
<column name="filtering_expression_legacy" type="VARCHAR(4096)">
|
||||
<constraints nullable="true" />
|
||||
</column>
|
||||
</addColumn>
|
||||
<update tableName="FEEDSUBSCRIPTIONS">
|
||||
<column name="filtering_expression_legacy" valueComputed="filtering_expression" />
|
||||
</update>
|
||||
<update tableName="FEEDSUBSCRIPTIONS">
|
||||
<column name="filtering_expression" valueComputed="NULL" />
|
||||
</update>
|
||||
</changeSet>
|
||||
|
||||
</databaseChangeLog>
|
||||
|
||||
@@ -37,5 +37,6 @@
|
||||
<include file="changelogs/db.changelog-5.8.xml" />
|
||||
<include file="changelogs/db.changelog-5.11.xml" />
|
||||
<include file="changelogs/db.changelog-5.12.xml" />
|
||||
<include file="changelogs/db.changelog-7.0.xml" />
|
||||
|
||||
</databaseChangeLog>
|
||||
@@ -4,6 +4,7 @@ import java.time.Duration;
|
||||
|
||||
import org.junit.jupiter.api.Assertions;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Nested;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.mockito.Mockito;
|
||||
|
||||
@@ -48,49 +49,219 @@ class FeedEntryFilteringServiceTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
void simpleExpression() throws FeedEntryFilterException {
|
||||
Assertions.assertTrue(service.filterMatchesEntry("author.toString() eq 'athou'", entry));
|
||||
void simpleEqualsExpression() throws FeedEntryFilterException {
|
||||
Assertions.assertTrue(service.filterMatchesEntry("author == \"Athou\"", entry));
|
||||
}
|
||||
|
||||
@Test
|
||||
void newIsDisabled() {
|
||||
Assertions.assertThrows(FeedEntryFilterException.class,
|
||||
() -> service.filterMatchesEntry("null eq new ('java.lang.String', 'athou')", entry));
|
||||
void simpleNotEqualsExpression() throws FeedEntryFilterException {
|
||||
Assertions.assertTrue(service.filterMatchesEntry("author != \"other\"", entry));
|
||||
}
|
||||
|
||||
@Test
|
||||
void getClassMethodIsDisabled() {
|
||||
Assertions.assertThrows(FeedEntryFilterException.class, () -> service.filterMatchesEntry("null eq ''.getClass()", entry));
|
||||
void containsExpression() throws FeedEntryFilterException {
|
||||
Assertions.assertTrue(service.filterMatchesEntry("author.contains(\"Athou\")", entry));
|
||||
}
|
||||
|
||||
@Test
|
||||
void dotClassIsDisabled() throws FeedEntryFilterException {
|
||||
Assertions.assertTrue(service.filterMatchesEntry("null eq ''.class", entry));
|
||||
void titleContainsExpression() throws FeedEntryFilterException {
|
||||
Assertions.assertTrue(service.filterMatchesEntry("title.contains(\"Merge\")", entry));
|
||||
}
|
||||
|
||||
@Test
|
||||
void cannotLoopForever() {
|
||||
Mockito.when(config.feedRefresh().filteringExpressionEvaluationTimeout()).thenReturn(Duration.ofMillis(200));
|
||||
service = new FeedEntryFilteringService(config);
|
||||
|
||||
Assertions.assertThrows(FeedEntryFilterException.class, () -> service.filterMatchesEntry("while(true) {}", entry));
|
||||
void urlContainsExpression() throws FeedEntryFilterException {
|
||||
Assertions.assertTrue(service.filterMatchesEntry("url.contains(\"github\")", entry));
|
||||
}
|
||||
|
||||
@Test
|
||||
void handlesNullCorrectly() {
|
||||
entry.setUrl(null);
|
||||
entry.setContent(new FeedEntryContent());
|
||||
Assertions.assertDoesNotThrow(() -> service.filterMatchesEntry("author eq 'athou'", entry));
|
||||
void andExpression() throws FeedEntryFilterException {
|
||||
Assertions.assertTrue(service.filterMatchesEntry("author == \"Athou\" && url.contains(\"github\")", entry));
|
||||
}
|
||||
|
||||
@Test
|
||||
void incorrectScriptThrowsException() {
|
||||
Assertions.assertThrows(FeedEntryFilterException.class, () -> service.filterMatchesEntry("aa eqz bb", entry));
|
||||
void orExpression() throws FeedEntryFilterException {
|
||||
Assertions.assertTrue(service.filterMatchesEntry("author == \"other\" || url.contains(\"github\")", entry));
|
||||
}
|
||||
|
||||
@Test
|
||||
void incorrectReturnTypeThrowsException() {
|
||||
Assertions.assertThrows(FeedEntryFilterException.class, () -> service.filterMatchesEntry("1", entry));
|
||||
void notExpression() throws FeedEntryFilterException {
|
||||
Assertions.assertTrue(service.filterMatchesEntry("!(author == \"other\")", entry));
|
||||
}
|
||||
|
||||
@Test
|
||||
void incorrectExpressionThrowsException() {
|
||||
Assertions.assertThrows(FeedEntryFilterException.class, () -> service.filterMatchesEntry("not valid cel", entry));
|
||||
}
|
||||
|
||||
@Test
|
||||
void falseValueReturnsFalse() throws FeedEntryFilterException {
|
||||
Assertions.assertFalse(service.filterMatchesEntry("false", entry));
|
||||
}
|
||||
|
||||
@Test
|
||||
void trueValueReturnsTrue() throws FeedEntryFilterException {
|
||||
Assertions.assertTrue(service.filterMatchesEntry("true", entry));
|
||||
}
|
||||
|
||||
@Test
|
||||
void startsWithExpression() throws FeedEntryFilterException {
|
||||
Assertions.assertTrue(service.filterMatchesEntry("title.startsWith(\"Merge\")", entry));
|
||||
}
|
||||
|
||||
@Test
|
||||
void endsWithExpression() throws FeedEntryFilterException {
|
||||
Assertions.assertTrue(service.filterMatchesEntry("url.endsWith(\"commafeed\")", entry));
|
||||
}
|
||||
|
||||
@Test
|
||||
void categoriesContainsExpression() throws FeedEntryFilterException {
|
||||
FeedEntryContent content = entry.getContent();
|
||||
content.setCategories("tech, programming, java");
|
||||
entry.setContent(content);
|
||||
Assertions.assertTrue(service.filterMatchesEntry("categories.contains(\"programming\")", entry));
|
||||
}
|
||||
|
||||
@Test
|
||||
void caseInsensitiveAuthorMatchUsingLowerVariable() throws FeedEntryFilterException {
|
||||
Assertions.assertTrue(service.filterMatchesEntry("authorLower == \"athou\"", entry));
|
||||
}
|
||||
|
||||
@Test
|
||||
void caseInsensitiveTitleMatchUsingLowerVariable() throws FeedEntryFilterException {
|
||||
Assertions.assertTrue(service.filterMatchesEntry("titleLower.contains(\"merge\")", entry));
|
||||
}
|
||||
|
||||
@Test
|
||||
void caseInsensitiveUrlMatchUsingLowerVariable() throws FeedEntryFilterException {
|
||||
Assertions.assertTrue(service.filterMatchesEntry("urlLower.contains(\"github\")", entry));
|
||||
}
|
||||
|
||||
@Test
|
||||
void caseInsensitiveContentMatchUsingLowerVariable() throws FeedEntryFilterException {
|
||||
Assertions.assertTrue(service.filterMatchesEntry("contentLower.contains(\"merge\")", entry));
|
||||
}
|
||||
|
||||
@Test
|
||||
void caseInsensitiveCategoriesMatchUsingLowerVariable() throws FeedEntryFilterException {
|
||||
FeedEntryContent content = entry.getContent();
|
||||
content.setCategories("Tech, Programming, Java");
|
||||
entry.setContent(content);
|
||||
Assertions.assertTrue(service.filterMatchesEntry("categoriesLower.contains(\"tech\")", entry));
|
||||
}
|
||||
|
||||
@Nested
|
||||
class Sandbox {
|
||||
|
||||
@Test
|
||||
void sandboxBlocksSystemPropertyAccess() {
|
||||
Assertions.assertThrows(FeedEntryFilterException.class,
|
||||
() -> service.filterMatchesEntry("java.lang.System.getProperty(\"user.home\")", entry));
|
||||
}
|
||||
|
||||
@Test
|
||||
void sandboxBlocksRuntimeExec() {
|
||||
Assertions.assertThrows(FeedEntryFilterException.class,
|
||||
() -> service.filterMatchesEntry("java.lang.Runtime.getRuntime().exec(\"calc\")", entry));
|
||||
}
|
||||
|
||||
@Test
|
||||
void sandboxBlocksProcessBuilder() {
|
||||
Assertions.assertThrows(FeedEntryFilterException.class,
|
||||
() -> service.filterMatchesEntry("new java.lang.ProcessBuilder(\"cmd\").start()", entry));
|
||||
}
|
||||
|
||||
@Test
|
||||
void sandboxBlocksClassLoading() {
|
||||
Assertions.assertThrows(FeedEntryFilterException.class,
|
||||
() -> service.filterMatchesEntry("java.lang.Class.forName(\"java.lang.Runtime\")", entry));
|
||||
}
|
||||
|
||||
@Test
|
||||
void sandboxBlocksReflection() {
|
||||
Assertions.assertThrows(FeedEntryFilterException.class,
|
||||
() -> service.filterMatchesEntry("title.getClass().getMethods()", entry));
|
||||
}
|
||||
|
||||
@Test
|
||||
void sandboxBlocksFileAccess() {
|
||||
Assertions.assertThrows(FeedEntryFilterException.class,
|
||||
() -> service.filterMatchesEntry("new java.io.File(\"/etc/passwd\").exists()", entry));
|
||||
}
|
||||
|
||||
@Test
|
||||
void sandboxBlocksFileRead() {
|
||||
Assertions.assertThrows(FeedEntryFilterException.class,
|
||||
() -> service.filterMatchesEntry("java.nio.file.Files.readString(java.nio.file.Paths.get(\"/etc/passwd\"))", entry));
|
||||
}
|
||||
|
||||
@Test
|
||||
void sandboxBlocksNetworkAccess() {
|
||||
Assertions.assertThrows(FeedEntryFilterException.class,
|
||||
() -> service.filterMatchesEntry("new java.net.URL(\"http://evil.com\").openConnection()", entry));
|
||||
}
|
||||
|
||||
@Test
|
||||
void sandboxBlocksScriptEngine() {
|
||||
Assertions.assertThrows(FeedEntryFilterException.class, () -> service
|
||||
.filterMatchesEntry("new javax.script.ScriptEngineManager().getEngineByName(\"js\").eval(\"1+1\")", entry));
|
||||
}
|
||||
|
||||
@Test
|
||||
void sandboxBlocksThreadCreation() {
|
||||
Assertions.assertThrows(FeedEntryFilterException.class,
|
||||
() -> service.filterMatchesEntry("new java.lang.Thread().start()", entry));
|
||||
}
|
||||
|
||||
@Test
|
||||
void sandboxBlocksEnvironmentVariableAccess() {
|
||||
Assertions.assertThrows(FeedEntryFilterException.class,
|
||||
() -> service.filterMatchesEntry("java.lang.System.getenv(\"PATH\")", entry));
|
||||
}
|
||||
|
||||
@Test
|
||||
void sandboxBlocksExitCall() {
|
||||
Assertions.assertThrows(FeedEntryFilterException.class, () -> service.filterMatchesEntry("java.lang.System.exit(0)", entry));
|
||||
}
|
||||
|
||||
@Test
|
||||
void sandboxBlocksUndeclaredVariables() {
|
||||
Assertions.assertThrows(FeedEntryFilterException.class, () -> service.filterMatchesEntry("unknownVariable == \"test\"", entry));
|
||||
}
|
||||
|
||||
@Test
|
||||
void sandboxBlocksMethodInvocationOnStrings() {
|
||||
Assertions.assertThrows(FeedEntryFilterException.class, () -> service.filterMatchesEntry("title.toCharArray()", entry));
|
||||
}
|
||||
|
||||
@Test
|
||||
void sandboxBlocksArbitraryJavaMethodCalls() {
|
||||
Assertions.assertThrows(FeedEntryFilterException.class, () -> service.filterMatchesEntry("title.getBytes()", entry));
|
||||
}
|
||||
|
||||
@Test
|
||||
void sandboxOnlyAllowsDeclaredVariables() {
|
||||
Assertions.assertThrows(FeedEntryFilterException.class, () -> service.filterMatchesEntry("System", entry));
|
||||
}
|
||||
|
||||
@Test
|
||||
void sandboxBlocksConstructorCalls() {
|
||||
Assertions.assertThrows(FeedEntryFilterException.class, () -> service.filterMatchesEntry("new String(\"test\")", entry));
|
||||
}
|
||||
|
||||
@Test
|
||||
void sandboxBlocksStaticMethodCalls() {
|
||||
Assertions.assertThrows(FeedEntryFilterException.class, () -> service.filterMatchesEntry("String.valueOf(123)", entry));
|
||||
}
|
||||
|
||||
@Test
|
||||
void sandboxBlocksLambdaExpressions() {
|
||||
Assertions.assertThrows(FeedEntryFilterException.class, () -> service.filterMatchesEntry("() -> true", entry));
|
||||
}
|
||||
|
||||
@Test
|
||||
void sandboxBlocksObjectInstantiation() {
|
||||
Assertions.assertThrows(FeedEntryFilterException.class, () -> service.filterMatchesEntry("java.util.HashMap{}", entry));
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -103,7 +103,7 @@ class WebSocketIT extends BaseIT {
|
||||
FeedModificationRequest req = new FeedModificationRequest();
|
||||
req.setId(subscriptionId);
|
||||
req.setName("feed-name");
|
||||
req.setFilter("!title.contains('item 4')");
|
||||
req.setFilter("!titleLower.contains('item 4')");
|
||||
RestAssured.given().body(req).contentType(ContentType.JSON).post("rest/feed/modify").then().statusCode(HttpStatus.SC_OK);
|
||||
|
||||
AtomicBoolean connected = new AtomicBoolean();
|
||||
|
||||
@@ -23,6 +23,7 @@ import org.junit.jupiter.api.Test;
|
||||
import org.xml.sax.InputSource;
|
||||
|
||||
import com.commafeed.TestConstants;
|
||||
import com.commafeed.frontend.model.Entries;
|
||||
import com.commafeed.frontend.model.Entry;
|
||||
import com.commafeed.frontend.model.FeedInfo;
|
||||
import com.commafeed.frontend.model.Subscription;
|
||||
@@ -263,4 +264,47 @@ class FeedIT extends BaseIT {
|
||||
}
|
||||
}
|
||||
|
||||
@Nested
|
||||
class Filter {
|
||||
@Test
|
||||
void filterEntriesOnNewFeedItems() throws IOException {
|
||||
// subscribe and wait for initial 2 entries
|
||||
Long subscriptionId = subscribeAndWaitForEntries(getFeedUrl());
|
||||
Entries initialEntries = getFeedEntries(subscriptionId);
|
||||
Assertions.assertEquals(2, initialEntries.getEntries().size());
|
||||
|
||||
// set up a filter that excludes entries with "item 4" in the title
|
||||
Subscription subscription = getSubscription(subscriptionId);
|
||||
FeedModificationRequest req = new FeedModificationRequest();
|
||||
req.setId(subscriptionId);
|
||||
req.setName(subscription.getName());
|
||||
req.setCategoryId(subscription.getCategoryId());
|
||||
req.setPosition(subscription.getPosition());
|
||||
req.setFilter("!titleLower.contains('item 4')");
|
||||
RestAssured.given().body(req).contentType(ContentType.JSON).post("rest/feed/modify").then().statusCode(HttpStatus.SC_OK);
|
||||
|
||||
// verify filter is set
|
||||
subscription = getSubscription(subscriptionId);
|
||||
Assertions.assertEquals("!titleLower.contains('item 4')", subscription.getFilter());
|
||||
|
||||
// feed now returns 2 more entries (Item 3 and Item 4)
|
||||
feedNowReturnsMoreEntries();
|
||||
forceRefreshAllFeeds();
|
||||
|
||||
// wait for new entries to be fetched
|
||||
Awaitility.await().atMost(Duration.ofSeconds(15)).until(() -> getCategoryEntries("all"), e -> e.getEntries().size() == 4);
|
||||
|
||||
// verify that Item 4 was marked as read because it matches the filter
|
||||
Entries unreadEntries = RestAssured.given()
|
||||
.get("rest/feed/entries?id={id}&readType=unread", subscriptionId)
|
||||
.then()
|
||||
.statusCode(HttpStatus.SC_OK)
|
||||
.extract()
|
||||
.as(Entries.class);
|
||||
Assertions.assertEquals(3, unreadEntries.getEntries().size());
|
||||
Assertions.assertTrue(unreadEntries.getEntries().stream().noneMatch(e -> e.getTitle().toLowerCase().contains("item 4")),
|
||||
"Item 4 should be filtered out (marked as read)");
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user