migrate filtering expressions to safer CEL and add a query builder

This commit is contained in:
Athou
2026-02-15 10:20:50 +01:00
parent 08bfcded7f
commit d444a7080d
46 changed files with 862 additions and 590 deletions

View File

@@ -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",

View File

@@ -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",

View File

@@ -28,6 +28,7 @@ export interface Subscription {
position: number
newestItemTime?: number
filter?: string
filterLegacy?: string
}
export interface Category {

View File

@@ -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>
)
}

View File

@@ -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}
/>

View File

@@ -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 (

View File

@@ -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 ""

View File

@@ -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."

View File

@@ -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 ""

View File

@@ -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 ""

View File

@@ -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 ""

View File

@@ -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"

View File

@@ -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"

View File

@@ -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"

View File

@@ -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 ""

View File

@@ -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 ""

View File

@@ -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"

View File

@@ -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"

View File

@@ -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 ""

View File

@@ -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 ""

View File

@@ -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 ""

View File

@@ -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キーを生成します。"

View File

@@ -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 ""

View File

@@ -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 ""

View File

@@ -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 ""

View File

@@ -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 ""

View File

@@ -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 ""

View File

@@ -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 ""

View File

@@ -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"

View File

@@ -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, воспользуйтесь формой в нижней части страницы"

View File

@@ -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 ""

View File

@@ -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 ""

View File

@@ -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 ""

View File

@@ -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密钥。"

View File

@@ -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())}>

View File

@@ -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>

View File

@@ -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;
}

View File

@@ -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")

View File

@@ -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;
}

View File

@@ -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;

View File

@@ -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());

View File

@@ -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>

View File

@@ -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>

View File

@@ -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));
}
}
}

View File

@@ -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();

View File

@@ -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)");
}
}
}