Compare commits

...

83 Commits
7.2.1 ... 7.3.2

Author SHA1 Message Date
Athou
689c20d367 release 7.3.2 2026-08-22 08:23:13 +02:00
Athou
43588df2b4 prevent users from starring/tagging entries that are not theirs 2026-08-22 08:14:21 +02:00
renovate[bot]
d2aaddea33 Update dependency vitest to v4.1.11 2026-08-21 18:52:50 +00:00
Jérémie Panzer
de233f8ae5 Merge pull request #2280 from Athou/renovate/patch-testing-library-monorepo
Update dependency @testing-library/user-event to v14.6.5
2026-08-21 15:06:31 +02:00
renovate[bot]
3179e74dc7 Update dependency @testing-library/user-event to v14.6.5 2026-08-21 04:47:20 +00:00
renovate[bot]
6f5e9f5d49 Update dependency @biomejs/biome to v2.5.9 2026-08-21 00:00:07 +00:00
Jérémie Panzer
8a43fdf200 Merge pull request #2279 from Athou/renovate/protobuf-monorepo
Update dependency com.google.protobuf:protobuf-java to v4.36.0
2026-08-21 01:59:09 +02:00
renovate[bot]
69051e5bbb Update dependency com.google.protobuf:protobuf-java to v4.36.0 2026-08-20 18:54:00 +00:00
renovate[bot]
c648e7e0ec Update dependency dayjs to v1.11.23 2026-08-20 15:32:41 +00:00
renovate[bot]
c91cf376ad Update ibm-semeru-runtimes:open-jdk-25.0.4.0-jre Docker digest to 524f0d0 2026-08-20 09:23:52 +00:00
Jérémie Panzer
f4e07597d0 Merge pull request #2278 from canoine/patch-11
Update fr/messages.po
2026-08-20 11:22:57 +02:00
CNE
64b64d3340 Update fr/messages.po
3 new fields
2026-08-20 10:47:45 +02:00
Jérémie Panzer
57ea87871e Merge pull request #2276 from Athou/renovate/querydsl.version
Update querydsl.version to v7.6
2026-08-20 09:06:03 +02:00
renovate[bot]
6154d80bb9 Update querydsl.version to v7.6 2026-08-20 06:17:16 +00:00
renovate[bot]
ad85745f85 Update quarkus.version to v3.38.3 2026-08-20 00:35:52 +00:00
renovate[bot]
41fc77322c Update dependency dayjs to v1.11.22 2026-08-19 20:44:22 +00:00
Jérémie Panzer
1b83c26417 Merge pull request #2274 from Athou/renovate/docker-setup-buildx-action-4.x
Update docker/setup-buildx-action action to v4.3.0
2026-08-19 22:43:38 +02:00
Athou
10ea5f0b93 move exception to its own package 2026-08-19 22:32:37 +02:00
Jérémie Panzer
032684de68 Merge pull request #2272 from n200534/fix/issue-2016-translate-backend-errors
Translate login authentication errors
2026-08-19 22:31:29 +02:00
n200534
3efec5ce58 Use typed backend errors for translations 2026-08-20 01:11:30 +05:30
renovate[bot]
4ed32964d9 Update docker/setup-buildx-action action to v4.3.0 2026-08-19 16:16:18 +00:00
renovate[bot]
05c1d0cc6e Update ibm-semeru-runtimes:open-jdk-25.0.4.0-jre Docker digest to 27d8480 2026-08-18 22:09:48 +00:00
Jérémie Panzer
3c54924971 Merge pull request #2273 from Athou/renovate/com.puppycrawl.tools-checkstyle-14.x
Update dependency com.puppycrawl.tools:checkstyle to v14
2026-08-18 21:48:54 +02:00
renovate[bot]
e57dfc0e3f Update dependency com.puppycrawl.tools:checkstyle to v14 2026-08-18 17:49:50 +00:00
n200534
eff8bb16a0 Translate login authentication errors 2026-08-18 15:33:42 +05:30
Jérémie Panzer
87e681b71e Merge pull request #2271 from Athou/renovate/dev.cel-cel-0.x
Update dependency dev.cel:cel to v0.14.0
2026-08-18 07:13:13 +02:00
renovate[bot]
ac73ef4ea7 Update dependency dev.cel:cel to v0.14.0 2026-08-18 02:44:30 +00:00
Jérémie Panzer
20945e7443 Merge pull request #2270 from Athou/renovate/react-querybuilder
Update react-querybuilder to v8.23.0
2026-08-17 23:16:27 +02:00
Jérémie Panzer
b7b1278111 Merge pull request #2269 from Athou/renovate/org.mock-server-mockserver-junit-jupiter-7.x
Update dependency org.mock-server:mockserver-junit-jupiter to v7.6.0
2026-08-17 23:14:40 +02:00
renovate[bot]
6f9378404f Update react-querybuilder to v8.23.0 2026-08-17 20:36:06 +00:00
renovate[bot]
79787aca6e Update dependency org.mock-server:mockserver-junit-jupiter to v7.6.0 2026-08-17 20:35:51 +00:00
Athou
2d90ccf856 release 7.3.1 2026-08-17 22:32:32 +02:00
Athou
7f77cf0268 prevent Host header injection attacks 2026-08-17 22:27:46 +02:00
Jérémie Panzer
f3c390e0e0 Merge pull request #2267 from Athou/renovate/com.diffplug.spotless-spotless-maven-plugin-3.x
Update dependency com.diffplug.spotless:spotless-maven-plugin to v3.10.0
2026-08-17 08:49:07 +02:00
Jérémie Panzer
1fdc471197 Merge pull request #2266 from Athou/renovate/com.puppycrawl.tools-checkstyle-13.x
Update dependency com.puppycrawl.tools:checkstyle to v13.11.0
2026-08-17 07:45:37 +02:00
renovate[bot]
ecf5d9e50e Update dependency com.diffplug.spotless:spotless-maven-plugin to v3.10.0 2026-08-17 05:44:41 +00:00
renovate[bot]
97a49e8faa Lock file maintenance 2026-08-17 00:41:26 +00:00
renovate[bot]
f5a020cdc2 Update dependency com.puppycrawl.tools:checkstyle to v13.11.0 2026-08-17 00:40:24 +00:00
renovate[bot]
1f0507ee65 Update dependency @testing-library/user-event to v14.6.4 2026-08-15 01:00:48 +00:00
renovate[bot]
393c90f334 Update react-querybuilder to v8.22.5 2026-08-14 23:10:07 +00:00
renovate[bot]
03ecc2f49b Update dependency @biomejs/biome to v2.5.8 2026-08-14 09:15:31 +00:00
renovate[bot]
17743adce6 Update quarkus.version to v3.38.2 2026-08-13 17:05:44 +00:00
renovate[bot]
126dde103a Update dependency @testing-library/jest-dom to v7.0.1 2026-08-13 00:07:02 +00:00
renovate[bot]
14a25581b4 Update react-querybuilder to v8.22.4 2026-08-11 06:18:20 +00:00
Athou
c7c95efb3a release 7.3.0 2026-08-11 08:17:05 +02:00
Athou
4c1293d357 change the default value of blockLocalAddresses 2026-08-11 08:07:56 +02:00
Athou
76a92ea44c more SSRF protection regarding IPv6 2026-08-11 07:46:02 +02:00
Jérémie Panzer
ba454322db Merge pull request #2263 from Athou/renovate/org.apache.commons-commons-collections4-4.x
fix(deps): update dependency org.apache.commons:commons-collections4 to v4.6.0
2026-08-10 16:55:55 +02:00
renovate[bot]
a3b9f471e1 fix(deps): update dependency org.apache.commons:commons-collections4 to v4.6.0 2026-08-10 13:47:04 +00:00
Athou
d58ddc888e fix flaky tests 2026-08-10 09:45:00 +02:00
Athou
fe9837c45c fix typo 2026-08-10 09:30:31 +02:00
Athou
332d126bd7 reduce log verbosity during tests 2026-08-10 09:08:15 +02:00
Athou
0c66bf2695 speed up tests 2026-08-10 09:05:11 +02:00
Athou
4d6d07c98b remove warning 2026-08-10 09:02:07 +02:00
renovate[bot]
8f7d4bfa10 chore(deps): lock file maintenance 2026-08-10 05:05:39 +00:00
renovate[bot]
7b3f39001f fix(deps): update react-querybuilder to v8.22.3 2026-08-10 03:27:17 +00:00
Jérémie Panzer
b9630809a0 Merge pull request #2262 from Athou/renovate/com.puppycrawl.tools-checkstyle-13.x
chore(deps): update dependency com.puppycrawl.tools:checkstyle to v13.10.0
2026-08-10 05:26:27 +02:00
renovate[bot]
ce3244da5e chore(deps): update dependency com.puppycrawl.tools:checkstyle to v13.10.0 2026-08-09 21:51:02 +00:00
renovate[bot]
4067297d2a chore(deps): update dependency vite to v8.2.1 2026-08-09 17:40:42 +00:00
renovate[bot]
03c11fa181 fix(deps): update dependency org.apache.httpcomponents.client5:httpclient5 to v5.6.4 2026-08-09 09:42:31 +00:00
renovate[bot]
3676e78eb9 fix(deps): update react-querybuilder to v8.22.2 2026-08-08 16:44:38 +00:00
Athou
1e08c4486f fix google reader api icon url 2026-08-08 16:06:47 +02:00
Athou
fd8bb60daa google reader api support 2026-08-08 10:22:58 +02:00
renovate[bot]
557d56ffc5 fix(deps): update react-querybuilder to v8.22.1 2026-08-08 04:30:28 +08:00
renovate[bot]
30f76a2349 chore(deps): update dependency @biomejs/biome to v2.5.7 2026-08-07 15:07:29 +00:00
renovate[bot]
cf0fa5e22e chore(deps): update dependency @testing-library/user-event to v14.6.3 2026-08-07 04:00:58 +00:00
Jérémie Panzer
f16becbd82 Merge pull request #2259 from Athou/renovate/graalvm-setup-graalvm-1.6.x
chore(deps): update graalvm/setup-graalvm action to v1.6.4
2026-08-07 06:00:37 +02:00
Jérémie Panzer
4855450ddc Merge pull request #2260 from Athou/renovate/react-querybuilder
fix(deps): update react-querybuilder to v8.22.0
2026-08-07 06:00:33 +02:00
Jérémie Panzer
0733aedce5 Merge pull request #2257 from Athou/renovate/lock-file-maintenance
chore(deps): lock file maintenance
2026-08-07 06:00:25 +02:00
renovate[bot]
399ad14d9b chore(deps): lock file maintenance 2026-08-07 02:59:35 +00:00
renovate[bot]
7881ea3c5c fix(deps): update react-querybuilder to v8.22.0 2026-08-07 02:58:54 +00:00
renovate[bot]
7e9de622da chore(deps): update graalvm/setup-graalvm action to v1.6.4 2026-08-07 02:58:46 +00:00
Athou
c6655078f2 fix build 2026-08-07 04:58:06 +02:00
Athou
b86cef9499 package-lock refresh 2026-08-06 21:47:31 +02:00
Athou
5375ab6e97 profile menu now works again on mobile when action buttons are at the bottom of the screen 2026-08-06 21:46:47 +02:00
renovate[bot]
75920a6473 fix(deps): update mantine monorepo to v9.5.1 2026-08-05 08:38:05 +00:00
renovate[bot]
f474e76866 chore(deps): update debian:13.6 docker digest to 34cd9e9 2026-08-05 04:50:23 +00:00
renovate[bot]
d0ae27c4ce fix(deps): update quarkus.version to v3.38.1 2026-08-04 17:07:29 +00:00
renovate[bot]
ab191dded6 chore(deps): update ibm-semeru-runtimes:open-jdk-25.0.4.0-jre docker digest to 02888a2 2026-08-04 10:32:11 +00:00
renovate[bot]
950b571a9f chore(deps): update ibm-semeru-runtimes docker tag to open-jdk-25.0.4.0-jre 2026-08-04 04:59:45 +00:00
Jérémie Panzer
7a153ca69a Merge pull request #2258 from Athou/renovate/node-24.x
chore(deps): update node.js to v24.19.0
2026-08-03 19:08:30 +02:00
renovate[bot]
effd8946de chore(deps): update node.js to v24.19.0 2026-08-03 16:42:20 +00:00
Athou
ae3ad3745c move global System.setProperty calls in CommaFeedApplication 2026-08-03 09:15:23 +02:00
75 changed files with 3124 additions and 619 deletions

View File

@@ -29,7 +29,7 @@ jobs:
# Setup # Setup
- name: Set up GraalVM - name: Set up GraalVM
uses: graalvm/setup-graalvm@0def53c0fd8534bc13416c9469f5be45265824fd # v1.6.3 uses: graalvm/setup-graalvm@5298d94fb55a4f185c602eeac5de1b553882abe2 # v1.6.4
with: with:
java-version: ${{ env.JAVA_VERSION }} java-version: ${{ env.JAVA_VERSION }}
distribution: "graalvm" distribution: "graalvm"
@@ -113,7 +113,7 @@ jobs:
uses: docker/setup-qemu-action@96fe6ef7f33517b61c61be40b68a1882f3264fb8 # v4.2.0 uses: docker/setup-qemu-action@96fe6ef7f33517b61c61be40b68a1882f3264fb8 # v4.2.0
- name: Set up Docker Buildx - name: Set up Docker Buildx
uses: docker/setup-buildx-action@bb05f3f5519dd87d3ba754cc423b652a5edd6d2c # v4.2.0 uses: docker/setup-buildx-action@37fe631027851001ddb9b187196cc803df7f5f0e # v4.3.0
- name: Install required packages - name: Install required packages
run: sudo apt-get install -y rename unzip run: sudo apt-get install -y rename unzip

View File

@@ -1,9 +1,30 @@
# Changelog # Changelog
## [7.3.2]
- Prevent users from starring/tagging entries that are not theirs and subsequently reading them (GHSA-prfv-88mm-5gpg)
## [7.3.1]
- Prevent Host header injection attacks on the password recovery endpoint. A new setting
`commafeed.password-recovery-public-base-url` has been added to specify the base URL to use in password recovery
(GHSA-hp8h-jqfm-v7x5)
emails. This setting is only required when the password recovery feature is enabled, which is not the default.
## [7.3.0]
- Add support for the Google Reader API for native mobile apps that don't support the Fever API
- The profile menu now works again on mobile when action buttons are at the bottom of the screen
- More SSRF protection regarding IPv6 (GHSA-q5qw-345w-55xg)
- Changed the default value of `blockLocalAddresses` to make CommaFeed more secure out-of-the-box. See the "Access to
local address blocked" section of the README if you subscribe to feeds that are only available on your local network.
## [7.2.1] ## [7.2.1]
- correctly handle favicons hosted at the root of the domain for feeds with non-default ports - correctly handle favicons hosted at the root of the domain for feeds with non-default ports
- prevent XXS injections in third party apps. The CommaFeed React client was already stripping malicious "javascript:" URLs, they are now filtered during feed parsing and no longer stored in the database/served through the REST API. - prevent XXS injections in third party apps. The CommaFeed React client was already stripping malicious "javascript:"
URLs, they are now filtered during feed parsing and no longer stored in the database/served through the REST API
(GHSA-44pq-9929-f8mq)
## [7.2.0] ## [7.2.0]
@@ -13,26 +34,36 @@
- Small performance improvements to the feed refresh engine - Small performance improvements to the feed refresh engine
- Removed the Pocket sharing feature because the Pocket service has been discontinued - Removed the Pocket sharing feature because the Pocket service has been discontinued
- Prevent the image proxy feature to access any URL to avoid SSRF attacks - Prevent the image proxy feature to access any URL to avoid SSRF attacks
- Strengthened the SSRF protection by also blocking ULA and CGNAT ranges - Strengthened the SSRF protection by also blocking ULA and CGNAT ranges (GHSA-hgrr-mjfp-gmr6)
- Prevent DDOS attacks by filtering OPML files during imports - Prevent DDOS attacks by filtering OPML files during imports (GHSA-83jp-rww3-57rr)
## [7.1.0] ## [7.1.0]
- Display the unread count in the header on mobile, since the tree is hidden by default and the unread count is not visible otherwise (#2055) - Display the unread count in the header on mobile, since the tree is hidden by default and the unread count is not
visible otherwise (#2055)
- The feed refresh engine is now a little bit more performant (#2089) - The feed refresh engine is now a little bit more performant (#2089)
- On shutdown, give the feed refresh engine some time to finish refreshing feeds before killing it. This is controlled by the new `commafeed.shutdown-timeout` setting, which defaults to 2 seconds - On shutdown, give the feed refresh engine some time to finish refreshing feeds before killing it. This is controlled
by the new `commafeed.shutdown-timeout` setting, which defaults to 2 seconds
## [7.0.0] ## [7.0.0]
- Replaced the JEXL filter expression for marking feed entries as read automatically with a user-friendly visual query builder. Expressions are now evaluated with Common Expression Language, which is safer than JEXL and sanboxed by default. - Replaced the JEXL filter expression for marking feed entries as read automatically with a user-friendly visual query
- Added a per-feed setting for sending push notifications to ntfy, Gotify or Pushover when new feed entries are discovered (#1610) builder. Expressions are now evaluated with Common Expression Language, which is safer than JEXL and sanboxed by
default.
- Added a per-feed setting for sending push notifications to ntfy, Gotify or Pushover when new feed entries are
discovered (#1610)
- Added a per-feed setting for marking entries as read after a number of days (#2041) - Added a per-feed setting for marking entries as read after a number of days (#2041)
- The default value of `commafeed.http-client.block-local-addresses` is now false, allowing users to subscribe to feeds only available on their local network. This may be a security risk (SSRF) if your instance is accessible by untrusted users, so you may want to set it to true if you host a public instance of CommaFeed with user registeration enabled. - The default value of `commafeed.http-client.block-local-addresses` is now false, allowing users to subscribe to feeds
- When `commafeed.http-client.block-local-addresses` is enabled, SSRF is now also mitigated by blocking public websites redirecting to local ones. only available on their local network. This may be a security risk (SSRF) if your instance is accessible by untrusted
users, so you may want to set it to true if you host a public instance of CommaFeed with user registeration enabled.
- When `commafeed.http-client.block-local-addresses` is enabled, SSRF is now also mitigated by blocking public websites
redirecting to local ones.
## [6.2.0] ## [6.2.0]
- Starred entries are no longer deleted after a certain amount of time, they are now kept indefinitely. The new `commafeed.database.cleanup.keep-starred-entries` setting can be disabled to restore the previous behavior if you want to keep deleting starred entries during normal entries cleanup (#1581) - Starred entries are no longer deleted after a certain amount of time, they are now kept indefinitely. The new
`commafeed.database.cleanup.keep-starred-entries` setting can be disabled to restore the previous behavior if you want
to keep deleting starred entries during normal entries cleanup (#1581)
## [6.1.1] ## [6.1.1]
@@ -40,16 +71,21 @@
## [6.1.0] ## [6.1.0]
- When clicking on the password reset link, a random password is no longer generated automatically. The user is now redirected to a page where they can set their own password (#2023) - When clicking on the password reset link, a random password is no longer generated automatically. The user is now
redirected to a page where they can set their own password (#2023)
- Use browser preferred language instead of English when using CommaFeed for the first time (#2018) - Use browser preferred language instead of English when using CommaFeed for the first time (#2018)
- The profile menu is now closed when scrolling the page (#2019) - The profile menu is now closed when scrolling the page (#2019)
- The "disable pull to refresh" feature is now disabled by default (#2030) - The "disable pull to refresh" feature is now disabled by default (#2030)
## [6.0.0] ## [6.0.0]
- When booting CommaFeed for the first time, the default "admin" account is no longer created automatically. A setup wizard will guide you through the creation of an admin account - When booting CommaFeed for the first time, the default "admin" account is no longer created automatically. A setup
- Default password complexity requirements have been lowered for local network deployments, where strict password rules are often unnecessary. The `commafeed.users.strict-password-policy` setting has been replaced by `commafeed.users.minimum-password-length` with a default value of `4` (#1916) wizard will guide you through the creation of an admin account
- Email addresses are no longer required when creating users and when they update their profile. The `commafeed.users.email-address-required` setting has been added to restore the previous behavior (#1914) - Default password complexity requirements have been lowered for local network deployments, where strict password rules
are often unnecessary. The `commafeed.users.strict-password-policy` setting has been replaced by
`commafeed.users.minimum-password-length` with a default value of `4` (#1916)
- Email addresses are no longer required when creating users and when they update their profile. The
`commafeed.users.email-address-required` setting has been added to restore the previous behavior (#1914)
- Java 25+ is now required to build and run CommaFeed - Java 25+ is now required to build and run CommaFeed
## [5.12.1] ## [5.12.1]
@@ -79,15 +115,18 @@
## [5.10.0] ## [5.10.0]
- Add an indicator next to each feed's unread count in the tree to show when new entries are discovered while the app is open (#1762) - Add an indicator next to each feed's unread count in the tree to show when new entries are discovered while the app is
open (#1762)
- Feeds with uppercase HTTP:// or HTTPS:// URLs are now correctly handled again - Feeds with uppercase HTTP:// or HTTPS:// URLs are now correctly handled again
- The aarch64 native executable now also works on the Raspberry Pi 5 (#1795) - The aarch64 native executable now also works on the Raspberry Pi 5 (#1795)
- Improve general performance of the UI by reducing the number of re-renders, especially when a lot of entries are displayed (#1087) - Improve general performance of the UI by reducing the number of re-renders, especially when a lot of entries are
displayed (#1087)
## [5.9.0] ## [5.9.0]
- A lot of CSS classes have been added to the elements of the application to ease custom CSS rules (#1757) - A lot of CSS classes have been added to the elements of the application to ease custom CSS rules (#1757)
- Added a link in the README to the [documentation](https://athou.github.io/commafeed/documentation/custom-css/) of the new CSS classes - Added a link in the README to the [documentation](https://athou.github.io/commafeed/documentation/custom-css/) of the
new CSS classes
- Static resources are now cached for much longer (#1782) - Static resources are now cached for much longer (#1782)
## [5.8.0] ## [5.8.0]
@@ -96,7 +135,8 @@
- A font size slider is now available to change the size of the text of feed entries (#1462) - A font size slider is now available to change the size of the text of feed entries (#1462)
- The "mark all as read" confirmation setting now also applies to the "shift+a" keyboard shortcut (#1744) - The "mark all as read" confirmation setting now also applies to the "shift+a" keyboard shortcut (#1744)
- CommaFeed wil try to match the language of the browser before defaulting to english (#1767) - CommaFeed wil try to match the language of the browser before defaulting to english (#1767)
- The default value for the number of entries to keep above the selected entry when scrolling is now 1 instead of 0 to match what other feed readers do - The default value for the number of entries to keep above the selected entry when scrolling is now 1 instead of 0 to
match what other feed readers do
## [5.7.0] ## [5.7.0]
@@ -115,14 +155,22 @@
## [5.6.0] ## [5.6.0]
- To better respect the bandwidth of feed owners, the default value of `commafeed.feed-refresh.interval-empirical` is now true. This means feeds no longer refresh exactly every 5 minutes (the default value of `commafeed.feed-refresh.interval`) but between 5 minutes and 4 hours (the default value of the new `commafeed.feed-refresh.max-interval` setting). The interval is calculated based on feed activity, so highly active feeds refresh more often (#1677) - To better respect the bandwidth of feed owners, the default value of `commafeed.feed-refresh.interval-empirical` is
now true. This means feeds no longer refresh exactly every 5 minutes (the default value of
`commafeed.feed-refresh.interval`) but between 5 minutes and 4 hours (the default value of the new
`commafeed.feed-refresh.max-interval` setting). The interval is calculated based on feed activity, so highly active
feeds refresh more often (#1677)
- Many previously hardcoded values used in feed refresh interval calculation are now exposed as settings (#1677) - Many previously hardcoded values used in feed refresh interval calculation are now exposed as settings (#1677)
- Access to local addresses is now blocked to mitigate server-side request forgery (SSRF) attacks, which could potentially expose internal resources. You might want to disable the new `commafeed.http-client.block-local-addresses` setting if you subscribe to feeds only available on your local network and you trust all your users - Access to local addresses is now blocked to mitigate server-side request forgery (SSRF) attacks, which could
- If a feed responds with a "429 - Too many requests" response, a backoff mechanism is triggered when the response does not contain a "Retry-After" header potentially expose internal resources. You might want to disable the new `commafeed.http-client.block-local-addresses`
setting if you subscribe to feeds only available on your local network and you trust all your users
- If a feed responds with a "429 - Too many requests" response, a backoff mechanism is triggered when the response does
not contain a "Retry-After" header
## [5.5.0] ## [5.5.0]
- CommaFeed now honors the Retry-After response header and will not try to refresh a feed sooner than the value of this header - CommaFeed now honors the Retry-After response header and will not try to refresh a feed sooner than the value of this
header
- Audio enclosures (e.g. podcasts) now fill available entry width - Audio enclosures (e.g. podcasts) now fill available entry width
- Fix an issue with some labels not correctly internationalized - Fix an issue with some labels not correctly internationalized
@@ -139,8 +187,10 @@
## [5.3.5] ## [5.3.5]
- Fixed an issue with the aspect ratio of images of some feeds (#1595) - Fixed an issue with the aspect ratio of images of some feeds (#1595)
- CommaFeed now honors the Cache-Control response header and will not try to refresh a feed sooner than its max-age property (#1615) - CommaFeed now honors the Cache-Control response header and will not try to refresh a feed sooner than its max-age
- Added support for compilation with JDK 23+. If you're building CommaFeed from sources with a JDK 17 or 21, you may need to update it to the most recent patch version to support `-proc:full` (#1618) property (#1615)
- Added support for compilation with JDK 23+. If you're building CommaFeed from sources with a JDK 17 or 21, you may
need to update it to the most recent patch version to support `-proc:full` (#1618)
## [5.3.4] ## [5.3.4]
@@ -167,19 +217,22 @@
- Added an option to keep a number of entries above the selected entry when scrolling - Added an option to keep a number of entries above the selected entry when scrolling
- Added a cache to the HTTP client to reduce the number of requests made to feeds when subscribing (#1431) - Added a cache to the HTTP client to reduce the number of requests made to feeds when subscribing (#1431)
- Feeds are no longer refreshed between the moment its last user unsubscribes and the moment the feed is cleaned up (every hour) - Feeds are no longer refreshed between the moment its last user unsubscribes and the moment the feed is cleaned up
(every hour)
- Fixed an issue that could cause entries to not correctly load when using keyboard navigation (#1557) - Fixed an issue that could cause entries to not correctly load when using keyboard navigation (#1557)
## [5.1.1] ## [5.1.1]
- Fixed database migration issue when upgrading from 5.0.0 to 5.1.0 on MariaDB (#1544) - Fixed database migration issue when upgrading from 5.0.0 to 5.1.0 on MariaDB (#1544)
- When feeds without unread entries are hidden from the tree, the feed is displayed in the tree until another one is selected (#1543) - When feeds without unread entries are hidden from the tree, the feed is displayed in the tree until another one is
selected (#1543)
## [5.1.0] ## [5.1.0]
- Added a setting for showing/hiding unread count in the browser's tab title/favicon (#1518) - Added a setting for showing/hiding unread count in the browser's tab title/favicon (#1518)
- Fixed an issue that could prevent the app from starting on some systems (#1532) - Fixed an issue that could prevent the app from starting on some systems (#1532)
- Added a cache busting filter for the webapp index.html and openapi documentation to make sure they are always up to date - Added a cache busting filter for the webapp index.html and openapi documentation to make sure they are always up to
date
- Reduced database cleanup log verbosity - Reduced database cleanup log verbosity
## [5.0.2] ## [5.0.2]
@@ -194,9 +247,9 @@
## [5.0.0] ## [5.0.0]
CommaFeed is now powered by Quarkus instead of Dropwizard. Read the rationale behind this change in CommaFeed is now powered by Quarkus instead of Dropwizard. Read the rationale behind this change in
the [announcement](https://github.com/Athou/commafeed/discussions/1517). the [announcement](https://github.com/Athou/commafeed/discussions/1517). The gist of it is that CommaFeed can now be
The gist of it is that CommaFeed can now be compiled to a native binary, resulting in blazing fast startup times (around compiled to a native binary, resulting in blazing fast startup times (around 0.3s) and very low memory footprint (<
0.3s) and very low memory footprint (< 50M). 50M).
- CommaFeed now has a different package for each supported database. - CommaFeed now has a different package for each supported database.
- If you are deploying CommaFeed with a precompiled package, please - If you are deploying CommaFeed with a precompiled package, please
@@ -252,8 +305,8 @@ The gist of it is that CommaFeed can now be compiled to a native binary, resulti
- add a button in the entry headers to open links in a new tab (#1333) - add a button in the entry headers to open links in a new tab (#1333)
- add two options in the settings to toggle those buttons - add two options in the settings to toggle those buttons
- accept .opml file extension when importing and export with the .opml extension - accept .opml file extension when importing and export with the .opml extension
- the "mark as read" option is no longer shown in the context menu for entries that are too old to be marked as read ( - the "mark as read" option is no longer shown in the context menu for entries that are too old to be marked as read
older than `keepStatusDays`) (#1303) (older than `keepStatusDays`) (#1303)
## [4.3.3] ## [4.3.3]
@@ -297,9 +350,8 @@ The gist of it is that CommaFeed can now be compiled to a native binary, resulti
- it is now possible to open the sidebar on mobile by swiping to the right (#1098) - it is now possible to open the sidebar on mobile by swiping to the right (#1098)
- swiping to mark entries as read/unread changed from swiping right to left because swiping right now opens the sidebar - swiping to mark entries as read/unread changed from swiping right to left because swiping right now opens the sidebar
- the full hierarchy of categories are now displayed in the category dropdown (#1045) - the full hierarchy of categories are now displayed in the category dropdown (#1045)
- added a setting `maxEntriesAgeDays` to delete old entries based on their age during database cleanup. - added a setting `maxEntriesAgeDays` to delete old entries based on their age during database cleanup. The setting is
The setting is disabled by default for existing installations, except for the docker image where it is enabled and set disabled by default for existing installations, except for the docker image where it is enabled and set to 365 days
to 365 days
- if user registrations are disabled on your instance which is the default behavior, users are redirected on the login - if user registrations are disabled on your instance which is the default behavior, users are redirected on the login
page instead of the welcome page when not logged in (#1185) page instead of the welcome page when not logged in (#1185)
- the sidebar resizer is no longer shown in the middle of the screen on mobile - the sidebar resizer is no longer shown in the middle of the screen on mobile
@@ -325,8 +377,8 @@ The gist of it is that CommaFeed can now be compiled to a native binary, resulti
request, reducing CPU usage request, reducing CPU usage
- updated UI library Mantine to 7.0, improving performance - updated UI library Mantine to 7.0, improving performance
- the h2 embedded database is now compacted on shutdown to reclaim unused space - the h2 embedded database is now compacted on shutdown to reclaim unused space
- the admin connector on port 8084 is now disabled in config.yml.example. Disabling it in your config.yml is - the admin connector on port 8084 is now disabled in config.yml.example. Disabling it in your config.yml is recommended
recommended (see https://github.com/Athou/commafeed/commit/929df60f09cce56020b0962ab111cd8349b271b0) (see https://github.com/Athou/commafeed/commit/929df60f09cce56020b0962ab111cd8349b271b0)
- migrated documentation from swagger 2 to openapi 3 - migrated documentation from swagger 2 to openapi 3
- added a GET method to the fever api to indicate that the endpoint is working correctly when accessed from a browser - added a GET method to the fever api to indicate that the endpoint is working correctly when accessed from a browser
- the websocket connection can now be disabled, the websocket ping interval and the tree reload interval can now be - the websocket connection can now be disabled, the websocket ping interval and the tree reload interval can now be
@@ -568,7 +620,6 @@ The gist of it is that CommaFeed can now be compiled to a native binary, resulti
## [2.0.0] ## [2.0.0]
- The backend has been completely rewritten using Dropwizard instead of TomEE, resulting in a lot less memory - The backend has been completely rewritten using Dropwizard instead of TomEE, resulting in a lot less memory
consumption and better overall performances. consumption and better overall performances. See the README on how to build CommaFeed from now on.
See the README on how to build CommaFeed from now on.
- CommaFeed should no longer fetch the same feed multiple times in a row - CommaFeed should no longer fetch the same feed multiple times in a row
- Users can use their username or email to log in - Users can use their username or email to log in

View File

@@ -15,7 +15,7 @@ Google Reader inspired self-hosted RSS reader, based on Quarkus and React/TypeSc
- Supports thousands of users and millions of feeds - Supports thousands of users and millions of feeds
- OPML import/export - OPML import/export
- REST API - REST API
- Fever-compatible API for native mobile apps - Fever and Google Reader API for native mobile apps
- Can automatically mark articles as read based on user-defined rules - Can automatically mark articles as read based on user-defined rules
- Push notifications when new articles are published - Push notifications when new articles are published
- Highly customizable with [custom CSS](https://athou.github.io/commafeed/documentation/custom-css) and JavaScript - Highly customizable with [custom CSS](https://athou.github.io/commafeed/documentation/custom-css) and JavaScript
@@ -159,6 +159,14 @@ named [Semeru](https://developer.ibm.com/languages/java/semeru-runtimes/download
This is the JVM used in This is the JVM used in
the [Docker image](https://github.com/Athou/commafeed/blob/master/commafeed-server/src/main/docker/Dockerfile.jvm). the [Docker image](https://github.com/Athou/commafeed/blob/master/commafeed-server/src/main/docker/Dockerfile.jvm).
## FAQ
### Getting "Access to local address blocked" when adding a feed
CommaFeed blocks access to local resources by default to prevent [SSRF](https://en.wikipedia.org/wiki/Server-side_request_forgery) attacks.
If you want to subscribe to feeds that are only available on your local network, you can disable this security measure by setting the `commafeed.http-client.block-local-addresses` variable to `false`.
Do this only if you trust all users of your CommaFeed instance not to access private resources.
## Translation ## Translation
Files for internationalization are Files for internationalization are

View File

@@ -1,5 +1,5 @@
{ {
"$schema": "https://biomejs.dev/schemas/2.5.6/schema.json", "$schema": "https://biomejs.dev/schemas/2.5.9/schema.json",
"formatter": { "formatter": {
"indentStyle": "space", "indentStyle": "space",
"indentWidth": 4, "indentWidth": 4,

File diff suppressed because it is too large Load Diff

View File

@@ -19,19 +19,19 @@
"@fontsource/open-sans": "5.3.0", "@fontsource/open-sans": "5.3.0",
"@lingui/core": "6.6.0", "@lingui/core": "6.6.0",
"@lingui/react": "6.6.0", "@lingui/react": "6.6.0",
"@mantine/core": "9.5.0", "@mantine/core": "9.5.1",
"@mantine/dates": "9.5.0", "@mantine/dates": "9.5.1",
"@mantine/form": "9.5.0", "@mantine/form": "9.5.1",
"@mantine/hooks": "9.5.0", "@mantine/hooks": "9.5.1",
"@mantine/modals": "9.5.0", "@mantine/modals": "9.5.1",
"@mantine/notifications": "9.5.0", "@mantine/notifications": "9.5.1",
"@mantine/spotlight": "9.5.0", "@mantine/spotlight": "9.5.1",
"@monaco-editor/react": "4.7.0", "@monaco-editor/react": "4.7.0",
"@react-querybuilder/mantine": "8.21.2", "@react-querybuilder/mantine": "8.23.0",
"@reduxjs/toolkit": "2.12.0", "@reduxjs/toolkit": "2.12.0",
"@rolldown/plugin-babel": "0.2.3", "@rolldown/plugin-babel": "0.2.3",
"axios": "1.19.0", "axios": "1.19.0",
"dayjs": "1.11.21", "dayjs": "1.11.23",
"escape-string-regexp": "5.0.0", "escape-string-regexp": "5.0.0",
"interweave": "13.1.1", "interweave": "13.1.1",
"monaco-editor": "0.56.0", "monaco-editor": "0.56.0",
@@ -43,7 +43,7 @@
"react-draggable": "4.7.1", "react-draggable": "4.7.1",
"react-icons": "5.7.0", "react-icons": "5.7.0",
"react-infinite-scroller": "1.2.6", "react-infinite-scroller": "1.2.6",
"react-querybuilder": "8.21.2", "react-querybuilder": "8.23.0",
"react-redux": "9.3.0", "react-redux": "9.3.0",
"react-router-dom": "7.18.2", "react-router-dom": "7.18.2",
"react-swipeable": "7.0.2", "react-swipeable": "7.0.2",
@@ -54,13 +54,14 @@
"websocket-heartbeat-js": "1.1.3" "websocket-heartbeat-js": "1.1.3"
}, },
"devDependencies": { "devDependencies": {
"@biomejs/biome": "2.5.6", "@babel/types": "^8.0.4",
"@biomejs/biome": "2.5.9",
"@lingui/babel-plugin-lingui-macro": "6.6.0", "@lingui/babel-plugin-lingui-macro": "6.6.0",
"@lingui/cli": "6.6.0", "@lingui/cli": "6.6.0",
"@lingui/vite-plugin": "6.6.0", "@lingui/vite-plugin": "6.6.0",
"@testing-library/jest-dom": "7.0.0", "@testing-library/jest-dom": "7.0.1",
"@testing-library/react": "16.3.2", "@testing-library/react": "16.3.2",
"@testing-library/user-event": "14.6.1", "@testing-library/user-event": "14.6.5",
"@types/mousetrap": "1.6.15", "@types/mousetrap": "1.6.15",
"@types/react": "19.2.18", "@types/react": "19.2.18",
"@types/react-dom": "19.2.4", "@types/react-dom": "19.2.4",
@@ -71,9 +72,9 @@
"babel-plugin-react-compiler": "1.0.0", "babel-plugin-react-compiler": "1.0.0",
"jsdom": "30.0.1", "jsdom": "30.0.1",
"typescript": "7.0.2", "typescript": "7.0.2",
"vite": "8.2.0", "vite": "8.2.1",
"vite-plugin-checker": "0.14.5", "vite-plugin-checker": "0.14.5",
"vitest": "4.1.10", "vitest": "4.1.11",
"yaml": "2.9.0" "yaml": "2.9.0"
}, },
"overrides": { "overrides": {

View File

@@ -6,14 +6,14 @@
<parent> <parent>
<groupId>com.commafeed</groupId> <groupId>com.commafeed</groupId>
<artifactId>commafeed</artifactId> <artifactId>commafeed</artifactId>
<version>7.2.1</version> <version>7.3.2</version>
</parent> </parent>
<artifactId>commafeed-client</artifactId> <artifactId>commafeed-client</artifactId>
<name>CommaFeed Client</name> <name>CommaFeed Client</name>
<properties> <properties>
<!-- renovate: datasource=node-version depName=node --> <!-- renovate: datasource=node-version depName=node -->
<node.version>v24.18.1</node.version> <node.version>v24.19.0</node.version>
<!-- renovate: datasource=npm depName=npm --> <!-- renovate: datasource=npm depName=npm -->
<npm.version>12.0.2</npm.version> <npm.version>12.0.2</npm.version>
</properties> </properties>

View File

@@ -0,0 +1,36 @@
import { i18n } from "@lingui/core"
import type { AxiosError } from "axios"
import { afterEach, describe, expect, it, vi } from "vitest"
import { errorToStrings } from "./client"
const axiosError = (status: number, data: unknown) =>
({
isAxiosError: true,
response: { status, data },
}) as AxiosError
describe("errorToStrings", () => {
afterEach(() => vi.restoreAllMocks())
it("translates known application error types", () => {
vi.spyOn(i18n, "_").mockReturnValue("Translated authentication error")
const error = axiosError(401, {
type: "WRONG_USERNAME_OR_PASSWORD",
message: "wrong username or password",
})
expect(errorToStrings(error)).toEqual(["Translated authentication error"])
})
it("preserves backend messages for unexpected errors", () => {
const error = axiosError(500, { message: "unexpected error" })
expect(errorToStrings(error)).toEqual(["unexpected error"])
})
it("preserves backend messages for unknown application error types", () => {
const error = axiosError(400, { type: "UNKNOWN_ERROR", message: "unknown error" })
expect(errorToStrings(error)).toEqual(["unknown error"])
})
})

View File

@@ -1,3 +1,5 @@
import { i18n, type MessageDescriptor } from "@lingui/core"
import { msg } from "@lingui/core/macro"
import axios, { type AxiosError } from "axios" import axios, { type AxiosError } from "axios"
import type { import type {
AddCategoryRequest, AddCategoryRequest,
@@ -6,6 +8,8 @@ import type {
Category, Category,
CategoryModificationRequest, CategoryModificationRequest,
CollapseRequest, CollapseRequest,
CommaFeedApplicationError,
CommaFeedExceptionType,
Entries, Entries,
FeedInfo, FeedInfo,
FeedInfoRequest, FeedInfoRequest,
@@ -31,6 +35,10 @@ import type {
UserModel, UserModel,
} from "./types" } from "./types"
const applicationErrorMessages = {
WRONG_USERNAME_OR_PASSWORD: msg`Wrong username or password`,
} satisfies Record<CommaFeedExceptionType, MessageDescriptor>
const axiosInstance = axios.create({ baseURL: "./rest", withCredentials: true }) const axiosInstance = axios.create({ baseURL: "./rest", withCredentials: true })
axiosInstance.interceptors.response.use( axiosInstance.interceptors.response.use(
response => response, response => response,
@@ -128,14 +136,25 @@ export const errorToStrings = (err: unknown) => {
let strings: string[] = [] let strings: string[] = []
if (axios.isAxiosError(err) && err.response) { if (axios.isAxiosError(err) && err.response) {
if (typeof err.response.data === "string") strings.push(err.response.data) if (isCommaFeedApplicationError(err)) {
if (isMessageError(err)) strings.push(err.response.data.message) strings.push(i18n._(applicationErrorMessages[err.response.data.type]))
if (isMessageArrayError(err)) strings = [...strings, ...err.response.data.errors] } else {
if (typeof err.response.data === "string") strings.push(err.response.data)
if (isMessageError(err)) strings.push(err.response.data.message)
if (isMessageArrayError(err)) strings = [...strings, ...err.response.data.errors]
}
} }
return strings return strings
} }
function isCommaFeedApplicationError(err: AxiosError): err is AxiosError<CommaFeedApplicationError> {
const data = err.response?.data
if (!data || typeof data !== "object" || !("type" in data)) return false
const type = data.type
return typeof type === "string" && Object.hasOwn(applicationErrorMessages, type)
}
function isMessageError(err: AxiosError): err is AxiosError<{ message: string }> { function isMessageError(err: AxiosError): err is AxiosError<{ message: string }> {
return !!err.response && !!err.response.data && typeof err.response.data === "object" && "message" in err.response.data return !!err.response && !!err.response.data && typeof err.response.data === "object" && "message" in err.response.data
} }

View File

@@ -166,7 +166,6 @@ export const starEntry = createAppAsyncThunk(
(arg: { entry: Entry; starred: boolean }) => { (arg: { entry: Entry; starred: boolean }) => {
client.entry.star({ client.entry.star({
id: arg.entry.id, id: arg.entry.id,
feedId: +arg.entry.feedId,
starred: arg.starred, starred: arg.starred,
}) })
}, },

View File

@@ -297,7 +297,6 @@ export interface LocalSettings {
export interface StarRequest { export interface StarRequest {
id: string id: string
feedId: number
starred: boolean starred: boolean
} }
@@ -338,3 +337,10 @@ export interface AuthenticationError {
message: string message: string
allowRegistrations: boolean allowRegistrations: boolean
} }
export type CommaFeedExceptionType = "WRONG_USERNAME_OR_PASSWORD"
export interface CommaFeedApplicationError {
type: CommaFeedExceptionType
message: string
}

View File

@@ -124,7 +124,7 @@ export function ProfileMenu(props: Readonly<ProfileMenuProps>) {
} }
return ( return (
<Menu position="bottom-end" closeOnItemClick={false} opened={opened} onChange={setOpened}> <Menu position="bottom-end" closeOnItemClick={false} opened={opened} onChange={setOpened} preventPositionChangeWhenVisible={false}>
<Menu.Target>{props.control}</Menu.Target> <Menu.Target>{props.control}</Menu.Target>
<Menu.Dropdown> <Menu.Dropdown>
{profile && <Menu.Label>{profile.name}</Menu.Label>} {profile && <Menu.Label>{profile.name}</Menu.Label>}

View File

@@ -91,8 +91,8 @@ export function ProfileSettings() {
label={<Trans>API key</Trans>} label={<Trans>API key</Trans>}
description={ description={
<Trans> <Trans>
This is your API key. It can be used for some read-only API operations and grants access to the Fever API. 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 and the Google Reader API. Use the form at the bottom of the page to generate a new API key
</Trans> </Trans>
} }
readOnly readOnly
@@ -130,6 +130,22 @@ export function ProfileSettings() {
</Box> </Box>
</Input.Wrapper> </Input.Wrapper>
<Input.Wrapper
label={<Trans>Google Reader API</Trans>}
description={
<Trans>
CommaFeed is compatible with the Google Reader API. Use the following URL in your Google Reader-compatible
mobile client. Login with your username and your <u>API key</u>.
</Trans>
}
>
<Box>
<Anchor href="rest/googlereader" target="_blank">
<Trans>Google Reader API URL</Trans>
</Anchor>
</Box>
</Input.Wrapper>
<Divider /> <Divider />
<PasswordInput <PasswordInput

View File

@@ -210,6 +210,10 @@ msgstr "إصدار إضافة متصفح CommaFeed هو {browserExtensionVersion
msgid "CommaFeed is compatible with the Fever API. Use the following URL in your Fever-compatible mobile client. Login with your username and your <0>API key</0>." msgid "CommaFeed is compatible with the Fever API. Use the following URL in your Fever-compatible mobile client. Login with your username and your <0>API key</0>."
msgstr "CommaFeed متوافق مع Fever API. استخدم عنوان URL التالي في تطبيق الهاتف المتوافق مع Fever. قم بتسجيل الدخول باستخدام اسم المستخدم و <0>مفتاح API</0> الخاص بك." msgstr "CommaFeed متوافق مع Fever API. استخدم عنوان URL التالي في تطبيق الهاتف المتوافق مع Fever. قم بتسجيل الدخول باستخدام اسم المستخدم و <0>مفتاح API</0> الخاص بك."
#: src/components/settings/ProfileSettings.tsx
msgid "CommaFeed is compatible with the Google Reader API. Use the following URL in your Google Reader-compatible mobile client. Login with your username and your <0>API key</0>."
msgstr "CommaFeed متوافق مع Google Reader API. استخدم عنوان URL التالي في تطبيق الهاتف المتوافق مع Google Reader. قم بتسجيل الدخول باستخدام اسم المستخدم و <0>مفتاح API</0> الخاص بك."
#: src/pages/app/AboutPage.tsx #: src/pages/app/AboutPage.tsx
msgid "CommaFeed next unread item" msgid "CommaFeed next unread item"
msgstr "CommaFeed التالي العنصر غير المقروء" msgstr "CommaFeed التالي العنصر غير المقروء"
@@ -454,6 +458,14 @@ msgstr "انتقل إلى وثائق API."
msgid "Goodies" msgid "Goodies"
msgstr "الأشياء الجيدة" msgstr "الأشياء الجيدة"
#: src/components/settings/ProfileSettings.tsx
msgid "Google Reader API"
msgstr ""
#: src/components/settings/ProfileSettings.tsx
msgid "Google Reader API URL"
msgstr ""
#: src/components/settings/DisplaySettings.tsx #: src/components/settings/DisplaySettings.tsx
msgid "Grape" msgid "Grape"
msgstr "عنبي" msgstr "عنبي"
@@ -1065,8 +1077,8 @@ msgid "This feed has a legacy filter that cannot be edited and is not applied. P
msgstr "تحتوي هذه الخلاصة على فلتر قديم لا يمكن تحريره ولا يتم تطبيقه. يرجى إعادة إنشاء الفلتر باستخدام محرر التعبيرات الجديد. كان تعبير الفلتر القديم هو: <0>{0}</0>" msgstr "تحتوي هذه الخلاصة على فلتر قديم لا يمكن تحريره ولا يتم تطبيقه. يرجى إعادة إنشاء الفلتر باستخدام محرر التعبيرات الجديد. كان تعبير الفلتر القديم هو: <0>{0}</0>"
#: src/components/settings/ProfileSettings.tsx #: 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" msgid "This is your API key. It can be used for some read-only API operations and grants access to the Fever API and the Google Reader API. Use the form at the bottom of the page to generate a new API key"
msgstr "هذا هو مفتاح API الخاص بك. يمكن استخدامه لبعض عمليات API للقراءة فقط ويمنح الوصول إلى Fever API. استخدم النموذج الموجود في أسفل الصفحة لإنشاء مفتاح API جديد" msgstr "هذا هو مفتاح API الخاص بك. يمكن استخدامه لبعض عمليات API للقراءة فقط ويمنح حق الوصول إلى Fever API و Google Reader API. استخدم النموذج الموجود أسفل الصفحة لإنشاء مفتاح API جديد"
#: src/components/settings/DisplaySettings.tsx #: src/components/settings/DisplaySettings.tsx
msgid "This setting can cause scrolling issues on some browsers (e.g. Safari)" msgid "This setting can cause scrolling issues on some browsers (e.g. Safari)"
@@ -1138,6 +1150,10 @@ msgstr "موقع الكتروني"
msgid "Welcome! This appears to be the first time you're running CommaFeed. Please create an administrator account to get started." msgid "Welcome! This appears to be the first time you're running CommaFeed. Please create an administrator account to get started."
msgstr "مرحباً! يبدو أن هذه هي المرة الأولى التي تقوم فيها بتشغيل CommaFeed. يرجى إنشاء حساب مسؤول للبدء." msgstr "مرحباً! يبدو أن هذه هي المرة الأولى التي تقوم فيها بتشغيل CommaFeed. يرجى إنشاء حساب مسؤول للبدء."
#: src/app/client.ts
msgid "Wrong username or password"
msgstr ""
#: src/components/settings/DisplaySettings.tsx #: src/components/settings/DisplaySettings.tsx
msgid "Yellow" msgid "Yellow"
msgstr "أصفر" msgstr "أصفر"

View File

@@ -210,6 +210,10 @@ msgstr "Versió de l'extensió del navegador CommaFeed {browserExtensionVersion}
msgid "CommaFeed is compatible with the Fever API. Use the following URL in your Fever-compatible mobile client. Login with your username and your <0>API key</0>." msgid "CommaFeed is compatible with the Fever API. Use the following URL in your Fever-compatible mobile client. Login with your username and your <0>API key</0>."
msgstr "CommaFeed és compatible amb l'API Fever. Utilitzeu l'URL següent al vostre client mòbil compatible amb Fever. Inicieu sessió amb el vostre nom d'usuari i la vostra <0>clau API</0>." msgstr "CommaFeed és compatible amb l'API Fever. Utilitzeu l'URL següent al vostre client mòbil compatible amb Fever. Inicieu sessió amb el vostre nom d'usuari i la vostra <0>clau API</0>."
#: src/components/settings/ProfileSettings.tsx
msgid "CommaFeed is compatible with the Google Reader API. Use the following URL in your Google Reader-compatible mobile client. Login with your username and your <0>API key</0>."
msgstr "CommaFeed és compatible amb l'API de Google Reader. Utilitzeu l'URL següent al vostre client mòbil compatible amb Google Reader. Inicieu sessió amb el vostre nom d'usuari i la vostra <0>clau API</0>."
#: src/pages/app/AboutPage.tsx #: src/pages/app/AboutPage.tsx
msgid "CommaFeed next unread item" msgid "CommaFeed next unread item"
msgstr "CommaFeed següent element no llegit" msgstr "CommaFeed següent element no llegit"
@@ -454,6 +458,14 @@ msgstr "Vés a la documentació de l'API."
msgid "Goodies" msgid "Goodies"
msgstr "Extres" msgstr "Extres"
#: src/components/settings/ProfileSettings.tsx
msgid "Google Reader API"
msgstr ""
#: src/components/settings/ProfileSettings.tsx
msgid "Google Reader API URL"
msgstr ""
#: src/components/settings/DisplaySettings.tsx #: src/components/settings/DisplaySettings.tsx
msgid "Grape" msgid "Grape"
msgstr "Raïm" msgstr "Raïm"
@@ -1065,8 +1077,8 @@ msgid "This feed has a legacy filter that cannot be edited and is not applied. P
msgstr "Aquest feed té un filtre heretat que no es pot editar i no s'aplica. Torneu a crear el filtre amb el nou editor d'expressions. L'expressió del filtre heretat era: <0>{0}</0>" msgstr "Aquest feed té un filtre heretat que no es pot editar i no s'aplica. Torneu a crear el filtre amb el nou editor d'expressions. L'expressió del filtre heretat era: <0>{0}</0>"
#: src/components/settings/ProfileSettings.tsx #: 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" msgid "This is your API key. It can be used for some read-only API operations and grants access to the Fever API and the Google Reader 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." msgstr "Aquesta és la vostra clau API. Es pot utilitzar per a algunes operacions de l'API de només lectura i dona accés a l'API Fever i a l'API de Google Reader. Utilitzeu el formulari a la part inferior de la pàgina per generar una nova clau API"
#: src/components/settings/DisplaySettings.tsx #: src/components/settings/DisplaySettings.tsx
msgid "This setting can cause scrolling issues on some browsers (e.g. Safari)" msgid "This setting can cause scrolling issues on some browsers (e.g. Safari)"
@@ -1138,6 +1150,10 @@ msgstr "Lloc web"
msgid "Welcome! This appears to be the first time you're running CommaFeed. Please create an administrator account to get started." msgid "Welcome! This appears to be the first time you're running CommaFeed. Please create an administrator account to get started."
msgstr "Benvingut! Sembla que aquesta és la primera vegada que executeu CommaFeed. Creeu un compte d'administrador per començar." msgstr "Benvingut! Sembla que aquesta és la primera vegada que executeu CommaFeed. Creeu un compte d'administrador per començar."
#: src/app/client.ts
msgid "Wrong username or password"
msgstr ""
#: src/components/settings/DisplaySettings.tsx #: src/components/settings/DisplaySettings.tsx
msgid "Yellow" msgid "Yellow"
msgstr "Groc" msgstr "Groc"

View File

@@ -210,6 +210,10 @@ msgstr "Verze rozšíření prohlížeče CommaFeed {browserExtensionVersion}."
msgid "CommaFeed is compatible with the Fever API. Use the following URL in your Fever-compatible mobile client. Login with your username and your <0>API key</0>." msgid "CommaFeed is compatible with the Fever API. Use the following URL in your Fever-compatible mobile client. Login with your username and your <0>API key</0>."
msgstr "CommaFeed je kompatibilní s Fever API. Použijte následující URL ve svém mobilním klientovi kompatibilním s Fever. Přihlaste se pomocí svého uživatelského jména a svého <0>klíče API</0>." msgstr "CommaFeed je kompatibilní s Fever API. Použijte následující URL ve svém mobilním klientovi kompatibilním s Fever. Přihlaste se pomocí svého uživatelského jména a svého <0>klíče API</0>."
#: src/components/settings/ProfileSettings.tsx
msgid "CommaFeed is compatible with the Google Reader API. Use the following URL in your Google Reader-compatible mobile client. Login with your username and your <0>API key</0>."
msgstr "CommaFeed je kompatibilní s Google Reader API. Použijte následující URL ve svém mobilním klientovi kompatibilním s Google Reader. Přihlaste se pomocí svého uživatelského jména a svého <0>klíče API</0>."
#: src/pages/app/AboutPage.tsx #: src/pages/app/AboutPage.tsx
msgid "CommaFeed next unread item" msgid "CommaFeed next unread item"
msgstr "CommaFeed další nepřečtená položka" msgstr "CommaFeed další nepřečtená položka"
@@ -454,6 +458,14 @@ msgstr "Přejděte na dokumentaci API."
msgid "Goodies" msgid "Goodies"
msgstr "Dobroty" msgstr "Dobroty"
#: src/components/settings/ProfileSettings.tsx
msgid "Google Reader API"
msgstr ""
#: src/components/settings/ProfileSettings.tsx
msgid "Google Reader API URL"
msgstr ""
#: src/components/settings/DisplaySettings.tsx #: src/components/settings/DisplaySettings.tsx
msgid "Grape" msgid "Grape"
msgstr "Hroznová" msgstr "Hroznová"
@@ -1065,8 +1077,8 @@ msgid "This feed has a legacy filter that cannot be edited and is not applied. P
msgstr "Tento zdroj má zastaralý filtr, který nelze upravovat a není aplikován. Znovu vytvořte filtr pomocí nového editoru výrazů. Původní výraz filtru byl: <0>{0}</0>" msgstr "Tento zdroj má zastaralý filtr, který nelze upravovat a není aplikován. Znovu vytvořte filtr pomocí nového editoru výrazů. Původní výraz filtru byl: <0>{0}</0>"
#: src/components/settings/ProfileSettings.tsx #: 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" msgid "This is your API key. It can be used for some read-only API operations and grants access to the Fever API and the Google Reader API. Use the form at the bottom of the page to generate a new API key"
msgstr "Toto je váš klíč API. Lze jej použít pro některé operace API pouze pro čtení a poskytuje přístup k Fever API. Pro vygenerování nového klíče API použijte formulář v dolní části stránky." msgstr "Toto je váš klíč API. Lze jej použít pro některé operace API pouze pro čtení a uděluje přístup k Fever API a Google Reader API. Použijte formulář v dolní části stránky k vygenerování nového klíče API"
#: src/components/settings/DisplaySettings.tsx #: src/components/settings/DisplaySettings.tsx
msgid "This setting can cause scrolling issues on some browsers (e.g. Safari)" msgid "This setting can cause scrolling issues on some browsers (e.g. Safari)"
@@ -1138,6 +1150,10 @@ msgstr "Webová stránka"
msgid "Welcome! This appears to be the first time you're running CommaFeed. Please create an administrator account to get started." msgid "Welcome! This appears to be the first time you're running CommaFeed. Please create an administrator account to get started."
msgstr "Vítejte! Zdá se, že je to poprvé, co spouštíte CommaFeed. Chcete-li začít, vytvořte si účet správce." msgstr "Vítejte! Zdá se, že je to poprvé, co spouštíte CommaFeed. Chcete-li začít, vytvořte si účet správce."
#: src/app/client.ts
msgid "Wrong username or password"
msgstr ""
#: src/components/settings/DisplaySettings.tsx #: src/components/settings/DisplaySettings.tsx
msgid "Yellow" msgid "Yellow"
msgstr "Žlutá" msgstr "Žlutá"

View File

@@ -210,6 +210,10 @@ msgstr "Fersiwn estyniad porwr CommaFeed {browserExtensionVersion}."
msgid "CommaFeed is compatible with the Fever API. Use the following URL in your Fever-compatible mobile client. Login with your username and your <0>API key</0>." msgid "CommaFeed is compatible with the Fever API. Use the following URL in your Fever-compatible mobile client. Login with your username and your <0>API key</0>."
msgstr "Mae CommaFeed yn gydnaws â'r Fever API. Defnyddiwch y URL canlynol yn eich cleient symudol sy'n gydnaws â Fever. Mewngofnodwch gyda'ch enw defnyddiwr a'ch <0>allwedd API</0>." msgstr "Mae CommaFeed yn gydnaws â'r Fever API. Defnyddiwch y URL canlynol yn eich cleient symudol sy'n gydnaws â Fever. Mewngofnodwch gyda'ch enw defnyddiwr a'ch <0>allwedd API</0>."
#: src/components/settings/ProfileSettings.tsx
msgid "CommaFeed is compatible with the Google Reader API. Use the following URL in your Google Reader-compatible mobile client. Login with your username and your <0>API key</0>."
msgstr "Mae CommaFeed yn gydnaws â Google Reader API. Defnyddiwch y URL canlynol yn eich cleient symudol sy'n gydnaws â Google Reader. Mewngofnodwch gyda'ch enw defnyddiwr a'ch <0>allwedd API</0>."
#: src/pages/app/AboutPage.tsx #: src/pages/app/AboutPage.tsx
msgid "CommaFeed next unread item" msgid "CommaFeed next unread item"
msgstr "CommaFeed eitem nesaf heb ei darllen" msgstr "CommaFeed eitem nesaf heb ei darllen"
@@ -454,6 +458,14 @@ msgstr "Ewch i'r ddogfennaeth API."
msgid "Goodies" msgid "Goodies"
msgstr "nwyddau" msgstr "nwyddau"
#: src/components/settings/ProfileSettings.tsx
msgid "Google Reader API"
msgstr ""
#: src/components/settings/ProfileSettings.tsx
msgid "Google Reader API URL"
msgstr ""
#: src/components/settings/DisplaySettings.tsx #: src/components/settings/DisplaySettings.tsx
msgid "Grape" msgid "Grape"
msgstr "Grawnwin" msgstr "Grawnwin"
@@ -1065,8 +1077,8 @@ msgid "This feed has a legacy filter that cannot be edited and is not applied. P
msgstr "Mae gan y porthiant hwn hen hidlydd na ellir ei olygu ac nid yw'n cael ei gymhwyso. Crëwch yr hidlydd eto gan ddefnyddio'r golygydd mynegiant newydd. Yr hen fynegiant hidlo oedd: <0>{0}</0>" msgstr "Mae gan y porthiant hwn hen hidlydd na ellir ei olygu ac nid yw'n cael ei gymhwyso. Crëwch yr hidlydd eto gan ddefnyddio'r golygydd mynegiant newydd. Yr hen fynegiant hidlo oedd: <0>{0}</0>"
#: src/components/settings/ProfileSettings.tsx #: 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" msgid "This is your API key. It can be used for some read-only API operations and grants access to the Fever API and the Google Reader API. Use the form at the bottom of the page to generate a new API key"
msgstr "Dyma'ch allwedd API. Gellir ei ddefnyddio ar gyfer rhai gweithrediadau API darllen-yn-unig ac mae'n caniatáu mynediad i'r Fever API. Defnyddiwch y ffurflen ar waelod y dudalen i gynhyrchu allwedd API newydd" msgstr "Dyma eich allwedd API. Gellir ei defnyddio ar gyfer rhai gweithrediadau API darllen-yn-unig ac mae'n rhoi mynediad i'r Fever API a'r Google Reader API. Defnyddiwch y ffurflen ar waelod y dudalen i gynhyrchu allwedd API newydd"
#: src/components/settings/DisplaySettings.tsx #: src/components/settings/DisplaySettings.tsx
msgid "This setting can cause scrolling issues on some browsers (e.g. Safari)" msgid "This setting can cause scrolling issues on some browsers (e.g. Safari)"
@@ -1138,6 +1150,10 @@ msgstr "Gwefan"
msgid "Welcome! This appears to be the first time you're running CommaFeed. Please create an administrator account to get started." msgid "Welcome! This appears to be the first time you're running CommaFeed. Please create an administrator account to get started."
msgstr "Croeso! Ymddengys mai dyma'r tro cyntaf i chi redeg CommaFeed. Creuwch gyfrif gweinyddwr i ddechrau." msgstr "Croeso! Ymddengys mai dyma'r tro cyntaf i chi redeg CommaFeed. Creuwch gyfrif gweinyddwr i ddechrau."
#: src/app/client.ts
msgid "Wrong username or password"
msgstr ""
#: src/components/settings/DisplaySettings.tsx #: src/components/settings/DisplaySettings.tsx
msgid "Yellow" msgid "Yellow"
msgstr "Melyn" msgstr "Melyn"

View File

@@ -210,6 +210,10 @@ msgstr "CommaFeed browserudvidelse version {browserExtensionVersion}."
msgid "CommaFeed is compatible with the Fever API. Use the following URL in your Fever-compatible mobile client. Login with your username and your <0>API key</0>." msgid "CommaFeed is compatible with the Fever API. Use the following URL in your Fever-compatible mobile client. Login with your username and your <0>API key</0>."
msgstr "CommaFeed er kompatibel med Fever API. Brug følgende URL i din Fever-kompatible mobilklient. Log ind med dit brugernavn og din <0>API-nøgle</0>." msgstr "CommaFeed er kompatibel med Fever API. Brug følgende URL i din Fever-kompatible mobilklient. Log ind med dit brugernavn og din <0>API-nøgle</0>."
#: src/components/settings/ProfileSettings.tsx
msgid "CommaFeed is compatible with the Google Reader API. Use the following URL in your Google Reader-compatible mobile client. Login with your username and your <0>API key</0>."
msgstr "CommaFeed er kompatibel med Google Reader API. Brug følgende URL i din Google Reader-kompatible mobilklient. Log ind med dit brugernavn og din <0>API-nøgle</0>."
#: src/pages/app/AboutPage.tsx #: src/pages/app/AboutPage.tsx
msgid "CommaFeed next unread item" msgid "CommaFeed next unread item"
msgstr "CommaFeed næste ulæste element" msgstr "CommaFeed næste ulæste element"
@@ -454,6 +458,14 @@ msgstr "Gå til API-dokumentationen."
msgid "Goodies" msgid "Goodies"
msgstr "Godbidder" msgstr "Godbidder"
#: src/components/settings/ProfileSettings.tsx
msgid "Google Reader API"
msgstr ""
#: src/components/settings/ProfileSettings.tsx
msgid "Google Reader API URL"
msgstr ""
#: src/components/settings/DisplaySettings.tsx #: src/components/settings/DisplaySettings.tsx
msgid "Grape" msgid "Grape"
msgstr "Grape" msgstr "Grape"
@@ -1065,8 +1077,8 @@ msgid "This feed has a legacy filter that cannot be edited and is not applied. P
msgstr "Dette feed har et ældre filter, der ikke kan redigeres og ikke anvendes. Genopret venligst filteret ved hjælp af den nye udtrykseditor. Det ældre filterudtryk var: <0>{0}</0>" msgstr "Dette feed har et ældre filter, der ikke kan redigeres og ikke anvendes. Genopret venligst filteret ved hjælp af den nye udtrykseditor. Det ældre filterudtryk var: <0>{0}</0>"
#: src/components/settings/ProfileSettings.tsx #: 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" msgid "This is your API key. It can be used for some read-only API operations and grants access to the Fever API and the Google Reader API. Use the form at the bottom of the page to generate a new API key"
msgstr "Dette er din API-nøgle. Den kan bruges til visse skrivebeskyttede API-operationer og giver adgang til Fever API. Brug formularen nederst på siden til at generere en ny API-nøgle" msgstr "Dette er din API-nøgle. Den kan bruges til nogle skrivebeskyttede API-operationer og giver adgang til Fever API og Google Reader API. Brug formularen nederst på siden for at generere en ny API-nøgle"
#: src/components/settings/DisplaySettings.tsx #: src/components/settings/DisplaySettings.tsx
msgid "This setting can cause scrolling issues on some browsers (e.g. Safari)" msgid "This setting can cause scrolling issues on some browsers (e.g. Safari)"
@@ -1138,6 +1150,10 @@ msgstr "Hjemmeside"
msgid "Welcome! This appears to be the first time you're running CommaFeed. Please create an administrator account to get started." msgid "Welcome! This appears to be the first time you're running CommaFeed. Please create an administrator account to get started."
msgstr "Velkommen! Det ser ud til at være første gang, du kører CommaFeed. Opret venligst en administratorkonto for at komme i gang." msgstr "Velkommen! Det ser ud til at være første gang, du kører CommaFeed. Opret venligst en administratorkonto for at komme i gang."
#: src/app/client.ts
msgid "Wrong username or password"
msgstr ""
#: src/components/settings/DisplaySettings.tsx #: src/components/settings/DisplaySettings.tsx
msgid "Yellow" msgid "Yellow"
msgstr "Gul" msgstr "Gul"

View File

@@ -210,6 +210,10 @@ msgstr "CommaFeed Browser Erweiterung Version {browserExtensionVersion}."
msgid "CommaFeed is compatible with the Fever API. Use the following URL in your Fever-compatible mobile client. Login with your username and your <0>API key</0>." msgid "CommaFeed is compatible with the Fever API. Use the following URL in your Fever-compatible mobile client. Login with your username and your <0>API key</0>."
msgstr "CommaFeed ist mit der Fever-API kompatibel. Verwenden Sie die folgende URL in Ihrem Fever-kompatiblen mobilen Client. Melden Sie sich mit Ihrem Benutzernamen und Ihrem <0>API-Schlüssel</0> an." msgstr "CommaFeed ist mit der Fever-API kompatibel. Verwenden Sie die folgende URL in Ihrem Fever-kompatiblen mobilen Client. Melden Sie sich mit Ihrem Benutzernamen und Ihrem <0>API-Schlüssel</0> an."
#: src/components/settings/ProfileSettings.tsx
msgid "CommaFeed is compatible with the Google Reader API. Use the following URL in your Google Reader-compatible mobile client. Login with your username and your <0>API key</0>."
msgstr "CommaFeed ist mit der Google-Reader-API kompatibel. Verwenden Sie die folgende URL in Ihrem Google-Reader-kompatiblen mobilen Client. Melden Sie sich mit Ihrem Benutzernamen und Ihrem <0>API-Schlüssel</0> an."
#: src/pages/app/AboutPage.tsx #: src/pages/app/AboutPage.tsx
msgid "CommaFeed next unread item" msgid "CommaFeed next unread item"
msgstr "CommaFeed nächstes ungelesenes Element" msgstr "CommaFeed nächstes ungelesenes Element"
@@ -454,6 +458,14 @@ msgstr "Gehen Sie zur API-Dokumentation."
msgid "Goodies" msgid "Goodies"
msgstr "Goodies" msgstr "Goodies"
#: src/components/settings/ProfileSettings.tsx
msgid "Google Reader API"
msgstr ""
#: src/components/settings/ProfileSettings.tsx
msgid "Google Reader API URL"
msgstr ""
#: src/components/settings/DisplaySettings.tsx #: src/components/settings/DisplaySettings.tsx
msgid "Grape" msgid "Grape"
msgstr "Traube" msgstr "Traube"
@@ -1065,8 +1077,8 @@ msgid "This feed has a legacy filter that cannot be edited and is not applied. P
msgstr "Dieser Feed verfügt über einen veralteten Filter, der nicht bearbeitet werden kann und nicht angewendet wird. Bitte erstellen Sie den Filter mit dem neuen Ausdruckseditor neu. Der veraltete Filterausdruck war: <0>{0}</0>" msgstr "Dieser Feed verfügt über einen veralteten Filter, der nicht bearbeitet werden kann und nicht angewendet wird. Bitte erstellen Sie den Filter mit dem neuen Ausdruckseditor neu. Der veraltete Filterausdruck war: <0>{0}</0>"
#: src/components/settings/ProfileSettings.tsx #: 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" msgid "This is your API key. It can be used for some read-only API operations and grants access to the Fever API and the Google Reader 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" msgstr "Dies ist Ihr API-Schlüssel. Er kann für einige schreibgeschützte API-Vorgänge verwendet werden und gewährt Zugriff auf die Fever-API und die Google-Reader-API. Verwenden Sie das Formular am Ende der Seite, um einen neuen API-Schlüssel zu generieren"
#: src/components/settings/DisplaySettings.tsx #: src/components/settings/DisplaySettings.tsx
msgid "This setting can cause scrolling issues on some browsers (e.g. Safari)" msgid "This setting can cause scrolling issues on some browsers (e.g. Safari)"
@@ -1138,6 +1150,10 @@ msgstr "Webseite"
msgid "Welcome! This appears to be the first time you're running CommaFeed. Please create an administrator account to get started." msgid "Welcome! This appears to be the first time you're running CommaFeed. Please create an administrator account to get started."
msgstr "Willkommen! Dies scheint das erste Mal zu sein, dass Sie CommaFeed ausführen. Bitte erstellen Sie ein Administrator-Konto, um zu beginnen." msgstr "Willkommen! Dies scheint das erste Mal zu sein, dass Sie CommaFeed ausführen. Bitte erstellen Sie ein Administrator-Konto, um zu beginnen."
#: src/app/client.ts
msgid "Wrong username or password"
msgstr ""
#: src/components/settings/DisplaySettings.tsx #: src/components/settings/DisplaySettings.tsx
msgid "Yellow" msgid "Yellow"
msgstr "Gelb" msgstr "Gelb"

View File

@@ -210,6 +210,10 @@ msgstr "CommaFeed browser extension version {browserExtensionVersion}."
msgid "CommaFeed is compatible with the Fever API. Use the following URL in your Fever-compatible mobile client. Login with your username and your <0>API key</0>." msgid "CommaFeed is compatible with the Fever API. Use the following URL in your Fever-compatible mobile client. Login with your username and your <0>API key</0>."
msgstr "CommaFeed is compatible with the Fever API. Use the following URL in your Fever-compatible mobile client. Login with your username and your <0>API key</0>." msgstr "CommaFeed is compatible with the Fever API. Use the following URL in your Fever-compatible mobile client. Login with your username and your <0>API key</0>."
#: src/components/settings/ProfileSettings.tsx
msgid "CommaFeed is compatible with the Google Reader API. Use the following URL in your Google Reader-compatible mobile client. Login with your username and your <0>API key</0>."
msgstr "CommaFeed is compatible with the Google Reader API. Use the following URL in your Google Reader-compatible mobile client. Login with your username and your <0>API key</0>."
#: src/pages/app/AboutPage.tsx #: src/pages/app/AboutPage.tsx
msgid "CommaFeed next unread item" msgid "CommaFeed next unread item"
msgstr "CommaFeed next unread item" msgstr "CommaFeed next unread item"
@@ -454,6 +458,14 @@ msgstr "Go to the API documentation."
msgid "Goodies" msgid "Goodies"
msgstr "Goodies" msgstr "Goodies"
#: src/components/settings/ProfileSettings.tsx
msgid "Google Reader API"
msgstr "Google Reader API"
#: src/components/settings/ProfileSettings.tsx
msgid "Google Reader API URL"
msgstr "Google Reader API URL"
#: src/components/settings/DisplaySettings.tsx #: src/components/settings/DisplaySettings.tsx
msgid "Grape" msgid "Grape"
msgstr "Grape" msgstr "Grape"
@@ -1065,8 +1077,8 @@ msgid "This feed has a legacy filter that cannot be edited and is not applied. P
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>" 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 #: 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" msgid "This is your API key. It can be used for some read-only API operations and grants access to the Fever API and the Google Reader 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" msgstr "This is your API key. It can be used for some read-only API operations and grants access to the Fever API and the Google Reader API. Use the form at the bottom of the page to generate a new API key"
#: src/components/settings/DisplaySettings.tsx #: src/components/settings/DisplaySettings.tsx
msgid "This setting can cause scrolling issues on some browsers (e.g. Safari)" msgid "This setting can cause scrolling issues on some browsers (e.g. Safari)"
@@ -1138,6 +1150,10 @@ msgstr "Website"
msgid "Welcome! This appears to be the first time you're running CommaFeed. Please create an administrator account to get started." msgid "Welcome! This appears to be the first time you're running CommaFeed. Please create an administrator account to get started."
msgstr "Welcome! This appears to be the first time you're running CommaFeed. Please create an administrator account to get started." msgstr "Welcome! This appears to be the first time you're running CommaFeed. Please create an administrator account to get started."
#: src/app/client.ts
msgid "Wrong username or password"
msgstr "Wrong username or password"
#: src/components/settings/DisplaySettings.tsx #: src/components/settings/DisplaySettings.tsx
msgid "Yellow" msgid "Yellow"
msgstr "Yellow" msgstr "Yellow"

View File

@@ -211,6 +211,10 @@ msgstr "Versión de la extensión del navegador CommaFeed {browserExtensionVersi
msgid "CommaFeed is compatible with the Fever API. Use the following URL in your Fever-compatible mobile client. Login with your username and your <0>API key</0>." msgid "CommaFeed is compatible with the Fever API. Use the following URL in your Fever-compatible mobile client. Login with your username and your <0>API key</0>."
msgstr "CommaFeed es compatible con Fever API. Utilice la siguiente URL en su cliente móvil compatible con Fever. Inicie sesión con su nombre de usuario y su <0>clave API</0>." msgstr "CommaFeed es compatible con Fever API. Utilice la siguiente URL en su cliente móvil compatible con Fever. Inicie sesión con su nombre de usuario y su <0>clave API</0>."
#: src/components/settings/ProfileSettings.tsx
msgid "CommaFeed is compatible with the Google Reader API. Use the following URL in your Google Reader-compatible mobile client. Login with your username and your <0>API key</0>."
msgstr "CommaFeed es compatible con Google Reader API. Utilice la siguiente URL en su cliente móvil compatible con Google Reader. Inicie sesión con su nombre de usuario y su <0>clave API</0>."
#: src/pages/app/AboutPage.tsx #: src/pages/app/AboutPage.tsx
msgid "CommaFeed next unread item" msgid "CommaFeed next unread item"
msgstr "CommaFeed siguiente elemento no leído" msgstr "CommaFeed siguiente elemento no leído"
@@ -455,6 +459,14 @@ msgstr "Ir a la documentación de la API."
msgid "Goodies" msgid "Goodies"
msgstr "Golosinas" msgstr "Golosinas"
#: src/components/settings/ProfileSettings.tsx
msgid "Google Reader API"
msgstr ""
#: src/components/settings/ProfileSettings.tsx
msgid "Google Reader API URL"
msgstr ""
#: src/components/settings/DisplaySettings.tsx #: src/components/settings/DisplaySettings.tsx
msgid "Grape" msgid "Grape"
msgstr "Uva" msgstr "Uva"
@@ -1066,8 +1078,8 @@ msgid "This feed has a legacy filter that cannot be edited and is not applied. P
msgstr "Este feed tiene un filtro heredado que no se puede editar y no se aplica. Por favor, vuelve a crear el filtro usando el nuevo editor de expresiones. La expresión del filtro heredado era: <0>{0}</0>" msgstr "Este feed tiene un filtro heredado que no se puede editar y no se aplica. Por favor, vuelve a crear el filtro usando el nuevo editor de expresiones. La expresión del filtro heredado era: <0>{0}</0>"
#: src/components/settings/ProfileSettings.tsx #: 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" msgid "This is your API key. It can be used for some read-only API operations and grants access to the Fever API and the Google Reader 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" msgstr "Esta es su clave API. Se puede utilizar para algunas operaciones API de solo lectura y otorga acceso a la API Fever y a la API de Google Reader. Utilice el formulario en la parte inferior de la página para generar una nueva clave API"
#: src/components/settings/DisplaySettings.tsx #: src/components/settings/DisplaySettings.tsx
msgid "This setting can cause scrolling issues on some browsers (e.g. Safari)" msgid "This setting can cause scrolling issues on some browsers (e.g. Safari)"
@@ -1139,6 +1151,10 @@ msgstr "Sitio web"
msgid "Welcome! This appears to be the first time you're running CommaFeed. Please create an administrator account to get started." msgid "Welcome! This appears to be the first time you're running CommaFeed. Please create an administrator account to get started."
msgstr "¡Bienvenido! Parece que esta es la primera vez que ejecutas CommaFeed. Por favor, crea una cuenta de administrador para empezar." msgstr "¡Bienvenido! Parece que esta es la primera vez que ejecutas CommaFeed. Por favor, crea una cuenta de administrador para empezar."
#: src/app/client.ts
msgid "Wrong username or password"
msgstr ""
#: src/components/settings/DisplaySettings.tsx #: src/components/settings/DisplaySettings.tsx
msgid "Yellow" msgid "Yellow"
msgstr "Amarillo" msgstr "Amarillo"

View File

@@ -210,6 +210,10 @@ msgstr "نسخه افزونه مرورگر CommaFeed {browserExtensionVersion}."
msgid "CommaFeed is compatible with the Fever API. Use the following URL in your Fever-compatible mobile client. Login with your username and your <0>API key</0>." msgid "CommaFeed is compatible with the Fever API. Use the following URL in your Fever-compatible mobile client. Login with your username and your <0>API key</0>."
msgstr "CommaFeed با Fever API سازگار است. از آدرس زیر در کلاینت موبایل سازگار با Fever استفاده کنید. با نام کاربری و <0>کلید API</0> خود وارد شوید." msgstr "CommaFeed با Fever API سازگار است. از آدرس زیر در کلاینت موبایل سازگار با Fever استفاده کنید. با نام کاربری و <0>کلید API</0> خود وارد شوید."
#: src/components/settings/ProfileSettings.tsx
msgid "CommaFeed is compatible with the Google Reader API. Use the following URL in your Google Reader-compatible mobile client. Login with your username and your <0>API key</0>."
msgstr "CommaFeed با Google Reader API سازگار است. از آدرس زیر در کلاینت موبایل سازگار با Google Reader استفاده کنید. با نام کاربری و <0>کلید API</0> خود وارد شوید."
#: src/pages/app/AboutPage.tsx #: src/pages/app/AboutPage.tsx
msgid "CommaFeed next unread item" msgid "CommaFeed next unread item"
msgstr "مورد خوانده نشده بعدی CommaFeed" msgstr "مورد خوانده نشده بعدی CommaFeed"
@@ -454,6 +458,14 @@ msgstr "به مستندات API بروید."
msgid "Goodies" msgid "Goodies"
msgstr "خوبی ها" msgstr "خوبی ها"
#: src/components/settings/ProfileSettings.tsx
msgid "Google Reader API"
msgstr ""
#: src/components/settings/ProfileSettings.tsx
msgid "Google Reader API URL"
msgstr ""
#: src/components/settings/DisplaySettings.tsx #: src/components/settings/DisplaySettings.tsx
msgid "Grape" msgid "Grape"
msgstr "عنبی" msgstr "عنبی"
@@ -1065,8 +1077,8 @@ msgid "This feed has a legacy filter that cannot be edited and is not applied. P
msgstr "این فید دارای یک فیلتر قدیمی است که قابل ویرایش نیست و اعمال نمی‌شود. لطفاً فیلتر را با استفاده از ویرایشگر جدید بازسازی کنید. عبارت فیلتر قدیمی: <0>{0}</0>" msgstr "این فید دارای یک فیلتر قدیمی است که قابل ویرایش نیست و اعمال نمی‌شود. لطفاً فیلتر را با استفاده از ویرایشگر جدید بازسازی کنید. عبارت فیلتر قدیمی: <0>{0}</0>"
#: src/components/settings/ProfileSettings.tsx #: 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" msgid "This is your API key. It can be used for some read-only API operations and grants access to the Fever API and the Google Reader API. Use the form at the bottom of the page to generate a new API key"
msgstr "این کلید API شماست. می‌توان از آن برای برخی عملیات API فقط خواندنی استفاده کرد و دسترسی به Fever API را فراهم می‌کند. از فرم پایین صفحه برای تولید کلید جدید استفاده کنید" msgstr "این کلید API شماست. می‌توان از آن برای برخی عملیات فقط خواندنی API استفاده کرد و دسترسی به Fever API و Google Reader API را می‌دهد. از فرم پایین صفحه برای ایجاد کلید API جدید استفاده کنید"
#: src/components/settings/DisplaySettings.tsx #: src/components/settings/DisplaySettings.tsx
msgid "This setting can cause scrolling issues on some browsers (e.g. Safari)" msgid "This setting can cause scrolling issues on some browsers (e.g. Safari)"
@@ -1138,6 +1150,10 @@ msgstr "وب سایت"
msgid "Welcome! This appears to be the first time you're running CommaFeed. Please create an administrator account to get started." msgid "Welcome! This appears to be the first time you're running CommaFeed. Please create an administrator account to get started."
msgstr "خوش آمدید! به نظر می‌رسد این اولین بار است که CommaFeed را اجرا می‌کنید. لطفاً برای شروع یک حساب مدیر ایجاد کنید." msgstr "خوش آمدید! به نظر می‌رسد این اولین بار است که CommaFeed را اجرا می‌کنید. لطفاً برای شروع یک حساب مدیر ایجاد کنید."
#: src/app/client.ts
msgid "Wrong username or password"
msgstr ""
#: src/components/settings/DisplaySettings.tsx #: src/components/settings/DisplaySettings.tsx
msgid "Yellow" msgid "Yellow"
msgstr "زرد" msgstr "زرد"

View File

@@ -210,6 +210,10 @@ msgstr "CommaFeed-selainlaajennuksen versio {browserExtensionVersion}."
msgid "CommaFeed is compatible with the Fever API. Use the following URL in your Fever-compatible mobile client. Login with your username and your <0>API key</0>." msgid "CommaFeed is compatible with the Fever API. Use the following URL in your Fever-compatible mobile client. Login with your username and your <0>API key</0>."
msgstr "CommaFeed on yhteensopiva Fever-API:n kanssa. Käytä seuraavaa URL-osoitetta Fever-yhteensopivassa mobiiliohjelmassasi. Kirjaudu sisään käyttäjätunnuksellasi ja <0>API-avaimellasi</0>." msgstr "CommaFeed on yhteensopiva Fever-API:n kanssa. Käytä seuraavaa URL-osoitetta Fever-yhteensopivassa mobiiliohjelmassasi. Kirjaudu sisään käyttäjätunnuksellasi ja <0>API-avaimellasi</0>."
#: src/components/settings/ProfileSettings.tsx
msgid "CommaFeed is compatible with the Google Reader API. Use the following URL in your Google Reader-compatible mobile client. Login with your username and your <0>API key</0>."
msgstr "CommaFeed on yhteensopiva Google Reader -API:n kanssa. Käytä seuraavaa URL-osoitetta Google Reader -yhteensopivassa mobiiliohjelmassasi. Kirjaudu sisään käyttäjätunnuksellasi ja <0>API-avaimellasi</0>."
#: src/pages/app/AboutPage.tsx #: src/pages/app/AboutPage.tsx
msgid "CommaFeed next unread item" msgid "CommaFeed next unread item"
msgstr "CommaFeed seuraava lukematon kohde" msgstr "CommaFeed seuraava lukematon kohde"
@@ -454,6 +458,14 @@ msgstr "Siirry API-dokumentaatioon."
msgid "Goodies" msgid "Goodies"
msgstr "Hyvää" msgstr "Hyvää"
#: src/components/settings/ProfileSettings.tsx
msgid "Google Reader API"
msgstr ""
#: src/components/settings/ProfileSettings.tsx
msgid "Google Reader API URL"
msgstr ""
#: src/components/settings/DisplaySettings.tsx #: src/components/settings/DisplaySettings.tsx
msgid "Grape" msgid "Grape"
msgstr "Viinirypäle" msgstr "Viinirypäle"
@@ -1065,8 +1077,8 @@ msgid "This feed has a legacy filter that cannot be edited and is not applied. P
msgstr "Tässä syötteessä on vanha suodatin, jota ei voi muokata ja jota ei sovelleta. Luo suodatin uudelleen käyttämällä uutta lauseke-editoria. Vanha suodatinlauseke oli: <0>{0}</0>" msgstr "Tässä syötteessä on vanha suodatin, jota ei voi muokata ja jota ei sovelleta. Luo suodatin uudelleen käyttämällä uutta lauseke-editoria. Vanha suodatinlauseke oli: <0>{0}</0>"
#: src/components/settings/ProfileSettings.tsx #: 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" msgid "This is your API key. It can be used for some read-only API operations and grants access to the Fever API and the Google Reader API. Use the form at the bottom of the page to generate a new API key"
msgstr "Tämä on API-avaimesi. Sitä voidaan käyttää joihinkin vain luku -API-toimintoihin ja se antaa pääsyn Fever-API:iin. Käytä sivun alareunassa olevaa lomaketta uuden API-avaimen luomiseen" msgstr "Tämä on API-avaimesi. Sitä voidaan käyttää joihinkin vain luku -API-toimintoihin, ja se myöntää pääsyn Fever-API:hin ja Google Reader -API:hin. Käytä sivun alareunassa olevaa lomaketta luodaksesi uuden API-avaimen"
#: src/components/settings/DisplaySettings.tsx #: src/components/settings/DisplaySettings.tsx
msgid "This setting can cause scrolling issues on some browsers (e.g. Safari)" msgid "This setting can cause scrolling issues on some browsers (e.g. Safari)"
@@ -1138,6 +1150,10 @@ msgstr "Verkkosivusto"
msgid "Welcome! This appears to be the first time you're running CommaFeed. Please create an administrator account to get started." msgid "Welcome! This appears to be the first time you're running CommaFeed. Please create an administrator account to get started."
msgstr "Tervetuloa! Näyttää siltä, että suoritat CommaFeediä ensimmäistä kertaa. Luo ylläpitäjän tili aloittaaksesi." msgstr "Tervetuloa! Näyttää siltä, että suoritat CommaFeediä ensimmäistä kertaa. Luo ylläpitäjän tili aloittaaksesi."
#: src/app/client.ts
msgid "Wrong username or password"
msgstr ""
#: src/components/settings/DisplaySettings.tsx #: src/components/settings/DisplaySettings.tsx
msgid "Yellow" msgid "Yellow"
msgstr "Keltainen" msgstr "Keltainen"

View File

@@ -210,6 +210,10 @@ msgstr "Extension CommaFeed pour navigateur version {browserExtensionVersion}."
msgid "CommaFeed is compatible with the Fever API. Use the following URL in your Fever-compatible mobile client. Login with your username and your <0>API key</0>." msgid "CommaFeed is compatible with the Fever API. Use the following URL in your Fever-compatible mobile client. Login with your username and your <0>API key</0>."
msgstr "Commafeed est compatible avec l'API Fever, en inscrivant l'URL suivante dans votre client mobile compatible. Entrez votre nom d'utilisateur habituel, et votre <0>clef API</0> comme mot de passe." msgstr "Commafeed est compatible avec l'API Fever, en inscrivant l'URL suivante dans votre client mobile compatible. Entrez votre nom d'utilisateur habituel, et votre <0>clef API</0> comme mot de passe."
#: src/components/settings/ProfileSettings.tsx
msgid "CommaFeed is compatible with the Google Reader API. Use the following URL in your Google Reader-compatible mobile client. Login with your username and your <0>API key</0>."
msgstr "Commafeed est compatible avec l'API Google Reader, en inscrivant l'URL suivante dans votre client mobile compatible. Entrez votre nom d'utilisateur habituel, et votre <0>clef API</0> comme mot de passe."
#: src/pages/app/AboutPage.tsx #: src/pages/app/AboutPage.tsx
msgid "CommaFeed next unread item" msgid "CommaFeed next unread item"
msgstr "CommaFeed prochain article non lu" msgstr "CommaFeed prochain article non lu"
@@ -454,6 +458,14 @@ msgstr "Aller à la documentation de l'API."
msgid "Goodies" msgid "Goodies"
msgstr "Extensions" msgstr "Extensions"
#: src/components/settings/ProfileSettings.tsx
msgid "Google Reader API"
msgstr "API Google Reader"
#: src/components/settings/ProfileSettings.tsx
msgid "Google Reader API URL"
msgstr "URL de l'API Google Reader"
#: src/components/settings/DisplaySettings.tsx #: src/components/settings/DisplaySettings.tsx
msgid "Grape" msgid "Grape"
msgstr "Bordeaux" msgstr "Bordeaux"
@@ -1065,8 +1077,8 @@ msgid "This feed has a legacy filter that cannot be edited and is not applied. P
msgstr "Ce flux est configuré avec un filtre obsolète, qui ne peut être ni modifié ni appliqué. Vous devez recréer ce filtre à l'aide du nouvel éditeur d'expressions régulières. Le filtre configuré était : <0>{0}</0>" msgstr "Ce flux est configuré avec un filtre obsolète, qui ne peut être ni modifié ni appliqué. Vous devez recréer ce filtre à l'aide du nouvel éditeur d'expressions régulières. Le filtre configuré était : <0>{0}</0>"
#: src/components/settings/ProfileSettings.tsx #: 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" msgid "This is your API key. It can be used for some read-only API operations and grants access to the Fever API and the Google Reader 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" 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 et à l'API Google Reader. Utilisez le formulaire en bas de la page pour générer une nouvelle clef API"
#: src/components/settings/DisplaySettings.tsx #: src/components/settings/DisplaySettings.tsx
msgid "This setting can cause scrolling issues on some browsers (e.g. Safari)" msgid "This setting can cause scrolling issues on some browsers (e.g. Safari)"
@@ -1138,6 +1150,10 @@ msgstr "Site web"
msgid "Welcome! This appears to be the first time you're running CommaFeed. Please create an administrator account to get started." msgid "Welcome! This appears to be the first time you're running CommaFeed. Please create an administrator account to get started."
msgstr "Bienvenue ! Il semble que ce soit le premier démarrage de Commafeed. Avant tout, vous devez créer un compte administrateur." msgstr "Bienvenue ! Il semble que ce soit le premier démarrage de Commafeed. Avant tout, vous devez créer un compte administrateur."
#: src/app/client.ts
msgid "Wrong username or password"
msgstr "Utilisateur ou mot de passe incorrect"
#: src/components/settings/DisplaySettings.tsx #: src/components/settings/DisplaySettings.tsx
msgid "Yellow" msgid "Yellow"
msgstr "Jaune" msgstr "Jaune"

View File

@@ -211,6 +211,10 @@ msgstr "Versión do complemento do navegador para CommaFeed {browserExtensionVer
msgid "CommaFeed is compatible with the Fever API. Use the following URL in your Fever-compatible mobile client. Login with your username and your <0>API key</0>." msgid "CommaFeed is compatible with the Fever API. Use the following URL in your Fever-compatible mobile client. Login with your username and your <0>API key</0>."
msgstr "CommaFeed é compatible coa API de Fever. Usa o seguinte URL no teu cliente móbil compatible con Fever. Accede co identificador e a túa <0>clave API</0>." msgstr "CommaFeed é compatible coa API de Fever. Usa o seguinte URL no teu cliente móbil compatible con Fever. Accede co identificador e a túa <0>clave API</0>."
#: src/components/settings/ProfileSettings.tsx
msgid "CommaFeed is compatible with the Google Reader API. Use the following URL in your Google Reader-compatible mobile client. Login with your username and your <0>API key</0>."
msgstr "CommaFeed é compatible coa API de Google Reader. Usa o seguinte URL no teu cliente móbil compatible con Google Reader. Accede co identificador e a túa <0>clave API</0>."
#: src/pages/app/AboutPage.tsx #: src/pages/app/AboutPage.tsx
msgid "CommaFeed next unread item" msgid "CommaFeed next unread item"
msgstr "Seguinte elemento non lido en CommaFeed" msgstr "Seguinte elemento non lido en CommaFeed"
@@ -455,6 +459,14 @@ msgstr "Ir á documentación da API."
msgid "Goodies" msgid "Goodies"
msgstr "Agasallos" msgstr "Agasallos"
#: src/components/settings/ProfileSettings.tsx
msgid "Google Reader API"
msgstr ""
#: src/components/settings/ProfileSettings.tsx
msgid "Google Reader API URL"
msgstr ""
#: src/components/settings/DisplaySettings.tsx #: src/components/settings/DisplaySettings.tsx
msgid "Grape" msgid "Grape"
msgstr "Bago" msgstr "Bago"
@@ -1066,8 +1078,8 @@ msgid "This feed has a legacy filter that cannot be edited and is not applied. P
msgstr "Esta canle ten un filtro antigo que non se pode editar e que non se aplica. Volve crear o filtro usando o novo editor de expresións. A expresión do filtro antigo era: <0>{0}</0>" msgstr "Esta canle ten un filtro antigo que non se pode editar e que non se aplica. Volve crear o filtro usando o novo editor de expresións. A expresión do filtro antigo era: <0>{0}</0>"
#: src/components/settings/ProfileSettings.tsx #: 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" msgid "This is your API key. It can be used for some read-only API operations and grants access to the Fever API and the Google Reader 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" msgstr "Esta é a túa clave da API. Pode usarse para algunhas operacións da API de só-lectura e concede acceso á API Fever e á API de Google Reader. Usa o formulario da parte inferior da páxina para crear unha nova clave da API"
#: src/components/settings/DisplaySettings.tsx #: src/components/settings/DisplaySettings.tsx
msgid "This setting can cause scrolling issues on some browsers (e.g. Safari)" msgid "This setting can cause scrolling issues on some browsers (e.g. Safari)"
@@ -1139,6 +1151,10 @@ msgstr "Páxina web"
msgid "Welcome! This appears to be the first time you're running CommaFeed. Please create an administrator account to get started." msgid "Welcome! This appears to be the first time you're running CommaFeed. Please create an administrator account to get started."
msgstr "Benvida! Semella que é a primeira vez que executas CommaFeed. Para comezar, crea unha conta de administración." msgstr "Benvida! Semella que é a primeira vez que executas CommaFeed. Para comezar, crea unha conta de administración."
#: src/app/client.ts
msgid "Wrong username or password"
msgstr ""
#: src/components/settings/DisplaySettings.tsx #: src/components/settings/DisplaySettings.tsx
msgid "Yellow" msgid "Yellow"
msgstr "Amarelo" msgstr "Amarelo"

View File

@@ -210,6 +210,10 @@ msgstr "CommaFeed böngészőbővítmény {browserExtensionVersion} verzió."
msgid "CommaFeed is compatible with the Fever API. Use the following URL in your Fever-compatible mobile client. Login with your username and your <0>API key</0>." msgid "CommaFeed is compatible with the Fever API. Use the following URL in your Fever-compatible mobile client. Login with your username and your <0>API key</0>."
msgstr "A CommaFeed kompatibilis a Fever API-val. Használja a következő URL-t Fever-kompatibilis mobilkliensében. Jelentkezzen be felhasználónevével és <0>API-kulcsával</0>." msgstr "A CommaFeed kompatibilis a Fever API-val. Használja a következő URL-t Fever-kompatibilis mobilkliensében. Jelentkezzen be felhasználónevével és <0>API-kulcsával</0>."
#: src/components/settings/ProfileSettings.tsx
msgid "CommaFeed is compatible with the Google Reader API. Use the following URL in your Google Reader-compatible mobile client. Login with your username and your <0>API key</0>."
msgstr "A CommaFeed kompatibilis a Google Reader API-val. Használja a következő URL-t Google Reader-kompatibilis mobilkliensében. Jelentkezzen be felhasználónevével és <0>API-kulcsával</0>."
#: src/pages/app/AboutPage.tsx #: src/pages/app/AboutPage.tsx
msgid "CommaFeed next unread item" msgid "CommaFeed next unread item"
msgstr "CommaFeed következő olvasatlan elem" msgstr "CommaFeed következő olvasatlan elem"
@@ -454,6 +458,14 @@ msgstr "Nyissa meg az API dokumentációját."
msgid "Goodies" msgid "Goodies"
msgstr "Jók" msgstr "Jók"
#: src/components/settings/ProfileSettings.tsx
msgid "Google Reader API"
msgstr ""
#: src/components/settings/ProfileSettings.tsx
msgid "Google Reader API URL"
msgstr ""
#: src/components/settings/DisplaySettings.tsx #: src/components/settings/DisplaySettings.tsx
msgid "Grape" msgid "Grape"
msgstr "Szőlő" msgstr "Szőlő"
@@ -1065,8 +1077,8 @@ msgid "This feed has a legacy filter that cannot be edited and is not applied. P
msgstr "Ez a feed egy régi szűrővel rendelkezik, amely nem szerkeszthető és nem kerül alkalmazásra. Kérjük, hozza létre újra a szűrőt az új kifejezésszerkesztővel. A régi szűrőkifejezés a következő volt: <0>{0}</0>" msgstr "Ez a feed egy régi szűrővel rendelkezik, amely nem szerkeszthető és nem kerül alkalmazásra. Kérjük, hozza létre újra a szűrőt az új kifejezésszerkesztővel. A régi szűrőkifejezés a következő volt: <0>{0}</0>"
#: src/components/settings/ProfileSettings.tsx #: 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" msgid "This is your API key. It can be used for some read-only API operations and grants access to the Fever API and the Google Reader API. Use the form at the bottom of the page to generate a new API key"
msgstr "Ez az Ön API-kulcsa. Felhasználható néhány írásvédett API-művelethez, és hozzáférést biztosít a Fever API-hoz. Használja az oldal alján található űrlapot egy új API-kulcs létrehozásához" msgstr "Ez az Ön API-kulcsa. Néhány csak olvasható API-művelethez használható, és hozzáférést biztosít a Fever API-hoz és a Google Reader API-hoz. Használja az oldal alján található űrlapot új API-kulcs létrehozásához"
#: src/components/settings/DisplaySettings.tsx #: src/components/settings/DisplaySettings.tsx
msgid "This setting can cause scrolling issues on some browsers (e.g. Safari)" msgid "This setting can cause scrolling issues on some browsers (e.g. Safari)"
@@ -1138,6 +1150,10 @@ msgstr "Webhely"
msgid "Welcome! This appears to be the first time you're running CommaFeed. Please create an administrator account to get started." msgid "Welcome! This appears to be the first time you're running CommaFeed. Please create an administrator account to get started."
msgstr "Üdvözöljük! Úgy tűnik, ez az első alkalom, hogy a CommaFeedet futtatja. Kérjük, hozzon létre egy rendszergazdai fiókot a kezdéshez." msgstr "Üdvözöljük! Úgy tűnik, ez az első alkalom, hogy a CommaFeedet futtatja. Kérjük, hozzon létre egy rendszergazdai fiókot a kezdéshez."
#: src/app/client.ts
msgid "Wrong username or password"
msgstr ""
#: src/components/settings/DisplaySettings.tsx #: src/components/settings/DisplaySettings.tsx
msgid "Yellow" msgid "Yellow"
msgstr "Sárga" msgstr "Sárga"

View File

@@ -210,6 +210,10 @@ msgstr "Ekstensi browser CommaFeed versi {browserExtensionVersion}."
msgid "CommaFeed is compatible with the Fever API. Use the following URL in your Fever-compatible mobile client. Login with your username and your <0>API key</0>." msgid "CommaFeed is compatible with the Fever API. Use the following URL in your Fever-compatible mobile client. Login with your username and your <0>API key</0>."
msgstr "CommaFeed kompatibel dengan Fever API. Gunakan URL berikut di klien seluler Anda yang kompatibel dengan Fever. Login dengan nama pengguna dan <0>kunci API</0> Anda." msgstr "CommaFeed kompatibel dengan Fever API. Gunakan URL berikut di klien seluler Anda yang kompatibel dengan Fever. Login dengan nama pengguna dan <0>kunci API</0> Anda."
#: src/components/settings/ProfileSettings.tsx
msgid "CommaFeed is compatible with the Google Reader API. Use the following URL in your Google Reader-compatible mobile client. Login with your username and your <0>API key</0>."
msgstr "CommaFeed kompatibel dengan Google Reader API. Gunakan URL berikut di klien seluler Anda yang kompatibel dengan Google Reader. Login dengan nama pengguna dan <0>kunci API</0> Anda."
#: src/pages/app/AboutPage.tsx #: src/pages/app/AboutPage.tsx
msgid "CommaFeed next unread item" msgid "CommaFeed next unread item"
msgstr "CommaFeed item yang belum dibaca berikutnya" msgstr "CommaFeed item yang belum dibaca berikutnya"
@@ -454,6 +458,14 @@ msgstr "Buka dokumentasi API."
msgid "Goodies" msgid "Goodies"
msgstr "Pernak-pernik" msgstr "Pernak-pernik"
#: src/components/settings/ProfileSettings.tsx
msgid "Google Reader API"
msgstr ""
#: src/components/settings/ProfileSettings.tsx
msgid "Google Reader API URL"
msgstr ""
#: src/components/settings/DisplaySettings.tsx #: src/components/settings/DisplaySettings.tsx
msgid "Grape" msgid "Grape"
msgstr "Anggur" msgstr "Anggur"
@@ -1065,8 +1077,8 @@ msgid "This feed has a legacy filter that cannot be edited and is not applied. P
msgstr "Umpan ini memiliki filter lama yang tidak dapat diedit dan tidak diterapkan. Silakan buat ulang filter menggunakan editor ekspresi baru. Ekspresi filter lama adalah: <0>{0}</0>" msgstr "Umpan ini memiliki filter lama yang tidak dapat diedit dan tidak diterapkan. Silakan buat ulang filter menggunakan editor ekspresi baru. Ekspresi filter lama adalah: <0>{0}</0>"
#: src/components/settings/ProfileSettings.tsx #: 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" msgid "This is your API key. It can be used for some read-only API operations and grants access to the Fever API and the Google Reader API. Use the form at the bottom of the page to generate a new API key"
msgstr "Ini adalah kunci API Anda. Ini dapat digunakan untuk beberapa operasi API baca-saja dan memberikan akses ke Fever API. Gunakan formulir di bagian bawah halaman untuk membuat kunci API baru" msgstr "Ini adalah kunci API Anda. Ini dapat digunakan untuk beberapa operasi API yang hanya-baca dan memberikan akses ke Fever API dan Google Reader API. Gunakan formulir di bagian bawah halaman untuk membuat kunci API baru"
#: src/components/settings/DisplaySettings.tsx #: src/components/settings/DisplaySettings.tsx
msgid "This setting can cause scrolling issues on some browsers (e.g. Safari)" msgid "This setting can cause scrolling issues on some browsers (e.g. Safari)"
@@ -1138,6 +1150,10 @@ msgstr "Situs Web"
msgid "Welcome! This appears to be the first time you're running CommaFeed. Please create an administrator account to get started." msgid "Welcome! This appears to be the first time you're running CommaFeed. Please create an administrator account to get started."
msgstr "Selamat datang! Ini sepertinya pertama kalinya Anda menjalankan CommaFeed. Silakan buat akun administrator untuk memulai." msgstr "Selamat datang! Ini sepertinya pertama kalinya Anda menjalankan CommaFeed. Silakan buat akun administrator untuk memulai."
#: src/app/client.ts
msgid "Wrong username or password"
msgstr ""
#: src/components/settings/DisplaySettings.tsx #: src/components/settings/DisplaySettings.tsx
msgid "Yellow" msgid "Yellow"
msgstr "Kuning" msgstr "Kuning"

View File

@@ -210,6 +210,10 @@ msgstr "Versione dell'estensione del browser CommaFeed {browserExtensionVersion}
msgid "CommaFeed is compatible with the Fever API. Use the following URL in your Fever-compatible mobile client. Login with your username and your <0>API key</0>." msgid "CommaFeed is compatible with the Fever API. Use the following URL in your Fever-compatible mobile client. Login with your username and your <0>API key</0>."
msgstr "CommaFeed è compatibile con l'API Fever. Utilizza il seguente URL nel tuo client mobile compatibile con Fever. Accedi con il tuo nome utente e la tua <0>chiave API</0>." msgstr "CommaFeed è compatibile con l'API Fever. Utilizza il seguente URL nel tuo client mobile compatibile con Fever. Accedi con il tuo nome utente e la tua <0>chiave API</0>."
#: src/components/settings/ProfileSettings.tsx
msgid "CommaFeed is compatible with the Google Reader API. Use the following URL in your Google Reader-compatible mobile client. Login with your username and your <0>API key</0>."
msgstr "CommaFeed è compatibile con l'API Google Reader. Utilizza il seguente URL nel tuo client mobile compatibile con Google Reader. Accedi con il tuo nome utente e la tua <0>chiave API</0>."
#: src/pages/app/AboutPage.tsx #: src/pages/app/AboutPage.tsx
msgid "CommaFeed next unread item" msgid "CommaFeed next unread item"
msgstr "CommaFeed successivo elemento non letto" msgstr "CommaFeed successivo elemento non letto"
@@ -454,6 +458,14 @@ msgstr "Vai alla documentazione dell'API."
msgid "Goodies" msgid "Goodies"
msgstr "Chicche" msgstr "Chicche"
#: src/components/settings/ProfileSettings.tsx
msgid "Google Reader API"
msgstr ""
#: src/components/settings/ProfileSettings.tsx
msgid "Google Reader API URL"
msgstr ""
#: src/components/settings/DisplaySettings.tsx #: src/components/settings/DisplaySettings.tsx
msgid "Grape" msgid "Grape"
msgstr "Uva" msgstr "Uva"
@@ -1065,8 +1077,8 @@ msgid "This feed has a legacy filter that cannot be edited and is not applied. P
msgstr "Questo feed ha un filtro legacy che non può essere modificato e non viene applicato. Ricrea il filtro utilizzando il nuovo editor di espressioni. L'espressione del filtro legacy era: <0>{0}</0>" msgstr "Questo feed ha un filtro legacy che non può essere modificato e non viene applicato. Ricrea il filtro utilizzando il nuovo editor di espressioni. L'espressione del filtro legacy era: <0>{0}</0>"
#: src/components/settings/ProfileSettings.tsx #: 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" msgid "This is your API key. It can be used for some read-only API operations and grants access to the Fever API and the Google Reader API. Use the form at the bottom of the page to generate a new API key"
msgstr "Questa è la tua chiave API. Può essere utilizzata per alcune operazioni API di sola lettura e garantisce l'accesso all'API Fever. Utilizza il modulo in fondo alla pagina per generare una nuova chiave API" msgstr "Questa è la tua chiave API. Può essere utilizzata per alcune operazioni API di sola lettura e concede l'accesso all'API Fever e all'API Google Reader. Usa il modulo in fondo alla pagina per generare una nuova chiave API"
#: src/components/settings/DisplaySettings.tsx #: src/components/settings/DisplaySettings.tsx
msgid "This setting can cause scrolling issues on some browsers (e.g. Safari)" msgid "This setting can cause scrolling issues on some browsers (e.g. Safari)"
@@ -1138,6 +1150,10 @@ msgstr "Sito web"
msgid "Welcome! This appears to be the first time you're running CommaFeed. Please create an administrator account to get started." msgid "Welcome! This appears to be the first time you're running CommaFeed. Please create an administrator account to get started."
msgstr "Benvenuto! Sembra che sia la prima volta che esegui CommaFeed. Crea un account amministratore per iniziare." msgstr "Benvenuto! Sembra che sia la prima volta che esegui CommaFeed. Crea un account amministratore per iniziare."
#: src/app/client.ts
msgid "Wrong username or password"
msgstr ""
#: src/components/settings/DisplaySettings.tsx #: src/components/settings/DisplaySettings.tsx
msgid "Yellow" msgid "Yellow"
msgstr "Giallo" msgstr "Giallo"

View File

@@ -210,6 +210,10 @@ msgstr "CommaFeed ブラウザー拡張機能のバージョンは {browserExten
msgid "CommaFeed is compatible with the Fever API. Use the following URL in your Fever-compatible mobile client. Login with your username and your <0>API key</0>." msgid "CommaFeed is compatible with the Fever API. Use the following URL in your Fever-compatible mobile client. Login with your username and your <0>API key</0>."
msgstr "CommaFeedはFever APIと互換性があります。Fever互換のモバイルクライアントで次のURLを使用してください。ユーザー名と <0>API キー</0> でログインしてください。" msgstr "CommaFeedはFever APIと互換性があります。Fever互換のモバイルクライアントで次のURLを使用してください。ユーザー名と <0>API キー</0> でログインしてください。"
#: src/components/settings/ProfileSettings.tsx
msgid "CommaFeed is compatible with the Google Reader API. Use the following URL in your Google Reader-compatible mobile client. Login with your username and your <0>API key</0>."
msgstr "CommaFeedはGoogle Reader APIと互換性があります。Google Reader互換のモバイルクライアントで次のURLを使用してください。ユーザー名と <0>API キー</0> でログインしてください。"
#: src/pages/app/AboutPage.tsx #: src/pages/app/AboutPage.tsx
msgid "CommaFeed next unread item" msgid "CommaFeed next unread item"
msgstr "CommaFeed 次の未読アイテム" msgstr "CommaFeed 次の未読アイテム"
@@ -454,6 +458,14 @@ msgstr "APIドキュメントに移動します。"
msgid "Goodies" msgid "Goodies"
msgstr "グッズ" msgstr "グッズ"
#: src/components/settings/ProfileSettings.tsx
msgid "Google Reader API"
msgstr ""
#: src/components/settings/ProfileSettings.tsx
msgid "Google Reader API URL"
msgstr ""
#: src/components/settings/DisplaySettings.tsx #: src/components/settings/DisplaySettings.tsx
msgid "Grape" msgid "Grape"
msgstr "グレープ" msgstr "グレープ"
@@ -1065,8 +1077,8 @@ msgid "This feed has a legacy filter that cannot be edited and is not applied. P
msgstr "このフィードには、編集できず適用もされない古い形式のフィルタがあります。新しい式エディタを使用してフィルタを再作成してください。古いフィルタ式は次のとおりでした: <0>{0}</0>" msgstr "このフィードには、編集できず適用もされない古い形式のフィルタがあります。新しい式エディタを使用してフィルタを再作成してください。古いフィルタ式は次のとおりでした: <0>{0}</0>"
#: src/components/settings/ProfileSettings.tsx #: 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" msgid "This is your API key. It can be used for some read-only API operations and grants access to the Fever API and the Google Reader API. Use the form at the bottom of the page to generate a new API key"
msgstr "これはあなたのAPIキーです。いくつかの読み取り専用API操作に使用できます。これにより、Fever APIへのアクセスが可能になります。ページの下部のフォームを使用して新しいAPIキーを生成します。" msgstr "これはあなたのAPIキーです。一部の読み取り専用API操作に使用でき、Fever APIおよびGoogle Reader APIへのアクセスを許可します。新しいAPIキーを生成するには、ページ下部のフォームを使用してください"
#: src/components/settings/DisplaySettings.tsx #: src/components/settings/DisplaySettings.tsx
msgid "This setting can cause scrolling issues on some browsers (e.g. Safari)" msgid "This setting can cause scrolling issues on some browsers (e.g. Safari)"
@@ -1138,6 +1150,10 @@ msgstr "ウェブサイト"
msgid "Welcome! This appears to be the first time you're running CommaFeed. Please create an administrator account to get started." msgid "Welcome! This appears to be the first time you're running CommaFeed. Please create an administrator account to get started."
msgstr "ようこそ!CommaFeedを初めて実行するようです。開始するには管理者アカウントを作成してください。" msgstr "ようこそ!CommaFeedを初めて実行するようです。開始するには管理者アカウントを作成してください。"
#: src/app/client.ts
msgid "Wrong username or password"
msgstr ""
#: src/components/settings/DisplaySettings.tsx #: src/components/settings/DisplaySettings.tsx
msgid "Yellow" msgid "Yellow"
msgstr "イエロー" msgstr "イエロー"

View File

@@ -210,6 +210,10 @@ msgstr "CommaFeed 브라우저 확장 프로그램 버전 {browserExtensionVersi
msgid "CommaFeed is compatible with the Fever API. Use the following URL in your Fever-compatible mobile client. Login with your username and your <0>API key</0>." msgid "CommaFeed is compatible with the Fever API. Use the following URL in your Fever-compatible mobile client. Login with your username and your <0>API key</0>."
msgstr "CommaFeed는 Fever API와 호환됩니다. Fever 호환 모바일 클라이언트에서 다음 URL을 사용하십시오. 사용자 이름과 <0>API 키</0>로 로그인하십시오." msgstr "CommaFeed는 Fever API와 호환됩니다. Fever 호환 모바일 클라이언트에서 다음 URL을 사용하십시오. 사용자 이름과 <0>API 키</0>로 로그인하십시오."
#: src/components/settings/ProfileSettings.tsx
msgid "CommaFeed is compatible with the Google Reader API. Use the following URL in your Google Reader-compatible mobile client. Login with your username and your <0>API key</0>."
msgstr "CommaFeed는 Google Reader API와 호환됩니다. Google Reader 호환 모바일 클라이언트에서 다음 URL을 사용하십시오. 사용자 이름과 <0>API 키</0>로 로그인하십시오."
#: src/pages/app/AboutPage.tsx #: src/pages/app/AboutPage.tsx
msgid "CommaFeed next unread item" msgid "CommaFeed next unread item"
msgstr "다음 읽지 않은 항목을 쉼표로 피드" msgstr "다음 읽지 않은 항목을 쉼표로 피드"
@@ -454,6 +458,14 @@ msgstr "API 문서로 이동합니다."
msgid "Goodies" msgid "Goodies"
msgstr "굿즈" msgstr "굿즈"
#: src/components/settings/ProfileSettings.tsx
msgid "Google Reader API"
msgstr ""
#: src/components/settings/ProfileSettings.tsx
msgid "Google Reader API URL"
msgstr ""
#: src/components/settings/DisplaySettings.tsx #: src/components/settings/DisplaySettings.tsx
msgid "Grape" msgid "Grape"
msgstr "그레이프" msgstr "그레이프"
@@ -1065,8 +1077,8 @@ msgid "This feed has a legacy filter that cannot be edited and is not applied. P
msgstr "이 피드에는 편집할 수 없고 적용되지 않는 기존 필터가 있습니다. 새 표현식 편집기를 사용하여 필터를 다시 만드십시오. 기존 필터 표현식은 <0>{0}</0>이었습니다." msgstr "이 피드에는 편집할 수 없고 적용되지 않는 기존 필터가 있습니다. 새 표현식 편집기를 사용하여 필터를 다시 만드십시오. 기존 필터 표현식은 <0>{0}</0>이었습니다."
#: src/components/settings/ProfileSettings.tsx #: 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" msgid "This is your API key. It can be used for some read-only API operations and grants access to the Fever API and the Google Reader API. Use the form at the bottom of the page to generate a new API key"
msgstr "이것은 귀하의 API 키입니다. 일부 읽기 전용 API 작업에 사용할 수 있으며 Fever API에 대한 액세스 권한을 부여합니다. 새 API 키를 생성하려면 페이지 하단의 양식을 사용하십시오." msgstr "이것은 회원님의 API 키입니다. 일부 읽기 전용 API 작업에 사용할 수 있으며 Fever API 및 Google Reader API에 대한 액세스 권한을 부여합니다. 새 API 키를 생성하려면 페이지 하단의 양식을 사용하십시오"
#: src/components/settings/DisplaySettings.tsx #: src/components/settings/DisplaySettings.tsx
msgid "This setting can cause scrolling issues on some browsers (e.g. Safari)" msgid "This setting can cause scrolling issues on some browsers (e.g. Safari)"
@@ -1138,6 +1150,10 @@ msgstr "웹사이트"
msgid "Welcome! This appears to be the first time you're running CommaFeed. Please create an administrator account to get started." msgid "Welcome! This appears to be the first time you're running CommaFeed. Please create an administrator account to get started."
msgstr "환영합니다! CommaFeed를 처음 실행하는 것 같습니다. 시작하려면 관리자 계정을 만드십시오." msgstr "환영합니다! CommaFeed를 처음 실행하는 것 같습니다. 시작하려면 관리자 계정을 만드십시오."
#: src/app/client.ts
msgid "Wrong username or password"
msgstr ""
#: src/components/settings/DisplaySettings.tsx #: src/components/settings/DisplaySettings.tsx
msgid "Yellow" msgid "Yellow"
msgstr "옐로우" msgstr "옐로우"

View File

@@ -210,6 +210,10 @@ msgstr "Ekstensi penyemak imbas CommaFeed versi {browserExtensionVersion}."
msgid "CommaFeed is compatible with the Fever API. Use the following URL in your Fever-compatible mobile client. Login with your username and your <0>API key</0>." msgid "CommaFeed is compatible with the Fever API. Use the following URL in your Fever-compatible mobile client. Login with your username and your <0>API key</0>."
msgstr "CommaFeed serasi dengan Fever API. Gunakan URL berikut dalam klien mudah alih serasi Fever anda. Log masuk dengan nama pengguna anda dan <0>kunci API</0> anda." msgstr "CommaFeed serasi dengan Fever API. Gunakan URL berikut dalam klien mudah alih serasi Fever anda. Log masuk dengan nama pengguna anda dan <0>kunci API</0> anda."
#: src/components/settings/ProfileSettings.tsx
msgid "CommaFeed is compatible with the Google Reader API. Use the following URL in your Google Reader-compatible mobile client. Login with your username and your <0>API key</0>."
msgstr "CommaFeed serasi dengan Google Reader API. Gunakan URL berikut dalam klien mudah alih serasi Google Reader anda. Log masuk dengan nama pengguna anda dan <0>kunci API</0> anda."
#: src/pages/app/AboutPage.tsx #: src/pages/app/AboutPage.tsx
msgid "CommaFeed next unread item" msgid "CommaFeed next unread item"
msgstr "CommaFeed item belum dibaca seterusnya" msgstr "CommaFeed item belum dibaca seterusnya"
@@ -454,6 +458,14 @@ msgstr "Pergi ke dokumentasi API."
msgid "Goodies" msgid "Goodies"
msgstr "Barangan" msgstr "Barangan"
#: src/components/settings/ProfileSettings.tsx
msgid "Google Reader API"
msgstr ""
#: src/components/settings/ProfileSettings.tsx
msgid "Google Reader API URL"
msgstr ""
#: src/components/settings/DisplaySettings.tsx #: src/components/settings/DisplaySettings.tsx
msgid "Grape" msgid "Grape"
msgstr "Anggur" msgstr "Anggur"
@@ -1065,8 +1077,8 @@ msgid "This feed has a legacy filter that cannot be edited and is not applied. P
msgstr "Suapan ini mempunyai penapis warisan yang tidak boleh diedit dan tidak digunakan. Sila buat semula penapis menggunakan editor ungkapan baharu. Ungkapan penapis warisan ialah: <0>{0}</0>" msgstr "Suapan ini mempunyai penapis warisan yang tidak boleh diedit dan tidak digunakan. Sila buat semula penapis menggunakan editor ungkapan baharu. Ungkapan penapis warisan ialah: <0>{0}</0>"
#: src/components/settings/ProfileSettings.tsx #: 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" msgid "This is your API key. It can be used for some read-only API operations and grants access to the Fever API and the Google Reader API. Use the form at the bottom of the page to generate a new API key"
msgstr "Ini ialah kunci API anda. Ia boleh digunakan untuk beberapa operasi API baca sahaja dan memberikan akses kepada Fever API. Gunakan borang di bahagian bawah halaman untuk menjana kunci API baharu" msgstr "Ini ialah kunci API anda. Ia boleh digunakan untuk sesetengah operasi API baca sahaja dan memberikan akses kepada Fever API dan Google Reader API. Gunakan borang di bahagian bawah halaman untuk menjana kunci API baharu"
#: src/components/settings/DisplaySettings.tsx #: src/components/settings/DisplaySettings.tsx
msgid "This setting can cause scrolling issues on some browsers (e.g. Safari)" msgid "This setting can cause scrolling issues on some browsers (e.g. Safari)"
@@ -1138,6 +1150,10 @@ msgstr "Laman web"
msgid "Welcome! This appears to be the first time you're running CommaFeed. Please create an administrator account to get started." msgid "Welcome! This appears to be the first time you're running CommaFeed. Please create an administrator account to get started."
msgstr "Selamat datang! Ini nampaknya kali pertama anda menjalankan CommaFeed. Sila cipta akaun pentadbir untuk bermula." msgstr "Selamat datang! Ini nampaknya kali pertama anda menjalankan CommaFeed. Sila cipta akaun pentadbir untuk bermula."
#: src/app/client.ts
msgid "Wrong username or password"
msgstr ""
#: src/components/settings/DisplaySettings.tsx #: src/components/settings/DisplaySettings.tsx
msgid "Yellow" msgid "Yellow"
msgstr "Kuning" msgstr "Kuning"

View File

@@ -210,6 +210,10 @@ msgstr "CommaFeed nettleserutvidelse versjon {browserExtensionVersion}."
msgid "CommaFeed is compatible with the Fever API. Use the following URL in your Fever-compatible mobile client. Login with your username and your <0>API key</0>." msgid "CommaFeed is compatible with the Fever API. Use the following URL in your Fever-compatible mobile client. Login with your username and your <0>API key</0>."
msgstr "CommaFeed er kompatibel med Fever API. Bruk følgende URL i din Fever-kompatible mobilklient. Logg inn med brukernavn og din <0>API-nøkkel</0>." msgstr "CommaFeed er kompatibel med Fever API. Bruk følgende URL i din Fever-kompatible mobilklient. Logg inn med brukernavn og din <0>API-nøkkel</0>."
#: src/components/settings/ProfileSettings.tsx
msgid "CommaFeed is compatible with the Google Reader API. Use the following URL in your Google Reader-compatible mobile client. Login with your username and your <0>API key</0>."
msgstr "CommaFeed er kompatibel med Google Reader API. Bruk følgende URL i din Google Reader-kompatible mobilklient. Logg inn med brukernavn og din <0>API-nøkkel</0>."
#: src/pages/app/AboutPage.tsx #: src/pages/app/AboutPage.tsx
msgid "CommaFeed next unread item" msgid "CommaFeed next unread item"
msgstr "CommaFeed neste uleste element" msgstr "CommaFeed neste uleste element"
@@ -454,6 +458,14 @@ msgstr "Gå til API-dokumentasjonen."
msgid "Goodies" msgid "Goodies"
msgstr "Godbiter" msgstr "Godbiter"
#: src/components/settings/ProfileSettings.tsx
msgid "Google Reader API"
msgstr ""
#: src/components/settings/ProfileSettings.tsx
msgid "Google Reader API URL"
msgstr ""
#: src/components/settings/DisplaySettings.tsx #: src/components/settings/DisplaySettings.tsx
msgid "Grape" msgid "Grape"
msgstr "Drue" msgstr "Drue"
@@ -1065,8 +1077,8 @@ msgid "This feed has a legacy filter that cannot be edited and is not applied. P
msgstr "Denne feeden har et utdatert filter som ikke kan redigeres og ikke blir brukt. Vennligst opprett filteret på nytt med den nye uttrykkseditoren. Det utdaterte filteruttrykket var: <0>{0}</0>" msgstr "Denne feeden har et utdatert filter som ikke kan redigeres og ikke blir brukt. Vennligst opprett filteret på nytt med den nye uttrykkseditoren. Det utdaterte filteruttrykket var: <0>{0}</0>"
#: src/components/settings/ProfileSettings.tsx #: 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" msgid "This is your API key. It can be used for some read-only API operations and grants access to the Fever API and the Google Reader API. Use the form at the bottom of the page to generate a new API key"
msgstr "Dette er API-nøkkelen din. Den kan brukes til enkelte skrivebeskyttede API-operasjoner og gir tilgang til Fever API. Bruk skjemaet nederst på siden for å generere en ny API-nøkkel" msgstr "Dette er din API-nøkkel. Den kan brukes til noen skrivebeskyttede API-operasjoner og gir tilgang til Fever API og Google Reader API. Bruk skjemaet nederst på siden for å generere en ny API-nøkkel"
#: src/components/settings/DisplaySettings.tsx #: src/components/settings/DisplaySettings.tsx
msgid "This setting can cause scrolling issues on some browsers (e.g. Safari)" msgid "This setting can cause scrolling issues on some browsers (e.g. Safari)"
@@ -1138,6 +1150,10 @@ msgstr "Nettsted"
msgid "Welcome! This appears to be the first time you're running CommaFeed. Please create an administrator account to get started." msgid "Welcome! This appears to be the first time you're running CommaFeed. Please create an administrator account to get started."
msgstr "Velkommen! Det ser ut til å være første gang du kjører CommaFeed. Vennligst opprett en administratorkonto for å komme i gang." msgstr "Velkommen! Det ser ut til å være første gang du kjører CommaFeed. Vennligst opprett en administratorkonto for å komme i gang."
#: src/app/client.ts
msgid "Wrong username or password"
msgstr ""
#: src/components/settings/DisplaySettings.tsx #: src/components/settings/DisplaySettings.tsx
msgid "Yellow" msgid "Yellow"
msgstr "Gul" msgstr "Gul"

View File

@@ -210,6 +210,10 @@ msgstr "CommaFeed browser extensie versie {browserExtensionVersion}."
msgid "CommaFeed is compatible with the Fever API. Use the following URL in your Fever-compatible mobile client. Login with your username and your <0>API key</0>." msgid "CommaFeed is compatible with the Fever API. Use the following URL in your Fever-compatible mobile client. Login with your username and your <0>API key</0>."
msgstr "CommaFeed is compatibel met de Fever API. Gebruik de volgende URL in uw Fever-compatibele mobiele client. Log in met uw gebruikersnaam en uw <0>API-sleutel</0>." msgstr "CommaFeed is compatibel met de Fever API. Gebruik de volgende URL in uw Fever-compatibele mobiele client. Log in met uw gebruikersnaam en uw <0>API-sleutel</0>."
#: src/components/settings/ProfileSettings.tsx
msgid "CommaFeed is compatible with the Google Reader API. Use the following URL in your Google Reader-compatible mobile client. Login with your username and your <0>API key</0>."
msgstr "CommaFeed is compatibel met de Google Reader API. Gebruik de volgende URL in uw Google Reader-compatibele mobiele client. Log in met uw gebruikersnaam en uw <0>API-sleutel</0>."
#: src/pages/app/AboutPage.tsx #: src/pages/app/AboutPage.tsx
msgid "CommaFeed next unread item" msgid "CommaFeed next unread item"
msgstr "CommaFeed volgende ongelezen item" msgstr "CommaFeed volgende ongelezen item"
@@ -454,6 +458,14 @@ msgstr "Ga naar de API-documentatie."
msgid "Goodies" msgid "Goodies"
msgstr "Goederen" msgstr "Goederen"
#: src/components/settings/ProfileSettings.tsx
msgid "Google Reader API"
msgstr ""
#: src/components/settings/ProfileSettings.tsx
msgid "Google Reader API URL"
msgstr ""
#: src/components/settings/DisplaySettings.tsx #: src/components/settings/DisplaySettings.tsx
msgid "Grape" msgid "Grape"
msgstr "Druif" msgstr "Druif"
@@ -1065,8 +1077,8 @@ msgid "This feed has a legacy filter that cannot be edited and is not applied. P
msgstr "Deze feed heeft een legacy-filter dat niet kan worden bewerkt en niet wordt toegepast. Maak het filter opnieuw met de nieuwe expressie-editor. De legacy-filterexpressie was: <0>{0}</0>" msgstr "Deze feed heeft een legacy-filter dat niet kan worden bewerkt en niet wordt toegepast. Maak het filter opnieuw met de nieuwe expressie-editor. De legacy-filterexpressie was: <0>{0}</0>"
#: src/components/settings/ProfileSettings.tsx #: 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" msgid "This is your API key. It can be used for some read-only API operations and grants access to the Fever API and the Google Reader API. Use the form at the bottom of the page to generate a new API key"
msgstr "Dit is je API-sleutel. Het kan worden gebruikt voor sommige alleen-lezen API-bewerkingen en geeft toegang tot de Fever API. Gebruik het formulier onderaan de pagina om een nieuwe API-sleutel te genereren" msgstr "Dit is uw API-sleutel. Deze kan worden gebruikt voor sommige alleen-lezen API-bewerkingen en geeft toegang tot de Fever API en de Google Reader API. Gebruik het formulier onderaan de pagina om een nieuwe API-sleutel te genereren"
#: src/components/settings/DisplaySettings.tsx #: src/components/settings/DisplaySettings.tsx
msgid "This setting can cause scrolling issues on some browsers (e.g. Safari)" msgid "This setting can cause scrolling issues on some browsers (e.g. Safari)"
@@ -1138,6 +1150,10 @@ msgstr "Website"
msgid "Welcome! This appears to be the first time you're running CommaFeed. Please create an administrator account to get started." msgid "Welcome! This appears to be the first time you're running CommaFeed. Please create an administrator account to get started."
msgstr "Welkom! Dit lijkt de eerste keer te zijn dat je CommaFeed draait. Maak een beheerdersaccount aan om aan de slag te gaan." msgstr "Welkom! Dit lijkt de eerste keer te zijn dat je CommaFeed draait. Maak een beheerdersaccount aan om aan de slag te gaan."
#: src/app/client.ts
msgid "Wrong username or password"
msgstr ""
#: src/components/settings/DisplaySettings.tsx #: src/components/settings/DisplaySettings.tsx
msgid "Yellow" msgid "Yellow"
msgstr "Geel" msgstr "Geel"

View File

@@ -210,6 +210,10 @@ msgstr "CommaFeed nettlesarutviding versjon {browserExtensionVersion}."
msgid "CommaFeed is compatible with the Fever API. Use the following URL in your Fever-compatible mobile client. Login with your username and your <0>API key</0>." msgid "CommaFeed is compatible with the Fever API. Use the following URL in your Fever-compatible mobile client. Login with your username and your <0>API key</0>."
msgstr "CommaFeed er kompatibel med Fever API. Bruk følgjande URL i din Fever-kompatible mobilklient. Logg inn med brukarnamn og din <0>API-nøkkel</0>." msgstr "CommaFeed er kompatibel med Fever API. Bruk følgjande URL i din Fever-kompatible mobilklient. Logg inn med brukarnamn og din <0>API-nøkkel</0>."
#: src/components/settings/ProfileSettings.tsx
msgid "CommaFeed is compatible with the Google Reader API. Use the following URL in your Google Reader-compatible mobile client. Login with your username and your <0>API key</0>."
msgstr "CommaFeed er kompatibel med Google Reader API. Bruk følgjande URL i din Google Reader-kompatible mobilklient. Logg inn med brukarnamn og din <0>API-nøkkel</0>."
#: src/pages/app/AboutPage.tsx #: src/pages/app/AboutPage.tsx
msgid "CommaFeed next unread item" msgid "CommaFeed next unread item"
msgstr "CommaFeed neste ulesne element" msgstr "CommaFeed neste ulesne element"
@@ -454,6 +458,14 @@ msgstr "Gå til API-dokumentasjonen."
msgid "Goodies" msgid "Goodies"
msgstr "Godbitar" msgstr "Godbitar"
#: src/components/settings/ProfileSettings.tsx
msgid "Google Reader API"
msgstr ""
#: src/components/settings/ProfileSettings.tsx
msgid "Google Reader API URL"
msgstr ""
#: src/components/settings/DisplaySettings.tsx #: src/components/settings/DisplaySettings.tsx
msgid "Grape" msgid "Grape"
msgstr "Drue" msgstr "Drue"
@@ -1065,8 +1077,8 @@ msgid "This feed has a legacy filter that cannot be edited and is not applied. P
msgstr "Denne suapen har eit gamalt filter som ikkje kan redigerast og som ikkje vert brukt. Ver venleg og lag filteret på nytt med den nye uttrykkseditoren. Det gamle filteruttrykket var: <0>{0}</0>" msgstr "Denne suapen har eit gamalt filter som ikkje kan redigerast og som ikkje vert brukt. Ver venleg og lag filteret på nytt med den nye uttrykkseditoren. Det gamle filteruttrykket var: <0>{0}</0>"
#: src/components/settings/ProfileSettings.tsx #: 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" msgid "This is your API key. It can be used for some read-only API operations and grants access to the Fever API and the Google Reader API. Use the form at the bottom of the page to generate a new API key"
msgstr "Dette er API-nøkkelen din. Han kan brukast til enkelte skrivebeskytta API-handlingar og gjev tilgang til Fever API. Bruk skjemaet nedst på sida for å laga ein ny API-nøkkel" msgstr "Dette er din API-nøkkel. Ho kan brukast til nokre skrivebeskytta API-operasjonar og gir tilgang til Fever API og Google Reader API. Bruk skjemaet nedst på sida for å generere ein ny API-nøkkel"
#: src/components/settings/DisplaySettings.tsx #: src/components/settings/DisplaySettings.tsx
msgid "This setting can cause scrolling issues on some browsers (e.g. Safari)" msgid "This setting can cause scrolling issues on some browsers (e.g. Safari)"
@@ -1138,6 +1150,10 @@ msgstr "Nettstad"
msgid "Welcome! This appears to be the first time you're running CommaFeed. Please create an administrator account to get started." msgid "Welcome! This appears to be the first time you're running CommaFeed. Please create an administrator account to get started."
msgstr "Velkomen! Det ser ut til å vera fyrste gongen du køyrer CommaFeed. Ver venleg og opprett ein administratorkonto for å koma i gang." msgstr "Velkomen! Det ser ut til å vera fyrste gongen du køyrer CommaFeed. Ver venleg og opprett ein administratorkonto for å koma i gang."
#: src/app/client.ts
msgid "Wrong username or password"
msgstr ""
#: src/components/settings/DisplaySettings.tsx #: src/components/settings/DisplaySettings.tsx
msgid "Yellow" msgid "Yellow"
msgstr "Gul" msgstr "Gul"

View File

@@ -210,6 +210,10 @@ msgstr "Wersja rozszerzenia przeglądarki CommaFeed {browserExtensionVersion}."
msgid "CommaFeed is compatible with the Fever API. Use the following URL in your Fever-compatible mobile client. Login with your username and your <0>API key</0>." msgid "CommaFeed is compatible with the Fever API. Use the following URL in your Fever-compatible mobile client. Login with your username and your <0>API key</0>."
msgstr "CommaFeed jest kompatybilny z API Fever. Użyj następującego adresu URL w swoim kliencie mobilnym zgodnym z Fever. Zaloguj się przy użyciu swojej nazwy użytkownika i <0>klucza API</0>." msgstr "CommaFeed jest kompatybilny z API Fever. Użyj następującego adresu URL w swoim kliencie mobilnym zgodnym z Fever. Zaloguj się przy użyciu swojej nazwy użytkownika i <0>klucza API</0>."
#: src/components/settings/ProfileSettings.tsx
msgid "CommaFeed is compatible with the Google Reader API. Use the following URL in your Google Reader-compatible mobile client. Login with your username and your <0>API key</0>."
msgstr "CommaFeed jest kompatybilny z API Google Reader. Użyj następującego adresu URL w swoim kliencie mobilnym zgodnym z Google Reader. Zaloguj się przy użyciu swojej nazwy użytkownika i <0>klucza API</0>."
#: src/pages/app/AboutPage.tsx #: src/pages/app/AboutPage.tsx
msgid "CommaFeed next unread item" msgid "CommaFeed next unread item"
msgstr "Przecinek następny nieprzeczytany element" msgstr "Przecinek następny nieprzeczytany element"
@@ -454,6 +458,14 @@ msgstr "Przejdź do dokumentacji API."
msgid "Goodies" msgid "Goodies"
msgstr "Gadżety" msgstr "Gadżety"
#: src/components/settings/ProfileSettings.tsx
msgid "Google Reader API"
msgstr ""
#: src/components/settings/ProfileSettings.tsx
msgid "Google Reader API URL"
msgstr ""
#: src/components/settings/DisplaySettings.tsx #: src/components/settings/DisplaySettings.tsx
msgid "Grape" msgid "Grape"
msgstr "Winogronowy" msgstr "Winogronowy"
@@ -1065,8 +1077,8 @@ msgid "This feed has a legacy filter that cannot be edited and is not applied. P
msgstr "Ten kanał ma stary filtr, którego nie można edytować i który nie jest stosowany. Proszę utworzyć filtr ponownie, korzystając z nowego edytora wyrażeń. Stare wyrażenie filtra to: <0>{0}</0>" msgstr "Ten kanał ma stary filtr, którego nie można edytować i który nie jest stosowany. Proszę utworzyć filtr ponownie, korzystając z nowego edytora wyrażeń. Stare wyrażenie filtra to: <0>{0}</0>"
#: src/components/settings/ProfileSettings.tsx #: 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" msgid "This is your API key. It can be used for some read-only API operations and grants access to the Fever API and the Google Reader API. Use the form at the bottom of the page to generate a new API key"
msgstr "To jest Twój klucz API. Może być używany do niektórych operacji API tylko do odczytu i zapewnia dostęp do API Fever. Użyj formularza na dole strony, aby wygenerować nowy klucz API" msgstr "To jest Twój klucz API. Może być używany do niektórych operacji API tylko do odczytu i zapewnia dostęp do API Fever i API Google Reader. Użyj formularza na dole strony, aby wygenerować nowy klucz API"
#: src/components/settings/DisplaySettings.tsx #: src/components/settings/DisplaySettings.tsx
msgid "This setting can cause scrolling issues on some browsers (e.g. Safari)" msgid "This setting can cause scrolling issues on some browsers (e.g. Safari)"
@@ -1138,6 +1150,10 @@ msgstr "Strona internetowa"
msgid "Welcome! This appears to be the first time you're running CommaFeed. Please create an administrator account to get started." msgid "Welcome! This appears to be the first time you're running CommaFeed. Please create an administrator account to get started."
msgstr "Witaj! Wygląda na to, że uruchamiasz CommaFeed po raz pierwszy. Aby rozpocząć, utwórz konto administratora." msgstr "Witaj! Wygląda na to, że uruchamiasz CommaFeed po raz pierwszy. Aby rozpocząć, utwórz konto administratora."
#: src/app/client.ts
msgid "Wrong username or password"
msgstr ""
#: src/components/settings/DisplaySettings.tsx #: src/components/settings/DisplaySettings.tsx
msgid "Yellow" msgid "Yellow"
msgstr "Żółty" msgstr "Żółty"

View File

@@ -210,6 +210,10 @@ msgstr "Extensão de navegador CommaFeed versão {browserExtensionVersion}."
msgid "CommaFeed is compatible with the Fever API. Use the following URL in your Fever-compatible mobile client. Login with your username and your <0>API key</0>." msgid "CommaFeed is compatible with the Fever API. Use the following URL in your Fever-compatible mobile client. Login with your username and your <0>API key</0>."
msgstr "CommaFeed é compatível com a API Fever. Use a URL a seguir no seu aplicativo móvel compatível com Fever. Entre com seu nome de usuário e sua <0>chave de API</0>." msgstr "CommaFeed é compatível com a API Fever. Use a URL a seguir no seu aplicativo móvel compatível com Fever. Entre com seu nome de usuário e sua <0>chave de API</0>."
#: src/components/settings/ProfileSettings.tsx
msgid "CommaFeed is compatible with the Google Reader API. Use the following URL in your Google Reader-compatible mobile client. Login with your username and your <0>API key</0>."
msgstr "CommaFeed é compatível com a API Google Reader. Use a URL a seguir no seu aplicativo móvel compatível com Google Reader. Entre com seu nome de usuário e sua <0>chave de API</0>."
#: src/pages/app/AboutPage.tsx #: src/pages/app/AboutPage.tsx
msgid "CommaFeed next unread item" msgid "CommaFeed next unread item"
msgstr "CommaFeed próximo item não lido" msgstr "CommaFeed próximo item não lido"
@@ -454,6 +458,14 @@ msgstr "Vá para a documentação da API."
msgid "Goodies" msgid "Goodies"
msgstr "Brindes" msgstr "Brindes"
#: src/components/settings/ProfileSettings.tsx
msgid "Google Reader API"
msgstr ""
#: src/components/settings/ProfileSettings.tsx
msgid "Google Reader API URL"
msgstr ""
#: src/components/settings/DisplaySettings.tsx #: src/components/settings/DisplaySettings.tsx
msgid "Grape" msgid "Grape"
msgstr "Uva" msgstr "Uva"
@@ -1065,8 +1077,8 @@ msgid "This feed has a legacy filter that cannot be edited and is not applied. P
msgstr "Este feed tem um filtro legado que não pode ser editado e não é aplicado. Recrie o filtro usando o novo editor de expressões. A expressão do filtro legado era: <0>{0}</0>" msgstr "Este feed tem um filtro legado que não pode ser editado e não é aplicado. Recrie o filtro usando o novo editor de expressões. A expressão do filtro legado era: <0>{0}</0>"
#: src/components/settings/ProfileSettings.tsx #: 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" msgid "This is your API key. It can be used for some read-only API operations and grants access to the Fever API and the Google Reader 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" msgstr "Esta é sua chave de API. Ela pode ser usada para algumas operações de API somente leitura e concede acesso à API Fever e à API Google Reader. Use o formulário na parte inferior da página para gerar uma nova chave de API"
#: src/components/settings/DisplaySettings.tsx #: src/components/settings/DisplaySettings.tsx
msgid "This setting can cause scrolling issues on some browsers (e.g. Safari)" msgid "This setting can cause scrolling issues on some browsers (e.g. Safari)"
@@ -1138,6 +1150,10 @@ msgstr "Site"
msgid "Welcome! This appears to be the first time you're running CommaFeed. Please create an administrator account to get started." msgid "Welcome! This appears to be the first time you're running CommaFeed. Please create an administrator account to get started."
msgstr "Bem-vindo! Esta parece ser a primeira vez que você está executando o CommaFeed. Crie uma conta de administrador para começar." msgstr "Bem-vindo! Esta parece ser a primeira vez que você está executando o CommaFeed. Crie uma conta de administrador para começar."
#: src/app/client.ts
msgid "Wrong username or password"
msgstr ""
#: src/components/settings/DisplaySettings.tsx #: src/components/settings/DisplaySettings.tsx
msgid "Yellow" msgid "Yellow"
msgstr "Amarelo" msgstr "Amarelo"

View File

@@ -210,6 +210,10 @@ msgstr "Версия расширения браузера CommaFeed {browserExt
msgid "CommaFeed is compatible with the Fever API. Use the following URL in your Fever-compatible mobile client. Login with your username and your <0>API key</0>." msgid "CommaFeed is compatible with the Fever API. Use the following URL in your Fever-compatible mobile client. Login with your username and your <0>API key</0>."
msgstr "CommaFeed совместим с API Fever. Используйте следующий URL-адрес в мобильном клиенте, совместимом с Fever. Войдите в систему, указав свое имя пользователя и <0>API-ключ</0>." msgstr "CommaFeed совместим с API Fever. Используйте следующий URL-адрес в мобильном клиенте, совместимом с Fever. Войдите в систему, указав свое имя пользователя и <0>API-ключ</0>."
#: src/components/settings/ProfileSettings.tsx
msgid "CommaFeed is compatible with the Google Reader API. Use the following URL in your Google Reader-compatible mobile client. Login with your username and your <0>API key</0>."
msgstr "CommaFeed совместим с API Google Reader. Используйте следующий URL-адрес в мобильном клиенте, совместимом с Google Reader. Войдите в систему, указав свое имя пользователя и <0>API-ключ</0>."
#: src/pages/app/AboutPage.tsx #: src/pages/app/AboutPage.tsx
msgid "CommaFeed next unread item" msgid "CommaFeed next unread item"
msgstr "CommaFeed следующий непрочитанный элемент" msgstr "CommaFeed следующий непрочитанный элемент"
@@ -454,6 +458,14 @@ msgstr "Перейдите к документации по API."
msgid "Goodies" msgid "Goodies"
msgstr "Сладости" msgstr "Сладости"
#: src/components/settings/ProfileSettings.tsx
msgid "Google Reader API"
msgstr ""
#: src/components/settings/ProfileSettings.tsx
msgid "Google Reader API URL"
msgstr ""
#: src/components/settings/DisplaySettings.tsx #: src/components/settings/DisplaySettings.tsx
msgid "Grape" msgid "Grape"
msgstr "Виноградный" msgstr "Виноградный"
@@ -1065,8 +1077,8 @@ msgid "This feed has a legacy filter that cannot be edited and is not applied. P
msgstr "Этот фид имеет устаревший фильтр, который нельзя редактировать и который не применяется. Пожалуйста, создайте фильтр заново, используя новый редактор выражений. Устаревшее выражение фильтра было: <0>{0}</0>" msgstr "Этот фид имеет устаревший фильтр, который нельзя редактировать и который не применяется. Пожалуйста, создайте фильтр заново, используя новый редактор выражений. Устаревшее выражение фильтра было: <0>{0}</0>"
#: src/components/settings/ProfileSettings.tsx #: 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" msgid "This is your API key. It can be used for some read-only API operations and grants access to the Fever API and the Google Reader API. Use the form at the bottom of the page to generate a new API key"
msgstr "Это ваш ключ API. Он может использоваться для некоторых операций API только для чтения и предоставляет доступ к API Fever. Чтобы сгенерировать новый ключ API, воспользуйтесь формой в нижней части страницы" msgstr "Это ваш ключ API. Он может использоваться для некоторых операций API только для чтения и предоставляет доступ к API Fever и API Google Reader. Используйте форму внизу страницы, чтобы сгенерировать новый ключ API"
#: src/components/settings/DisplaySettings.tsx #: src/components/settings/DisplaySettings.tsx
msgid "This setting can cause scrolling issues on some browsers (e.g. Safari)" msgid "This setting can cause scrolling issues on some browsers (e.g. Safari)"
@@ -1138,6 +1150,10 @@ msgstr "Веб-сайт"
msgid "Welcome! This appears to be the first time you're running CommaFeed. Please create an administrator account to get started." msgid "Welcome! This appears to be the first time you're running CommaFeed. Please create an administrator account to get started."
msgstr "Добро пожаловать! Похоже, вы запускаете CommaFeed в первый раз. Пожалуйста, создайте учетную запись администратора, чтобы начать работу." msgstr "Добро пожаловать! Похоже, вы запускаете CommaFeed в первый раз. Пожалуйста, создайте учетную запись администратора, чтобы начать работу."
#: src/app/client.ts
msgid "Wrong username or password"
msgstr ""
#: src/components/settings/DisplaySettings.tsx #: src/components/settings/DisplaySettings.tsx
msgid "Yellow" msgid "Yellow"
msgstr "Желтый" msgstr "Желтый"

View File

@@ -210,6 +210,10 @@ msgstr "Rozšírenie prehliadača CommaFeed verzie {browserExtensionVersion}."
msgid "CommaFeed is compatible with the Fever API. Use the following URL in your Fever-compatible mobile client. Login with your username and your <0>API key</0>." msgid "CommaFeed is compatible with the Fever API. Use the following URL in your Fever-compatible mobile client. Login with your username and your <0>API key</0>."
msgstr "CommaFeed je kompatibilný s Fever API. Vo svojom mobilnom klientovi kompatibilnom s Fever použite nasledujúcu adresu URL. Prihláste sa pomocou svojho používateľského mena a <0>kľúča API</0>." msgstr "CommaFeed je kompatibilný s Fever API. Vo svojom mobilnom klientovi kompatibilnom s Fever použite nasledujúcu adresu URL. Prihláste sa pomocou svojho používateľského mena a <0>kľúča API</0>."
#: src/components/settings/ProfileSettings.tsx
msgid "CommaFeed is compatible with the Google Reader API. Use the following URL in your Google Reader-compatible mobile client. Login with your username and your <0>API key</0>."
msgstr "CommaFeed je kompatibilný s Google Reader API. Vo svojom mobilnom klientovi kompatibilnom s Google Reader použite nasledujúcu adresu URL. Prihláste sa pomocou svojho používateľského mena a <0>kľúča API</0>."
#: src/pages/app/AboutPage.tsx #: src/pages/app/AboutPage.tsx
msgid "CommaFeed next unread item" msgid "CommaFeed next unread item"
msgstr "CommaFeed ďalšia neprečítaná položka" msgstr "CommaFeed ďalšia neprečítaná položka"
@@ -454,6 +458,14 @@ msgstr "Prejdite na dokumentáciu API."
msgid "Goodies" msgid "Goodies"
msgstr "Dobrôtky" msgstr "Dobrôtky"
#: src/components/settings/ProfileSettings.tsx
msgid "Google Reader API"
msgstr ""
#: src/components/settings/ProfileSettings.tsx
msgid "Google Reader API URL"
msgstr ""
#: src/components/settings/DisplaySettings.tsx #: src/components/settings/DisplaySettings.tsx
msgid "Grape" msgid "Grape"
msgstr "Hroznová" msgstr "Hroznová"
@@ -1065,8 +1077,8 @@ msgid "This feed has a legacy filter that cannot be edited and is not applied. P
msgstr "Tento kanál má starší filter, ktorý sa nedá upravovať a nepoužíva sa. Znova vytvorte filter pomocou nového editora výrazov. Pôvodný výraz filtra bol: <0>{0}</0>" msgstr "Tento kanál má starší filter, ktorý sa nedá upravovať a nepoužíva sa. Znova vytvorte filter pomocou nového editora výrazov. Pôvodný výraz filtra bol: <0>{0}</0>"
#: src/components/settings/ProfileSettings.tsx #: 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" msgid "This is your API key. It can be used for some read-only API operations and grants access to the Fever API and the Google Reader API. Use the form at the bottom of the page to generate a new API key"
msgstr "Toto je váš kľúč API. Môže sa použiť na niektoré operácie rozhrania API len na čítanie a poskytuje prístup k rozhraniu Fever API. Na vygenerovanie nového kľúča API použite formulár v dolnej časti stránky." msgstr "Toto je váš kľúč API. Môže sa použiť pre niektoré operácie API iba na čítanie a udeľuje prístup k Fever API a Google Reader API. Na vygenerovanie nového kľúča API použite formulár v spodnej časti stránky"
#: src/components/settings/DisplaySettings.tsx #: src/components/settings/DisplaySettings.tsx
msgid "This setting can cause scrolling issues on some browsers (e.g. Safari)" msgid "This setting can cause scrolling issues on some browsers (e.g. Safari)"
@@ -1138,6 +1150,10 @@ msgstr "Webová stránka"
msgid "Welcome! This appears to be the first time you're running CommaFeed. Please create an administrator account to get started." msgid "Welcome! This appears to be the first time you're running CommaFeed. Please create an administrator account to get started."
msgstr "Vitajte! Zdá sa, že CommaFeed spúšťate prvýkrát. Ak chcete začať, vytvorte si účet správcu." msgstr "Vitajte! Zdá sa, že CommaFeed spúšťate prvýkrát. Ak chcete začať, vytvorte si účet správcu."
#: src/app/client.ts
msgid "Wrong username or password"
msgstr ""
#: src/components/settings/DisplaySettings.tsx #: src/components/settings/DisplaySettings.tsx
msgid "Yellow" msgid "Yellow"
msgstr "Žltá" msgstr "Žltá"

View File

@@ -210,6 +210,10 @@ msgstr "Webbläsartillägget CommaFeed version {browserExtensionVersion}."
msgid "CommaFeed is compatible with the Fever API. Use the following URL in your Fever-compatible mobile client. Login with your username and your <0>API key</0>." msgid "CommaFeed is compatible with the Fever API. Use the following URL in your Fever-compatible mobile client. Login with your username and your <0>API key</0>."
msgstr "CommaFeed är kompatibelt med Fever API. Använd följande URL i din Fever-kompatibla mobilklient. Logga in med ditt användarnamn och din <0>API-nyckel</0>." msgstr "CommaFeed är kompatibelt med Fever API. Använd följande URL i din Fever-kompatibla mobilklient. Logga in med ditt användarnamn och din <0>API-nyckel</0>."
#: src/components/settings/ProfileSettings.tsx
msgid "CommaFeed is compatible with the Google Reader API. Use the following URL in your Google Reader-compatible mobile client. Login with your username and your <0>API key</0>."
msgstr "CommaFeed är kompatibelt med Google Reader API. Använd följande URL i din Google Reader-kompatibla mobilklient. Logga in med ditt användarnamn och din <0>API-nyckel</0>."
#: src/pages/app/AboutPage.tsx #: src/pages/app/AboutPage.tsx
msgid "CommaFeed next unread item" msgid "CommaFeed next unread item"
msgstr "CommaFeed nästa olästa objekt" msgstr "CommaFeed nästa olästa objekt"
@@ -454,6 +458,14 @@ msgstr "Gå till API-dokumentationen."
msgid "Goodies" msgid "Goodies"
msgstr "Godsaker" msgstr "Godsaker"
#: src/components/settings/ProfileSettings.tsx
msgid "Google Reader API"
msgstr ""
#: src/components/settings/ProfileSettings.tsx
msgid "Google Reader API URL"
msgstr ""
#: src/components/settings/DisplaySettings.tsx #: src/components/settings/DisplaySettings.tsx
msgid "Grape" msgid "Grape"
msgstr "Lila" msgstr "Lila"
@@ -1065,8 +1077,8 @@ msgid "This feed has a legacy filter that cannot be edited and is not applied. P
msgstr "Detta flöde har ett gammalt filter som inte kan redigeras och inte tillämpas. Skapa filtret på nytt med den nya uttryckseditorern. Det gamla filteruttrycket var: <0>{0}</0>" msgstr "Detta flöde har ett gammalt filter som inte kan redigeras och inte tillämpas. Skapa filtret på nytt med den nya uttryckseditorern. Det gamla filteruttrycket var: <0>{0}</0>"
#: src/components/settings/ProfileSettings.tsx #: 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" msgid "This is your API key. It can be used for some read-only API operations and grants access to the Fever API and the Google Reader API. Use the form at the bottom of the page to generate a new API key"
msgstr "Detta är din API-nyckel. Den kan användas för vissa skrivskyddade API-operationer och ger åtkomst till Fever API. Använd formuläret längst ner på sidan för att generera en ny API-nyckel" msgstr "Det här är din API-nyckel. Den kan användas för vissa skrivskyddade API-åtgärder och ger åtkomst till Fever API och Google Reader API. Använd formuläret längst ner på sidan för att generera en ny API-nyckel"
#: src/components/settings/DisplaySettings.tsx #: src/components/settings/DisplaySettings.tsx
msgid "This setting can cause scrolling issues on some browsers (e.g. Safari)" msgid "This setting can cause scrolling issues on some browsers (e.g. Safari)"
@@ -1138,6 +1150,10 @@ msgstr "Webbplats"
msgid "Welcome! This appears to be the first time you're running CommaFeed. Please create an administrator account to get started." msgid "Welcome! This appears to be the first time you're running CommaFeed. Please create an administrator account to get started."
msgstr "Välkommen! Det verkar vara första gången du kör CommaFeed. Skapa ett administratörskonto för att komma igång." msgstr "Välkommen! Det verkar vara första gången du kör CommaFeed. Skapa ett administratörskonto för att komma igång."
#: src/app/client.ts
msgid "Wrong username or password"
msgstr ""
#: src/components/settings/DisplaySettings.tsx #: src/components/settings/DisplaySettings.tsx
msgid "Yellow" msgid "Yellow"
msgstr "Gul" msgstr "Gul"

View File

@@ -210,6 +210,10 @@ msgstr "CommaFeed tarayıcı eklentisi sürüm {browserExtensionVersion}."
msgid "CommaFeed is compatible with the Fever API. Use the following URL in your Fever-compatible mobile client. Login with your username and your <0>API key</0>." msgid "CommaFeed is compatible with the Fever API. Use the following URL in your Fever-compatible mobile client. Login with your username and your <0>API key</0>."
msgstr "CommaFeed, Fever API ile uyumludur. Fever uyumlu mobil istemcinizde aşağıdaki URL'yi kullanın. Kullanıcı adınız ve <0>API anahtarınız</0> ile giriş yapın." msgstr "CommaFeed, Fever API ile uyumludur. Fever uyumlu mobil istemcinizde aşağıdaki URL'yi kullanın. Kullanıcı adınız ve <0>API anahtarınız</0> ile giriş yapın."
#: src/components/settings/ProfileSettings.tsx
msgid "CommaFeed is compatible with the Google Reader API. Use the following URL in your Google Reader-compatible mobile client. Login with your username and your <0>API key</0>."
msgstr "CommaFeed, Google Reader API ile uyumludur. Google Reader uyumlu mobil istemcinizde aşağıdaki URL'yi kullanın. Kullanıcı adınız ve <0>API anahtarınız</0> ile giriş yapın."
#: src/pages/app/AboutPage.tsx #: src/pages/app/AboutPage.tsx
msgid "CommaFeed next unread item" msgid "CommaFeed next unread item"
msgstr "CommaFeed sonraki okunmamış öğe" msgstr "CommaFeed sonraki okunmamış öğe"
@@ -454,6 +458,14 @@ msgstr "API belgelerine gidin."
msgid "Goodies" msgid "Goodies"
msgstr "İyilikler" msgstr "İyilikler"
#: src/components/settings/ProfileSettings.tsx
msgid "Google Reader API"
msgstr ""
#: src/components/settings/ProfileSettings.tsx
msgid "Google Reader API URL"
msgstr ""
#: src/components/settings/DisplaySettings.tsx #: src/components/settings/DisplaySettings.tsx
msgid "Grape" msgid "Grape"
msgstr "Üzüm" msgstr "Üzüm"
@@ -1065,8 +1077,8 @@ msgid "This feed has a legacy filter that cannot be edited and is not applied. P
msgstr "Bu beslemenin düzenlenemeyen ve uygulanmayan eski bir filtresi var. Lütfen yeni ifade düzenleyiciyi kullanarak filtreyi yeniden oluşturun. Eski filtre ifadesi şuydu: <0>{0}</0>" msgstr "Bu beslemenin düzenlenemeyen ve uygulanmayan eski bir filtresi var. Lütfen yeni ifade düzenleyiciyi kullanarak filtreyi yeniden oluşturun. Eski filtre ifadesi şuydu: <0>{0}</0>"
#: src/components/settings/ProfileSettings.tsx #: 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" msgid "This is your API key. It can be used for some read-only API operations and grants access to the Fever API and the Google Reader API. Use the form at the bottom of the page to generate a new API key"
msgstr "Bu sizin API anahtarınızdır. Bazı salt okunur API işlemleri için kullanılabilir ve Fever API'sine erişim sağlar. Yeni bir API anahtarı oluşturmak için sayfanın altındaki formu kullanın" msgstr "Bu, API anahtarınızdır. Bazı salt okunur API işlemleri için kullanılabilir ve Fever API ile Google Reader API'ye erişim sağlar. Yeni bir API anahtarı oluşturmak için sayfanın altındaki formu kullanın"
#: src/components/settings/DisplaySettings.tsx #: src/components/settings/DisplaySettings.tsx
msgid "This setting can cause scrolling issues on some browsers (e.g. Safari)" msgid "This setting can cause scrolling issues on some browsers (e.g. Safari)"
@@ -1138,6 +1150,10 @@ msgstr "Web sitesi"
msgid "Welcome! This appears to be the first time you're running CommaFeed. Please create an administrator account to get started." msgid "Welcome! This appears to be the first time you're running CommaFeed. Please create an administrator account to get started."
msgstr "Hoş geldiniz! Görünüşe göre CommaFeed'i ilk kez çalıştırıyorsunuz. Başlamak için lütfen bir yönetici hesabı oluşturun." msgstr "Hoş geldiniz! Görünüşe göre CommaFeed'i ilk kez çalıştırıyorsunuz. Başlamak için lütfen bir yönetici hesabı oluşturun."
#: src/app/client.ts
msgid "Wrong username or password"
msgstr ""
#: src/components/settings/DisplaySettings.tsx #: src/components/settings/DisplaySettings.tsx
msgid "Yellow" msgid "Yellow"
msgstr "Sarı" msgstr "Sarı"

View File

@@ -210,6 +210,10 @@ msgstr "CommaFeed的浏览器扩展版本:{browserExtensionVersion}。"
msgid "CommaFeed is compatible with the Fever API. Use the following URL in your Fever-compatible mobile client. Login with your username and your <0>API key</0>." msgid "CommaFeed is compatible with the Fever API. Use the following URL in your Fever-compatible mobile client. Login with your username and your <0>API key</0>."
msgstr "CommaFeed 和 Fever API 兼容,可以在您Fever兼容的移动客户端中使用以下网址,通过用户名和<0>API密钥</0>登录。" msgstr "CommaFeed 和 Fever API 兼容,可以在您Fever兼容的移动客户端中使用以下网址,通过用户名和<0>API密钥</0>登录。"
#: src/components/settings/ProfileSettings.tsx
msgid "CommaFeed is compatible with the Google Reader API. Use the following URL in your Google Reader-compatible mobile client. Login with your username and your <0>API key</0>."
msgstr "CommaFeed 和 Google Reader API 兼容,可以在您Google Reader兼容的移动客户端中使用以下网址,通过用户名和<0>API密钥</0>登录。"
#: src/pages/app/AboutPage.tsx #: src/pages/app/AboutPage.tsx
msgid "CommaFeed next unread item" msgid "CommaFeed next unread item"
msgstr "CommaFeed 下一个未读项目" msgstr "CommaFeed 下一个未读项目"
@@ -454,6 +458,14 @@ msgstr "API 文档"
msgid "Goodies" msgid "Goodies"
msgstr "好东西" msgstr "好东西"
#: src/components/settings/ProfileSettings.tsx
msgid "Google Reader API"
msgstr ""
#: src/components/settings/ProfileSettings.tsx
msgid "Google Reader API URL"
msgstr ""
#: src/components/settings/DisplaySettings.tsx #: src/components/settings/DisplaySettings.tsx
msgid "Grape" msgid "Grape"
msgstr "深紫" msgstr "深紫"
@@ -1065,8 +1077,8 @@ msgid "This feed has a legacy filter that cannot be edited and is not applied. P
msgstr "此信息流具有无法编辑且未应用的旧版过滤器。请使用新的表达式编辑器重新创建过滤器。旧版过滤器表达式为:<0>{0}</0>" msgstr "此信息流具有无法编辑且未应用的旧版过滤器。请使用新的表达式编辑器重新创建过滤器。旧版过滤器表达式为:<0>{0}</0>"
#: src/components/settings/ProfileSettings.tsx #: 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" msgid "This is your API key. It can be used for some read-only API operations and grants access to the Fever API and the Google Reader API. Use the form at the bottom of the page to generate a new API key"
msgstr "这是您的API 密钥,它可以被用于Fever API的只读操作及访问授权。使用页面底部的表单生成一个新的API密钥。" msgstr "这是您的 API 密钥。它可用于某些只读 API 操作,并授予对 Fever API 和 Google Reader API 的访问权限。使用页面底部的表单生成新的 API 密钥"
#: src/components/settings/DisplaySettings.tsx #: src/components/settings/DisplaySettings.tsx
msgid "This setting can cause scrolling issues on some browsers (e.g. Safari)" msgid "This setting can cause scrolling issues on some browsers (e.g. Safari)"
@@ -1138,6 +1150,10 @@ msgstr "网站"
msgid "Welcome! This appears to be the first time you're running CommaFeed. Please create an administrator account to get started." msgid "Welcome! This appears to be the first time you're running CommaFeed. Please create an administrator account to get started."
msgstr "欢迎!当前页仅当您第一次使用CommaFeed时出现,请创建一个管理员帐号以开始使用。" msgstr "欢迎!当前页仅当您第一次使用CommaFeed时出现,请创建一个管理员帐号以开始使用。"
#: src/app/client.ts
msgid "Wrong username or password"
msgstr "用户名或密码错误"
#: src/components/settings/DisplaySettings.tsx #: src/components/settings/DisplaySettings.tsx
msgid "Yellow" msgid "Yellow"
msgstr "黄" msgstr "黄"

View File

@@ -15,9 +15,10 @@ dayjs.extend(relativeTime)
dayjs.extend(duration) dayjs.extend(duration)
const root = document.getElementById("root") const root = document.getElementById("root")
root && if (!root) throw new Error("root element not found")
ReactDOM.createRoot(root).render(
<Provider store={store}> ReactDOM.createRoot(root).render(
<App /> <Provider store={store}>
</Provider> <App />
) </Provider>
)

View File

@@ -6,15 +6,15 @@
<parent> <parent>
<groupId>com.commafeed</groupId> <groupId>com.commafeed</groupId>
<artifactId>commafeed</artifactId> <artifactId>commafeed</artifactId>
<version>7.2.1</version> <version>7.3.2</version>
</parent> </parent>
<artifactId>commafeed-server</artifactId> <artifactId>commafeed-server</artifactId>
<name>CommaFeed Server</name> <name>CommaFeed Server</name>
<packaging>quarkus</packaging> <packaging>quarkus</packaging>
<properties> <properties>
<quarkus.version>3.38.0</quarkus.version> <quarkus.version>3.38.3</quarkus.version>
<querydsl.version>7.5</querydsl.version> <querydsl.version>7.6</querydsl.version>
<rome.version>2.1.0</rome.version> <rome.version>2.1.0</rome.version>
<!-- renovate: depName=com.google.googlejavaformat:google-java-format --> <!-- renovate: depName=com.google.googlejavaformat:google-java-format -->
<google-java-format.version>1.36.1</google-java-format.version> <google-java-format.version>1.36.1</google-java-format.version>
@@ -35,7 +35,7 @@
<dependency> <dependency>
<groupId>com.google.protobuf</groupId> <groupId>com.google.protobuf</groupId>
<artifactId>protobuf-java</artifactId> <artifactId>protobuf-java</artifactId>
<version>4.35.1</version> <version>4.36.0</version>
</dependency> </dependency>
</dependencies> </dependencies>
</dependencyManagement> </dependencyManagement>
@@ -245,7 +245,7 @@
<dependency> <dependency>
<groupId>com.puppycrawl.tools</groupId> <groupId>com.puppycrawl.tools</groupId>
<artifactId>checkstyle</artifactId> <artifactId>checkstyle</artifactId>
<version>13.9.0</version> <version>14.0.0</version>
</dependency> </dependency>
</dependencies> </dependencies>
<executions> <executions>
@@ -274,7 +274,7 @@
<plugin> <plugin>
<groupId>com.diffplug.spotless</groupId> <groupId>com.diffplug.spotless</groupId>
<artifactId>spotless-maven-plugin</artifactId> <artifactId>spotless-maven-plugin</artifactId>
<version>3.9.0</version> <version>3.10.0</version>
<?m2e ignore?> <?m2e ignore?>
<executions> <executions>
<execution> <execution>
@@ -302,7 +302,7 @@
<dependency> <dependency>
<groupId>com.commafeed</groupId> <groupId>com.commafeed</groupId>
<artifactId>commafeed-client</artifactId> <artifactId>commafeed-client</artifactId>
<version>7.2.1</version> <version>7.3.2</version>
</dependency> </dependency>
<!-- compile-time processors --> <!-- compile-time processors -->
@@ -389,7 +389,7 @@
<dependency> <dependency>
<groupId>org.apache.commons</groupId> <groupId>org.apache.commons</groupId>
<artifactId>commons-collections4</artifactId> <artifactId>commons-collections4</artifactId>
<version>4.5.0</version> <version>4.6.0</version>
</dependency> </dependency>
<dependency> <dependency>
<groupId>org.apache.commons</groupId> <groupId>org.apache.commons</groupId>
@@ -399,7 +399,7 @@
<dependency> <dependency>
<groupId>dev.cel</groupId> <groupId>dev.cel</groupId>
<artifactId>cel</artifactId> <artifactId>cel</artifactId>
<version>0.13.1</version> <version>0.14.0</version>
</dependency> </dependency>
<dependency> <dependency>
<groupId>org.passay</groupId> <groupId>org.passay</groupId>
@@ -449,7 +449,7 @@
<dependency> <dependency>
<groupId>org.apache.httpcomponents.client5</groupId> <groupId>org.apache.httpcomponents.client5</groupId>
<artifactId>httpclient5</artifactId> <artifactId>httpclient5</artifactId>
<version>5.6.3</version> <version>5.6.4</version>
</dependency> </dependency>
<dependency> <dependency>
<groupId>io.github.hakky54</groupId> <groupId>io.github.hakky54</groupId>
@@ -482,7 +482,7 @@
<dependency> <dependency>
<groupId>org.mock-server</groupId> <groupId>org.mock-server</groupId>
<artifactId>mockserver-junit-jupiter</artifactId> <artifactId>mockserver-junit-jupiter</artifactId>
<version>7.5.0</version> <version>7.6.0</version>
<scope>test</scope> <scope>test</scope>
</dependency> </dependency>
<dependency> <dependency>

View File

@@ -1,4 +1,4 @@
FROM ibm-semeru-runtimes:open-jdk-25.0.3.0-jre@sha256:43ec947fee9c4c2ed880620ae9e78911ac5eef66391e5a2c43f956dcc9000491 FROM ibm-semeru-runtimes:open-jdk-25.0.4.0-jre@sha256:524f0d04723aff42ed4142ab2621fa55f22cb691763cd75b59eee86749e2e872
EXPOSE 8082 EXPOSE 8082
RUN mkdir -p /commafeed/data RUN mkdir -p /commafeed/data

View File

@@ -1,4 +1,4 @@
FROM debian:13.6@sha256:fac46bff2e02f51425b6e33b0e1169f55dfb053d83511ca28aa50c09fd5ed7a4 FROM debian:13.6@sha256:34cd9e9fd437c0a095ec39cb2e73422c9f30821b0d0848ed74fd0d43bae4d958
ARG TARGETARCH ARG TARGETARCH
EXPOSE 8082 EXPOSE 8082

View File

@@ -93,3 +93,12 @@ Tags are of the form `<version>-<database>[-jvm]` where:
- `master` (always points to the latest git commit) - `master` (always points to the latest git commit)
- `<database>` is the database to use (`h2`, `postgresql`, `mysql` or `mariadb`) - `<database>` is the database to use (`h2`, `postgresql`, `mysql` or `mariadb`)
- `-jvm` is optional and indicates that CommaFeed is running on a JVM, and not compiled natively. - `-jvm` is optional and indicates that CommaFeed is running on a JVM, and not compiled natively.
## FAQ
### Getting "Access to local address blocked" when adding a feed
CommaFeed blocks access to local resources by default to prevent [SSRF](https://en.wikipedia.org/wiki/Server-side_request_forgery) attacks.
If you want to subscribe to feeds that are only available on your local network, you can disable this security measure by setting the `COMMAFEED_HTTP_CLIENT_BLOCK_LOCAL_ADDRESSES` variable to `false`.
Do this only if you trust all users of your CommaFeed instance not to access private resources.

View File

@@ -14,6 +14,8 @@ import jakarta.inject.Singleton;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.SystemProperties;
@Slf4j @Slf4j
@Singleton @Singleton
@RequiredArgsConstructor @RequiredArgsConstructor
@@ -26,6 +28,12 @@ public class CommaFeedApplication {
public void start(@Observes StartupEvent ev) { public void start(@Observes StartupEvent ev) {
log.info("starting up..."); log.info("starting up...");
// disable entity expansion limits added in JDK24+ (#1961)
// we already strip doctype declarations in XMLCleaner to prevent xxe attacks
// we also already limit the size of feeds we download in HttpGetter
System.setProperty(SystemProperties.JDK_XML_MAX_GENERAL_ENTITY_SIZE_LIMIT, "0");
System.setProperty(SystemProperties.JDK_XML_TOTAL_ENTITY_SIZE_LIMIT, "0");
PasswordConstraintValidator.setMinimumPasswordLength( PasswordConstraintValidator.setMinimumPasswordLength(
config.users().minimumPasswordLength()); config.users().minimumPasswordLength());
@@ -33,6 +41,11 @@ public class CommaFeedApplication {
ImageProxyUrl.generateKey(); ImageProxyUrl.generateKey();
} }
if (config.passwordRecoveryEnabled() && config.passwordRecoveryPublicBaseUrl().isEmpty()) {
throw new IllegalStateException(
"password recovery is enabled but no public base url is set");
}
feedRefreshEngine.start(); feedRefreshEngine.start();
taskScheduler.start(); taskScheduler.start();
} }

View File

@@ -49,6 +49,13 @@ public interface CommaFeedConfiguration {
@WithDefault("false") @WithDefault("false")
boolean passwordRecoveryEnabled(); boolean passwordRecoveryEnabled();
/**
* The base URL of the application to use in the password recovery link in the email. We can't
* use the URL sent by the browser of the user because of malicious Host header injection
* attacks.
*/
Optional<String> passwordRecoveryPublicBaseUrl();
/** Message displayed in a notification at the bottom of the page. */ /** Message displayed in a notification at the bottom of the page. */
Optional<String> announcement(); Optional<String> announcement();
@@ -130,7 +137,7 @@ public interface CommaFeedConfiguration {
* <p>You may want to enable this if you host a public instance of CommaFeed with * <p>You may want to enable this if you host a public instance of CommaFeed with
* registrations open. * registrations open.
*/ */
@WithDefault("false") @WithDefault("true")
boolean blockLocalAddresses(); boolean blockLocalAddresses();
/** HTTP client cache configuration */ /** HTTP client cache configuration */

View File

@@ -1,5 +1,7 @@
package com.commafeed; package com.commafeed;
import com.commafeed.frontend.exception.CommaFeedApplicationException;
import com.commafeed.frontend.exception.CommaFeedExceptionType;
import com.commafeed.security.CookieService; import com.commafeed.security.CookieService;
import io.quarkus.runtime.annotations.RegisterForReflection; import io.quarkus.runtime.annotations.RegisterForReflection;
@@ -26,6 +28,18 @@ public class ExceptionMappers {
private final CookieService cookieService; private final CookieService cookieService;
private final CommaFeedConfiguration config; private final CommaFeedConfiguration config;
@ServerExceptionMapper(CommaFeedApplicationException.class)
public RestResponse<CommaFeedApplicationError> applicationError(
CommaFeedApplicationException e) {
ResponseBuilder<CommaFeedApplicationError> response =
ResponseBuilder.create(
e.type().status(), new CommaFeedApplicationError(e.type(), e.getMessage()));
if (e.type().status() == Status.UNAUTHORIZED) {
response.cookie(cookieService.buildLogoutCookie());
}
return response.build();
}
@ServerExceptionMapper(UnauthorizedException.class) @ServerExceptionMapper(UnauthorizedException.class)
public RestResponse<UnauthorizedResponse> unauthorized(UnauthorizedException e) { public RestResponse<UnauthorizedResponse> unauthorized(UnauthorizedException e) {
return RestResponse.status( return RestResponse.status(
@@ -55,4 +69,7 @@ public class ExceptionMappers {
@RegisterForReflection @RegisterForReflection
public record ValidationFailed(String message) {} public record ValidationFailed(String message) {}
@RegisterForReflection
public record CommaFeedApplicationError(CommaFeedExceptionType type, String message) {}
} }

View File

@@ -7,6 +7,8 @@ import com.google.common.net.HttpHeaders;
import inet.ipaddr.IPAddress; import inet.ipaddr.IPAddress;
import inet.ipaddr.IPAddressNetwork; import inet.ipaddr.IPAddressNetwork;
import inet.ipaddr.IPAddressString; import inet.ipaddr.IPAddressString;
import inet.ipaddr.ipv4.IPv4Address;
import inet.ipaddr.ipv6.IPv6Address;
import jakarta.inject.Singleton; import jakarta.inject.Singleton;
@@ -137,8 +139,38 @@ public class HttpClientFactory {
} }
private static boolean isLocalAddress(InetAddress address) { private static boolean isLocalAddress(InetAddress address) {
IPAddress ip = new IPAddressNetwork.IPAddressGenerator().from(address); return isLocalAddress(new IPAddressNetwork.IPAddressGenerator().from(address));
return ip.isLocal() || ip.isLoopback() || ip.isMulticast() || CGNAT_RANGE.contains(ip); }
private static boolean isLocalAddress(IPAddress ip) {
if (ip.isLocal() || ip.isLoopback() || ip.isMulticast() || CGNAT_RANGE.contains(ip)) {
return true;
}
if (!ip.isIPv6()) {
return false;
}
// IPv6 transition mechanisms embed an IPv4 address that must be validated too, otherwise
// they could be used to smuggle a blocked IPv4 target past the IPv6-only checks above
IPv6Address ipv6 = ip.toIPv6();
if (ipv6.isIPv4Mapped() || ipv6.isIPv4Compatible() || ipv6.isWellKnownIPv4Translatable()) {
// IPv4-mapped (::ffff:x.x.x.x), IPv4-compatible (::x.x.x.x) and NAT64
// (64:ff9b::/96, RFC 6052) addresses all embed the IPv4 address in the lowest 32 bits
return isLocalAddress(ipv6.getEmbeddedIPv4Address());
}
if (ipv6.is6To4()) {
// 6to4 (2002::/16, RFC 3056) embeds the IPv4 address in bits 16-47
return isLocalAddress(ipv6.get6To4IPv4Address());
}
if (ipv6.isTeredo()) {
// Teredo (2001::/32, RFC 4380) embeds the IPv4 address in the lowest 32 bits,
// obfuscated with a bitwise complement
IPv4Address obfuscated = ipv6.getEmbeddedIPv4Address();
return isLocalAddress(new IPv4Address(~obfuscated.intValue()));
}
return false;
} }
private record BlockLocalAddressesDnsResolver(DnsResolver delegate) implements DnsResolver { private record BlockLocalAddressesDnsResolver(DnsResolver delegate) implements DnsResolver {

View File

@@ -23,7 +23,6 @@ import jakarta.inject.Singleton;
import org.apache.commons.lang3.ArrayUtils; import org.apache.commons.lang3.ArrayUtils;
import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.SystemProperties;
import org.apache.commons.math3.stat.descriptive.SummaryStatistics; import org.apache.commons.math3.stat.descriptive.SummaryStatistics;
import org.jdom2.Element; import org.jdom2.Element;
import org.jdom2.Namespace; import org.jdom2.Namespace;
@@ -58,12 +57,6 @@ public class FeedParser {
public FeedParser(EncodingDetector encodingDetector, XMLCleaner xmlCleaner) { public FeedParser(EncodingDetector encodingDetector, XMLCleaner xmlCleaner) {
this.encodingDetector = encodingDetector; this.encodingDetector = encodingDetector;
this.xmlCleaner = xmlCleaner; this.xmlCleaner = xmlCleaner;
// disable entity expansion limits added in JDK24+ (#1961)
// we already strip doctype declarations in XMLCleaner to prevent xxe attacks
// we also already limit the size of feeds we download in HttpGetter
System.setProperty(SystemProperties.JDK_XML_MAX_GENERAL_ENTITY_SIZE_LIMIT, "0");
System.setProperty(SystemProperties.JDK_XML_TOTAL_ENTITY_SIZE_LIMIT, "0");
} }
public FeedParserResult parse(String feedUrl, byte[] xml) throws FeedParsingException { public FeedParserResult parse(String feedUrl, byte[] xml) throws FeedParsingException {

View File

@@ -98,15 +98,14 @@ public class FeedEntryService {
} }
} }
public void starEntry(User user, Long entryId, Long subscriptionId, boolean starred) { public void starEntry(User user, Long entryId, boolean starred) {
FeedEntry entry = feedEntryDAO.findById(entryId);
FeedSubscription sub = feedSubscriptionDAO.findById(user, subscriptionId); if (entry == null) {
if (sub == null) {
return; return;
} }
FeedEntry entry = feedEntryDAO.findById(entryId); FeedSubscription sub = feedSubscriptionDAO.findByFeed(user, entry.getFeed());
if (entry == null) { if (sub == null) {
return; return;
} }

View File

@@ -2,8 +2,10 @@ package com.commafeed.backend.service;
import com.commafeed.backend.dao.FeedEntryDAO; import com.commafeed.backend.dao.FeedEntryDAO;
import com.commafeed.backend.dao.FeedEntryTagDAO; import com.commafeed.backend.dao.FeedEntryTagDAO;
import com.commafeed.backend.dao.FeedSubscriptionDAO;
import com.commafeed.backend.model.FeedEntry; import com.commafeed.backend.model.FeedEntry;
import com.commafeed.backend.model.FeedEntryTag; import com.commafeed.backend.model.FeedEntryTag;
import com.commafeed.backend.model.FeedSubscription;
import com.commafeed.backend.model.User; import com.commafeed.backend.model.User;
import jakarta.inject.Singleton; import jakarta.inject.Singleton;
@@ -20,6 +22,7 @@ public class FeedEntryTagService {
private final FeedEntryDAO feedEntryDAO; private final FeedEntryDAO feedEntryDAO;
private final FeedEntryTagDAO feedEntryTagDAO; private final FeedEntryTagDAO feedEntryTagDAO;
private final FeedSubscriptionDAO feedSubscriptionDAO;
public void updateTags(User user, Long entryId, List<String> tagNames) { public void updateTags(User user, Long entryId, List<String> tagNames) {
FeedEntry entry = feedEntryDAO.findById(entryId); FeedEntry entry = feedEntryDAO.findById(entryId);
@@ -27,6 +30,11 @@ public class FeedEntryTagService {
return; return;
} }
FeedSubscription sub = feedSubscriptionDAO.findByFeed(user, entry.getFeed());
if (sub == null) {
return;
}
List<FeedEntryTag> existingTags = feedEntryTagDAO.findByEntry(user, entry); List<FeedEntryTag> existingTags = feedEntryTagDAO.findByEntry(user, entry);
Set<String> existingTagNames = Set<String> existingTagNames =
existingTags.stream().map(FeedEntryTag::getName).collect(Collectors.toSet()); existingTags.stream().map(FeedEntryTag::getName).collect(Collectors.toSet());

View File

@@ -0,0 +1,20 @@
package com.commafeed.frontend.exception;
import java.io.Serial;
import java.util.Objects;
public class CommaFeedApplicationException extends RuntimeException {
@Serial private static final long serialVersionUID = 1L;
private final CommaFeedExceptionType type;
public CommaFeedApplicationException(CommaFeedExceptionType type) {
super(Objects.requireNonNull(type).message());
this.type = type;
}
public CommaFeedExceptionType type() {
return type;
}
}

View File

@@ -0,0 +1,23 @@
package com.commafeed.frontend.exception;
import org.jboss.resteasy.reactive.RestResponse.Status;
public enum CommaFeedExceptionType {
WRONG_USERNAME_OR_PASSWORD(Status.UNAUTHORIZED, "wrong username or password");
private final Status status;
private final String message;
CommaFeedExceptionType(Status status, String message) {
this.status = status;
this.message = message;
}
public Status status() {
return status;
}
public String message() {
return message;
}
}

View File

@@ -19,9 +19,6 @@ public class StarRequest implements Serializable {
@Size(max = 128) @Size(max = 128)
private String id; private String id;
@Schema(description = "feed id", required = true)
private Long feedId;
@Schema(description = "starred or not", required = true) @Schema(description = "starred or not", required = true)
private boolean starred; private boolean starred;
} }

View File

@@ -89,11 +89,9 @@ public class EntryREST {
@Valid @Parameter(description = "Star Request", required = true) StarRequest req) { @Valid @Parameter(description = "Star Request", required = true) StarRequest req) {
Preconditions.checkNotNull(req); Preconditions.checkNotNull(req);
Preconditions.checkNotNull(req.getId()); Preconditions.checkNotNull(req.getId());
Preconditions.checkNotNull(req.getFeedId());
User user = authenticationContext.getCurrentUser(); User user = authenticationContext.getCurrentUser();
feedEntryService.starEntry( feedEntryService.starEntry(user, Long.valueOf(req.getId()), req.isStarred());
user, Long.valueOf(req.getId()), req.getFeedId(), req.isStarred());
return Response.ok().build(); return Response.ok().build();
} }

View File

@@ -413,7 +413,8 @@ public class UserREST {
} }
private String buildEmailContent(User user) throws URISyntaxException { private String buildEmailContent(User user) throws URISyntaxException {
String publicUrl = Urls.removeTrailingSlash(uri.getBaseUri().toString()); String publicUrl =
Urls.removeTrailingSlash(config.passwordRecoveryPublicBaseUrl().orElseThrow());
return String.format( return String.format(
"You asked for password recovery for account '%s', <a href='%s'>follow this link</a> to change your password. Ignore this if you didn't request a password recovery.", "You asked for password recovery for account '%s', <a href='%s'>follow this link</a> to change your password. Ignore this if you didn't request a password recovery.",
user.getName(), callbackUrl(user, publicUrl)); user.getName(), callbackUrl(user, publicUrl));

View File

@@ -388,9 +388,7 @@ public class FeverREST {
if ("read".equals(action) || "unread".equals(action)) { if ("read".equals(action) || "unread".equals(action)) {
feedEntryService.markEntry(user, id, "read".equals(action)); feedEntryService.markEntry(user, id, "read".equals(action));
} else if ("saved".equals(action) || "unsaved".equals(action)) { } else if ("saved".equals(action) || "unsaved".equals(action)) {
FeedEntry entry = feedEntryDAO.findById(id); feedEntryService.starEntry(user, id, "saved".equals(action));
FeedSubscription sub = feedSubscriptionDAO.findByFeed(user, entry.getFeed());
feedEntryService.starEntry(user, id, sub.getId(), "saved".equals(action));
} }
} else if ("feed".equals(source)) { } else if ("feed".equals(source)) {
FeedSubscription subscription = feedSubscriptionDAO.findById(user, id); FeedSubscription subscription = feedSubscriptionDAO.findById(user, id);

View File

@@ -0,0 +1,229 @@
package com.commafeed.frontend.resource.googlereader;
import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.annotation.JsonInclude.Include;
import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.util.ArrayList;
import java.util.List;
@JsonInclude(Include.NON_NULL)
public class GoogleReaderModel {
private GoogleReaderModel() {}
@Data
public static class UserInfo {
@JsonProperty("userId")
private String userId;
@JsonProperty("userName")
private String userName;
@JsonProperty("userProfileId")
private String userProfileId;
@JsonProperty("userEmail")
private String userEmail;
}
@Data
public static class SubscriptionList {
@JsonProperty("subscriptions")
private List<Subscription> subscriptions = new ArrayList<>();
}
@Data
public static class Subscription {
@JsonProperty("id")
private String id;
@JsonProperty("title")
private String title;
@JsonProperty("categories")
private List<Category> categories = new ArrayList<>();
@JsonProperty("url")
private String url;
@JsonProperty("htmlUrl")
private String htmlUrl;
@JsonProperty("iconUrl")
private String iconUrl;
}
@Data
@NoArgsConstructor
@AllArgsConstructor
public static class Category {
@JsonProperty("id")
private String id;
@JsonProperty("label")
private String label;
}
@Data
public static class TagList {
@JsonProperty("tags")
private List<Tag> tags = new ArrayList<>();
}
@Data
@NoArgsConstructor
@AllArgsConstructor
public static class Tag {
@JsonProperty("id")
private String id;
}
@Data
public static class StreamContents {
@JsonProperty("id")
private String id;
@JsonProperty("title")
private String title;
@JsonProperty("updated")
private long updated;
@JsonProperty("items")
private List<StreamItem> items = new ArrayList<>();
@JsonProperty("continuation")
private String continuation;
}
@Data
public static class StreamItem {
@JsonProperty("id")
private String id;
@JsonProperty("crawlTimeMsec")
private String crawlTimeMsec;
@JsonProperty("timestampUsec")
private String timestampUsec;
@JsonProperty("published")
private long published;
@JsonProperty("updated")
private long updated;
@JsonProperty("title")
private String title;
@JsonProperty("author")
private String author;
@JsonProperty("categories")
private List<String> categories = new ArrayList<>();
@JsonProperty("canonical")
private List<HrefEntry> canonical = new ArrayList<>();
@JsonProperty("alternate")
private List<HrefEntry> alternate = new ArrayList<>();
@JsonProperty("summary")
private Content summary;
@JsonProperty("origin")
private Origin origin;
}
@Data
@NoArgsConstructor
@AllArgsConstructor
public static class HrefEntry {
@JsonProperty("href")
private String href;
@JsonProperty("type")
private String type;
}
@Data
@NoArgsConstructor
@AllArgsConstructor
public static class Content {
@JsonProperty("content")
private String content;
@JsonProperty("direction")
private String direction;
}
@Data
public static class Origin {
@JsonProperty("streamId")
private String streamId;
@JsonProperty("title")
private String title;
@JsonProperty("htmlUrl")
private String htmlUrl;
}
@Data
public static class ItemRefs {
@JsonProperty("itemRefs")
private List<ItemRef> itemRefs = new ArrayList<>();
@JsonProperty("continuation")
private String continuation;
}
@Data
@NoArgsConstructor
@AllArgsConstructor
public static class ItemRef {
@JsonProperty("id")
private String id;
}
@Data
public static class UnreadCounts {
@JsonProperty("max")
private long max;
@JsonProperty("unreadcounts")
private List<UnreadCountEntry> unreadCounts = new ArrayList<>();
}
@Data
public static class UnreadCountEntry {
@JsonProperty("id")
private String id;
@JsonProperty("count")
private long count;
@JsonProperty("newestItemTimestampUsec")
private String newestItemTimestampUsec;
}
}

View File

@@ -0,0 +1,829 @@
package com.commafeed.frontend.resource.googlereader;
import com.commafeed.backend.Digests;
import com.commafeed.backend.dao.FeedCategoryDAO;
import com.commafeed.backend.dao.FeedEntryDAO;
import com.commafeed.backend.dao.FeedEntryStatusDAO;
import com.commafeed.backend.dao.FeedSubscriptionDAO;
import com.commafeed.backend.feed.FeedUtils;
import com.commafeed.backend.model.FeedCategory;
import com.commafeed.backend.model.FeedEntry;
import com.commafeed.backend.model.FeedEntryContent;
import com.commafeed.backend.model.FeedEntryStatus;
import com.commafeed.backend.model.FeedSubscription;
import com.commafeed.backend.model.User;
import com.commafeed.backend.model.UserSettings.ReadingOrder;
import com.commafeed.backend.service.FeedEntryService;
import com.commafeed.backend.service.FeedSubscriptionService;
import com.commafeed.backend.service.UserService;
import com.commafeed.frontend.model.UnreadCount;
import com.commafeed.security.AuthenticationContext;
import com.commafeed.security.Roles;
import com.commafeed.security.mechanism.GoogleReaderAuthenticationMechanism;
import jakarta.annotation.security.PermitAll;
import jakarta.annotation.security.RolesAllowed;
import jakarta.inject.Singleton;
import jakarta.transaction.Transactional;
import jakarta.ws.rs.Consumes;
import jakarta.ws.rs.FormParam;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.POST;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.PathParam;
import jakarta.ws.rs.Produces;
import jakarta.ws.rs.QueryParam;
import jakarta.ws.rs.core.Context;
import jakarta.ws.rs.core.MediaType;
import jakarta.ws.rs.core.MultivaluedMap;
import jakarta.ws.rs.core.Response;
import jakarta.ws.rs.core.Response.Status;
import jakarta.ws.rs.core.UriInfo;
import lombok.RequiredArgsConstructor;
import org.apache.commons.lang3.StringUtils;
import org.eclipse.microprofile.openapi.annotations.Operation;
import java.time.Instant;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.stream.Collectors;
/**
* Google Reader compatible API
*
* <ul>
* <li>url: /rest/googlereader
* <li>login: username
* <li>password: api key
* </ul>
*/
@Path(GoogleReaderAuthenticationMechanism.PATH_PREFIX)
@RolesAllowed(Roles.USER)
@Produces(MediaType.APPLICATION_JSON)
@RequiredArgsConstructor
@Singleton
public class GoogleReaderREST {
private static final int DEFAULT_ITEM_COUNT = 20;
private static final int MAX_ITEM_COUNT = 1000;
// cap at which clients should display "count+" instead of the exact unread count
private static final int MAX_UNREAD_COUNT = 10000;
private final AuthenticationContext authenticationContext;
private final UserService userService;
private final FeedSubscriptionService feedSubscriptionService;
private final FeedEntryService feedEntryService;
private final FeedSubscriptionDAO feedSubscriptionDAO;
private final FeedCategoryDAO feedCategoryDAO;
private final FeedEntryDAO feedEntryDAO;
private final FeedEntryStatusDAO feedEntryStatusDAO;
@GET
@PermitAll
@Path("/")
@Produces(MediaType.TEXT_PLAIN)
@Operation(hidden = true)
public Response welcome() {
return Response.ok("This is the Google Reader API endpoint.").build();
}
@POST
@PermitAll
@Path("/accounts/ClientLogin")
@Consumes(MediaType.APPLICATION_FORM_URLENCODED)
@Produces(MediaType.TEXT_PLAIN)
@Transactional
@Operation(hidden = true)
public Response clientLogin(@Context UriInfo uri, MultivaluedMap<String, String> form) {
Map<String, String> params = new HashMap<>();
uri.getQueryParameters().forEach((k, v) -> params.put(k, v.getFirst()));
form.forEach((k, v) -> params.put(k, v.getFirst()));
String password = params.get("Passwd");
User user = StringUtils.isBlank(password) ? null : userService.login(password).orElse(null);
if (user == null) {
return Response.status(Status.FORBIDDEN).entity("Error=BadAuthentication\n").build();
}
String token = user.getName() + "/" + user.getApiKey();
String body = "SID=" + token + "\nLSID=" + token + "\nAuth=" + token + "\n";
return Response.ok(body).build();
}
@GET
@Path("/reader/api/0/token")
@Produces(MediaType.TEXT_PLAIN)
@Transactional
@Operation(hidden = true)
public Response token() {
return Response.ok(computeEditToken(currentUser())).build();
}
@GET
@Path("/reader/api/0/user-info")
@Transactional
@Operation(hidden = true)
public GoogleReaderModel.UserInfo userInfo() {
User user = currentUser();
GoogleReaderModel.UserInfo info = new GoogleReaderModel.UserInfo();
info.setUserId(String.valueOf(user.getId()));
info.setUserName(user.getName());
info.setUserProfileId(String.valueOf(user.getId()));
info.setUserEmail(user.getEmail());
return info;
}
@GET
@Path("/reader/api/0/subscription/list")
@Transactional
@Operation(hidden = true)
public GoogleReaderModel.SubscriptionList subscriptionList() {
User user = currentUser();
GoogleReaderModel.SubscriptionList resp = new GoogleReaderModel.SubscriptionList();
resp.setSubscriptions(
feedSubscriptionDAO.findAll(user).stream().map(this::mapSubscription).toList());
return resp;
}
@POST
@Path("/reader/api/0/subscription/edit")
@Consumes(MediaType.APPLICATION_FORM_URLENCODED)
@Produces(MediaType.TEXT_PLAIN)
@Transactional
@Operation(hidden = true)
public Response subscriptionEdit(
@FormParam("ac") String action,
@FormParam("s") String streamId,
@FormParam("t") String title,
@FormParam("a") String addLabel,
@FormParam("r") String removeLabel,
@FormParam("T") String token) {
User user = currentUser();
if (!validToken(user, token)) {
return Response.status(Status.UNAUTHORIZED).build();
}
if (StringUtils.isBlank(action) || StringUtils.isBlank(streamId)) {
return Response.status(Status.BAD_REQUEST).build();
}
if ("subscribe".equals(action)) {
String url =
streamId.startsWith(GoogleReaderStreamId.FEED_PREFIX)
? streamId.substring(GoogleReaderStreamId.FEED_PREFIX.length())
: streamId;
FeedCategory category = resolveLabelCategory(user, addLabel, true);
String subTitle = StringUtils.defaultIfBlank(title, url);
feedSubscriptionService.subscribe(user, url, subTitle, category, 0);
} else {
GoogleReaderStreamId parsed = GoogleReaderStreamId.parse(streamId);
Long subId = parsed.feedSubscriptionId();
if (subId == null) {
return Response.status(Status.BAD_REQUEST).build();
}
if ("unsubscribe".equals(action)) {
feedSubscriptionService.unsubscribe(user, subId);
} else if ("edit".equals(action)) {
FeedSubscription sub = feedSubscriptionDAO.findById(user, subId);
if (sub != null) {
if (StringUtils.isNotBlank(title)) {
sub.setTitle(title);
}
if (StringUtils.isNotBlank(addLabel)) {
sub.setCategory(resolveLabelCategory(user, addLabel, true));
}
if (StringUtils.isNotBlank(removeLabel)) {
sub.setCategory(null);
}
}
}
}
return Response.ok("OK").build();
}
@GET
@Path("/reader/api/0/tag/list")
@Transactional
@Operation(hidden = true)
public GoogleReaderModel.TagList tagList() {
User user = currentUser();
List<GoogleReaderModel.Tag> tags = new ArrayList<>();
feedCategoryDAO
.findAll(user)
.forEach(
c ->
tags.add(
new GoogleReaderModel.Tag(
GoogleReaderStreamId.label(c.getName()))));
tags.add(new GoogleReaderModel.Tag(GoogleReaderStreamId.STARRED));
GoogleReaderModel.TagList resp = new GoogleReaderModel.TagList();
resp.setTags(tags);
return resp;
}
@GET
@Path("/reader/api/0/unread-count")
@Transactional
@Operation(hidden = true)
public GoogleReaderModel.UnreadCounts unreadCounts() {
User user = currentUser();
List<FeedSubscription> subs = feedSubscriptionDAO.findAll(user);
Map<Long, UnreadCount> unreadCounts = feedSubscriptionService.getUnreadCount(user);
List<GoogleReaderModel.UnreadCountEntry> entries = new ArrayList<>();
Map<Long, Long> unreadByCategory = new HashMap<>();
long totalUnread = 0;
for (FeedSubscription sub : subs) {
UnreadCount uc = unreadCounts.get(sub.getId());
GoogleReaderModel.UnreadCountEntry entry = new GoogleReaderModel.UnreadCountEntry();
entry.setId(GoogleReaderStreamId.feed(sub.getId()));
entry.setCount(uc.getUnreadCount());
entry.setNewestItemTimestampUsec(toUsec(uc.getNewestItemTime()));
entries.add(entry);
totalUnread += uc.getUnreadCount();
long categoryId = sub.getCategory() == null ? 0 : sub.getCategory().getId();
unreadByCategory.merge(categoryId, uc.getUnreadCount(), Long::sum);
}
Map<Long, FeedCategory> categoriesById =
feedCategoryDAO.findAll(user).stream()
.collect(Collectors.toMap(FeedCategory::getId, c -> c));
unreadByCategory.forEach(
(categoryId, count) -> {
if (categoryId == 0) {
return;
}
FeedCategory category = categoriesById.get(categoryId);
if (category == null) {
return;
}
GoogleReaderModel.UnreadCountEntry entry =
new GoogleReaderModel.UnreadCountEntry();
entry.setId(GoogleReaderStreamId.label(category.getName()));
entry.setCount(count);
entries.add(entry);
});
GoogleReaderModel.UnreadCountEntry readingList = new GoogleReaderModel.UnreadCountEntry();
readingList.setId(GoogleReaderStreamId.READING_LIST);
readingList.setCount(totalUnread);
entries.add(readingList);
GoogleReaderModel.UnreadCounts resp = new GoogleReaderModel.UnreadCounts();
resp.setMax(MAX_UNREAD_COUNT);
resp.setUnreadCounts(entries);
return resp;
}
@GET
@Path("/reader/api/0/stream/contents/{streamId : .+}")
@Transactional
@Operation(hidden = true)
public GoogleReaderModel.StreamContents streamContentsPath(
@PathParam("streamId") String streamId,
@QueryParam("n") Integer n,
@QueryParam("c") String continuation,
@QueryParam("r") String order,
@QueryParam("xt") List<String> excludeTargets) {
return buildStreamContents(streamId, n, continuation, order, excludeTargets);
}
@GET
@Path("/reader/api/0/stream/contents")
@Transactional
@Operation(hidden = true)
public GoogleReaderModel.StreamContents streamContentsQuery(
@QueryParam("s") String streamId,
@QueryParam("n") Integer n,
@QueryParam("c") String continuation,
@QueryParam("r") String order,
@QueryParam("xt") List<String> excludeTargets) {
return buildStreamContents(streamId, n, continuation, order, excludeTargets);
}
@GET
@Path("/reader/api/0/stream/items/ids")
@Transactional
@Operation(hidden = true)
public GoogleReaderModel.ItemRefs streamItemIds(
@QueryParam("s") String streamId,
@QueryParam("n") Integer n,
@QueryParam("c") String continuation,
@QueryParam("r") String order,
@QueryParam("xt") List<String> excludeTargets) {
User user = currentUser();
GoogleReaderStreamId parsed = GoogleReaderStreamId.parse(streamId);
int limit = clampLimit(n);
ReadingOrder readingOrder = "o".equals(order) ? ReadingOrder.ASC : ReadingOrder.DESC;
boolean unreadOnly = isUnreadOnly(excludeTargets);
PagedStatuses paged =
fetchStatuses(user, parsed, limit, continuation, readingOrder, unreadOnly);
GoogleReaderModel.ItemRefs resp = new GoogleReaderModel.ItemRefs();
resp.setItemRefs(
paged.statuses().stream()
.map(
s ->
new GoogleReaderModel.ItemRef(
String.valueOf(s.getEntry().getId())))
.toList());
resp.setContinuation(paged.continuation());
return resp;
}
@POST
@Path("/reader/api/0/stream/items/contents")
@Consumes(MediaType.APPLICATION_FORM_URLENCODED)
@Transactional
@Operation(hidden = true)
public GoogleReaderModel.StreamContents streamItemsContentsPost(
@FormParam("i") List<String> ids) {
return buildStreamItemsContents(ids);
}
@GET
@Path("/reader/api/0/stream/items/contents")
@Transactional
@Operation(hidden = true)
public GoogleReaderModel.StreamContents streamItemsContentsGet(
@QueryParam("i") List<String> ids) {
return buildStreamItemsContents(ids);
}
@POST
@Path("/reader/api/0/edit-tag")
@Consumes(MediaType.APPLICATION_FORM_URLENCODED)
@Produces(MediaType.TEXT_PLAIN)
@Transactional
@Operation(hidden = true)
public Response editTag(
@FormParam("i") List<String> ids,
@FormParam("a") List<String> add,
@FormParam("r") List<String> remove,
@FormParam("T") String token) {
User user = currentUser();
if (!validToken(user, token)) {
return Response.status(Status.UNAUTHORIZED).build();
}
if (ids == null) {
return Response.ok("OK").build();
}
boolean markRead = containsTag(add, GoogleReaderStreamId.READ);
boolean markUnread = containsTag(remove, GoogleReaderStreamId.READ);
boolean star = containsTag(add, GoogleReaderStreamId.STARRED);
boolean unstar = containsTag(remove, GoogleReaderStreamId.STARRED);
for (String rawId : ids) {
Long entryId = parseItemId(rawId);
if (entryId == null) {
continue;
}
FeedEntry entry = feedEntryDAO.findById(entryId);
if (entry == null) {
continue;
}
if (markRead) {
feedEntryService.markEntry(user, entryId, true);
}
if (markUnread) {
feedEntryService.markEntry(user, entryId, false);
}
if (star) {
feedEntryService.starEntry(user, entryId, true);
}
if (unstar) {
feedEntryService.starEntry(user, entryId, false);
}
}
return Response.ok("OK").build();
}
@POST
@Path("/reader/api/0/mark-all-as-read")
@Consumes(MediaType.APPLICATION_FORM_URLENCODED)
@Produces(MediaType.TEXT_PLAIN)
@Transactional
@Operation(hidden = true)
public Response markAllAsRead(
@FormParam("s") String streamId,
@FormParam("ts") Long ts,
@FormParam("T") String token) {
User user = currentUser();
if (!validToken(user, token)) {
return Response.status(Status.UNAUTHORIZED).build();
}
GoogleReaderStreamId parsed = GoogleReaderStreamId.parse(streamId);
Instant olderThan = (ts != null && ts > 0) ? Instant.ofEpochSecond(ts / 1_000_000) : null;
switch (parsed.type()) {
case STARRED -> feedEntryService.markStarredEntries(user, olderThan, null);
case FEED -> {
Long subId = parsed.feedSubscriptionId();
FeedSubscription sub =
subId == null ? null : feedSubscriptionDAO.findById(user, subId);
if (sub != null) {
feedEntryService.markSubscriptionEntries(
user, List.of(sub), olderThan, null, null);
}
}
case LABEL -> {
FeedCategory category = findCategoryByName(user, parsed.value());
if (category != null) {
List<FeedCategory> categories =
feedCategoryDAO.findAllChildrenCategories(user, category);
List<FeedSubscription> subs =
feedSubscriptionDAO.findByCategories(user, categories);
feedEntryService.markSubscriptionEntries(user, subs, olderThan, null, null);
}
}
default ->
feedEntryService.markSubscriptionEntries(
user, feedSubscriptionDAO.findAll(user), olderThan, null, null);
}
return Response.ok("OK").build();
}
private GoogleReaderModel.StreamContents buildStreamContents(
String rawStreamId,
Integer n,
String continuation,
String order,
List<String> excludeTargets) {
User user = currentUser();
GoogleReaderStreamId parsed = GoogleReaderStreamId.parse(rawStreamId);
int limit = clampLimit(n);
ReadingOrder readingOrder = "o".equals(order) ? ReadingOrder.ASC : ReadingOrder.DESC;
boolean unreadOnly = isUnreadOnly(excludeTargets);
PagedStatuses paged =
fetchStatuses(user, parsed, limit, continuation, readingOrder, unreadOnly);
GoogleReaderModel.StreamContents resp = new GoogleReaderModel.StreamContents();
resp.setId(streamIdString(parsed));
resp.setTitle(streamTitle(user, parsed));
resp.setUpdated(Instant.now().getEpochSecond());
resp.setItems(paged.statuses().stream().map(this::mapItem).toList());
resp.setContinuation(paged.continuation());
return resp;
}
private GoogleReaderModel.StreamContents buildStreamItemsContents(List<String> ids) {
User user = currentUser();
List<FeedSubscription> subs = feedSubscriptionDAO.findAll(user);
Map<Long, FeedSubscription> subsByFeedId =
subs.stream()
.collect(Collectors.toMap(s -> s.getFeed().getId(), s -> s, (a, b) -> a));
List<GoogleReaderModel.StreamItem> items = new ArrayList<>();
if (ids != null) {
for (String rawId : ids) {
Long entryId = parseItemId(rawId);
if (entryId == null) {
continue;
}
FeedEntry entry = feedEntryDAO.findById(entryId);
if (entry == null) {
continue;
}
FeedSubscription sub = subsByFeedId.get(entry.getFeed().getId());
if (sub == null) {
continue;
}
FeedEntryStatus status = feedEntryStatusDAO.getStatus(user, sub, entry);
items.add(mapItem(status));
}
}
GoogleReaderModel.StreamContents resp = new GoogleReaderModel.StreamContents();
resp.setId(GoogleReaderStreamId.READING_LIST);
resp.setUpdated(Instant.now().getEpochSecond());
resp.setItems(items);
return resp;
}
private PagedStatuses fetchStatuses(
User user,
GoogleReaderStreamId streamId,
int limit,
String continuation,
ReadingOrder order,
boolean unreadOnly) {
Long minEntryId = null;
Long maxEntryId = null;
int offset = 0;
if (streamId.type() == GoogleReaderStreamId.Type.STARRED) {
offset = parseOffset(continuation);
} else if (StringUtils.isNotBlank(continuation)) {
try {
long cursor = Long.parseLong(continuation);
if (order == ReadingOrder.ASC) {
minEntryId = cursor;
} else {
maxEntryId = cursor;
}
} catch (NumberFormatException ignored) {
// invalid continuation token, ignore and start from the beginning
}
}
List<FeedEntryStatus> statuses;
int fetchedCount;
switch (streamId.type()) {
case STARRED -> {
List<FeedEntryStatus> starred =
feedEntryStatusDAO.findStarred(
user, null, null, offset, limit, order, true);
fetchedCount = starred.size();
statuses =
unreadOnly ? starred.stream().filter(s -> !s.isRead()).toList() : starred;
}
case FEED -> {
Long subId = streamId.feedSubscriptionId();
FeedSubscription sub =
subId == null ? null : feedSubscriptionDAO.findById(user, subId);
List<FeedSubscription> subs = sub == null ? List.of() : List.of(sub);
statuses =
feedEntryStatusDAO.findBySubscriptions(
user,
subs,
unreadOnly,
null,
null,
0,
limit,
order,
true,
null,
minEntryId,
maxEntryId);
fetchedCount = statuses.size();
}
case LABEL -> {
FeedCategory category = findCategoryByName(user, streamId.value());
List<FeedSubscription> subs =
category == null
? List.of()
: feedSubscriptionDAO.findByCategories(
user,
feedCategoryDAO.findAllChildrenCategories(user, category));
statuses =
feedEntryStatusDAO.findBySubscriptions(
user,
subs,
unreadOnly,
null,
null,
0,
limit,
order,
true,
null,
minEntryId,
maxEntryId);
fetchedCount = statuses.size();
}
default -> {
List<FeedSubscription> subs = feedSubscriptionDAO.findAll(user);
statuses =
feedEntryStatusDAO.findBySubscriptions(
user,
subs,
unreadOnly,
null,
null,
0,
limit,
order,
true,
null,
minEntryId,
maxEntryId);
fetchedCount = statuses.size();
}
}
String nextContinuation = null;
if (fetchedCount >= limit && fetchedCount > 0) {
if (streamId.type() == GoogleReaderStreamId.Type.STARRED) {
nextContinuation = String.valueOf(offset + limit);
} else if (!statuses.isEmpty()) {
nextContinuation = String.valueOf(statuses.getLast().getEntry().getId());
}
}
return new PagedStatuses(statuses, nextContinuation);
}
private GoogleReaderModel.StreamItem mapItem(FeedEntryStatus status) {
FeedEntry entry = status.getEntry();
FeedSubscription sub = status.getSubscription();
GoogleReaderModel.StreamItem item = new GoogleReaderModel.StreamItem();
item.setId(formatItemId(entry.getId()));
Instant published = entry.getPublished() != null ? entry.getPublished() : Instant.EPOCH;
item.setPublished(published.getEpochSecond());
item.setUpdated(published.getEpochSecond());
item.setCrawlTimeMsec(String.valueOf(entry.getInserted().toEpochMilli()));
item.setTimestampUsec(String.valueOf(published.toEpochMilli() * 1000));
item.setTitle(entry.getContent().getTitle());
item.setAuthor(entry.getContent().getAuthor());
if (StringUtils.isNotBlank(entry.getUrl())) {
item.setCanonical(List.of(new GoogleReaderModel.HrefEntry(entry.getUrl(), null)));
item.setAlternate(
List.of(new GoogleReaderModel.HrefEntry(entry.getUrl(), "text/html")));
}
item.setSummary(
new GoogleReaderModel.Content(
Optional.ofNullable(entry.getContent().getContent()).orElse(""),
entry.getContent().getDirection() == FeedEntryContent.Direction.RTL
? "rtl"
: "ltr"));
List<String> categories = new ArrayList<>();
categories.add(GoogleReaderStreamId.READING_LIST);
if (status.isRead()) {
categories.add(GoogleReaderStreamId.READ);
}
if (status.isStarred()) {
categories.add(GoogleReaderStreamId.STARRED);
}
if (sub != null && sub.getCategory() != null) {
categories.add(GoogleReaderStreamId.label(sub.getCategory().getName()));
}
item.setCategories(categories);
if (sub != null) {
GoogleReaderModel.Origin origin = new GoogleReaderModel.Origin();
origin.setStreamId(GoogleReaderStreamId.feed(sub.getId()));
origin.setTitle(sub.getTitle());
origin.setHtmlUrl(sub.getFeed().getLink());
item.setOrigin(origin);
}
return item;
}
private GoogleReaderModel.Subscription mapSubscription(FeedSubscription sub) {
GoogleReaderModel.Subscription s = new GoogleReaderModel.Subscription();
s.setId(GoogleReaderStreamId.feed(sub.getId()));
s.setTitle(sub.getTitle());
s.setUrl(sub.getFeed().getUrl());
s.setHtmlUrl(sub.getFeed().getLink());
s.setIconUrl(FeedUtils.getFaviconUrl(sub));
if (sub.getCategory() != null) {
s.getCategories()
.add(
new GoogleReaderModel.Category(
GoogleReaderStreamId.label(sub.getCategory().getName()),
sub.getCategory().getName()));
}
return s;
}
private FeedCategory resolveLabelCategory(
User user, String labelParam, boolean createIfMissing) {
if (StringUtils.isBlank(labelParam)) {
return null;
}
GoogleReaderStreamId parsed = GoogleReaderStreamId.parse(labelParam);
if (parsed.type() != GoogleReaderStreamId.Type.LABEL) {
return null;
}
String name = parsed.value();
FeedCategory category = findCategoryByName(user, name);
if (category == null && createIfMissing) {
category = new FeedCategory();
category.setUser(user);
category.setName(FeedUtils.truncate(name, 128));
feedCategoryDAO.persist(category);
}
return category;
}
private FeedCategory findCategoryByName(User user, String name) {
return feedCategoryDAO.findAll(user).stream()
.filter(c -> c.getName().equals(name))
.findFirst()
.orElse(null);
}
private String streamIdString(GoogleReaderStreamId parsed) {
return switch (parsed.type()) {
case FEED -> GoogleReaderStreamId.FEED_PREFIX + parsed.value();
case LABEL -> GoogleReaderStreamId.label(parsed.value());
case STARRED -> GoogleReaderStreamId.STARRED;
default -> GoogleReaderStreamId.READING_LIST;
};
}
private String streamTitle(User user, GoogleReaderStreamId parsed) {
return switch (parsed.type()) {
case FEED -> {
Long subId = parsed.feedSubscriptionId();
FeedSubscription sub =
subId == null ? null : feedSubscriptionDAO.findById(user, subId);
yield sub == null ? null : sub.getTitle();
}
case LABEL -> parsed.value();
case STARRED -> "Starred items";
default -> "reading list";
};
}
private boolean isUnreadOnly(List<String> excludeTargets) {
return excludeTargets != null
&& excludeTargets.stream().anyMatch(t -> t.endsWith("/state/com.google/read"));
}
private boolean containsTag(List<String> tags, String suffix) {
return tags != null && tags.stream().anyMatch(t -> t.endsWith(suffix));
}
private String formatItemId(long id) {
// Google Reader's canonical long-form item id, e.g.
// "tag:google.com,2005:reader/item/00000000148b9369", where the suffix is the item's
// numeric id, represented as a zero-padded, 16-digit hex string
return "tag:google.com,2005:reader/item/" + String.format("%016x", id);
}
private Long parseItemId(String raw) {
if (StringUtils.isBlank(raw)) {
return null;
}
int idx = raw.lastIndexOf('/');
if (raw.startsWith("tag:google.com") && idx != -1) {
try {
return Long.parseUnsignedLong(raw.substring(idx + 1), 16);
} catch (NumberFormatException e) {
return null;
}
}
try {
return Long.valueOf(raw);
} catch (NumberFormatException e) {
return null;
}
}
private int parseOffset(String continuation) {
if (StringUtils.isBlank(continuation)) {
return 0;
}
try {
return Integer.parseInt(continuation);
} catch (NumberFormatException e) {
return 0;
}
}
private int clampLimit(Integer n) {
if (n == null || n <= 0) {
return DEFAULT_ITEM_COUNT;
}
return Math.min(n, MAX_ITEM_COUNT);
}
private String toUsec(Instant instant) {
return instant == null ? "0" : String.valueOf(instant.toEpochMilli() * 1000);
}
private boolean validToken(User user, String token) {
// the edit token is a CSRF protection inherited from Google Reader's cookie-based
// authentication model. Since this API is authenticated via an api key header instead of
// cookies, that risk does not apply here. Some real-world clients (e.g. RSSGuard, unless
// configured as "Reedah"/"Miniflux") never call the "token" endpoint and always submit a
// blank token, so, blank tokens are accepted.
return StringUtils.isBlank(token) || computeEditToken(user).equals(token);
}
private String computeEditToken(User user) {
return Digests.md5Hex(user.getApiKey() + ":" + user.getId());
}
private User currentUser() {
return authenticationContext.getCurrentUser();
}
private record PagedStatuses(List<FeedEntryStatus> statuses, String continuation) {}
}

View File

@@ -0,0 +1,60 @@
package com.commafeed.frontend.resource.googlereader;
import org.apache.commons.lang3.StringUtils;
public record GoogleReaderStreamId(Type type, String value) {
public enum Type {
ALL,
FEED,
LABEL,
STARRED
}
public static final String READING_LIST = "user/-/state/com.google/reading-list";
public static final String STARRED = "user/-/state/com.google/starred";
public static final String READ = "user/-/state/com.google/read";
public static final String LABEL_PREFIX = "user/-/label/";
public static final String FEED_PREFIX = "feed/";
public static final GoogleReaderStreamId ALL_STREAM = new GoogleReaderStreamId(Type.ALL, null);
public static final GoogleReaderStreamId STARRED_STREAM =
new GoogleReaderStreamId(Type.STARRED, null);
public static GoogleReaderStreamId parse(String raw) {
if (StringUtils.isBlank(raw) || "reading-list".equals(raw) || READING_LIST.equals(raw)) {
return ALL_STREAM;
}
if (STARRED.equals(raw)) {
return STARRED_STREAM;
}
if (raw.startsWith(LABEL_PREFIX)) {
return new GoogleReaderStreamId(Type.LABEL, raw.substring(LABEL_PREFIX.length()));
}
if (raw.startsWith(FEED_PREFIX)) {
return new GoogleReaderStreamId(Type.FEED, raw.substring(FEED_PREFIX.length()));
}
// unrecognized stream id, default to the whole reading list
return ALL_STREAM;
}
public static String feed(long subscriptionId) {
return FEED_PREFIX + subscriptionId;
}
public static String label(String categoryName) {
return LABEL_PREFIX + categoryName;
}
/** extracts the subscription id out of a "feed/&lt;id&gt;" stream id, or null if invalid */
public Long feedSubscriptionId() {
if (type != Type.FEED) {
return null;
}
try {
return Long.valueOf(value);
} catch (NumberFormatException e) {
return null;
}
}
}

View File

@@ -4,8 +4,9 @@ import com.commafeed.backend.dao.UnitOfWork;
import com.commafeed.backend.model.User; import com.commafeed.backend.model.User;
import com.commafeed.backend.model.UserRole.Role; import com.commafeed.backend.model.UserRole.Role;
import com.commafeed.backend.service.UserService; import com.commafeed.backend.service.UserService;
import com.commafeed.frontend.exception.CommaFeedApplicationException;
import com.commafeed.frontend.exception.CommaFeedExceptionType;
import io.quarkus.security.AuthenticationFailedException;
import io.quarkus.security.identity.AuthenticationRequestContext; import io.quarkus.security.identity.AuthenticationRequestContext;
import io.quarkus.security.identity.IdentityProvider; import io.quarkus.security.identity.IdentityProvider;
import io.quarkus.security.identity.SecurityIdentity; import io.quarkus.security.identity.SecurityIdentity;
@@ -48,7 +49,8 @@ public class DatabaseUsernamePasswordIdentityProvider
new String( new String(
request.getPassword().getPassword()))); request.getPassword().getPassword())));
if (user.isEmpty()) { if (user.isEmpty()) {
throw new AuthenticationFailedException("wrong username or password"); throw new CommaFeedApplicationException(
CommaFeedExceptionType.WRONG_USERNAME_OR_PASSWORD);
} }
Set<Role> roles = unitOfWork.call(() -> userService.getRoles(user.get())); Set<Role> roles = unitOfWork.call(() -> userService.getRoles(user.get()));

View File

@@ -16,7 +16,7 @@ import java.util.Optional;
import java.util.Set; import java.util.Set;
@Singleton @Singleton
public class ApiKeyAuthenticationMecanism implements HttpAuthenticationMechanism { public class ApiKeyAuthenticationMechanism implements HttpAuthenticationMechanism {
@Override @Override
public Uni<SecurityIdentity> authenticate( public Uni<SecurityIdentity> authenticate(

View File

@@ -0,0 +1,69 @@
package com.commafeed.security.mechanism;
import io.quarkus.security.credential.TokenCredential;
import io.quarkus.security.identity.IdentityProviderManager;
import io.quarkus.security.identity.SecurityIdentity;
import io.quarkus.security.identity.request.AuthenticationRequest;
import io.quarkus.security.identity.request.TokenAuthenticationRequest;
import io.quarkus.vertx.http.runtime.security.ChallengeData;
import io.quarkus.vertx.http.runtime.security.HttpAuthenticationMechanism;
import io.smallrye.mutiny.Uni;
import io.vertx.ext.web.RoutingContext;
import jakarta.inject.Singleton;
import org.apache.commons.lang3.StringUtils;
import java.util.Optional;
import java.util.Set;
/**
* Authenticates requests to the Google Reader compatible API, using the "Authorization: GoogleLogin
* auth=&lt;token&gt;" header set by Google Reader API clients.
*
* <p>This mechanism only applies to requests under {@link #PATH_PREFIX}, so that a stray
* "Authorization: GoogleLogin ..." header sent to any other endpoint is not mistakenly accepted as
* an api key authentication.
*/
@Singleton
public class GoogleReaderAuthenticationMechanism implements HttpAuthenticationMechanism {
public static final String PATH_PREFIX = "/rest/googlereader";
private static final String AUTH_HEADER = "Authorization";
private static final String AUTH_PREFIX = "GoogleLogin auth=";
@Override
public Uni<SecurityIdentity> authenticate(
RoutingContext context, IdentityProviderManager identityProviderManager) {
if (!context.normalizedPath().startsWith(PATH_PREFIX)) {
return Uni.createFrom().optional(Optional.empty());
}
String header = context.request().getHeader(AUTH_HEADER);
if (StringUtils.isBlank(header) || !header.startsWith(AUTH_PREFIX)) {
return Uni.createFrom().optional(Optional.empty());
}
String token = header.substring(AUTH_PREFIX.length()).trim();
int slashIndex = token.lastIndexOf('/');
String apiKey = slashIndex == -1 ? token : token.substring(slashIndex + 1);
if (StringUtils.isBlank(apiKey)) {
return Uni.createFrom().optional(Optional.empty());
}
TokenCredential credential = new TokenCredential(apiKey, "apiKey");
TokenAuthenticationRequest request = new TokenAuthenticationRequest(credential);
return identityProviderManager.authenticate(request);
}
@Override
public Uni<ChallengeData> getChallenge(RoutingContext context) {
return Uni.createFrom().optional(Optional.empty());
}
@Override
public Set<Class<? extends AuthenticationRequest>> getCredentialTypes() {
return Set.of(TokenAuthenticationRequest.class);
}
}

View File

@@ -3,7 +3,6 @@ quarkus.http.port=8082
quarkus.http.test-port=8085 quarkus.http.test-port=8085
quarkus.http.enable-compression=true quarkus.http.enable-compression=true
quarkus.http.enable-decompression=true quarkus.http.enable-decompression=true
# http cache # http cache
quarkus.http.static-resources.max-age=P365d quarkus.http.static-resources.max-age=P365d
## make sure the webapp is always up to date ## make sure the webapp is always up to date
@@ -15,7 +14,6 @@ quarkus.http.filter.openapi.matches=/openapi
## make sure /next always redirects to the next unread entry ## make sure /next always redirects to the next unread entry
quarkus.http.filter.next.header."Cache-Control"=no-cache quarkus.http.filter.next.header."Cache-Control"=no-cache
quarkus.http.filter.next.matches=/next quarkus.http.filter.next.matches=/next
# security # security
quarkus.http.auth.basic=true quarkus.http.auth.basic=true
quarkus.http.auth.form.enabled=true quarkus.http.auth.form.enabled=true
@@ -25,51 +23,42 @@ quarkus.http.auth.form.cookie-max-age=P30d
quarkus.http.auth.form.landing-page= quarkus.http.auth.form.landing-page=
quarkus.http.auth.form.login-page= quarkus.http.auth.form.login-page=
quarkus.http.auth.form.error-page= quarkus.http.auth.form.error-page=
# websocket # websocket
quarkus.websocket.dispatch-to-worker=true quarkus.websocket.dispatch-to-worker=true
# database # database
quarkus.liquibase.change-log=migrations.xml quarkus.liquibase.change-log=migrations.xml
quarkus.liquibase.migrate-at-start=true quarkus.liquibase.migrate-at-start=true
# openapi # openapi
quarkus.smallrye-openapi.path=/openapi quarkus.smallrye-openapi.path=/openapi
quarkus.swagger-ui.path=/api-documentation quarkus.swagger-ui.path=/api-documentation
quarkus.swagger-ui.always-include=true quarkus.swagger-ui.always-include=true
# shutdown # shutdown
quarkus.shutdown.timeout=5s quarkus.shutdown.timeout=5s
# native # native
quarkus.native.march=compatibility quarkus.native.march=compatibility
quarkus.native.add-all-charsets=true quarkus.native.add-all-charsets=true
# fix for https://github.com/Athou/commafeed/issues/1795 # fix for https://github.com/Athou/commafeed/issues/1795
quarkus.native.additional-build-args=-H:PageSize=65536 quarkus.native.additional-build-args=-H:PageSize=65536
# dev profile overrides # dev profile overrides
%dev.quarkus.http.port=8083 %dev.quarkus.http.port=8083
%dev.quarkus.http.auth.session.encryption-key=123456789012345678901234567890 %dev.quarkus.http.auth.session.encryption-key=123456789012345678901234567890
%dev.quarkus.log.category."com.commafeed".level=DEBUG %dev.quarkus.log.category."com.commafeed".level=DEBUG
# %dev.quarkus.hibernate-orm.log.sql=true # %dev.quarkus.hibernate-orm.log.sql=true
%dev.commafeed.users.create-demo-account=true %dev.commafeed.users.create-demo-account=true
# test profile overrides # test profile overrides
%test.quarkus.log.category."org.mockserver".level=WARN %test.quarkus.log.level=WARN
%test.quarkus.log.category."liquibase".level=WARN %test.quarkus.log.category."org.mockserver".level=ERROR
%test.commafeed.shutdown-timeout=100ms %test.commafeed.shutdown-timeout=100ms
%test.commafeed.users.create-demo-account=true %test.commafeed.users.create-demo-account=true
%test.commafeed.users.allow-registrations=true %test.commafeed.users.allow-registrations=true
%test.commafeed.password-recovery-enabled=true %test.commafeed.password-recovery-enabled=true
%test.commafeed.password-recovery-public-base-url=https://commafeed.example.com
%test.commafeed.http-client.cache.enabled=false %test.commafeed.http-client.cache.enabled=false
%test.commafeed.http-client.block-local-addresses=false %test.commafeed.http-client.block-local-addresses=false
%test.commafeed.database.cleanup.entries-max-age=0 %test.commafeed.database.cleanup.entries-max-age=0
%test.commafeed.feed-refresh.force-refresh-cooldown-duration=1m %test.commafeed.feed-refresh.force-refresh-cooldown-duration=1m
## GitHub actions runners are very slow, we need to increase this timeout to avoid intermittent test failures ## GitHub actions runners are very slow, we need to increase this timeout to avoid intermittent test failures
%test.commafeed.feed-refresh.filtering-expression-evaluation-timeout=5s %test.commafeed.feed-refresh.filtering-expression-evaluation-timeout=5s
# prod profile overrides # prod profile overrides
%prod.quarkus.datasource.jdbc.url=jdbc:h2:./data/db;DEFRAG_ALWAYS=TRUE %prod.quarkus.datasource.jdbc.url=jdbc:h2:./data/db;DEFRAG_ALWAYS=TRUE
%prod.quarkus.datasource.username=sa %prod.quarkus.datasource.username=sa

View File

@@ -547,5 +547,47 @@ class HttpGetterTest {
Assertions.assertThrows( Assertions.assertThrows(
UnknownHostException.class, () -> getter.get("http://[fd00:dead:beef::50]")); UnknownHostException.class, () -> getter.get("http://[fd00:dead:beef::50]"));
} }
@Test
void nat64() {
// 64:ff9b::/96 embeds the IPv4 address in the lower 32 bits (RFC 6052)
Assertions.assertThrows(
UnknownHostException.class,
() -> getter.get("http://[64:ff9b::a9fe:a9fe]")); // 169.254.169.254
Assertions.assertThrows(
UnknownHostException.class,
() -> getter.get("http://[64:ff9b::a00:1]")); // 10.0.0.1
}
@Test
void sixToFour() {
// 2002::/16 embeds the IPv4 address in bits 16-47 (RFC 3056)
Assertions.assertThrows(
UnknownHostException.class,
() -> getter.get("http://[2002:a9fe:a9fe::]")); // 169.254.169.254
Assertions.assertThrows(
UnknownHostException.class,
() -> getter.get("http://[2002:a00:1::]")); // 10.0.0.1
}
@Test
void teredo() {
// 2001:0000::/32 embeds the obfuscated (bitwise NOT) IPv4 address in the lower 32
// bits (RFC 4380)
Assertions.assertThrows(
UnknownHostException.class,
() -> getter.get("http://[2001:0:4136:e378:8000:63bf:f5ff:fffe]")); // 10.0.0.1
}
@Test
void ipv4Compatible() {
// deprecated ::/96 IPv4-compatible addresses embed the IPv4 address in the lower 32
// bits
Assertions.assertThrows(
UnknownHostException.class,
() -> getter.get("http://[::a9fe:a9fe]")); // 169.254.169.254
Assertions.assertThrows(
UnknownHostException.class, () -> getter.get("http://[::a00:1]")); // 10.0.0.1
}
} }
} }

View File

@@ -36,7 +36,7 @@ class AuthentificationIT {
page.navigate(getLoginPageUrl()); page.navigate(getLoginPageUrl());
PlaywrightTestUtils.login(page, TestConstants.ADMIN_USERNAME, "wrong_password"); PlaywrightTestUtils.login(page, TestConstants.ADMIN_USERNAME, "wrong_password");
PlaywrightAssertions.assertThat(page.getByRole(AriaRole.ALERT)) PlaywrightAssertions.assertThat(page.getByRole(AriaRole.ALERT))
.containsText("wrong username or password"); .containsText("Wrong username or password");
} }
@Test @Test

View File

@@ -1,7 +1,9 @@
package com.commafeed.integration; package com.commafeed.integration;
import com.commafeed.ExceptionMappers.CommaFeedApplicationError;
import com.commafeed.ExceptionMappers.UnauthorizedResponse; import com.commafeed.ExceptionMappers.UnauthorizedResponse;
import com.commafeed.TestConstants; import com.commafeed.TestConstants;
import com.commafeed.frontend.exception.CommaFeedExceptionType;
import com.commafeed.frontend.model.Entries; import com.commafeed.frontend.model.Entries;
import com.commafeed.frontend.model.UserModel; import com.commafeed.frontend.model.UserModel;
import com.commafeed.frontend.model.request.MarkRequest; import com.commafeed.frontend.model.request.MarkRequest;
@@ -57,6 +59,27 @@ class SecurityIT extends BaseIT {
.statusCode(HttpStatus.SC_OK); .statusCode(HttpStatus.SC_OK);
} }
@Test
void formLoginWrongPassword() {
CommaFeedApplicationError error =
RestAssured.given()
.auth()
.none()
.formParams(
"j_username",
TestConstants.ADMIN_USERNAME,
"j_password",
"wrong-password")
.post("j_security_check")
.then()
.statusCode(HttpStatus.SC_UNAUTHORIZED)
.extract()
.as(CommaFeedApplicationError.class);
Assertions.assertEquals(CommaFeedExceptionType.WRONG_USERNAME_OR_PASSWORD, error.type());
Assertions.assertEquals("wrong username or password", error.message());
}
@Test @Test
void basicAuthLogin() { void basicAuthLogin() {
RestAssured.given() RestAssured.given()

View File

@@ -45,7 +45,6 @@ class DatabaseCleaningIT extends BaseIT {
private void starEntry(String entryId, Long subscriptionId) { private void starEntry(String entryId, Long subscriptionId) {
StarRequest starRequest = new StarRequest(); StarRequest starRequest = new StarRequest();
starRequest.setId(entryId); starRequest.setId(entryId);
starRequest.setFeedId(subscriptionId);
starRequest.setStarred(true); starRequest.setStarred(true);
RestAssured.given() RestAssured.given()
.body(starRequest) .body(starRequest)
@@ -58,7 +57,6 @@ class DatabaseCleaningIT extends BaseIT {
private void unstarEntry(String entryId, Long subscriptionId) { private void unstarEntry(String entryId, Long subscriptionId) {
StarRequest starRequest = new StarRequest(); StarRequest starRequest = new StarRequest();
starRequest.setId(entryId); starRequest.setId(entryId);
starRequest.setFeedId(subscriptionId);
starRequest.setStarred(false); starRequest.setStarred(false);
RestAssured.given() RestAssured.given()
.body(starRequest) .body(starRequest)

View File

@@ -240,7 +240,6 @@ class CategoryIT extends BaseIT {
StarRequest starRequest = new StarRequest(); StarRequest starRequest = new StarRequest();
starRequest.setId(entry.getId()); starRequest.setId(entry.getId());
starRequest.setFeedId(subscriptionId);
starRequest.setStarred(true); starRequest.setStarred(true);
RestAssured.given() RestAssured.given()
.body(starRequest) .body(starRequest)

View File

@@ -121,7 +121,6 @@ class FeverIT extends BaseIT {
StarRequest starRequest = new StarRequest(); StarRequest starRequest = new StarRequest();
starRequest.setId(entry.getId()); starRequest.setId(entry.getId());
starRequest.setFeedId(subscriptionId);
starRequest.setStarred(true); starRequest.setStarred(true);
RestAssured.given().body(starRequest).contentType(ContentType.JSON).post("rest/entry/star"); RestAssured.given().body(starRequest).contentType(ContentType.JSON).post("rest/entry/star");

View File

@@ -0,0 +1,322 @@
package com.commafeed.integration.rest;
import com.commafeed.TestConstants;
import com.commafeed.frontend.model.Entry;
import com.commafeed.frontend.model.UserModel;
import com.commafeed.frontend.model.request.ProfileModificationRequest;
import com.commafeed.frontend.resource.googlereader.GoogleReaderModel.ItemRefs;
import com.commafeed.frontend.resource.googlereader.GoogleReaderModel.StreamContents;
import com.commafeed.frontend.resource.googlereader.GoogleReaderModel.SubscriptionList;
import com.commafeed.frontend.resource.googlereader.GoogleReaderModel.TagList;
import com.commafeed.frontend.resource.googlereader.GoogleReaderModel.UnreadCounts;
import com.commafeed.frontend.resource.googlereader.GoogleReaderModel.UserInfo;
import com.commafeed.integration.BaseIT;
import io.quarkus.test.junit.QuarkusTest;
import io.restassured.RestAssured;
import io.restassured.http.ContentType;
import io.restassured.response.Response;
import io.restassured.specification.RequestSpecification;
import lombok.Getter;
import org.apache.hc.core5.http.HttpStatus;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import java.util.Arrays;
import java.util.Map;
import java.util.stream.Collectors;
@QuarkusTest
class GoogleReaderIT extends BaseIT {
private static final String BASE = "rest/googlereader";
@Getter private String apiKey;
private GoogleReaderClient client;
@BeforeEach
void setup() {
initialSetup(TestConstants.ADMIN_USERNAME, TestConstants.ADMIN_PASSWORD);
RestAssured.authentication =
RestAssured.preemptive()
.basic(TestConstants.ADMIN_USERNAME, TestConstants.ADMIN_PASSWORD);
// create api key
ProfileModificationRequest req = new ProfileModificationRequest();
req.setCurrentPassword(TestConstants.ADMIN_PASSWORD);
req.setNewApiKey(true);
RestAssured.given()
.body(req)
.contentType(ContentType.JSON)
.post("rest/user/profile")
.then()
.statusCode(HttpStatus.SC_OK);
// retrieve api key
UserModel user =
RestAssured.given()
.get("rest/user/profile")
.then()
.statusCode(HttpStatus.SC_OK)
.extract()
.as(UserModel.class);
this.apiKey = user.getApiKey();
this.client = new GoogleReaderClient(clientLogin(TestConstants.ADMIN_USERNAME, apiKey));
}
@AfterEach
void cleanup() {
RestAssured.reset();
}
private String clientLogin(String email, String password) {
String body =
RestAssured.given()
.auth()
.none()
.formParam("Email", email)
.formParam("Passwd", password)
.post(BASE + "/accounts/ClientLogin")
.then()
.statusCode(HttpStatus.SC_OK)
.extract()
.asString();
Map<String, String> values =
Arrays.stream(body.split("\n"))
.map(line -> line.split("=", 2))
.collect(Collectors.toMap(kv -> kv[0], kv -> kv[1]));
return values.get("Auth");
}
@Test
void invalidClientLogin() {
RestAssured.given()
.auth()
.none()
.formParam("Email", TestConstants.ADMIN_USERNAME)
.formParam("Passwd", "invalid-key")
.post(BASE + "/accounts/ClientLogin")
.then()
.statusCode(HttpStatus.SC_FORBIDDEN);
}
@Test
void validClientLogin() {
String token = clientLogin(TestConstants.ADMIN_USERNAME, apiKey);
Assertions.assertNotNull(token);
Assertions.assertTrue(token.endsWith("/" + apiKey));
}
@Test
void invalidAuthToken() {
Response response =
RestAssured.given()
.auth()
.none()
.header("Authorization", "GoogleLogin auth=invalid/invalid-key")
.get(BASE + "/reader/api/0/user-info");
Assertions.assertEquals(HttpStatus.SC_UNAUTHORIZED, response.statusCode());
}
@Test
void authTokenOnlyAppliesToGoogleReaderEndpoints() {
String token = clientLogin(TestConstants.ADMIN_USERNAME, apiKey);
Response response =
RestAssured.given()
.auth()
.none()
.header("Authorization", "GoogleLogin auth=" + token)
.get("rest/user/profile");
Assertions.assertNotEquals(HttpStatus.SC_OK, response.statusCode());
}
@Test
void userInfo() {
UserInfo info = client.get(BASE + "/reader/api/0/user-info").as(UserInfo.class);
Assertions.assertEquals(TestConstants.ADMIN_USERNAME, info.getUserName());
}
@Test
void subscriptionList() {
subscribe(getFeedUrl());
SubscriptionList list =
client.get(BASE + "/reader/api/0/subscription/list").as(SubscriptionList.class);
Assertions.assertEquals(1, list.getSubscriptions().size());
Assertions.assertEquals(
"my title for this feed", list.getSubscriptions().getFirst().getTitle());
}
@Test
void tagList() {
createCategory("test-category");
TagList tagList = client.get(BASE + "/reader/api/0/tag/list").as(TagList.class);
Assertions.assertTrue(
tagList.getTags().stream()
.anyMatch(t -> t.getId().equals("user/-/label/test-category")));
Assertions.assertTrue(
tagList.getTags().stream()
.anyMatch(t -> t.getId().equals("user/-/state/com.google/starred")));
}
@Test
void unreadCount() {
subscribeAndWaitForEntries(getFeedUrl());
UnreadCounts counts =
client.get(BASE + "/reader/api/0/unread-count").as(UnreadCounts.class);
Assertions.assertTrue(
counts.getUnreadCounts().stream()
.anyMatch(
c ->
c.getId().equals("user/-/state/com.google/reading-list")
&& c.getCount() == 2));
}
@Test
void streamContents() {
subscribeAndWaitForEntries(getFeedUrl());
StreamContents contents =
client.get(BASE + "/reader/api/0/stream/contents/reading-list")
.as(StreamContents.class);
Assertions.assertEquals(2, contents.getItems().size());
Assertions.assertEquals("Item 2", contents.getItems().getFirst().getTitle());
Assertions.assertTrue(
contents.getItems()
.getFirst()
.getId()
.matches("tag:google\\.com,2005:reader/item/[0-9a-f]{16}"));
}
@Test
void streamItemIds() {
subscribeAndWaitForEntries(getFeedUrl());
ItemRefs refs =
client.get(
BASE
+ "/reader/api/0/stream/items/ids?s=user/-/state/com.google/reading-list")
.as(ItemRefs.class);
Assertions.assertEquals(2, refs.getItemRefs().size());
}
@Test
void editTagMarkRead() {
Long subscriptionId = subscribeAndWaitForEntries(getFeedUrl());
Entry entry = getFeedEntries(subscriptionId).getEntries().getFirst();
String token = client.get(BASE + "/reader/api/0/token").asString();
client.postForm(
BASE + "/reader/api/0/edit-tag",
Map.of("i", entry.getId(), "a", "user/-/state/com.google/read", "T", token));
Assertions.assertEquals(
1,
getFeedEntries(subscriptionId).getEntries().stream().filter(Entry::isRead).count());
client.postForm(
BASE + "/reader/api/0/edit-tag",
Map.of("i", entry.getId(), "r", "user/-/state/com.google/read", "T", token));
Assertions.assertEquals(
0,
getFeedEntries(subscriptionId).getEntries().stream().filter(Entry::isRead).count());
}
@Test
void editTagMarkReadWithoutToken() {
// some real-world Google Reader API clients never fetch the edit token and always submit
// a blank one, this must still be accepted
Long subscriptionId = subscribeAndWaitForEntries(getFeedUrl());
Entry entry = getFeedEntries(subscriptionId).getEntries().getFirst();
client.postForm(
BASE + "/reader/api/0/edit-tag",
Map.of("i", entry.getId(), "a", "user/-/state/com.google/read"));
Assertions.assertEquals(
1,
getFeedEntries(subscriptionId).getEntries().stream().filter(Entry::isRead).count());
}
@Test
void editTagStar() {
Long subscriptionId = subscribeAndWaitForEntries(getFeedUrl());
Entry entry = getFeedEntries(subscriptionId).getEntries().getFirst();
String token = client.get(BASE + "/reader/api/0/token").asString();
client.postForm(
BASE + "/reader/api/0/edit-tag",
Map.of("i", entry.getId(), "a", "user/-/state/com.google/starred", "T", token));
Assertions.assertEquals(
1,
getFeedEntries(subscriptionId).getEntries().stream()
.filter(Entry::isStarred)
.count());
}
@Test
void markAllAsRead() {
Long subscriptionId = subscribeAndWaitForEntries(getFeedUrl());
String token = client.get(BASE + "/reader/api/0/token").asString();
client.postForm(
BASE + "/reader/api/0/mark-all-as-read",
Map.of("s", "user/-/state/com.google/reading-list", "T", token));
Assertions.assertTrue(
getFeedEntries(subscriptionId).getEntries().stream().allMatch(Entry::isRead));
}
@Test
void subscribeAndUnsubscribe() {
String token = client.get(BASE + "/reader/api/0/token").asString();
client.postForm(
BASE + "/reader/api/0/subscription/edit",
Map.of(
"ac",
"subscribe",
"s",
"feed/" + getFeedUrl(),
"t",
"new subscription",
"T",
token));
SubscriptionList list =
client.get(BASE + "/reader/api/0/subscription/list").as(SubscriptionList.class);
Assertions.assertEquals(1, list.getSubscriptions().size());
String feedStreamId = list.getSubscriptions().getFirst().getId();
client.postForm(
BASE + "/reader/api/0/subscription/edit",
Map.of("ac", "unsubscribe", "s", feedStreamId, "T", token));
list = client.get(BASE + "/reader/api/0/subscription/list").as(SubscriptionList.class);
Assertions.assertTrue(list.getSubscriptions().isEmpty());
}
private record GoogleReaderClient(String token) {
private RequestSpecification given() {
return RestAssured.given()
.auth()
.none()
.header("Authorization", "GoogleLogin auth=" + token);
}
Response get(String path) {
return given().get(path).then().statusCode(HttpStatus.SC_OK).extract().response();
}
void postForm(String path, Map<String, ?> form) {
RequestSpecification spec = given();
form.forEach(spec::formParam);
spec.post(path).then().statusCode(HttpStatus.SC_OK);
}
}
}

View File

@@ -52,6 +52,7 @@ class UserIT extends BaseIT {
RestAssured.given() RestAssured.given()
.body(req) .body(req)
.contentType(ContentType.JSON) .contentType(ContentType.JSON)
.header("Host", "malicious.url.com")
.post("rest/user/passwordReset") .post("rest/user/passwordReset")
.then() .then()
.statusCode(200); .statusCode(200);
@@ -64,6 +65,7 @@ class UserIT extends BaseIT {
Assertions.assertTrue( Assertions.assertTrue(
message.getHtml() message.getHtml()
.startsWith("You asked for password recovery for account 'admin'")); .startsWith("You asked for password recovery for account 'admin'"));
Assertions.assertTrue(message.getHtml().contains("https://commafeed.example.com"));
Assertions.assertEquals("admin@commafeed.com", message.getTo().getFirst()); Assertions.assertEquals("admin@commafeed.com", message.getTo().getFirst());
Element a = Jsoup.parse(message.getHtml()).select("a").getFirst(); Element a = Jsoup.parse(message.getHtml()).select("a").getFirst();

View File

@@ -5,7 +5,7 @@
<groupId>com.commafeed</groupId> <groupId>com.commafeed</groupId>
<artifactId>commafeed</artifactId> <artifactId>commafeed</artifactId>
<version>7.2.1</version> <version>7.3.2</version>
<name>CommaFeed</name> <name>CommaFeed</name>
<packaging>pom</packaging> <packaging>pom</packaging>