commit 3e98dd1e7e700e30746b5cb4f271cdd90ce918f2 Author: Felitendo Date: Fri Oct 2 10:39:46 2026 +0200 feat: first version diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..dfe0770 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,2 @@ +# Auto detect text files and perform LF normalization +* text=auto diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml new file mode 100644 index 0000000..2c85677 --- /dev/null +++ b/.github/FUNDING.yml @@ -0,0 +1 @@ +ko_fi: felitendo diff --git a/.github/ISSUE_TEMPLATE/1-feature_request.yml b/.github/ISSUE_TEMPLATE/1-feature_request.yml new file mode 100644 index 0000000..14ebb7d --- /dev/null +++ b/.github/ISSUE_TEMPLATE/1-feature_request.yml @@ -0,0 +1,35 @@ +name: ๐Ÿ’ก Feature request +description: An idea to make rdfeed better. +labels: +- enhancement +body: +- type: checkboxes + id: checks + attributes: + label: Before you start + options: + - label: I searched the issues and this is not requested yet. + required: true +- type: textarea + id: problem + attributes: + label: What problem would this solve? + description: What are you trying to do, and what gets in the way? + validations: + required: true +- type: textarea + id: idea + attributes: + label: What would you like? + description: How it could work. A rough idea is fine. + validations: + required: true +- type: textarea + id: alternatives + attributes: + label: Other ways you thought of +- type: textarea + id: more + attributes: + label: Anything else? + description: Mockups, screenshots, or other tools that do it well. diff --git a/.github/ISSUE_TEMPLATE/2-bug_report.yml b/.github/ISSUE_TEMPLATE/2-bug_report.yml new file mode 100644 index 0000000..68afdd9 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/2-bug_report.yml @@ -0,0 +1,98 @@ +name: ๐Ÿ› Bug report +description: Something does not work as it should. +labels: +- bug +body: +- type: markdown + attributes: + value: Thanks for taking the time! The more you fill in, the faster it can be fixed. Please leave out your company's server names and your user name. +- type: checkboxes + id: checks + attributes: + label: Before you start + options: + - label: I searched the issues and this is not reported yet. + required: true + - label: I use the latest version. + required: true +- type: textarea + id: what + attributes: + label: What happened? + description: What did you do, and what went wrong? + validations: + required: true +- type: textarea + id: steps + attributes: + label: Steps to reproduce + placeholder: | + 1. + 2. + 3. +- type: textarea + id: expected + attributes: + label: What did you expect? + validations: + required: true +- type: dropdown + id: where + attributes: + label: Where does it happen? + multiple: true + options: + - Adding a workspace + - Starting an app + - The app window (drawing, keyboard, size) + - MFA prompts + - The app menu + - The menu + - Other + validations: + required: true +- type: dropdown + id: mfa + attributes: + label: Does your company use MFA? + options: + - "Yes" + - "No" + - I don't know +- type: input + id: version + attributes: + label: Version + description: The output of `rdfeed --version` and `xfreerdp3 /version`. + placeholder: rdfeed 1.0.0, FreeRDP 3.32.1 + validations: + required: true +- type: dropdown + id: install + attributes: + label: How did you install it? + options: + - AUR (Arch, CachyOS, EndeavourOS, Manjaro) + - Built from source + - Other + validations: + required: true +- type: input + id: system + attributes: + label: System + description: Distribution, desktop and its version, Wayland or X11. + placeholder: CachyOS, KDE Plasma 6.7, Wayland + validations: + required: true +- type: textarea + id: logs + attributes: + label: Log + description: For a start that goes wrong, the log from `~/.cache/rdfeed/` (one file per app). Remove server names and user names first. + render: shell +- type: textarea + id: more + attributes: + label: Anything else? + description: Screenshots or anything else that could help. diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml new file mode 100644 index 0000000..3ba13e0 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -0,0 +1 @@ +blank_issues_enabled: false diff --git a/.github/check-version.sh b/.github/check-version.sh new file mode 100755 index 0000000..68e69a9 --- /dev/null +++ b/.github/check-version.sh @@ -0,0 +1,36 @@ +#!/usr/bin/env bash +# +# Refuses a release whose tag and Makefile disagree. +# +# The version is baked into the program at install time from the Makefile, and +# the packages take theirs from the same place. But the tag is what people see +# and what the release is named after. A tag that says something else produces +# a package called 1.0.4 containing a program that reports 1.0.3, and nothing +# would have complained. +# +# Anything that is not a v-tag (a run started by hand from a branch) is not a +# release and has nothing to check. + +set -euo pipefail + +here="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")/.." && pwd)" +ref="${1:-}" + +case "$ref" in + v[0-9]*) ;; + *) + echo "not a release tag (${ref:-none}), nothing to check against" + exit 0 + ;; +esac + +tag_version="${ref#v}" +make_version="$(make -s -C "$here" version)" + +if [[ $tag_version != "$make_version" ]]; then + echo "tag $ref says $tag_version, the Makefile says $make_version" >&2 + echo "Bump VERSION in the Makefile to match the tag, or retag." >&2 + exit 1 +fi + +echo "$ref matches the Makefile" diff --git a/.github/release-notes.sh b/.github/release-notes.sh new file mode 100755 index 0000000..276fd04 --- /dev/null +++ b/.github/release-notes.sh @@ -0,0 +1,136 @@ +#!/usr/bin/env bash +# +# Builds the GitHub release notes for a tag, the way big projects such as +# Immich lay them out: the hand-written entry from CHANGELOG.md (a welcome, +# the highlights), a support section, then every commit since the last +# release, sorted by kind, and a link to the full changelog. +# +# .github/release-notes.sh v1.2.0 the notes +# .github/release-notes.sh --title v1.2.0 the title (checks the entry exists) +# +# Commit authors come from the GitHub API through gh. Without it the list +# goes without them. + +set -euo pipefail +cd -- "$(dirname -- "${BASH_SOURCE[0]}")/.." + +title=0 +if [[ ${1:-} == --title ]]; then + title=1 + shift +fi +tag="${1:?usage: release-notes.sh [--title] vX.Y.Z}" + +if ! grep -qx "## $tag" CHANGELOG.md; then + echo "CHANGELOG.md has no entry for $tag. Add \"## $tag\" first." >&2 + exit 1 +fi +if (( title )); then + printf '%s\n' "$tag" + exit 0 +fi + +repo="${GITHUB_REPOSITORY:-$(git remote get-url origin | sed -E 's#^.*github\.com[:/]##; s#\.git$##')}" +name="${repo#*/}" + +# The entry: up to the next one, without the date line (GitHub shows the +# date), one heading level up, outside code blocks. +entry="$(awk -v h="## $tag" ' + $0 == h { on = 1; next } + on && /^## / { exit } + !on { next } + /^```/ { code = !code } + !code && /^_[0-9]{4}-[0-9]{2}-[0-9]{2}_$/ { next } + !code && /^###/ { sub(/^#/, "") } + { print } +' CHANGELOG.md | sed -e '/./,$!d')" + +# The commits: from the last release, or from the start. A tag that does not +# exist yet is a preview of what HEAD would become. +if git rev-parse -q --verify "refs/tags/$tag" > /dev/null; then + target="$tag" + prev="$(git describe --tags --abbrev=0 --match 'v[0-9]*' "$tag^" 2>/dev/null || true)" +else + target="$(git rev-parse HEAD)" + prev="$(git describe --tags --abbrev=0 --match 'v[0-9]*' HEAD 2>/dev/null || true)" +fi + +declare -A login=() +if command -v gh > /dev/null; then + if [[ -n $prev ]]; then + api=(api "repos/$repo/compare/$prev...$target" --jq '.commits[] | [.sha, (.author.login // "")] | @tsv') + else + api=(api --paginate "repos/$repo/commits?sha=$target&per_page=100" --jq '.[] | [.sha, (.author.login // "")] | @tsv') + fi + while IFS=$'\t' read -r sha who; do + [[ -n $who ]] && login[$sha]="$who" + done < <(gh "${api[@]}" 2>/dev/null || true) +fi + +declare -A list=() +count_maint=0 +while IFS=$'\t' read -r sha subject body; do + # Version bumps say nothing a reader needs. + [[ $subject =~ ^(chore:\ )?($name\ )?v?[0-9]+\.[0-9]+\.[0-9]+$ ]] && continue + + case "$subject" in + *!:*) kind=breaking ;; + feat:* | feat\(*) kind=feat ;; + fix:* | fix\(*) kind=fix ;; + perf:* | perf\(*) kind=enh ;; + docs:* | docs\(*) kind=docs ;; + chore* | ci:* | ci\(* | build* | refactor* | test* | style*) kind=maint ;; + # Older commits without a prefix, sorted by what they say. + *README* | *readme* | *Readme*) kind=docs ;; + Add\ * | Put\ * | Introduce\ *) kind=feat ;; + Fix\ * | Repair\ * | Recover\ * | Stop\ *) kind=fix ;; + *\ *) kind=enh ;; + *) kind=maint ;; + esac + [[ $body == *"BREAKING CHANGE"* ]] && kind=breaking + [[ $subject == "Initial commit" ]] && kind=maint + + line="* $subject" + [[ -n ${login[$sha]:-} ]] && line+=" by @${login[$sha]}" + line+=" in https://github.com/$repo/commit/$sha" + list[$kind]+="$line"$'\n' + [[ $kind == maint ]] && count_maint=$((count_maint + 1)) +done < <(git log --reverse --no-merges --format='%H%x09%s%x09%b%x1e' "${prev:+$prev..}$target" | tr '\n\036' ' \n' | sed 's/^ //') + +changes='' +for pair in "breaking:๐Ÿšจ Breaking Changes" "feat:๐Ÿš€ Features" "enh:๐ŸŒŸ Enhancements" "fix:๐Ÿ› Bug fixes" "docs:๐Ÿ“š Documentation"; do + kind="${pair%%:*}" + [[ -n ${list[$kind]:-} ]] || continue + changes+="### ${pair#*:}"$'\n'"${list[$kind]}" +done +if [[ -n ${list[maint]:-} ]]; then + changes+=$'\n'"
๐Ÿงฐ Maintenance ($count_maint)"$'\n\n'"${list[maint]}"$'\n'"
"$'\n' +fi + +if [[ -n $prev ]]; then + full="**Full Changelog**: https://github.com/$repo/compare/$prev...$tag" +else + full="**Full Changelog**: https://github.com/$repo/commits/$tag" +fi + +# A release with highlights is a big one: it gets a heading and the support +# section, as in Immich. A patch is a sentence or two and the list. +if grep -q '^## Highlights$' <<< "$entry"; then + cat <Buy me a coffee on Ko-fi + +---- + +EOF +else + printf '%s\n\n' "$entry" +fi +printf "## What's Changed\n%s\n\n%s\n" "$changes" "$full" diff --git a/.github/workflows/check.yml b/.github/workflows/check.yml new file mode 100644 index 0000000..c28d3e0 --- /dev/null +++ b/.github/workflows/check.yml @@ -0,0 +1,30 @@ +name: check + +on: + push: + branches: [main] + pull_request: + workflow_dispatch: + +jobs: + check: + name: tests and shellcheck + runs-on: ubuntu-latest + # Arch has FreeRDP 3, which the hook test loads. + container: archlinux:latest + steps: + - name: Install the tools + run: | + pacman -Syu --noconfirm --needed git base-devel python freerdp shared-mime-info \ + gettext scdoc shellcheck desktop-file-utils + + - uses: actions/checkout@v7 + + - run: make check + + # NTLM against the spec's test vectors, the feed, the .rdp files, the + # icons, a fake RD Web server, the hook and the command end to end. + - run: make test + + - name: Build the catalogs and the man page + run: make build diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..960bd3f --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,42 @@ +name: release + +on: + push: + tags: ['v*'] + +permissions: + contents: write + +jobs: + release: + name: Test and release + runs-on: ubuntu-latest + container: archlinux:latest + steps: + - name: Install the tools + run: | + pacman -Syu --noconfirm --needed git base-devel python freerdp shared-mime-info \ + gettext scdoc github-cli + + - uses: actions/checkout@v7 + with: + # All tags, for the link to the changes since the last release. + fetch-depth: 0 + + - name: Check the tag against the Makefile + run: .github/check-version.sh "${{ github.ref_name }}" + + - name: Check CHANGELOG.md has this release + run: .github/release-notes.sh --title "${{ github.ref_name }}" + + - run: make test + + - name: Create the release + env: + GH_TOKEN: ${{ github.token }} + run: | + git config --global --add safe.directory "$PWD" + .github/release-notes.sh "${{ github.ref_name }}" > notes.md + gh release create "${{ github.ref_name }}" \ + --title "$(.github/release-notes.sh --title "${{ github.ref_name }}")" \ + --notes-file notes.md diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..be26d69 --- /dev/null +++ b/.gitignore @@ -0,0 +1,8 @@ +# build artifacts +build/ +po/*.mo +doc/*.1 +__pycache__/ + +# packages +dist/ diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..18fc1ac --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,24 @@ +# Changelog + +What each release brings, newest first. The [GitHub releases](https://github.com/LoonixTools/rdfeed/releases) add every commit that went into it. + +## v1.0.0 + +_2026-10-02_ + +Welcome to the very first release of rdfeed! It brings the RemoteApps of your company to Linux: add the workspace once, and Word, Excel and the other apps show up in your app menu and open as windows of their own. + +

+ The rdfeed menu in Konsole with the Contoso Apps workspace +

+ +### Highlights + +- The apps of your company in the app menu +- Open your files in them with "Open with" +- Signs in like Windows, also with a work e-mail address +- Works with MFA on the gateway +- Sharp on scaled screens, with the right keyboard layout +- New apps show up by themselves every day +- The password stays in the keyring +- In 14 languages diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..040b0f1 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,102 @@ +# CLAUDE.md + +## Style + +- Keep everything short: replies, explanations, comments, docs. +- Use simple English: short sentences, common words. +- Avoid em dashes (โ€”). Do not just swap them for "-" either. Rewrite the sentence instead, for + example with a comma, a colon, brackets or two sentences. + +## Commits + +- English only. +- Conventional Commits prefix: `feat:`, `fix:`, `chore:`, `docs:`, `refactor:`, `ci:`, `build:`. +- Subject as short as possible. Imperative, lowercase, no trailing period. +- Body only when something genuinely cannot be inferred from the diff. +- Never add `Co-Authored-By`, "Generated with" or any other AI attribution to commits or PR descriptions. +- Do not commit or push before I have checked the changes locally and said they are fine. Then + commit and push. Several attempts at the same thing make one commit, and attempts that did not + work make none. Different things done in one session get a commit each. This also goes for + releases and tags. + +## Layout + +- `src/rdfeed` and `src/lib/*.sh`: the command and its menu, plain bash. +- `src/fetch/rdfeed-fetch`: signs in to the feed (NTLM) and downloads the apps. Python, standard + library only. +- `src/hook/rdfeed-hook.c`: goes into FreeRDP with `LD_PRELOAD`. Waits before the second gateway + sign-in so the second MFA prompt comes, and keeps GFX off. +- `res/systemd`: the daily refresh, a user timer. + +## Releases + +Releases look like the ones of big projects such as Immich. `.github/release-notes.sh ` builds +the notes: the entry from `CHANGELOG.md` (welcome and highlights), a support section, every commit +since the last release sorted by its prefix (`feat`, `fix`, `docs`, ...) with author and link, and +the full changelog link. So commit subjects end up in public: keep them clear. + +1. Read `git log ..HEAD` and pick the version: only fixes โ†’ patch, something new โ†’ minor, + something that breaks or needs the user to act โ†’ major. +2. Bump the version and add the entry at the top of `CHANGELOG.md`, in one commit (`chore: 1.4.0`). +3. Push, then push the tag: `git tag v1.4.0 && git push origin main v1.4.0`. The `release` workflow + runs the tests and creates the release. It stops when the entry is missing. +4. PKGBUILDS picks up the new release for the AUR on its own. + +A minor or major release (has `### Highlights`, gets a heading and the support section): + +```markdown +## v1.4.0 + +_2026-09-24_ + +Welcome to rdfeed `v1.4.0`! One or two sentences on what this release is about. + +

+ What the picture shows +

+ +### ๐Ÿšจ Breaking changes + +- Only if there are any: what changed, and what the user has to do. + +### Highlights + +- First highlight, a few words +- Second highlight +``` + +A picture under the welcome is optional. To show the menu or another screen of the program, use a +real screenshot of it running in Konsole, in English. Never a text copy of the screen. The same goes +for the README. The highlights stay a plain list: no heading or text per highlight, the list of +commits explains the rest. + +A patch release is just a sentence or two, for example: "A small patch. The menu no longer closes +when you press Enter." The list of commits follows on its own. + +- Write for users: what they notice, not how the code does it. Friendly and simple. +- Commands and settings they type go in backticks, buttons and labels in bold. +- To change an old release: edit its entry, commit, then + `gh release edit --title --notes "$(.github/release-notes.sh )"`. + +## README + +- New UI (a window, a screen, a menu, a setting, a notification) gets a screenshot in the README, + next to the text about it. Like for releases: a real screenshot of it running, in English. +- When a screen changes, take its screenshot again. The README never shows an old one. +- Screenshots show the demo workspace (Contoso Apps, `CONTOSO\jdoe`) from `tests/fake_rdweb.py`, + never a real company. + +## Testing + +Never test against the real setup: the workspaces, the keyring, the timer and the app menu belong +to the user's machine, and a real server counts every wrong password. + +- `make test` runs NTLM against the test vectors of MS-NLMP, the feed, the .rdp files and the + icons, whole subscriptions against `tests/fake_rdweb.py`, the hook, and the command from `add` to + `remove`. No real server, no root. +- `tests/test_cli.sh` runs in a temporary home, with fakes for `secret-tool`, `systemctl`, + `notify-send`, `kbuildsycoca6` and `xfreerdp3` on `PATH`. Do the same for anything by hand. +- `python3 tests/fake_rdweb.py [port]` serves the demo workspace (user `jdoe`, domain `CONTOSO`, + the password is in the file). Point a temporary home at it with `XDG_CONFIG_HOME`, + `XDG_DATA_HOME`, `XDG_CACHE_HOME` and `RF_LIBDIR=src/lib RF_FETCH=src/fetch/rdfeed-fetch`. +- `make check` after every change. New strings: `po/update-pot.sh`, then translate them in every `po/*.po`. diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..f288702 --- /dev/null +++ b/LICENSE @@ -0,0 +1,674 @@ + GNU GENERAL PUBLIC LICENSE + Version 3, 29 June 2007 + + Copyright (C) 2007 Free Software Foundation, Inc. + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The GNU General Public License is a free, copyleft license for +software and other kinds of works. + + The licenses for most software and other practical works are designed +to take away your freedom to share and change the works. By contrast, +the GNU General Public License is intended to guarantee your freedom to +share and change all versions of a program--to make sure it remains free +software for all its users. We, the Free Software Foundation, use the +GNU General Public License for most of our software; it applies also to +any other work released this way by its authors. You can apply it to +your programs, too. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +them if you wish), that you receive source code or can get it if you +want it, that you can change the software or use pieces of it in new +free programs, and that you know you can do these things. + + To protect your rights, we need to prevent others from denying you +these rights or asking you to surrender the rights. Therefore, you have +certain responsibilities if you distribute copies of the software, or if +you modify it: responsibilities to respect the freedom of others. + + For example, if you distribute copies of such a program, whether +gratis or for a fee, you must pass on to the recipients the same +freedoms that you received. You must make sure that they, too, receive +or can get the source code. And you must show them these terms so they +know their rights. + + Developers that use the GNU GPL protect your rights with two steps: +(1) assert copyright on the software, and (2) offer you this License +giving you legal permission to copy, distribute and/or modify it. + + For the developers' and authors' protection, the GPL clearly explains +that there is no warranty for this free software. For both users' and +authors' sake, the GPL requires that modified versions be marked as +changed, so that their problems will not be attributed erroneously to +authors of previous versions. + + Some devices are designed to deny users access to install or run +modified versions of the software inside them, although the manufacturer +can do so. This is fundamentally incompatible with the aim of +protecting users' freedom to change the software. The systematic +pattern of such abuse occurs in the area of products for individuals to +use, which is precisely where it is most unacceptable. Therefore, we +have designed this version of the GPL to prohibit the practice for those +products. If such problems arise substantially in other domains, we +stand ready to extend this provision to those domains in future versions +of the GPL, as needed to protect the freedom of users. + + Finally, every program is threatened constantly by software patents. +States should not allow patents to restrict development and use of +software on general-purpose computers, but in those that do, we wish to +avoid the special danger that patents applied to a free program could +make it effectively proprietary. To prevent this, the GPL assures that +patents cannot be used to render the program non-free. + + The precise terms and conditions for copying, distribution and +modification follow. + + TERMS AND CONDITIONS + + 0. Definitions. + + "This License" refers to version 3 of the GNU General Public License. + + "Copyright" also means copyright-like laws that apply to other kinds of +works, such as semiconductor masks. + + "The Program" refers to any copyrightable work licensed under this +License. Each licensee is addressed as "you". "Licensees" and +"recipients" may be individuals or organizations. + + To "modify" a work means to copy from or adapt all or part of the work +in a fashion requiring copyright permission, other than the making of an +exact copy. The resulting work is called a "modified version" of the +earlier work or a work "based on" the earlier work. + + A "covered work" means either the unmodified Program or a work based +on the Program. + + To "propagate" a work means to do anything with it that, without +permission, would make you directly or secondarily liable for +infringement under applicable copyright law, except executing it on a +computer or modifying a private copy. Propagation includes copying, +distribution (with or without modification), making available to the +public, and in some countries other activities as well. + + To "convey" a work means any kind of propagation that enables other +parties to make or receive copies. Mere interaction with a user through +a computer network, with no transfer of a copy, is not conveying. + + An interactive user interface displays "Appropriate Legal Notices" +to the extent that it includes a convenient and prominently visible +feature that (1) displays an appropriate copyright notice, and (2) +tells the user that there is no warranty for the work (except to the +extent that warranties are provided), that licensees may convey the +work under this License, and how to view a copy of this License. If +the interface presents a list of user commands or options, such as a +menu, a prominent item in the list meets this criterion. + + 1. Source Code. + + The "source code" for a work means the preferred form of the work +for making modifications to it. "Object code" means any non-source +form of a work. + + A "Standard Interface" means an interface that either is an official +standard defined by a recognized standards body, or, in the case of +interfaces specified for a particular programming language, one that +is widely used among developers working in that language. + + The "System Libraries" of an executable work include anything, other +than the work as a whole, that (a) is included in the normal form of +packaging a Major Component, but which is not part of that Major +Component, and (b) serves only to enable use of the work with that +Major Component, or to implement a Standard Interface for which an +implementation is available to the public in source code form. A +"Major Component", in this context, means a major essential component +(kernel, window system, and so on) of the specific operating system +(if any) on which the executable work runs, or a compiler used to +produce the work, or an object code interpreter used to run it. + + The "Corresponding Source" for a work in object code form means all +the source code needed to generate, install, and (for an executable +work) run the object code and to modify the work, including scripts to +control those activities. However, it does not include the work's +System Libraries, or general-purpose tools or generally available free +programs which are used unmodified in performing those activities but +which are not part of the work. For example, Corresponding Source +includes interface definition files associated with source files for +the work, and the source code for shared libraries and dynamically +linked subprograms that the work is specifically designed to require, +such as by intimate data communication or control flow between those +subprograms and other parts of the work. + + The Corresponding Source need not include anything that users +can regenerate automatically from other parts of the Corresponding +Source. + + The Corresponding Source for a work in source code form is that +same work. + + 2. Basic Permissions. + + All rights granted under this License are granted for the term of +copyright on the Program, and are irrevocable provided the stated +conditions are met. This License explicitly affirms your unlimited +permission to run the unmodified Program. The output from running a +covered work is covered by this License only if the output, given its +content, constitutes a covered work. This License acknowledges your +rights of fair use or other equivalent, as provided by copyright law. + + You may make, run and propagate covered works that you do not +convey, without conditions so long as your license otherwise remains +in force. You may convey covered works to others for the sole purpose +of having them make modifications exclusively for you, or provide you +with facilities for running those works, provided that you comply with +the terms of this License in conveying all material for which you do +not control copyright. Those thus making or running the covered works +for you must do so exclusively on your behalf, under your direction +and control, on terms that prohibit them from making any copies of +your copyrighted material outside their relationship with you. + + Conveying under any other circumstances is permitted solely under +the conditions stated below. Sublicensing is not allowed; section 10 +makes it unnecessary. + + 3. Protecting Users' Legal Rights From Anti-Circumvention Law. + + No covered work shall be deemed part of an effective technological +measure under any applicable law fulfilling obligations under article +11 of the WIPO copyright treaty adopted on 20 December 1996, or +similar laws prohibiting or restricting circumvention of such +measures. + + When you convey a covered work, you waive any legal power to forbid +circumvention of technological measures to the extent such circumvention +is effected by exercising rights under this License with respect to +the covered work, and you disclaim any intention to limit operation or +modification of the work as a means of enforcing, against the work's +users, your or third parties' legal rights to forbid circumvention of +technological measures. + + 4. Conveying Verbatim Copies. + + You may convey verbatim copies of the Program's source code as you +receive it, in any medium, provided that you conspicuously and +appropriately publish on each copy an appropriate copyright notice; +keep intact all notices stating that this License and any +non-permissive terms added in accord with section 7 apply to the code; +keep intact all notices of the absence of any warranty; and give all +recipients a copy of this License along with the Program. + + You may charge any price or no price for each copy that you convey, +and you may offer support or warranty protection for a fee. + + 5. Conveying Modified Source Versions. + + You may convey a work based on the Program, or the modifications to +produce it from the Program, in the form of source code under the +terms of section 4, provided that you also meet all of these conditions: + + a) The work must carry prominent notices stating that you modified + it, and giving a relevant date. + + b) The work must carry prominent notices stating that it is + released under this License and any conditions added under section + 7. This requirement modifies the requirement in section 4 to + "keep intact all notices". + + c) You must license the entire work, as a whole, under this + License to anyone who comes into possession of a copy. This + License will therefore apply, along with any applicable section 7 + additional terms, to the whole of the work, and all its parts, + regardless of how they are packaged. This License gives no + permission to license the work in any other way, but it does not + invalidate such permission if you have separately received it. + + d) If the work has interactive user interfaces, each must display + Appropriate Legal Notices; however, if the Program has interactive + interfaces that do not display Appropriate Legal Notices, your + work need not make them do so. + + A compilation of a covered work with other separate and independent +works, which are not by their nature extensions of the covered work, +and which are not combined with it such as to form a larger program, +in or on a volume of a storage or distribution medium, is called an +"aggregate" if the compilation and its resulting copyright are not +used to limit the access or legal rights of the compilation's users +beyond what the individual works permit. Inclusion of a covered work +in an aggregate does not cause this License to apply to the other +parts of the aggregate. + + 6. Conveying Non-Source Forms. + + You may convey a covered work in object code form under the terms +of sections 4 and 5, provided that you also convey the +machine-readable Corresponding Source under the terms of this License, +in one of these ways: + + a) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by the + Corresponding Source fixed on a durable physical medium + customarily used for software interchange. + + b) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by a + written offer, valid for at least three years and valid for as + long as you offer spare parts or customer support for that product + model, to give anyone who possesses the object code either (1) a + copy of the Corresponding Source for all the software in the + product that is covered by this License, on a durable physical + medium customarily used for software interchange, for a price no + more than your reasonable cost of physically performing this + conveying of source, or (2) access to copy the + Corresponding Source from a network server at no charge. + + c) Convey individual copies of the object code with a copy of the + written offer to provide the Corresponding Source. This + alternative is allowed only occasionally and noncommercially, and + only if you received the object code with such an offer, in accord + with subsection 6b. + + d) Convey the object code by offering access from a designated + place (gratis or for a charge), and offer equivalent access to the + Corresponding Source in the same way through the same place at no + further charge. You need not require recipients to copy the + Corresponding Source along with the object code. If the place to + copy the object code is a network server, the Corresponding Source + may be on a different server (operated by you or a third party) + that supports equivalent copying facilities, provided you maintain + clear directions next to the object code saying where to find the + Corresponding Source. Regardless of what server hosts the + Corresponding Source, you remain obligated to ensure that it is + available for as long as needed to satisfy these requirements. + + e) Convey the object code using peer-to-peer transmission, provided + you inform other peers where the object code and Corresponding + Source of the work are being offered to the general public at no + charge under subsection 6d. + + A separable portion of the object code, whose source code is excluded +from the Corresponding Source as a System Library, need not be +included in conveying the object code work. + + A "User Product" is either (1) a "consumer product", which means any +tangible personal property which is normally used for personal, family, +or household purposes, or (2) anything designed or sold for incorporation +into a dwelling. In determining whether a product is a consumer product, +doubtful cases shall be resolved in favor of coverage. For a particular +product received by a particular user, "normally used" refers to a +typical or common use of that class of product, regardless of the status +of the particular user or of the way in which the particular user +actually uses, or expects or is expected to use, the product. A product +is a consumer product regardless of whether the product has substantial +commercial, industrial or non-consumer uses, unless such uses represent +the only significant mode of use of the product. + + "Installation Information" for a User Product means any methods, +procedures, authorization keys, or other information required to install +and execute modified versions of a covered work in that User Product from +a modified version of its Corresponding Source. The information must +suffice to ensure that the continued functioning of the modified object +code is in no case prevented or interfered with solely because +modification has been made. + + If you convey an object code work under this section in, or with, or +specifically for use in, a User Product, and the conveying occurs as +part of a transaction in which the right of possession and use of the +User Product is transferred to the recipient in perpetuity or for a +fixed term (regardless of how the transaction is characterized), the +Corresponding Source conveyed under this section must be accompanied +by the Installation Information. But this requirement does not apply +if neither you nor any third party retains the ability to install +modified object code on the User Product (for example, the work has +been installed in ROM). + + The requirement to provide Installation Information does not include a +requirement to continue to provide support service, warranty, or updates +for a work that has been modified or installed by the recipient, or for +the User Product in which it has been modified or installed. Access to a +network may be denied when the modification itself materially and +adversely affects the operation of the network or violates the rules and +protocols for communication across the network. + + Corresponding Source conveyed, and Installation Information provided, +in accord with this section must be in a format that is publicly +documented (and with an implementation available to the public in +source code form), and must require no special password or key for +unpacking, reading or copying. + + 7. Additional Terms. + + "Additional permissions" are terms that supplement the terms of this +License by making exceptions from one or more of its conditions. +Additional permissions that are applicable to the entire Program shall +be treated as though they were included in this License, to the extent +that they are valid under applicable law. If additional permissions +apply only to part of the Program, that part may be used separately +under those permissions, but the entire Program remains governed by +this License without regard to the additional permissions. + + When you convey a copy of a covered work, you may at your option +remove any additional permissions from that copy, or from any part of +it. (Additional permissions may be written to require their own +removal in certain cases when you modify the work.) You may place +additional permissions on material, added by you to a covered work, +for which you have or can give appropriate copyright permission. + + Notwithstanding any other provision of this License, for material you +add to a covered work, you may (if authorized by the copyright holders of +that material) supplement the terms of this License with terms: + + a) Disclaiming warranty or limiting liability differently from the + terms of sections 15 and 16 of this License; or + + b) Requiring preservation of specified reasonable legal notices or + author attributions in that material or in the Appropriate Legal + Notices displayed by works containing it; or + + c) Prohibiting misrepresentation of the origin of that material, or + requiring that modified versions of such material be marked in + reasonable ways as different from the original version; or + + d) Limiting the use for publicity purposes of names of licensors or + authors of the material; or + + e) Declining to grant rights under trademark law for use of some + trade names, trademarks, or service marks; or + + f) Requiring indemnification of licensors and authors of that + material by anyone who conveys the material (or modified versions of + it) with contractual assumptions of liability to the recipient, for + any liability that these contractual assumptions directly impose on + those licensors and authors. + + All other non-permissive additional terms are considered "further +restrictions" within the meaning of section 10. If the Program as you +received it, or any part of it, contains a notice stating that it is +governed by this License along with a term that is a further +restriction, you may remove that term. If a license document contains +a further restriction but permits relicensing or conveying under this +License, you may add to a covered work material governed by the terms +of that license document, provided that the further restriction does +not survive such relicensing or conveying. + + If you add terms to a covered work in accord with this section, you +must place, in the relevant source files, a statement of the +additional terms that apply to those files, or a notice indicating +where to find the applicable terms. + + Additional terms, permissive or non-permissive, may be stated in the +form of a separately written license, or stated as exceptions; +the above requirements apply either way. + + 8. Termination. + + You may not propagate or modify a covered work except as expressly +provided under this License. Any attempt otherwise to propagate or +modify it is void, and will automatically terminate your rights under +this License (including any patent licenses granted under the third +paragraph of section 11). + + However, if you cease all violation of this License, then your +license from a particular copyright holder is reinstated (a) +provisionally, unless and until the copyright holder explicitly and +finally terminates your license, and (b) permanently, if the copyright +holder fails to notify you of the violation by some reasonable means +prior to 60 days after the cessation. + + Moreover, your license from a particular copyright holder is +reinstated permanently if the copyright holder notifies you of the +violation by some reasonable means, this is the first time you have +received notice of violation of this License (for any work) from that +copyright holder, and you cure the violation prior to 30 days after +your receipt of the notice. + + Termination of your rights under this section does not terminate the +licenses of parties who have received copies or rights from you under +this License. If your rights have been terminated and not permanently +reinstated, you do not qualify to receive new licenses for the same +material under section 10. + + 9. Acceptance Not Required for Having Copies. + + You are not required to accept this License in order to receive or +run a copy of the Program. Ancillary propagation of a covered work +occurring solely as a consequence of using peer-to-peer transmission +to receive a copy likewise does not require acceptance. However, +nothing other than this License grants you permission to propagate or +modify any covered work. These actions infringe copyright if you do +not accept this License. Therefore, by modifying or propagating a +covered work, you indicate your acceptance of this License to do so. + + 10. Automatic Licensing of Downstream Recipients. + + Each time you convey a covered work, the recipient automatically +receives a license from the original licensors, to run, modify and +propagate that work, subject to this License. You are not responsible +for enforcing compliance by third parties with this License. + + An "entity transaction" is a transaction transferring control of an +organization, or substantially all assets of one, or subdividing an +organization, or merging organizations. If propagation of a covered +work results from an entity transaction, each party to that +transaction who receives a copy of the work also receives whatever +licenses to the work the party's predecessor in interest had or could +give under the previous paragraph, plus a right to possession of the +Corresponding Source of the work from the predecessor in interest, if +the predecessor has it or can get it with reasonable efforts. + + You may not impose any further restrictions on the exercise of the +rights granted or affirmed under this License. For example, you may +not impose a license fee, royalty, or other charge for exercise of +rights granted under this License, and you may not initiate litigation +(including a cross-claim or counterclaim in a lawsuit) alleging that +any patent claim is infringed by making, using, selling, offering for +sale, or importing the Program or any portion of it. + + 11. Patents. + + A "contributor" is a copyright holder who authorizes use under this +License of the Program or a work on which the Program is based. The +work thus licensed is called the contributor's "contributor version". + + A contributor's "essential patent claims" are all patent claims +owned or controlled by the contributor, whether already acquired or +hereafter acquired, that would be infringed by some manner, permitted +by this License, of making, using, or selling its contributor version, +but do not include claims that would be infringed only as a +consequence of further modification of the contributor version. For +purposes of this definition, "control" includes the right to grant +patent sublicenses in a manner consistent with the requirements of +this License. + + Each contributor grants you a non-exclusive, worldwide, royalty-free +patent license under the contributor's essential patent claims, to +make, use, sell, offer for sale, import and otherwise run, modify and +propagate the contents of its contributor version. + + In the following three paragraphs, a "patent license" is any express +agreement or commitment, however denominated, not to enforce a patent +(such as an express permission to practice a patent or covenant not to +sue for patent infringement). To "grant" such a patent license to a +party means to make such an agreement or commitment not to enforce a +patent against the party. + + If you convey a covered work, knowingly relying on a patent license, +and the Corresponding Source of the work is not available for anyone +to copy, free of charge and under the terms of this License, through a +publicly available network server or other readily accessible means, +then you must either (1) cause the Corresponding Source to be so +available, or (2) arrange to deprive yourself of the benefit of the +patent license for this particular work, or (3) arrange, in a manner +consistent with the requirements of this License, to extend the patent +license to downstream recipients. "Knowingly relying" means you have +actual knowledge that, but for the patent license, your conveying the +covered work in a country, or your recipient's use of the covered work +in a country, would infringe one or more identifiable patents in that +country that you have reason to believe are valid. + + If, pursuant to or in connection with a single transaction or +arrangement, you convey, or propagate by procuring conveyance of, a +covered work, and grant a patent license to some of the parties +receiving the covered work authorizing them to use, propagate, modify +or convey a specific copy of the covered work, then the patent license +you grant is automatically extended to all recipients of the covered +work and works based on it. + + A patent license is "discriminatory" if it does not include within +the scope of its coverage, prohibits the exercise of, or is +conditioned on the non-exercise of one or more of the rights that are +specifically granted under this License. You may not convey a covered +work if you are a party to an arrangement with a third party that is +in the business of distributing software, under which you make payment +to the third party based on the extent of your activity of conveying +the work, and under which the third party grants, to any of the +parties who would receive the covered work from you, a discriminatory +patent license (a) in connection with copies of the covered work +conveyed by you (or copies made from those copies), or (b) primarily +for and in connection with specific products or compilations that +contain the covered work, unless you entered into that arrangement, +or that patent license was granted, prior to 28 March 2007. + + Nothing in this License shall be construed as excluding or limiting +any implied license or other defenses to infringement that may +otherwise be available to you under applicable patent law. + + 12. No Surrender of Others' Freedom. + + If conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot convey a +covered work so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you may +not convey it at all. For example, if you agree to terms that obligate you +to collect a royalty for further conveying from those to whom you convey +the Program, the only way you could satisfy both those terms and this +License would be to refrain entirely from conveying the Program. + + 13. Use with the GNU Affero General Public License. + + Notwithstanding any other provision of this License, you have +permission to link or combine any covered work with a work licensed +under version 3 of the GNU Affero General Public License into a single +combined work, and to convey the resulting work. The terms of this +License will continue to apply to the part which is the covered work, +but the special requirements of the GNU Affero General Public License, +section 13, concerning interaction through a network will apply to the +combination as such. + + 14. Revised Versions of this License. + + The Free Software Foundation may publish revised and/or new versions of +the GNU General Public License from time to time. Such new versions will +be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + + Each version is given a distinguishing version number. If the +Program specifies that a certain numbered version of the GNU General +Public License "or any later version" applies to it, you have the +option of following the terms and conditions either of that numbered +version or of any later version published by the Free Software +Foundation. If the Program does not specify a version number of the +GNU General Public License, you may choose any version ever published +by the Free Software Foundation. + + If the Program specifies that a proxy can decide which future +versions of the GNU General Public License can be used, that proxy's +public statement of acceptance of a version permanently authorizes you +to choose that version for the Program. + + Later license versions may give you additional or different +permissions. However, no additional obligations are imposed on any +author or copyright holder as a result of your choosing to follow a +later version. + + 15. Disclaimer of Warranty. + + THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY +APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT +HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY +OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, +THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM +IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF +ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + + 16. Limitation of Liability. + + IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS +THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY +GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE +USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF +DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD +PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), +EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF +SUCH DAMAGES. + + 17. Interpretation of Sections 15 and 16. + + If the disclaimer of warranty and limitation of liability provided +above cannot be given local legal effect according to their terms, +reviewing courts shall apply local law that most closely approximates +an absolute waiver of all civil liability in connection with the +Program, unless a warranty or assumption of liability accompanies a +copy of the Program in return for a fee. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +state the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + + Copyright (C) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +Also add information on how to contact you by electronic and paper mail. + + If the program does terminal interaction, make it output a short +notice like this when it starts in an interactive mode: + + Copyright (C) + This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. + This is free software, and you are welcome to redistribute it + under certain conditions; type `show c' for details. + +The hypothetical commands `show w' and `show c' should show the appropriate +parts of the General Public License. Of course, your program's commands +might be different; for a GUI interface, you would use an "about box". + + You should also get your employer (if you work as a programmer) or school, +if any, to sign a "copyright disclaimer" for the program, if necessary. +For more information on this, and how to apply and follow the GNU GPL, see +. + + The GNU General Public License does not permit incorporating your program +into proprietary programs. If your program is a subroutine library, you +may consider it more useful to permit linking proprietary applications with +the library. If this is what you want to do, use the GNU Lesser General +Public License instead of this License. But first, please read +. diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..36217a8 --- /dev/null +++ b/Makefile @@ -0,0 +1,146 @@ +# rdfeed: build and install +# +# The shell front end and rdfeed-fetch install as they are. The one thing to +# compile is rdfeed-hook.so, which goes into FreeRDP. Translations and the +# man page are optional and skipped when msgfmt or scdoc are missing. + +# Overridable so a packager can pass the version it is actually building +# (`make VERSION=$pkgver`). The literal below is the fallback for builds +# straight from a checkout, and is what a release tag has to carry. +VERSION ?= 1.0.0 + +PREFIX ?= /usr +DESTDIR ?= +BINDIR ?= $(PREFIX)/bin +DATADIR ?= $(PREFIX)/share +LIBDIR ?= $(DATADIR)/rdfeed/lib +LIBEXECDIR ?= $(PREFIX)/lib/rdfeed +LOCALEDIR ?= $(DATADIR)/locale +MANDIR ?= $(DATADIR)/man +ICONDIR ?= $(DATADIR)/icons/hicolor/scalable/apps + +# Where systemd looks for user units, asked of systemd itself for a normal +# install. A build with a prefix of its own keeps them under that prefix. +ifeq ($(PREFIX),/usr) +USERUNITDIR ?= $(shell pkg-config --variable=systemduserunitdir systemd 2>/dev/null || echo /usr/lib/systemd/user) +else +USERUNITDIR ?= $(DATADIR)/systemd/user +endif + +CC ?= cc +CFLAGS ?= -O2 -Wall -Wextra +BUILDDIR ?= build +HOOK := $(BUILDDIR)/rdfeed-hook.so + +LINGUAS := de es fr it ja ko nl pl pt_BR ru tr uk zh_CN +MOFILES := $(patsubst %,po/%.mo,$(LINGUAS)) +MANPAGE := doc/rdfeed.1 + +LIBS := $(wildcard src/lib/*.sh) + +MSGFMT := $(shell command -v msgfmt 2>/dev/null) +SCDOC := $(shell command -v scdoc 2>/dev/null) + +.PHONY: all build check test install uninstall clean version + +all: build + +build: $(HOOK) $(MOFILES) $(MANPAGE) + +# The one place the version is written down, for everything that has to agree +# with it: the release workflow checking that the tag it was handed says the +# same thing, and the PKGBUILD. +version: + @echo $(VERSION) + +$(HOOK): src/hook/rdfeed-hook.c + @mkdir -p $(BUILDDIR) + $(CC) $(CFLAGS) $(CPPFLAGS) -shared -fPIC $(LDFLAGS) -o $@ $< -ldl + +po/%.mo: po/%.po +ifdef MSGFMT + $(MSGFMT) --check --output-file=$@ $< +else + @echo "msgfmt not found, skipping $@" +endif + +$(MANPAGE): doc/rdfeed.1.scd +ifdef SCDOC + $(SCDOC) < $< > $@ +else + @echo "scdoc not found, skipping $@" +endif + +# Syntax-check every shell file and rdfeed-fetch, and run shellcheck when it +# is available. SC2034 is off: the files share their variables, and +# shellcheck looks at one file at a time. +check: + @set -e; for f in src/rdfeed $(LIBS) tests/*.sh; do \ + bash -n "$$f" && echo "ok $$f"; \ + done + @python3 -m py_compile src/fetch/rdfeed-fetch tests/*.py && echo "ok python" + @rm -rf src/fetch/__pycache__ tests/__pycache__ + @if command -v shellcheck >/dev/null 2>&1; then \ + shellcheck -x -e SC1090,SC1091,SC2034 src/rdfeed $(LIBS) tests/*.sh \ + && echo "ok shellcheck"; \ + else \ + echo "shellcheck not found, skipped"; \ + fi + +# The NTLM sign-in against the test vectors of the spec, the feed, the .rdp +# files and the icons, a whole subscription against a fake RD Web server, and +# the hook. Nothing here talks to a real server or touches the real setup. +test: $(HOOK) + python3 -m unittest discover -s tests -p 'test_*.py' -v + RDFEED_HOOK=$(HOOK) bash tests/test_hook.sh + bash tests/test_cli.sh + +install: build + # the command + install -Dm755 src/rdfeed "$(DESTDIR)$(BINDIR)/rdfeed" + install -d "$(DESTDIR)$(LIBDIR)" + install -Dm644 -t "$(DESTDIR)$(LIBDIR)" $(LIBS) + sed -i -e 's|@VERSION@|$(VERSION)|g' \ + -e 's|@LIBDIR@|$(LIBDIR)|g' \ + -e 's|@LIBEXECDIR@|$(LIBEXECDIR)|g' \ + -e 's|@LOCALEDIR@|$(LOCALEDIR)|g' \ + "$(DESTDIR)$(BINDIR)/rdfeed" \ + "$(DESTDIR)$(LIBDIR)"/*.sh + + # the helpers + install -Dm755 src/fetch/rdfeed-fetch "$(DESTDIR)$(LIBEXECDIR)/rdfeed-fetch" + install -Dm755 $(HOOK) "$(DESTDIR)$(LIBEXECDIR)/rdfeed-hook.so" + + # the daily refresh, and the icon of the workspace folders + install -Dm644 res/systemd/rdfeed-refresh.service "$(DESTDIR)$(USERUNITDIR)/rdfeed-refresh.service" + install -Dm644 res/systemd/rdfeed-refresh.timer "$(DESTDIR)$(USERUNITDIR)/rdfeed-refresh.timer" + sed -i -e 's|@BINDIR@|$(BINDIR)|g' "$(DESTDIR)$(USERUNITDIR)/rdfeed-refresh.service" + install -Dm644 res/rdfeed.svg "$(DESTDIR)$(ICONDIR)/rdfeed.svg" + + # translations + @for l in $(LINGUAS); do \ + if [ -f "po/$$l.mo" ]; then \ + install -Dm644 "po/$$l.mo" \ + "$(DESTDIR)$(LOCALEDIR)/$$l/LC_MESSAGES/rdfeed.mo"; \ + fi; \ + done + + # documentation + @if [ -f $(MANPAGE) ]; then \ + install -Dm644 $(MANPAGE) "$(DESTDIR)$(MANDIR)/man1/rdfeed.1"; \ + fi + install -Dm644 README.md "$(DESTDIR)$(DATADIR)/doc/rdfeed/README.md" + +uninstall: + rm -f "$(DESTDIR)$(BINDIR)/rdfeed" + rm -rf "$(DESTDIR)$(DATADIR)/rdfeed" + rm -rf "$(DESTDIR)$(LIBEXECDIR)" + rm -f "$(DESTDIR)$(USERUNITDIR)/rdfeed-refresh.service" + rm -f "$(DESTDIR)$(USERUNITDIR)/rdfeed-refresh.timer" + rm -f "$(DESTDIR)$(ICONDIR)/rdfeed.svg" + rm -f "$(DESTDIR)$(MANDIR)/man1/rdfeed.1" + rm -rf "$(DESTDIR)$(DATADIR)/doc/rdfeed" + @for l in $(LINGUAS); do rm -f "$(DESTDIR)$(LOCALEDIR)/$$l/LC_MESSAGES/rdfeed.mo"; done + +clean: + rm -rf $(BUILDDIR) po/*.mo $(MANPAGE) diff --git a/README.md b/README.md new file mode 100644 index 0000000..8a4d4dc --- /dev/null +++ b/README.md @@ -0,0 +1,152 @@ +

+ rdfeed +

+ +

rdfeed

+ +

The RemoteApps of your company, in the Linux app menu.

+ +

+ Word, Excel and the other Windows apps your company publishes open as windows of their own.
+ Like "Work Resources" on Windows, through FreeRDP. +

+ +
+ Install | + How to use | + Is it safe? | + Report a bug +
+ +

+ Buy me a coffee on Ko-fi +

+ +

+ The rdfeed menu in Konsole: one workspace, Contoso Apps, with 4 apps for CONTOSO\jdoe +

+ +## Install + +
+Arch, CachyOS, EndeavourOS, Manjaro + +```bash +yay -S rdfeed +``` + +
+ +
+Other distributions + +See [Build from source](#more). It needs FreeRDP 3, Python 3, bash and +`secret-tool` (libsecret). + +
+ +## How to use + +```bash +rdfeed +``` + +Press **1** and type the address your IT department gave you. The server +name or your work e-mail address often works too. Then sign in with your +Windows user name and password. + +The apps show up in your app menu, in a folder with the name of the +workspace. Click one and it opens in a window of its own. If your company uses +MFA, confirm the sign-in in your authenticator app. Many servers ask twice: a +notification tells you when the second prompt is coming. + +Files on your computer open in the apps too: right-click a `.docx` and pick +**Open with**, then Word. + +
+Settings + +

+ The settings in Konsole: share a folder, open files with the apps, size, sound, graphics, the wait for the second MFA prompt, and a daily refresh, with the selected one explained at the bottom +

+ +
+ +## Is it safe? + +Your password goes into the keyring, not into a file. FreeRDP gets it through +a pipe, so it never shows up in the list of running programs. + +The connection files from the server can ask to share every drive, camera and +USB device of your computer. rdfeed removes that. The apps only see the folder +you pick under **Share a folder**, and none by default. When you open a file in +an app, its folder is shared while that app runs. + +## More + +
+What it fixes in FreeRDP + +Starting a RemoteApp with plain `xfreerdp3` often goes wrong. rdfeed takes care of it: + +- **It hangs after the MFA prompt.** A connection broker needs a second sign-in + at the gateway. Its prompt only comes if the client waits about ten seconds, + and FreeRDP does not wait. rdfeed does. +- **Y and Z are swapped.** Under Wayland, FreeRDP sees the wrong keyboard layout. + rdfeed asks the desktop. +- **The apps are blurry or tiny** with a scaled desktop. rdfeed has the server + draw them at your scale. +- **Signing in to the feed fails.** It needs NTLM, which `curl` no longer + always has. rdfeed signs in by itself. + +
+ +
+Known problems + +- **A window stays partly empty after you minimize or resize it.** This is a + bug in FreeRDP. Under **Settings**, **Graphics** on classic paints every + window right, but some servers end the session with it. +- **A second app of the same workspace takes over the first one's connection.** + Its windows move into the new one, nothing closes. + +
+ +
+How it works + +| | | +|---|---| +| `rdfeed` | The menu and the commands. | +| `rdfeed-fetch` | Signs in to the feed and downloads the apps and their icons. | +| `rdfeed-hook.so` | Goes into FreeRDP: the wait for the second MFA prompt, and the classic graphics if you pick them. | +| `rdfeed-refresh.timer` | Loads the apps again once a day, so new ones show up. | + +The feed is the one Windows subscribes to under *RemoteApp and Desktop +Connections* (`https://server/RDWeb/Feed/webfeed.aspx`). Azure Virtual Desktop +and Windows 365 sign in another way and are not supported. All details: +`man rdfeed`. + +
+ +
+Build from source + +```bash +make +make test +sudo make install +``` + +Needs a C compiler, and for the translations and the man page gettext and +scdoc. To run it: bash, Python 3, FreeRDP 3 (`xfreerdp3` or `xfreerdp`) and +`secret-tool`. + +
+ +## Credits + +- [FreeRDP](https://www.freerdp.com/), which does the actual work. +- [MS-NLMP](https://learn.microsoft.com/openspecs/windows_protocols/ms-nlmp/), for NTLM and its test vectors. + +GPL-3.0-or-later. diff --git a/doc/rdfeed.1.scd b/doc/rdfeed.1.scd new file mode 100644 index 0000000..4d4315a --- /dev/null +++ b/doc/rdfeed.1.scd @@ -0,0 +1,173 @@ +rdfeed(1) + +# NAME + +rdfeed - the RemoteApps of your company in the Linux app menu + +# SYNOPSIS + +*rdfeed* [_command_] + +# DESCRIPTION + +Companies publish their Windows apps (Word, Excel, their own tools) as an RD +Web Access feed. Windows subscribes to it under "RemoteApp and Desktop +Connections" and puts the apps in the start menu, in a folder with the name of +the workspace. rdfeed does the same on Linux: each app gets an entry in the +app menu and opens as a window of its own, not as a whole Windows desktop. +The apps run on the company's servers, through FreeRDP. + +Run without a command it shows an interactive menu. Everything it can be told +is reachable from there; the files behind it do not need to be edited by +hand. + +# COMMANDS + +*add* [_address_] [*--user* _name_] + Add a workspace. The address is the one of the feed, as Windows takes it + (*https://server/RDWeb/Feed/webfeed.aspx*), or just the server name, or + the RD Web Access page, or a work e-mail address. For an e-mail address + the feed is looked up in the TXT record *\_msradc.*_domain_, as Windows + does it. The user name can be _DOMAIN\\name_, _name@domain_, or just the + name, which takes the server's own domain. + + Without a terminal, the user name and the password are read from stdin, + one per line. + +*refresh* [_workspace_] + Load the apps of every workspace again, or of the one given. Apps that + were added show up in the app menu, apps that were taken away disappear. + +*list* + The workspaces with their apps, each as _workspace/app_, the name *run* + takes. + +*run* _app_ [_file_] + Start an app. _app_ is enough when only one workspace has an app of that + name, otherwise _workspace/app_. This is what the app menu entries run. + + With a file, the app opens it. The folder of the file is shared as a + drive for that session, and the app is started with the path on it, + _\\\\tsclient\\folder\\file_, the way Windows does it. If the server + does not let the app take a file this way, it opens empty, and the file + is on that drive. + +*remove* _workspace_ + Remove a workspace: its app menu entries, its folder, the downloaded + files, the saved password. + +*-h*, *--help* + Show the help. + +*-V*, *--version* + Show the version. + +# SETTINGS + +*Share a folder* + Off, Documents, Downloads or the home folder. The apps see it as a drive + and can open and save files there. Off by default: then they only see the + files on the server. + +*Open files with the apps* + On by default. The app menu entries name the file types of each app, so + file managers offer it under "Open with", for example Word for .docx + files. The types come from the feed when the administrator entered them, + as Windows takes them. Most feeds have none, and then rdfeed knows the + usual ones: Word, Excel, PowerPoint, OneNote, Outlook, Visio, Access, + Project, Publisher, PDF readers, AutoCAD and DWG TrueView, Notepad. Your + default apps stay as they are. + +*Size* + How big the apps are drawn on the server. Automatic takes the scale your + desktop gives X11 programs that scale themselves (KDE Plasma's Xwayland + scale, or Xft.dpi), so the apps come out sharp and the right size. + +*Sound* + Play the sound of the apps on this computer. + +*Graphics* + GFX (the default) or classic. See *FREERDP PROBLEMS*. + +*Wait for the second MFA prompt* + Automatic (the default), always or never. See *FREERDP PROBLEMS*. + +*Refresh the apps every day* + A systemd user timer (*rdfeed-refresh.timer*) that loads the apps again + once a day, as Windows does. It is switched on with the first workspace. + When the password stops working it says so in a notification. + +# FREERDP PROBLEMS + +rdfeed starts each app with *xfreerdp3* (*xfreerdp* on Fedora) in RemoteApp +mode, with *rdfeed-hook.so* loaded into it. The hook works around two +problems that leave FreeRDP hanging or windows half painted: + +*The second MFA prompt* + Most companies run a connection broker, which sends the client on to the + server that hosts the session. FreeRDP then opens a second tunnel through + the RD Gateway at once. With MFA on the gateway (the Azure MFA extension + of NPS), that second sign-in needs a second prompt in the authenticator + app. When it comes less than about ten seconds after the first one was + confirmed, no prompt is sent and the gateway waits forever. So the hook + waits 12 seconds before a new gateway connection, and a notification says + a second prompt is coming. Automatic waits only when the first sign-in + took long enough for somebody to confirm a prompt. + +*Windows that stay empty* + In RemoteApp mode with the graphics pipeline (GFX), FreeRDP may not + repaint a window after it was minimized or resized. FreeRDP 3 has no + switch to turn GFX off. With *Graphics* on classic, the hook keeps it off + and every window paints right. But some servers end the session right + after signing in without GFX, so GFX stays the default. Not fixed yet. + +# HOW SAFE IS THIS + +The password goes into the keyring (the Secret Service, as GNOME Keyring or +KWallet provide it), never into a file. FreeRDP gets it on stdin, never on its +command line, so it shows in no process list. + +The .rdp files from the server can ask to share every drive, camera and USB +device of this computer. rdfeed removes those lines. Only the folder picked +under *Share a folder* is shared, and the folder of a file you open in an app, +for as long as that session runs. + +The server's certificate is trusted the first time and checked after that +(*/cert:tofu*). The feed itself is fetched over HTTPS with the system's +certificates. + +# FILES + +_~/.config/rdfeed/config_ + The settings. + +_~/.config/rdfeed/workspaces/_ + One file per workspace: the address, the name, the user. + +_~/.local/share/rdfeed/_ + The apps of each workspace: the .rdp files and the icons. + +_~/.local/share/applications/rdfeed-\*.desktop_ + The app menu entries, with a folder per workspace in + _~/.config/menus/applications-merged/_. + +_~/.cache/rdfeed/_ + The log of the last start of each app, for when something goes wrong. + +# LIMITS + +Only feeds that sign in with Windows (NTLM) authentication, which is what RD +Web Access uses. Azure Virtual Desktop and Windows 365 sign in with Microsoft +Entra and are not supported. + +Starting a second app of the same workspace opens a second connection, which +takes over the session of the first: its windows move into the new one. +Nothing closes. + +# SEE ALSO + +*xfreerdp3*(1), *secret-tool*(1) + +# AUTHORS + +Felitendo. Source and bug reports: https://github.com/LoonixTools/rdfeed diff --git a/po/de.po b/po/de.po new file mode 100644 index 0000000..f7fb8ec --- /dev/null +++ b/po/de.po @@ -0,0 +1,564 @@ +# German translation for rdfeed. +# Copyright (C) 2026 Felitendo +# This file is distributed under the same license as rdfeed. +# +msgid "" +msgstr "" +"Project-Id-Version: rdfeed 1.0.0\n" +"Report-Msgid-Bugs-To: https://github.com/LoonixTools/rdfeed/issues\n" +"POT-Creation-Date: 2026-10-02 10:29+0200\n" +"PO-Revision-Date: 2026-10-02 09:30+0200\n" +"Last-Translator: Felitendo\n" +"Language-Team: German\n" +"Language: de\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" + +#: src/rdfeed:79 src/rdfeed:103 +#, sh-printf-format +msgid "Could not write to %s." +msgstr "Konnte nicht nach %s schreiben." + +#: src/rdfeed:82 +#, sh-printf-format +msgid "Signing in to %sโ€ฆ" +msgstr "Anmeldung bei %sโ€ฆ" + +#: src/rdfeed:100 +msgid "" +"The password could not be saved in the keyring, so each app asks for it." +msgstr "" +"Das Passwort konnte nicht im Schlรผsselbund gespeichert werden, darum fragt " +"jede App danach." + +#: src/rdfeed:111 +#, sh-printf-format +msgid "1 app from %s is now in your app menu." +msgstr "1 App von %s ist jetzt in deinem Anwendungsmenรผ." + +#: src/rdfeed:113 +#, sh-printf-format +msgid "%d apps from %s are now in your app menu." +msgstr "%d Apps von %s sind jetzt in deinem Anwendungsmenรผ." + +#: src/rdfeed:129 +msgid "" +"Your IT department has the address. A work e-mail address often works too." +msgstr "" +"Die Adresse bekommst du von deiner IT-Abteilung. Oft klappt auch deine " +"Firmen-E-Mail-Adresse." + +#: src/rdfeed:130 +msgid "Feed address, server or e-mail:" +msgstr "Feed-Adresse, Server oder E-Mail:" + +#: src/rdfeed:137 +#, sh-printf-format +msgid "No feed is registered for %s. Ask your IT department for the address." +msgstr "" +"Fรผr %s ist kein Feed hinterlegt. Frag deine IT-Abteilung nach der Adresse." + +#: src/rdfeed:142 src/rdfeed:156 +msgid "User name (DOMAIN\\name or name@domain):" +msgstr "Benutzername (DOMร„NE\\name oder name@domรคne):" + +#: src/rdfeed:146 src/rdfeed:159 +msgid "Password:" +msgstr "Passwort:" + +#: src/rdfeed:178 src/rdfeed:236 +#, sh-printf-format +msgid "No workspace yet. Add one with `%s add`." +msgstr "Noch kein Arbeitsbereich. Fรผge einen mit `%s add` hinzu." + +#: src/rdfeed:187 src/lib/launch.sh:220 +#, sh-printf-format +msgid "No password saved for %s. Sign in again in rdfeed." +msgstr "Fรผr %s ist kein Passwort gespeichert. Melde dich in rdfeed neu an." + +#: src/rdfeed:191 src/lib/launch.sh:149 +#, sh-printf-format +msgid "Password for %s:" +msgstr "Passwort fรผr %s:" + +#: src/rdfeed:195 +#, sh-printf-format +msgid "Refreshing %sโ€ฆ" +msgstr "%s wird aktualisiertโ€ฆ" + +#: src/rdfeed:205 +msgid "The password no longer works. Sign in again in rdfeed." +msgstr "Das Passwort funktioniert nicht mehr. Melde dich in rdfeed neu an." + +#: src/rdfeed:214 +#, sh-printf-format +msgid "%s: %d apps" +msgstr "%s: %d Apps" + +#: src/rdfeed:222 +#, sh-printf-format +msgid "Which workspace? See `%s list` for the names." +msgstr "Welcher Arbeitsbereich? Die Namen zeigt `%s list`." + +#: src/rdfeed:229 +#, sh-printf-format +msgid "%s and its apps are removed." +msgstr "%s und seine Apps sind entfernt." + +#: src/rdfeed:253 +#, sh-printf-format +msgid "Which app? See `%s list`." +msgstr "Welche App? Siehe `%s list`." + +#: src/rdfeed:268 src/lib/launch.sh:199 +#, sh-printf-format +msgid "There is no app %s. See `%s list`." +msgstr "Es gibt keine App %s. Siehe `%s list`." + +#: src/rdfeed:270 +#, sh-printf-format +msgid "Several workspaces have %s. Write it as WORKSPACE/APP, see `%s list`." +msgstr "" +"Mehrere Arbeitsbereiche haben %s. Schreib es als ARBEITSBEREICH/APP, siehe " +"`%s list`." + +#: src/rdfeed:278 +msgid "Usage: rdfeed [command]" +msgstr "Aufruf: rdfeed [Befehl]" + +#: src/rdfeed:280 +msgid "Commands:" +msgstr "Befehle:" + +#: src/rdfeed:281 +msgid "Add the workspace of your company" +msgstr "Den Arbeitsbereich deiner Firma hinzufรผgen" + +#: src/rdfeed:282 +msgid "Load the apps again" +msgstr "Die Apps neu laden" + +#: src/rdfeed:283 +msgid "List the workspaces and their apps" +msgstr "Arbeitsbereiche und ihre Apps auflisten" + +#: src/rdfeed:284 +msgid "Start an app, or open a file with it" +msgstr "Eine App starten oder eine Datei damit รถffnen" + +#: src/rdfeed:285 +msgid "Remove a workspace and its apps" +msgstr "Einen Arbeitsbereich mit seinen Apps entfernen" + +#: src/rdfeed:286 +msgid "Show this help" +msgstr "Diese Hilfe anzeigen" + +#: src/rdfeed:287 +msgid "Show the version" +msgstr "Die Version anzeigen" + +#: src/rdfeed:289 +msgid "Without a command an interactive menu is shown." +msgstr "Ohne Befehl wird ein interaktives Menรผ angezeigt." + +#: src/rdfeed:313 +#, sh-printf-format +msgid "Unknown command: %s" +msgstr "Unbekannter Befehl: %s" + +#: src/lib/common.sh:174 src/lib/menu.sh:251 +msgid "never" +msgstr "nie" + +#: src/lib/common.sh:184 +msgid "just now" +msgstr "gerade eben" + +#: src/lib/common.sh:186 +msgid "1 minute ago" +msgstr "vor 1 Minute" + +#: src/lib/common.sh:189 +#, sh-printf-format +msgid "%d minutes ago" +msgstr "vor %d Minuten" + +#: src/lib/common.sh:191 +msgid "1 hour ago" +msgstr "vor 1 Stunde" + +#: src/lib/common.sh:194 +#, sh-printf-format +msgid "%d hours ago" +msgstr "vor %d Stunden" + +#: src/lib/common.sh:196 +msgid "1 day ago" +msgstr "vor 1 Tag" + +#: src/lib/common.sh:199 +#, sh-printf-format +msgid "%d days ago" +msgstr "vor %d Tagen" + +#: src/lib/launch.sh:169 +msgid "" +"Your password has expired. Change it on Windows or on the RD Web page, then " +"sign in again in rdfeed." +msgstr "" +"Dein Passwort ist abgelaufen. ร„ndere es unter Windows oder auf der RD-Web-" +"Seite und melde dich dann in rdfeed neu an." + +#: src/lib/launch.sh:171 +msgid "Your account is locked. Ask your IT department." +msgstr "Dein Konto ist gesperrt. Frag deine IT-Abteilung." + +#: src/lib/launch.sh:173 +msgid "The server did not accept the password. Sign in again in rdfeed." +msgstr "" +"Der Server hat das Passwort nicht angenommen. Melde dich in rdfeed neu an." + +#: src/lib/launch.sh:175 +msgid "Could not reach the server. Check the internet connection." +msgstr "Der Server ist nicht erreichbar. Prรผf die Internetverbindung." + +#: src/lib/launch.sh:178 +#, sh-printf-format +msgid "FreeRDP stopped: %s" +msgstr "FreeRDP hat aufgegeben: %s" + +#: src/lib/launch.sh:205 src/lib/launch.sh:206 +#, sh-printf-format +msgid "File not found: %s" +msgstr "Datei nicht gefunden: %s" + +#: src/lib/launch.sh:212 src/lib/launch.sh:213 +msgid "" +"FreeRDP 3 is missing. Install it to start the apps (the package is called " +"freerdp)." +msgstr "" +"FreeRDP 3 fehlt. Installiere es, um die Apps zu starten (das Paket heiรŸt " +"freerdp)." + +#: src/lib/launch.sh:277 +#, sh-printf-format +msgid "Opening %s in %s" +msgstr "%s wird in %s geรถffnet" + +#: src/lib/launch.sh:278 +#, sh-printf-format +msgid "If it does not open by itself, open it in the app from the drive %s." +msgstr "" +"Falls sie sich nicht von selbst รถffnet, รถffne sie in der App vom Laufwerk %s." + +#: src/lib/launch.sh:280 +#, sh-printf-format +msgid "Starting %s" +msgstr "%s wird gestartet" + +#: src/lib/launch.sh:281 +msgid "" +"If your company uses MFA, confirm the sign-in in your authenticator app." +msgstr "" +"Wenn deine Firma MFA nutzt, bestรคtige die Anmeldung in deiner Authenticator-" +"App." + +#: src/lib/launch.sh:290 +msgid "A second sign-in is coming" +msgstr "Gleich kommt eine zweite Anmeldung" + +#: src/lib/launch.sh:291 +#, sh-printf-format +msgid "" +"Confirm the second prompt in your authenticator app too. It comes in about " +"%d seconds." +msgstr "" +"Bestรคtige auch die zweite Anfrage in deiner Authenticator-App. Sie kommt in " +"etwa %d Sekunden." + +#: src/lib/launch.sh:301 +#, sh-printf-format +msgid "%s could not start" +msgstr "%s konnte nicht starten" + +#: src/lib/launch.sh:303 +#, sh-printf-format +msgid "Log: %s" +msgstr "Protokoll: %s" + +#. TRANSLATORS: the letter after "[" is the key for yes. y works too. +#: src/lib/menu.sh:93 +msgid "[y/N]" +msgstr "[j/N]" + +#: src/lib/menu.sh:163 +msgid "1 app" +msgstr "1 App" + +#: src/lib/menu.sh:165 +#, sh-printf-format +msgid "%d apps" +msgstr "%d Apps" + +#: src/lib/menu.sh:190 +msgid "No workspace yet. Add the one of your company with [1]." +msgstr "Noch kein Arbeitsbereich. Fรผge den deiner Firma mit [1] hinzu." + +#: src/lib/menu.sh:233 +msgid "off" +msgstr "aus" + +#: src/lib/menu.sh:234 +msgid "home folder" +msgstr "Persรถnlicher Ordner" + +#: src/lib/menu.sh:237 +msgid "Documents" +msgstr "Dokumente" + +#: src/lib/menu.sh:238 +msgid "Downloads" +msgstr "Downloads" + +#: src/lib/menu.sh:243 +msgid "not found" +msgstr "nicht gefunden" + +#: src/lib/menu.sh:246 +#, sh-printf-format +msgid "automatic (%d %%)" +msgstr "automatisch (%d %%)" + +#: src/lib/menu.sh:248 +msgid "classic" +msgstr "klassisch" + +#: src/lib/menu.sh:249 +msgid "GFX" +msgstr "GFX" + +#: src/lib/menu.sh:250 +msgid "always" +msgstr "immer" + +#: src/lib/menu.sh:252 +msgid "automatic" +msgstr "automatisch" + +#: src/lib/menu.sh:261 +msgid "" +"The apps only see the files on the server. Pick a folder to open and save " +"files of this computer in them." +msgstr "" +"Die Apps sehen nur die Dateien auf dem Server. Wรคhl einen Ordner, damit sie " +"Dateien dieses Computers รถffnen und speichern kรถnnen." + +#: src/lib/menu.sh:262 +msgid "" +"The apps can open and save files in this folder. It shows up as a drive in " +"them." +msgstr "" +"Die Apps kรถnnen in diesem Ordner Dateien รถffnen und speichern. Er erscheint " +"in ihnen als Laufwerk." + +#: src/lib/menu.sh:263 +msgid "" +"Offers the apps under \"Open with\" for their file types, for example Word " +"for .docx. The folder of the file is shared with the app while it is open." +msgstr "" +"Bietet die Apps unter โ€žร–ffnen mitโ€œ fรผr ihre Dateitypen an, zum Beispiel Word " +"fรผr .docx. Der Ordner der Datei wird freigegeben, solange sie offen ist." + +#: src/lib/menu.sh:264 +msgid "" +"How big the apps are drawn. Automatic follows the scale of your desktop, so " +"they are sharp and the right size." +msgstr "" +"Wie groรŸ die Apps gezeichnet werden. Automatisch folgt der Skalierung deines " +"Desktops, so sind sie scharf und richtig groรŸ." + +#: src/lib/menu.sh:265 +msgid "Plays the sound of the apps here, for calls and videos." +msgstr "Spielt den Ton der Apps hier ab, fรผr Anrufe und Videos." + +#: src/lib/menu.sh:266 +msgid "" +"Paints every window right. But some servers end the session right after " +"signing in, then go back to GFX." +msgstr "Zeichnet jedes Fenster richtig. Manche Server beenden die Sitzung aber direkt nach der Anmeldung, dann zurรผck auf GFX." + +#: src/lib/menu.sh:267 +msgid "" +"Works with every server. FreeRDP may not repaint a window after it was " +"minimized or resized." +msgstr "Geht mit jedem Server. FreeRDP zeichnet ein Fenster nach dem Minimieren oder GrรถรŸe-ร„ndern eventuell nicht neu." + +#: src/lib/menu.sh:268 +msgid "" +"Always waits before the second sign-in. For when the second prompt does not " +"come with automatic." +msgstr "" +"Wartet immer vor der zweiten Anmeldung. Fรผr den Fall, dass mit automatisch " +"keine zweite Anfrage kommt." + +#: src/lib/menu.sh:269 +msgid "Never waits. Right when your company has no MFA." +msgstr "Wartet nie. Richtig, wenn deine Firma kein MFA nutzt." + +#: src/lib/menu.sh:270 +msgid "" +"Many servers with MFA need a second sign-in, and its prompt only comes after " +"a short wait. Automatic waits when the first sign-in took a while." +msgstr "" +"Viele Server mit MFA brauchen eine zweite Anmeldung, und deren Anfrage kommt " +"erst nach kurzem Warten. Automatisch wartet, wenn die erste Anmeldung etwas " +"gedauert hat." + +#: src/lib/menu.sh:273 +msgid "" +"Loads the list of apps again once a day, so new apps show up by themselves." +msgstr "" +"Lรคdt die Liste der Apps einmal am Tag neu, so tauchen neue Apps von selbst " +"auf." + +#: src/lib/menu.sh:275 src/lib/menu.sh:330 +msgid "Needs a systemd user session." +msgstr "Braucht eine systemd-Benutzersitzung." + +#: src/lib/menu.sh:322 src/lib/menu.sh:325 +msgid "Could not save the setting." +msgstr "Einstellung konnte nicht gespeichert werden." + +#: src/lib/menu.sh:371 src/lib/menu.sh:609 +msgid "Settings" +msgstr "Einstellungen" + +#: src/lib/menu.sh:372 +msgid "โ†‘โ†“ select โ†โ†’ or Space: change q: back" +msgstr "โ†‘โ†“ auswรคhlen โ†โ†’ oder Leertaste: รคndern q: zurรผck" + +#: src/lib/menu.sh:373 +msgid "ON" +msgstr "AN" + +#: src/lib/menu.sh:374 +msgid "OFF" +msgstr "AUS" + +#: src/lib/menu.sh:456 src/lib/menu.sh:608 +msgid "Workspaces" +msgstr "Arbeitsbereiche" + +#. TRANSLATORS: keep the letters, they are the keys. +#: src/lib/menu.sh:458 +msgid "" +"โ†‘โ†“ select r: refresh s: sign in again d: remove a: add q: back" +msgstr "" +"โ†‘โ†“ auswรคhlen r: aktualisieren s: neu anmelden d: entfernen a: " +"hinzufรผgen q: zurรผck" + +#: src/lib/menu.sh:459 +msgid "No workspace yet. Press a to add one." +msgstr "Noch kein Arbeitsbereich. Drรผck a, um einen hinzuzufรผgen." + +#: src/lib/menu.sh:484 +#, sh-printf-format +msgid "refreshed %s" +msgstr "aktualisiert %s" + +#: src/lib/menu.sh:512 +#, sh-printf-format +msgid "Remove \"%s\" and its apps?" +msgstr "โ€ž%sโ€œ und seine Apps entfernen?" + +#: src/lib/menu.sh:536 src/lib/menu.sh:607 +msgid "Start an app" +msgstr "Eine App starten" + +#: src/lib/menu.sh:537 +msgid "โ†‘โ†“ select Enter: start q: back" +msgstr "โ†‘โ†“ auswรคhlen Enter: starten q: zurรผck" + +#: src/lib/menu.sh:538 +msgid "No apps yet. Add a workspace first." +msgstr "Noch keine Apps. Fรผge zuerst einen Arbeitsbereich hinzu." + +#: src/lib/menu.sh:579 +#, sh-printf-format +msgid "%s is starting. It opens in a window of its own." +msgstr "%s startet. Es รถffnet sich in einem eigenen Fenster." + +#: src/lib/menu.sh:606 +msgid "Add a workspace" +msgstr "Arbeitsbereich hinzufรผgen" + +#: src/lib/menu.sh:610 +msgid "Quit" +msgstr "Beenden" + +#: src/lib/workspace.sh:163 +msgid "The server did not accept the user name or password." +msgstr "Der Server hat Benutzername oder Passwort nicht angenommen." + +#: src/lib/workspace.sh:164 +#, sh-printf-format +msgid "Could not reach the server: %s" +msgstr "Server nicht erreichbar: %s" + +#: src/lib/workspace.sh:165 +#, sh-printf-format +msgid "There is no RD Web feed at %s." +msgstr "Unter %s gibt es keinen RD-Web-Feed." + +#: src/lib/workspace.sh:166 +#, sh-printf-format +msgid "Could not load the apps: %s" +msgstr "Die Apps konnten nicht geladen werden: %s" + +#: src/lib/workspace.sh:282 +#, sh-printf-format +msgid "Remote desktop of %s" +msgstr "Remotedesktop von %s" + +#: src/lib/workspace.sh:284 +#, sh-printf-format +msgid "%s from %s" +msgstr "%s von %s" + +#: src/lib/menu.sh +msgid "Apps" +msgstr "Apps" + +#: src/lib/menu.sh +msgid "Share a folder" +msgstr "Ordner freigeben" + +#: src/lib/menu.sh +msgid "Open files with the apps" +msgstr "Dateien mit den Apps รถffnen" + +#: src/lib/menu.sh +msgid "Size" +msgstr "GrรถรŸe" + +#: src/lib/menu.sh +msgid "Sound" +msgstr "Ton" + +#: src/lib/menu.sh +msgid "Graphics" +msgstr "Grafik" + +#: src/lib/menu.sh +msgid "Signing in" +msgstr "Anmeldung" + +#: src/lib/menu.sh +msgid "Wait for the second MFA prompt" +msgstr "Auf zweite MFA-Anfrage warten" + +#: src/lib/menu.sh +msgid "Refresh the apps every day" +msgstr "Apps tรคglich aktualisieren" diff --git a/po/es.po b/po/es.po new file mode 100644 index 0000000..86bbbd2 --- /dev/null +++ b/po/es.po @@ -0,0 +1,565 @@ +# Spanish translation for rdfeed. +# Copyright (C) 2026 Felitendo +# This file is distributed under the same license as rdfeed. +# +msgid "" +msgstr "" +"Project-Id-Version: rdfeed 1.0.0\n" +"Report-Msgid-Bugs-To: https://github.com/LoonixTools/rdfeed/issues\n" +"POT-Creation-Date: 2026-10-02 10:29+0200\n" +"PO-Revision-Date: 2026-10-02 09:30+0200\n" +"Last-Translator: Felitendo\n" +"Language-Team: Spanish\n" +"Language: es\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" + +#: src/rdfeed:79 src/rdfeed:103 +#, sh-printf-format +msgid "Could not write to %s." +msgstr "No se pudo escribir en %s." + +#: src/rdfeed:82 +#, sh-printf-format +msgid "Signing in to %sโ€ฆ" +msgstr "Iniciando sesiรณn en %sโ€ฆ" + +#: src/rdfeed:100 +msgid "" +"The password could not be saved in the keyring, so each app asks for it." +msgstr "" +"No se pudo guardar la contraseรฑa en el llavero, asรญ que cada app la pedirรก." + +#: src/rdfeed:111 +#, sh-printf-format +msgid "1 app from %s is now in your app menu." +msgstr "1 app de %s ya estรก en tu menรบ de aplicaciones." + +#: src/rdfeed:113 +#, sh-printf-format +msgid "%d apps from %s are now in your app menu." +msgstr "%d apps de %s ya estรกn en tu menรบ de aplicaciones." + +#: src/rdfeed:129 +msgid "" +"Your IT department has the address. A work e-mail address often works too." +msgstr "" +"Tu departamento de TI tiene la direcciรณn. A menudo tambiรฉn sirve tu correo " +"del trabajo." + +#: src/rdfeed:130 +msgid "Feed address, server or e-mail:" +msgstr "Direcciรณn del feed, servidor o correo:" + +#: src/rdfeed:137 +#, sh-printf-format +msgid "No feed is registered for %s. Ask your IT department for the address." +msgstr "" +"No hay ningรบn feed registrado para %s. Pide la direcciรณn a tu departamento " +"de TI." + +#: src/rdfeed:142 src/rdfeed:156 +msgid "User name (DOMAIN\\name or name@domain):" +msgstr "Nombre de usuario (DOMINIO\\nombre o nombre@dominio):" + +#: src/rdfeed:146 src/rdfeed:159 +msgid "Password:" +msgstr "Contraseรฑa:" + +#: src/rdfeed:178 src/rdfeed:236 +#, sh-printf-format +msgid "No workspace yet. Add one with `%s add`." +msgstr "Todavรญa no hay ningรบn espacio de trabajo. Aรฑade uno con `%s add`." + +#: src/rdfeed:187 src/lib/launch.sh:220 +#, sh-printf-format +msgid "No password saved for %s. Sign in again in rdfeed." +msgstr "" +"No hay ninguna contraseรฑa guardada para %s. Vuelve a iniciar sesiรณn en " +"rdfeed." + +#: src/rdfeed:191 src/lib/launch.sh:149 +#, sh-printf-format +msgid "Password for %s:" +msgstr "Contraseรฑa de %s:" + +#: src/rdfeed:195 +#, sh-printf-format +msgid "Refreshing %sโ€ฆ" +msgstr "Actualizando %sโ€ฆ" + +#: src/rdfeed:205 +msgid "The password no longer works. Sign in again in rdfeed." +msgstr "La contraseรฑa ya no funciona. Vuelve a iniciar sesiรณn en rdfeed." + +#: src/rdfeed:214 +#, sh-printf-format +msgid "%s: %d apps" +msgstr "%s: %d apps" + +#: src/rdfeed:222 +#, sh-printf-format +msgid "Which workspace? See `%s list` for the names." +msgstr "ยฟQuรฉ espacio de trabajo? Los nombres estรกn en `%s list`." + +#: src/rdfeed:229 +#, sh-printf-format +msgid "%s and its apps are removed." +msgstr "%s y sus apps se han quitado." + +#: src/rdfeed:253 +#, sh-printf-format +msgid "Which app? See `%s list`." +msgstr "ยฟQuรฉ app? Mira `%s list`." + +#: src/rdfeed:268 src/lib/launch.sh:199 +#, sh-printf-format +msgid "There is no app %s. See `%s list`." +msgstr "No existe la app %s. Mira `%s list`." + +#: src/rdfeed:270 +#, sh-printf-format +msgid "Several workspaces have %s. Write it as WORKSPACE/APP, see `%s list`." +msgstr "" +"Varios espacios de trabajo tienen %s. Escrรญbelo como ESPACIO/APP, mira `%s " +"list`." + +#: src/rdfeed:278 +msgid "Usage: rdfeed [command]" +msgstr "Uso: rdfeed [comando]" + +#: src/rdfeed:280 +msgid "Commands:" +msgstr "Comandos:" + +#: src/rdfeed:281 +msgid "Add the workspace of your company" +msgstr "Aรฑadir el espacio de trabajo de tu empresa" + +#: src/rdfeed:282 +msgid "Load the apps again" +msgstr "Volver a cargar las apps" + +#: src/rdfeed:283 +msgid "List the workspaces and their apps" +msgstr "Listar los espacios de trabajo y sus apps" + +#: src/rdfeed:284 +msgid "Start an app, or open a file with it" +msgstr "Abrir una app, o un archivo con ella" + +#: src/rdfeed:285 +msgid "Remove a workspace and its apps" +msgstr "Quitar un espacio de trabajo y sus apps" + +#: src/rdfeed:286 +msgid "Show this help" +msgstr "Mostrar esta ayuda" + +#: src/rdfeed:287 +msgid "Show the version" +msgstr "Mostrar la versiรณn" + +#: src/rdfeed:289 +msgid "Without a command an interactive menu is shown." +msgstr "Sin comando se muestra un menรบ interactivo." + +#: src/rdfeed:313 +#, sh-printf-format +msgid "Unknown command: %s" +msgstr "Comando desconocido: %s" + +#: src/lib/common.sh:174 src/lib/menu.sh:251 +msgid "never" +msgstr "nunca" + +#: src/lib/common.sh:184 +msgid "just now" +msgstr "ahora mismo" + +#: src/lib/common.sh:186 +msgid "1 minute ago" +msgstr "hace 1 minuto" + +#: src/lib/common.sh:189 +#, sh-printf-format +msgid "%d minutes ago" +msgstr "hace %d minutos" + +#: src/lib/common.sh:191 +msgid "1 hour ago" +msgstr "hace 1 hora" + +#: src/lib/common.sh:194 +#, sh-printf-format +msgid "%d hours ago" +msgstr "hace %d horas" + +#: src/lib/common.sh:196 +msgid "1 day ago" +msgstr "hace 1 dรญa" + +#: src/lib/common.sh:199 +#, sh-printf-format +msgid "%d days ago" +msgstr "hace %d dรญas" + +#: src/lib/launch.sh:169 +msgid "" +"Your password has expired. Change it on Windows or on the RD Web page, then " +"sign in again in rdfeed." +msgstr "" +"Tu contraseรฑa ha caducado. Cรกmbiala en Windows o en la pรกgina de RD Web y " +"luego vuelve a iniciar sesiรณn en rdfeed." + +#: src/lib/launch.sh:171 +msgid "Your account is locked. Ask your IT department." +msgstr "Tu cuenta estรก bloqueada. Pregunta a tu departamento de TI." + +#: src/lib/launch.sh:173 +msgid "The server did not accept the password. Sign in again in rdfeed." +msgstr "" +"El servidor no aceptรณ la contraseรฑa. Vuelve a iniciar sesiรณn en rdfeed." + +#: src/lib/launch.sh:175 +msgid "Could not reach the server. Check the internet connection." +msgstr "No se pudo conectar con el servidor. Comprueba la conexiรณn a internet." + +#: src/lib/launch.sh:178 +#, sh-printf-format +msgid "FreeRDP stopped: %s" +msgstr "FreeRDP se detuvo: %s" + +#: src/lib/launch.sh:205 src/lib/launch.sh:206 +#, sh-printf-format +msgid "File not found: %s" +msgstr "No se encontrรณ el archivo: %s" + +#: src/lib/launch.sh:212 src/lib/launch.sh:213 +msgid "" +"FreeRDP 3 is missing. Install it to start the apps (the package is called " +"freerdp)." +msgstr "" +"Falta FreeRDP 3. Instรกlalo para abrir las apps (el paquete se llama freerdp)." + +#: src/lib/launch.sh:277 +#, sh-printf-format +msgid "Opening %s in %s" +msgstr "Abriendo %s en %s" + +#: src/lib/launch.sh:278 +#, sh-printf-format +msgid "If it does not open by itself, open it in the app from the drive %s." +msgstr "Si no se abre solo, รกbrelo en la app desde la unidad %s." + +#: src/lib/launch.sh:280 +#, sh-printf-format +msgid "Starting %s" +msgstr "Abriendo %s" + +#: src/lib/launch.sh:281 +msgid "" +"If your company uses MFA, confirm the sign-in in your authenticator app." +msgstr "" +"Si tu empresa usa MFA, confirma el inicio de sesiรณn en tu app de " +"autenticaciรณn." + +#: src/lib/launch.sh:290 +msgid "A second sign-in is coming" +msgstr "Viene un segundo inicio de sesiรณn" + +#: src/lib/launch.sh:291 +#, sh-printf-format +msgid "" +"Confirm the second prompt in your authenticator app too. It comes in about " +"%d seconds." +msgstr "" +"Confirma tambiรฉn la segunda solicitud en tu app de autenticaciรณn. Llega en " +"unos %d segundos." + +#: src/lib/launch.sh:301 +#, sh-printf-format +msgid "%s could not start" +msgstr "%s no se pudo abrir" + +#: src/lib/launch.sh:303 +#, sh-printf-format +msgid "Log: %s" +msgstr "Registro: %s" + +#. TRANSLATORS: the letter after "[" is the key for yes. y works too. +#: src/lib/menu.sh:93 +msgid "[y/N]" +msgstr "[s/N]" + +#: src/lib/menu.sh:163 +msgid "1 app" +msgstr "1 app" + +#: src/lib/menu.sh:165 +#, sh-printf-format +msgid "%d apps" +msgstr "%d apps" + +#: src/lib/menu.sh:190 +msgid "No workspace yet. Add the one of your company with [1]." +msgstr "" +"Todavรญa no hay ningรบn espacio de trabajo. Aรฑade el de tu empresa con [1]." + +#: src/lib/menu.sh:233 +msgid "off" +msgstr "desactivado" + +#: src/lib/menu.sh:234 +msgid "home folder" +msgstr "carpeta personal" + +#: src/lib/menu.sh:237 +msgid "Documents" +msgstr "Documentos" + +#: src/lib/menu.sh:238 +msgid "Downloads" +msgstr "Descargas" + +#: src/lib/menu.sh:243 +msgid "not found" +msgstr "no encontrada" + +#: src/lib/menu.sh:246 +#, sh-printf-format +msgid "automatic (%d %%)" +msgstr "automรกtico (%d %%)" + +#: src/lib/menu.sh:248 +msgid "classic" +msgstr "clรกsico" + +#: src/lib/menu.sh:249 +msgid "GFX" +msgstr "GFX" + +#: src/lib/menu.sh:250 +msgid "always" +msgstr "siempre" + +#: src/lib/menu.sh:252 +msgid "automatic" +msgstr "automรกtico" + +#: src/lib/menu.sh:261 +msgid "" +"The apps only see the files on the server. Pick a folder to open and save " +"files of this computer in them." +msgstr "" +"Las apps solo ven los archivos del servidor. Elige una carpeta para que " +"puedan abrir y guardar archivos de este equipo." + +#: src/lib/menu.sh:262 +msgid "" +"The apps can open and save files in this folder. It shows up as a drive in " +"them." +msgstr "" +"Las apps pueden abrir y guardar archivos en esta carpeta. Aparece en ellas " +"como una unidad." + +#: src/lib/menu.sh:263 +msgid "" +"Offers the apps under \"Open with\" for their file types, for example Word " +"for .docx. The folder of the file is shared with the app while it is open." +msgstr "" +"Ofrece las apps en ยซAbrir conยป para sus tipos de archivo, por ejemplo Word " +"para .docx. La carpeta del archivo se comparte mientras estรก abierto." + +#: src/lib/menu.sh:264 +msgid "" +"How big the apps are drawn. Automatic follows the scale of your desktop, so " +"they are sharp and the right size." +msgstr "" +"Quรฉ tamaรฑo tienen las apps. Automรกtico sigue la escala de tu escritorio, asรญ " +"se ven nรญtidas y del tamaรฑo correcto." + +#: src/lib/menu.sh:265 +msgid "Plays the sound of the apps here, for calls and videos." +msgstr "Reproduce aquรญ el sonido de las apps, para llamadas y vรญdeos." + +#: src/lib/menu.sh:266 +msgid "" +"Paints every window right. But some servers end the session right after " +"signing in, then go back to GFX." +msgstr "Dibuja bien cada ventana. Pero algunos servidores cierran la sesiรณn justo despuรฉs de entrar; entonces vuelve a GFX." + +#: src/lib/menu.sh:267 +msgid "" +"Works with every server. FreeRDP may not repaint a window after it was " +"minimized or resized." +msgstr "Funciona con cualquier servidor. Puede que FreeRDP no vuelva a dibujar una ventana tras minimizarla o cambiar su tamaรฑo." + +#: src/lib/menu.sh:268 +msgid "" +"Always waits before the second sign-in. For when the second prompt does not " +"come with automatic." +msgstr "" +"Espera siempre antes del segundo inicio de sesiรณn. Para cuando con " +"automรกtico no llega la segunda solicitud." + +#: src/lib/menu.sh:269 +msgid "Never waits. Right when your company has no MFA." +msgstr "Nunca espera. Lo adecuado si tu empresa no usa MFA." + +#: src/lib/menu.sh:270 +msgid "" +"Many servers with MFA need a second sign-in, and its prompt only comes after " +"a short wait. Automatic waits when the first sign-in took a while." +msgstr "" +"Muchos servidores con MFA piden un segundo inicio de sesiรณn, y su solicitud " +"solo llega tras una breve espera. Automรกtico espera si el primero tardรณ un " +"poco." + +#: src/lib/menu.sh:273 +msgid "" +"Loads the list of apps again once a day, so new apps show up by themselves." +msgstr "" +"Vuelve a cargar la lista de apps una vez al dรญa, asรญ las nuevas aparecen " +"solas." + +#: src/lib/menu.sh:275 src/lib/menu.sh:330 +msgid "Needs a systemd user session." +msgstr "Necesita una sesiรณn de usuario de systemd." + +#: src/lib/menu.sh:322 src/lib/menu.sh:325 +msgid "Could not save the setting." +msgstr "No se pudo guardar el ajuste." + +#: src/lib/menu.sh:371 src/lib/menu.sh:609 +msgid "Settings" +msgstr "Ajustes" + +#: src/lib/menu.sh:372 +msgid "โ†‘โ†“ select โ†โ†’ or Space: change q: back" +msgstr "โ†‘โ†“ elegir โ†โ†’ o Espacio: cambiar q: volver" + +#: src/lib/menu.sh:373 +msgid "ON" +msgstr "ACTIVADO" + +#: src/lib/menu.sh:374 +msgid "OFF" +msgstr "DESACTIVADO" + +#: src/lib/menu.sh:456 src/lib/menu.sh:608 +msgid "Workspaces" +msgstr "Espacios de trabajo" + +#. TRANSLATORS: keep the letters, they are the keys. +#: src/lib/menu.sh:458 +msgid "" +"โ†‘โ†“ select r: refresh s: sign in again d: remove a: add q: back" +msgstr "" +"โ†‘โ†“ elegir r: actualizar s: volver a entrar d: quitar a: aรฑadir q: " +"volver" + +#: src/lib/menu.sh:459 +msgid "No workspace yet. Press a to add one." +msgstr "Todavรญa no hay ningรบn espacio de trabajo. Pulsa a para aรฑadir uno." + +#: src/lib/menu.sh:484 +#, sh-printf-format +msgid "refreshed %s" +msgstr "actualizado %s" + +#: src/lib/menu.sh:512 +#, sh-printf-format +msgid "Remove \"%s\" and its apps?" +msgstr "ยฟQuitar ยซ%sยป y sus apps?" + +#: src/lib/menu.sh:536 src/lib/menu.sh:607 +msgid "Start an app" +msgstr "Abrir una app" + +#: src/lib/menu.sh:537 +msgid "โ†‘โ†“ select Enter: start q: back" +msgstr "โ†‘โ†“ elegir Intro: abrir q: volver" + +#: src/lib/menu.sh:538 +msgid "No apps yet. Add a workspace first." +msgstr "Todavรญa no hay apps. Aรฑade primero un espacio de trabajo." + +#: src/lib/menu.sh:579 +#, sh-printf-format +msgid "%s is starting. It opens in a window of its own." +msgstr "%s se estรก abriendo. Se abre en una ventana propia." + +#: src/lib/menu.sh:606 +msgid "Add a workspace" +msgstr "Aรฑadir un espacio de trabajo" + +#: src/lib/menu.sh:610 +msgid "Quit" +msgstr "Salir" + +#: src/lib/workspace.sh:163 +msgid "The server did not accept the user name or password." +msgstr "El servidor no aceptรณ el nombre de usuario o la contraseรฑa." + +#: src/lib/workspace.sh:164 +#, sh-printf-format +msgid "Could not reach the server: %s" +msgstr "No se pudo conectar con el servidor: %s" + +#: src/lib/workspace.sh:165 +#, sh-printf-format +msgid "There is no RD Web feed at %s." +msgstr "No hay ningรบn feed de RD Web en %s." + +#: src/lib/workspace.sh:166 +#, sh-printf-format +msgid "Could not load the apps: %s" +msgstr "No se pudieron cargar las apps: %s" + +#: src/lib/workspace.sh:282 +#, sh-printf-format +msgid "Remote desktop of %s" +msgstr "Escritorio remoto de %s" + +#: src/lib/workspace.sh:284 +#, sh-printf-format +msgid "%s from %s" +msgstr "%s de %s" + +#: src/lib/menu.sh +msgid "Apps" +msgstr "Apps" + +#: src/lib/menu.sh +msgid "Share a folder" +msgstr "Compartir una carpeta" + +#: src/lib/menu.sh +msgid "Open files with the apps" +msgstr "Abrir archivos con las apps" + +#: src/lib/menu.sh +msgid "Size" +msgstr "Tamaรฑo" + +#: src/lib/menu.sh +msgid "Sound" +msgstr "Sonido" + +#: src/lib/menu.sh +msgid "Graphics" +msgstr "Grรกficos" + +#: src/lib/menu.sh +msgid "Signing in" +msgstr "Inicio de sesiรณn" + +#: src/lib/menu.sh +msgid "Wait for the second MFA prompt" +msgstr "Esperar la segunda solicitud de MFA" + +#: src/lib/menu.sh +msgid "Refresh the apps every day" +msgstr "Actualizar las apps cada dรญa" diff --git a/po/fr.po b/po/fr.po new file mode 100644 index 0000000..6038159 --- /dev/null +++ b/po/fr.po @@ -0,0 +1,574 @@ +# French translation for rdfeed. +# Copyright (C) 2026 Felitendo +# This file is distributed under the same license as rdfeed. +# +msgid "" +msgstr "" +"Project-Id-Version: rdfeed 1.0.0\n" +"Report-Msgid-Bugs-To: https://github.com/LoonixTools/rdfeed/issues\n" +"POT-Creation-Date: 2026-10-02 10:29+0200\n" +"PO-Revision-Date: 2026-10-02 09:30+0200\n" +"Last-Translator: Felitendo\n" +"Language-Team: French\n" +"Language: fr\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" + +#: src/rdfeed:79 src/rdfeed:103 +#, sh-printf-format +msgid "Could not write to %s." +msgstr "Impossible d'รฉcrire dans %s." + +#: src/rdfeed:82 +#, sh-printf-format +msgid "Signing in to %sโ€ฆ" +msgstr "Connexion ร  %sโ€ฆ" + +#: src/rdfeed:100 +msgid "" +"The password could not be saved in the keyring, so each app asks for it." +msgstr "" +"Le mot de passe n'a pas pu รชtre enregistrรฉ dans le trousseau, chaque " +"application le demandera donc." + +#: src/rdfeed:111 +#, sh-printf-format +msgid "1 app from %s is now in your app menu." +msgstr "1 application de %s est maintenant dans votre menu des applications." + +#: src/rdfeed:113 +#, sh-printf-format +msgid "%d apps from %s are now in your app menu." +msgstr "" +"%d applications de %s sont maintenant dans votre menu des applications." + +#: src/rdfeed:129 +msgid "" +"Your IT department has the address. A work e-mail address often works too." +msgstr "" +"Votre service informatique connaรฎt l'adresse. Souvent, votre adresse e-mail " +"professionnelle fonctionne aussi." + +#: src/rdfeed:130 +msgid "Feed address, server or e-mail:" +msgstr "Adresse du flux, serveur ou e-mail :" + +#: src/rdfeed:137 +#, sh-printf-format +msgid "No feed is registered for %s. Ask your IT department for the address." +msgstr "" +"Aucun flux n'est enregistrรฉ pour %s. Demandez l'adresse ร  votre service " +"informatique." + +#: src/rdfeed:142 src/rdfeed:156 +msgid "User name (DOMAIN\\name or name@domain):" +msgstr "Nom d'utilisateur (DOMAINE\\nom ou nom@domaine) :" + +#: src/rdfeed:146 src/rdfeed:159 +msgid "Password:" +msgstr "Mot de passe :" + +#: src/rdfeed:178 src/rdfeed:236 +#, sh-printf-format +msgid "No workspace yet. Add one with `%s add`." +msgstr "Aucun espace de travail pour l'instant. Ajoutez-en un avec `%s add`." + +#: src/rdfeed:187 src/lib/launch.sh:220 +#, sh-printf-format +msgid "No password saved for %s. Sign in again in rdfeed." +msgstr "Aucun mot de passe enregistrรฉ pour %s. Reconnectez-vous dans rdfeed." + +#: src/rdfeed:191 src/lib/launch.sh:149 +#, sh-printf-format +msgid "Password for %s:" +msgstr "Mot de passe pour %s :" + +#: src/rdfeed:195 +#, sh-printf-format +msgid "Refreshing %sโ€ฆ" +msgstr "Actualisation de %sโ€ฆ" + +#: src/rdfeed:205 +msgid "The password no longer works. Sign in again in rdfeed." +msgstr "Le mot de passe ne fonctionne plus. Reconnectez-vous dans rdfeed." + +#: src/rdfeed:214 +#, sh-printf-format +msgid "%s: %d apps" +msgstr "%s : %d applications" + +#: src/rdfeed:222 +#, sh-printf-format +msgid "Which workspace? See `%s list` for the names." +msgstr "Quel espace de travail ? Les noms sont dans `%s list`." + +#: src/rdfeed:229 +#, sh-printf-format +msgid "%s and its apps are removed." +msgstr "%s et ses applications ont รฉtรฉ supprimรฉs." + +#: src/rdfeed:253 +#, sh-printf-format +msgid "Which app? See `%s list`." +msgstr "Quelle application ? Voir `%s list`." + +#: src/rdfeed:268 src/lib/launch.sh:199 +#, sh-printf-format +msgid "There is no app %s. See `%s list`." +msgstr "L'application %s n'existe pas. Voir `%s list`." + +#: src/rdfeed:270 +#, sh-printf-format +msgid "Several workspaces have %s. Write it as WORKSPACE/APP, see `%s list`." +msgstr "" +"Plusieurs espaces de travail ont %s. ร‰crivez-le sous la forme ESPACE/" +"APPLICATION, voir `%s list`." + +#: src/rdfeed:278 +msgid "Usage: rdfeed [command]" +msgstr "Utilisationย : rdfeed [commande]" + +#: src/rdfeed:280 +msgid "Commands:" +msgstr "Commandesย :" + +#: src/rdfeed:281 +msgid "Add the workspace of your company" +msgstr "Ajouter l'espace de travail de votre entreprise" + +#: src/rdfeed:282 +msgid "Load the apps again" +msgstr "Recharger les applications" + +#: src/rdfeed:283 +msgid "List the workspaces and their apps" +msgstr "Lister les espaces de travail et leurs applications" + +#: src/rdfeed:284 +msgid "Start an app, or open a file with it" +msgstr "Lancer une application, ou ouvrir un fichier avec" + +#: src/rdfeed:285 +msgid "Remove a workspace and its apps" +msgstr "Supprimer un espace de travail et ses applications" + +#: src/rdfeed:286 +msgid "Show this help" +msgstr "Afficher cette aide" + +#: src/rdfeed:287 +msgid "Show the version" +msgstr "Afficher la version" + +#: src/rdfeed:289 +msgid "Without a command an interactive menu is shown." +msgstr "Sans commande, un menu interactif s'affiche." + +#: src/rdfeed:313 +#, sh-printf-format +msgid "Unknown command: %s" +msgstr "Commande inconnueย : %s" + +#: src/lib/common.sh:174 src/lib/menu.sh:251 +msgid "never" +msgstr "jamais" + +#: src/lib/common.sh:184 +msgid "just now" +msgstr "ร  l'instant" + +#: src/lib/common.sh:186 +msgid "1 minute ago" +msgstr "il y a 1 minute" + +#: src/lib/common.sh:189 +#, sh-printf-format +msgid "%d minutes ago" +msgstr "il y a %d minutes" + +#: src/lib/common.sh:191 +msgid "1 hour ago" +msgstr "il y a 1 heure" + +#: src/lib/common.sh:194 +#, sh-printf-format +msgid "%d hours ago" +msgstr "il y a %d heures" + +#: src/lib/common.sh:196 +msgid "1 day ago" +msgstr "il y a 1 jour" + +#: src/lib/common.sh:199 +#, sh-printf-format +msgid "%d days ago" +msgstr "il y a %d jours" + +#: src/lib/launch.sh:169 +msgid "" +"Your password has expired. Change it on Windows or on the RD Web page, then " +"sign in again in rdfeed." +msgstr "" +"Votre mot de passe a expirรฉ. Changez-le sous Windows ou sur la page RD Web, " +"puis reconnectez-vous dans rdfeed." + +#: src/lib/launch.sh:171 +msgid "Your account is locked. Ask your IT department." +msgstr "" +"Votre compte est verrouillรฉ. Adressez-vous ร  votre service informatique." + +#: src/lib/launch.sh:173 +msgid "The server did not accept the password. Sign in again in rdfeed." +msgstr "" +"Le serveur n'a pas acceptรฉ le mot de passe. Reconnectez-vous dans rdfeed." + +#: src/lib/launch.sh:175 +msgid "Could not reach the server. Check the internet connection." +msgstr "Impossible de joindre le serveur. Vรฉrifiez la connexion internet." + +#: src/lib/launch.sh:178 +#, sh-printf-format +msgid "FreeRDP stopped: %s" +msgstr "FreeRDP s'est arrรชtรฉ : %s" + +#: src/lib/launch.sh:205 src/lib/launch.sh:206 +#, sh-printf-format +msgid "File not found: %s" +msgstr "Fichier introuvable : %s" + +#: src/lib/launch.sh:212 src/lib/launch.sh:213 +msgid "" +"FreeRDP 3 is missing. Install it to start the apps (the package is called " +"freerdp)." +msgstr "" +"FreeRDP 3 est absent. Installez-le pour lancer les applications (le paquet " +"s'appelle freerdp)." + +#: src/lib/launch.sh:277 +#, sh-printf-format +msgid "Opening %s in %s" +msgstr "Ouverture de %s dans %s" + +#: src/lib/launch.sh:278 +#, sh-printf-format +msgid "If it does not open by itself, open it in the app from the drive %s." +msgstr "" +"S'il ne s'ouvre pas tout seul, ouvrez-le dans l'application depuis le " +"lecteur %s." + +#: src/lib/launch.sh:280 +#, sh-printf-format +msgid "Starting %s" +msgstr "Lancement de %s" + +#: src/lib/launch.sh:281 +msgid "" +"If your company uses MFA, confirm the sign-in in your authenticator app." +msgstr "" +"Si votre entreprise utilise la MFA, confirmez la connexion dans votre " +"application d'authentification." + +#: src/lib/launch.sh:290 +msgid "A second sign-in is coming" +msgstr "Une deuxiรจme connexion arrive" + +#: src/lib/launch.sh:291 +#, sh-printf-format +msgid "" +"Confirm the second prompt in your authenticator app too. It comes in about " +"%d seconds." +msgstr "" +"Confirmez aussi la deuxiรจme demande dans votre application " +"d'authentification. Elle arrive dans environ %d secondes." + +#: src/lib/launch.sh:301 +#, sh-printf-format +msgid "%s could not start" +msgstr "%s n'a pas pu dรฉmarrer" + +#: src/lib/launch.sh:303 +#, sh-printf-format +msgid "Log: %s" +msgstr "Journal : %s" + +#. TRANSLATORS: the letter after "[" is the key for yes. y works too. +#: src/lib/menu.sh:93 +msgid "[y/N]" +msgstr "[o/N]" + +#: src/lib/menu.sh:163 +msgid "1 app" +msgstr "1 application" + +#: src/lib/menu.sh:165 +#, sh-printf-format +msgid "%d apps" +msgstr "%d applications" + +#: src/lib/menu.sh:190 +msgid "No workspace yet. Add the one of your company with [1]." +msgstr "" +"Aucun espace de travail pour l'instant. Ajoutez celui de votre entreprise " +"avec [1]." + +#: src/lib/menu.sh:233 +msgid "off" +msgstr "dรฉsactivรฉ" + +#: src/lib/menu.sh:234 +msgid "home folder" +msgstr "dossier personnel" + +#: src/lib/menu.sh:237 +msgid "Documents" +msgstr "Documents" + +#: src/lib/menu.sh:238 +msgid "Downloads" +msgstr "Tรฉlรฉchargements" + +#: src/lib/menu.sh:243 +msgid "not found" +msgstr "introuvable" + +#: src/lib/menu.sh:246 +#, sh-printf-format +msgid "automatic (%d %%)" +msgstr "automatique (%d %%)" + +#: src/lib/menu.sh:248 +msgid "classic" +msgstr "classique" + +#: src/lib/menu.sh:249 +msgid "GFX" +msgstr "GFX" + +#: src/lib/menu.sh:250 +msgid "always" +msgstr "toujours" + +#: src/lib/menu.sh:252 +msgid "automatic" +msgstr "automatique" + +#: src/lib/menu.sh:261 +msgid "" +"The apps only see the files on the server. Pick a folder to open and save " +"files of this computer in them." +msgstr "" +"Les applications ne voient que les fichiers du serveur. Choisissez un " +"dossier pour qu'elles puissent ouvrir et enregistrer des fichiers de cet " +"ordinateur." + +#: src/lib/menu.sh:262 +msgid "" +"The apps can open and save files in this folder. It shows up as a drive in " +"them." +msgstr "" +"Les applications peuvent ouvrir et enregistrer des fichiers dans ce dossier. " +"Il y apparaรฎt comme un lecteur." + +#: src/lib/menu.sh:263 +msgid "" +"Offers the apps under \"Open with\" for their file types, for example Word " +"for .docx. The folder of the file is shared with the app while it is open." +msgstr "" +"Propose les applications dans ยซ Ouvrir avec ยป pour leurs types de fichiers, " +"par exemple Word pour .docx. Le dossier du fichier est partagรฉ tant qu'il " +"est ouvert." + +#: src/lib/menu.sh:264 +msgid "" +"How big the apps are drawn. Automatic follows the scale of your desktop, so " +"they are sharp and the right size." +msgstr "" +"La taille ร  laquelle les applications sont dessinรฉes. Automatique suit " +"l'รฉchelle de votre bureau : elles sont nettes et ร  la bonne taille." + +#: src/lib/menu.sh:265 +msgid "Plays the sound of the apps here, for calls and videos." +msgstr "Joue ici le son des applications, pour les appels et les vidรฉos." + +#: src/lib/menu.sh:266 +msgid "" +"Paints every window right. But some servers end the session right after " +"signing in, then go back to GFX." +msgstr "Dessine correctement chaque fenรชtre. Mais certains serveurs ferment la session juste aprรจs la connexion : revenez alors ร  GFX." + +#: src/lib/menu.sh:267 +msgid "" +"Works with every server. FreeRDP may not repaint a window after it was " +"minimized or resized." +msgstr "Fonctionne avec tous les serveurs. FreeRDP peut ne pas redessiner une fenรชtre rรฉduite ou redimensionnรฉe." + +#: src/lib/menu.sh:268 +msgid "" +"Always waits before the second sign-in. For when the second prompt does not " +"come with automatic." +msgstr "" +"Attend toujours avant la deuxiรจme connexion. Si la deuxiรจme demande n'arrive " +"pas en automatique." + +#: src/lib/menu.sh:269 +msgid "Never waits. Right when your company has no MFA." +msgstr "" +"N'attend jamais. Le bon choix si votre entreprise n'utilise pas la MFA." + +#: src/lib/menu.sh:270 +msgid "" +"Many servers with MFA need a second sign-in, and its prompt only comes after " +"a short wait. Automatic waits when the first sign-in took a while." +msgstr "" +"Beaucoup de serveurs avec MFA demandent une deuxiรจme connexion, dont la " +"demande n'arrive qu'aprรจs une courte attente. Automatique attend si la " +"premiรจre a pris du temps." + +#: src/lib/menu.sh:273 +msgid "" +"Loads the list of apps again once a day, so new apps show up by themselves." +msgstr "" +"Recharge la liste des applications une fois par jour : les nouvelles " +"apparaissent d'elles-mรชmes." + +#: src/lib/menu.sh:275 src/lib/menu.sh:330 +msgid "Needs a systemd user session." +msgstr "Nรฉcessite une session utilisateur systemd." + +#: src/lib/menu.sh:322 src/lib/menu.sh:325 +msgid "Could not save the setting." +msgstr "Impossible d'enregistrer le rรฉglage." + +#: src/lib/menu.sh:371 src/lib/menu.sh:609 +msgid "Settings" +msgstr "Rรฉglages" + +#: src/lib/menu.sh:372 +msgid "โ†‘โ†“ select โ†โ†’ or Space: change q: back" +msgstr "โ†‘โ†“ choisir โ†โ†’ ou Espaceย : changer qย : retour" + +#: src/lib/menu.sh:373 +msgid "ON" +msgstr "ACTIVร‰" + +#: src/lib/menu.sh:374 +msgid "OFF" +msgstr "Dร‰SACTIVร‰" + +#: src/lib/menu.sh:456 src/lib/menu.sh:608 +msgid "Workspaces" +msgstr "Espaces de travail" + +#. TRANSLATORS: keep the letters, they are the keys. +#: src/lib/menu.sh:458 +msgid "" +"โ†‘โ†“ select r: refresh s: sign in again d: remove a: add q: back" +msgstr "" +"โ†‘โ†“ choisir r : actualiser s : se reconnecter d : supprimer a : " +"ajouter q : retour" + +#: src/lib/menu.sh:459 +msgid "No workspace yet. Press a to add one." +msgstr "" +"Aucun espace de travail pour l'instant. Appuyez sur a pour en ajouter un." + +#: src/lib/menu.sh:484 +#, sh-printf-format +msgid "refreshed %s" +msgstr "actualisรฉ %s" + +#: src/lib/menu.sh:512 +#, sh-printf-format +msgid "Remove \"%s\" and its apps?" +msgstr "Supprimer ยซ %s ยป et ses applications ?" + +#: src/lib/menu.sh:536 src/lib/menu.sh:607 +msgid "Start an app" +msgstr "Lancer une application" + +#: src/lib/menu.sh:537 +msgid "โ†‘โ†“ select Enter: start q: back" +msgstr "โ†‘โ†“ choisir Entrรฉe : lancer q : retour" + +#: src/lib/menu.sh:538 +msgid "No apps yet. Add a workspace first." +msgstr "Pas encore d'applications. Ajoutez d'abord un espace de travail." + +#: src/lib/menu.sh:579 +#, sh-printf-format +msgid "%s is starting. It opens in a window of its own." +msgstr "%s dรฉmarre. L'application s'ouvre dans sa propre fenรชtre." + +#: src/lib/menu.sh:606 +msgid "Add a workspace" +msgstr "Ajouter un espace de travail" + +#: src/lib/menu.sh:610 +msgid "Quit" +msgstr "Quitter" + +#: src/lib/workspace.sh:163 +msgid "The server did not accept the user name or password." +msgstr "Le serveur n'a pas acceptรฉ le nom d'utilisateur ou le mot de passe." + +#: src/lib/workspace.sh:164 +#, sh-printf-format +msgid "Could not reach the server: %s" +msgstr "Impossible de joindre le serveur : %s" + +#: src/lib/workspace.sh:165 +#, sh-printf-format +msgid "There is no RD Web feed at %s." +msgstr "Il n'y a pas de flux RD Web ร  l'adresse %s." + +#: src/lib/workspace.sh:166 +#, sh-printf-format +msgid "Could not load the apps: %s" +msgstr "Impossible de charger les applications : %s" + +#: src/lib/workspace.sh:282 +#, sh-printf-format +msgid "Remote desktop of %s" +msgstr "Bureau ร  distance de %s" + +#: src/lib/workspace.sh:284 +#, sh-printf-format +msgid "%s from %s" +msgstr "%s de %s" + +#: src/lib/menu.sh +msgid "Apps" +msgstr "Applications" + +#: src/lib/menu.sh +msgid "Share a folder" +msgstr "Partager un dossier" + +#: src/lib/menu.sh +msgid "Open files with the apps" +msgstr "Ouvrir des fichiers avec les applications" + +#: src/lib/menu.sh +msgid "Size" +msgstr "Taille" + +#: src/lib/menu.sh +msgid "Sound" +msgstr "Son" + +#: src/lib/menu.sh +msgid "Graphics" +msgstr "Graphismes" + +#: src/lib/menu.sh +msgid "Signing in" +msgstr "Connexion" + +#: src/lib/menu.sh +msgid "Wait for the second MFA prompt" +msgstr "Attendre la deuxiรจme demande MFA" + +#: src/lib/menu.sh +msgid "Refresh the apps every day" +msgstr "Actualiser les applications chaque jour" diff --git a/po/it.po b/po/it.po new file mode 100644 index 0000000..59cfcf4 --- /dev/null +++ b/po/it.po @@ -0,0 +1,561 @@ +# Italian translation for rdfeed. +# Copyright (C) 2026 Felitendo +# This file is distributed under the same license as rdfeed. +# +msgid "" +msgstr "" +"Project-Id-Version: rdfeed 1.0.0\n" +"Report-Msgid-Bugs-To: https://github.com/LoonixTools/rdfeed/issues\n" +"POT-Creation-Date: 2026-10-02 10:29+0200\n" +"PO-Revision-Date: 2026-10-02 09:30+0200\n" +"Last-Translator: Felitendo\n" +"Language-Team: Italian\n" +"Language: it\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" + +#: src/rdfeed:79 src/rdfeed:103 +#, sh-printf-format +msgid "Could not write to %s." +msgstr "Impossibile scrivere in %s." + +#: src/rdfeed:82 +#, sh-printf-format +msgid "Signing in to %sโ€ฆ" +msgstr "Accesso a %sโ€ฆ" + +#: src/rdfeed:100 +msgid "" +"The password could not be saved in the keyring, so each app asks for it." +msgstr "" +"Non รจ stato possibile salvare la password nel portachiavi, quindi ogni app " +"la chiederร ." + +#: src/rdfeed:111 +#, sh-printf-format +msgid "1 app from %s is now in your app menu." +msgstr "1 app di %s รจ ora nel menu delle applicazioni." + +#: src/rdfeed:113 +#, sh-printf-format +msgid "%d apps from %s are now in your app menu." +msgstr "%d app di %s sono ora nel menu delle applicazioni." + +#: src/rdfeed:129 +msgid "" +"Your IT department has the address. A work e-mail address often works too." +msgstr "" +"L'indirizzo lo ha il tuo reparto IT. Spesso funziona anche l'e-mail di " +"lavoro." + +#: src/rdfeed:130 +msgid "Feed address, server or e-mail:" +msgstr "Indirizzo del feed, server o e-mail:" + +#: src/rdfeed:137 +#, sh-printf-format +msgid "No feed is registered for %s. Ask your IT department for the address." +msgstr "Nessun feed registrato per %s. Chiedi l'indirizzo al tuo reparto IT." + +#: src/rdfeed:142 src/rdfeed:156 +msgid "User name (DOMAIN\\name or name@domain):" +msgstr "Nome utente (DOMINIO\\nome o nome@dominio):" + +#: src/rdfeed:146 src/rdfeed:159 +msgid "Password:" +msgstr "Password:" + +#: src/rdfeed:178 src/rdfeed:236 +#, sh-printf-format +msgid "No workspace yet. Add one with `%s add`." +msgstr "Nessuno spazio di lavoro ancora. Aggiungine uno con `%s add`." + +#: src/rdfeed:187 src/lib/launch.sh:220 +#, sh-printf-format +msgid "No password saved for %s. Sign in again in rdfeed." +msgstr "Nessuna password salvata per %s. Accedi di nuovo in rdfeed." + +#: src/rdfeed:191 src/lib/launch.sh:149 +#, sh-printf-format +msgid "Password for %s:" +msgstr "Password per %s:" + +#: src/rdfeed:195 +#, sh-printf-format +msgid "Refreshing %sโ€ฆ" +msgstr "Aggiornamento di %sโ€ฆ" + +#: src/rdfeed:205 +msgid "The password no longer works. Sign in again in rdfeed." +msgstr "La password non funziona piรน. Accedi di nuovo in rdfeed." + +#: src/rdfeed:214 +#, sh-printf-format +msgid "%s: %d apps" +msgstr "%s: %d app" + +#: src/rdfeed:222 +#, sh-printf-format +msgid "Which workspace? See `%s list` for the names." +msgstr "Quale spazio di lavoro? I nomi sono in `%s list`." + +#: src/rdfeed:229 +#, sh-printf-format +msgid "%s and its apps are removed." +msgstr "%s e le sue app sono stati rimossi." + +#: src/rdfeed:253 +#, sh-printf-format +msgid "Which app? See `%s list`." +msgstr "Quale app? Vedi `%s list`." + +#: src/rdfeed:268 src/lib/launch.sh:199 +#, sh-printf-format +msgid "There is no app %s. See `%s list`." +msgstr "Non esiste l'app %s. Vedi `%s list`." + +#: src/rdfeed:270 +#, sh-printf-format +msgid "Several workspaces have %s. Write it as WORKSPACE/APP, see `%s list`." +msgstr "" +"Piรน spazi di lavoro hanno %s. Scrivilo come SPAZIO/APP, vedi `%s list`." + +#: src/rdfeed:278 +msgid "Usage: rdfeed [command]" +msgstr "Uso: rdfeed [comando]" + +#: src/rdfeed:280 +msgid "Commands:" +msgstr "Comandi:" + +#: src/rdfeed:281 +msgid "Add the workspace of your company" +msgstr "Aggiungi lo spazio di lavoro della tua azienda" + +#: src/rdfeed:282 +msgid "Load the apps again" +msgstr "Ricarica le app" + +#: src/rdfeed:283 +msgid "List the workspaces and their apps" +msgstr "Elenca gli spazi di lavoro e le loro app" + +#: src/rdfeed:284 +msgid "Start an app, or open a file with it" +msgstr "Avvia un'app, o apri un file con essa" + +#: src/rdfeed:285 +msgid "Remove a workspace and its apps" +msgstr "Rimuovi uno spazio di lavoro e le sue app" + +#: src/rdfeed:286 +msgid "Show this help" +msgstr "Mostra questo aiuto" + +#: src/rdfeed:287 +msgid "Show the version" +msgstr "Mostra la versione" + +#: src/rdfeed:289 +msgid "Without a command an interactive menu is shown." +msgstr "Senza comando viene mostrato un menu interattivo." + +#: src/rdfeed:313 +#, sh-printf-format +msgid "Unknown command: %s" +msgstr "Comando sconosciuto: %s" + +#: src/lib/common.sh:174 src/lib/menu.sh:251 +msgid "never" +msgstr "mai" + +#: src/lib/common.sh:184 +msgid "just now" +msgstr "proprio ora" + +#: src/lib/common.sh:186 +msgid "1 minute ago" +msgstr "1 minuto fa" + +#: src/lib/common.sh:189 +#, sh-printf-format +msgid "%d minutes ago" +msgstr "%d minuti fa" + +#: src/lib/common.sh:191 +msgid "1 hour ago" +msgstr "1 ora fa" + +#: src/lib/common.sh:194 +#, sh-printf-format +msgid "%d hours ago" +msgstr "%d ore fa" + +#: src/lib/common.sh:196 +msgid "1 day ago" +msgstr "1 giorno fa" + +#: src/lib/common.sh:199 +#, sh-printf-format +msgid "%d days ago" +msgstr "%d giorni fa" + +#: src/lib/launch.sh:169 +msgid "" +"Your password has expired. Change it on Windows or on the RD Web page, then " +"sign in again in rdfeed." +msgstr "" +"La password รจ scaduta. Cambiala in Windows o sulla pagina RD Web, poi accedi " +"di nuovo in rdfeed." + +#: src/lib/launch.sh:171 +msgid "Your account is locked. Ask your IT department." +msgstr "Il tuo account รจ bloccato. Chiedi al reparto IT." + +#: src/lib/launch.sh:173 +msgid "The server did not accept the password. Sign in again in rdfeed." +msgstr "Il server non ha accettato la password. Accedi di nuovo in rdfeed." + +#: src/lib/launch.sh:175 +msgid "Could not reach the server. Check the internet connection." +msgstr "" +"Impossibile raggiungere il server. Controlla la connessione a internet." + +#: src/lib/launch.sh:178 +#, sh-printf-format +msgid "FreeRDP stopped: %s" +msgstr "FreeRDP si รจ fermato: %s" + +#: src/lib/launch.sh:205 src/lib/launch.sh:206 +#, sh-printf-format +msgid "File not found: %s" +msgstr "File non trovato: %s" + +#: src/lib/launch.sh:212 src/lib/launch.sh:213 +msgid "" +"FreeRDP 3 is missing. Install it to start the apps (the package is called " +"freerdp)." +msgstr "" +"Manca FreeRDP 3. Installalo per avviare le app (il pacchetto si chiama " +"freerdp)." + +#: src/lib/launch.sh:277 +#, sh-printf-format +msgid "Opening %s in %s" +msgstr "Apertura di %s in %s" + +#: src/lib/launch.sh:278 +#, sh-printf-format +msgid "If it does not open by itself, open it in the app from the drive %s." +msgstr "Se non si apre da solo, aprilo nell'app dall'unitร  %s." + +#: src/lib/launch.sh:280 +#, sh-printf-format +msgid "Starting %s" +msgstr "Avvio di %s" + +#: src/lib/launch.sh:281 +msgid "" +"If your company uses MFA, confirm the sign-in in your authenticator app." +msgstr "" +"Se la tua azienda usa l'MFA, conferma l'accesso nell'app di autenticazione." + +#: src/lib/launch.sh:290 +msgid "A second sign-in is coming" +msgstr "Sta arrivando un secondo accesso" + +#: src/lib/launch.sh:291 +#, sh-printf-format +msgid "" +"Confirm the second prompt in your authenticator app too. It comes in about " +"%d seconds." +msgstr "" +"Conferma anche la seconda richiesta nell'app di autenticazione. Arriva tra " +"circa %d secondi." + +#: src/lib/launch.sh:301 +#, sh-printf-format +msgid "%s could not start" +msgstr "Impossibile avviare %s" + +#: src/lib/launch.sh:303 +#, sh-printf-format +msgid "Log: %s" +msgstr "Log: %s" + +#. TRANSLATORS: the letter after "[" is the key for yes. y works too. +#: src/lib/menu.sh:93 +msgid "[y/N]" +msgstr "[s/N]" + +#: src/lib/menu.sh:163 +msgid "1 app" +msgstr "1 app" + +#: src/lib/menu.sh:165 +#, sh-printf-format +msgid "%d apps" +msgstr "%d app" + +#: src/lib/menu.sh:190 +msgid "No workspace yet. Add the one of your company with [1]." +msgstr "" +"Nessuno spazio di lavoro ancora. Aggiungi quello della tua azienda con [1]." + +#: src/lib/menu.sh:233 +msgid "off" +msgstr "disattivo" + +#: src/lib/menu.sh:234 +msgid "home folder" +msgstr "cartella home" + +#: src/lib/menu.sh:237 +msgid "Documents" +msgstr "Documenti" + +#: src/lib/menu.sh:238 +msgid "Downloads" +msgstr "Download" + +#: src/lib/menu.sh:243 +msgid "not found" +msgstr "non trovata" + +#: src/lib/menu.sh:246 +#, sh-printf-format +msgid "automatic (%d %%)" +msgstr "automatica (%d %%)" + +#: src/lib/menu.sh:248 +msgid "classic" +msgstr "classico" + +#: src/lib/menu.sh:249 +msgid "GFX" +msgstr "GFX" + +#: src/lib/menu.sh:250 +msgid "always" +msgstr "sempre" + +#: src/lib/menu.sh:252 +msgid "automatic" +msgstr "automatico" + +#: src/lib/menu.sh:261 +msgid "" +"The apps only see the files on the server. Pick a folder to open and save " +"files of this computer in them." +msgstr "" +"Le app vedono solo i file sul server. Scegli una cartella perchรฉ possano " +"aprire e salvare file di questo computer." + +#: src/lib/menu.sh:262 +msgid "" +"The apps can open and save files in this folder. It shows up as a drive in " +"them." +msgstr "" +"Le app possono aprire e salvare file in questa cartella. Compare in esse " +"come un'unitร ." + +#: src/lib/menu.sh:263 +msgid "" +"Offers the apps under \"Open with\" for their file types, for example Word " +"for .docx. The folder of the file is shared with the app while it is open." +msgstr "" +"Offre le app in ยซApri conยป per i loro tipi di file, per esempio Word " +"per .docx. La cartella del file viene condivisa finchรฉ รจ aperto." + +#: src/lib/menu.sh:264 +msgid "" +"How big the apps are drawn. Automatic follows the scale of your desktop, so " +"they are sharp and the right size." +msgstr "" +"Quanto grandi vengono disegnate le app. Automatica segue la scala del " +"desktop, cosรฌ sono nitide e della giusta dimensione." + +#: src/lib/menu.sh:265 +msgid "Plays the sound of the apps here, for calls and videos." +msgstr "Riproduce qui l'audio delle app, per chiamate e video." + +#: src/lib/menu.sh:266 +msgid "" +"Paints every window right. But some servers end the session right after " +"signing in, then go back to GFX." +msgstr "Disegna bene ogni finestra. Ma alcuni server chiudono la sessione subito dopo l'accesso: allora torna a GFX." + +#: src/lib/menu.sh:267 +msgid "" +"Works with every server. FreeRDP may not repaint a window after it was " +"minimized or resized." +msgstr "Funziona con ogni server. FreeRDP potrebbe non ridisegnare una finestra dopo averla ridotta o ridimensionata." + +#: src/lib/menu.sh:268 +msgid "" +"Always waits before the second sign-in. For when the second prompt does not " +"come with automatic." +msgstr "" +"Attende sempre prima del secondo accesso. Per quando con automatico la " +"seconda richiesta non arriva." + +#: src/lib/menu.sh:269 +msgid "Never waits. Right when your company has no MFA." +msgstr "Non attende mai. Giusto se la tua azienda non usa l'MFA." + +#: src/lib/menu.sh:270 +msgid "" +"Many servers with MFA need a second sign-in, and its prompt only comes after " +"a short wait. Automatic waits when the first sign-in took a while." +msgstr "" +"Molti server con MFA chiedono un secondo accesso, e la sua richiesta arriva " +"solo dopo una breve attesa. Automatico attende se il primo accesso ha " +"richiesto un po'." + +#: src/lib/menu.sh:273 +msgid "" +"Loads the list of apps again once a day, so new apps show up by themselves." +msgstr "" +"Ricarica l'elenco delle app una volta al giorno, cosรฌ quelle nuove compaiono " +"da sole." + +#: src/lib/menu.sh:275 src/lib/menu.sh:330 +msgid "Needs a systemd user session." +msgstr "Serve una sessione utente di systemd." + +#: src/lib/menu.sh:322 src/lib/menu.sh:325 +msgid "Could not save the setting." +msgstr "Impossibile salvare l'impostazione." + +#: src/lib/menu.sh:371 src/lib/menu.sh:609 +msgid "Settings" +msgstr "Impostazioni" + +#: src/lib/menu.sh:372 +msgid "โ†‘โ†“ select โ†โ†’ or Space: change q: back" +msgstr "โ†‘โ†“ scegli โ†โ†’ o Spazio: cambia q: indietro" + +#: src/lib/menu.sh:373 +msgid "ON" +msgstr "ATTIVO" + +#: src/lib/menu.sh:374 +msgid "OFF" +msgstr "DISATTIVO" + +#: src/lib/menu.sh:456 src/lib/menu.sh:608 +msgid "Workspaces" +msgstr "Spazi di lavoro" + +#. TRANSLATORS: keep the letters, they are the keys. +#: src/lib/menu.sh:458 +msgid "" +"โ†‘โ†“ select r: refresh s: sign in again d: remove a: add q: back" +msgstr "" +"โ†‘โ†“ scegli r: aggiorna s: accedi di nuovo d: rimuovi a: aggiungi q: " +"indietro" + +#: src/lib/menu.sh:459 +msgid "No workspace yet. Press a to add one." +msgstr "Nessuno spazio di lavoro ancora. Premi a per aggiungerne uno." + +#: src/lib/menu.sh:484 +#, sh-printf-format +msgid "refreshed %s" +msgstr "aggiornato %s" + +#: src/lib/menu.sh:512 +#, sh-printf-format +msgid "Remove \"%s\" and its apps?" +msgstr "Rimuovere ยซ%sยป e le sue app?" + +#: src/lib/menu.sh:536 src/lib/menu.sh:607 +msgid "Start an app" +msgstr "Avvia un'app" + +#: src/lib/menu.sh:537 +msgid "โ†‘โ†“ select Enter: start q: back" +msgstr "โ†‘โ†“ scegli Invio: avvia q: indietro" + +#: src/lib/menu.sh:538 +msgid "No apps yet. Add a workspace first." +msgstr "Nessuna app ancora. Aggiungi prima uno spazio di lavoro." + +#: src/lib/menu.sh:579 +#, sh-printf-format +msgid "%s is starting. It opens in a window of its own." +msgstr "%s si sta avviando. Si apre in una finestra propria." + +#: src/lib/menu.sh:606 +msgid "Add a workspace" +msgstr "Aggiungi uno spazio di lavoro" + +#: src/lib/menu.sh:610 +msgid "Quit" +msgstr "Esci" + +#: src/lib/workspace.sh:163 +msgid "The server did not accept the user name or password." +msgstr "Il server non ha accettato il nome utente o la password." + +#: src/lib/workspace.sh:164 +#, sh-printf-format +msgid "Could not reach the server: %s" +msgstr "Impossibile raggiungere il server: %s" + +#: src/lib/workspace.sh:165 +#, sh-printf-format +msgid "There is no RD Web feed at %s." +msgstr "Non c'รจ nessun feed RD Web in %s." + +#: src/lib/workspace.sh:166 +#, sh-printf-format +msgid "Could not load the apps: %s" +msgstr "Impossibile caricare le app: %s" + +#: src/lib/workspace.sh:282 +#, sh-printf-format +msgid "Remote desktop of %s" +msgstr "Desktop remoto di %s" + +#: src/lib/workspace.sh:284 +#, sh-printf-format +msgid "%s from %s" +msgstr "%s di %s" + +#: src/lib/menu.sh +msgid "Apps" +msgstr "App" + +#: src/lib/menu.sh +msgid "Share a folder" +msgstr "Condividi una cartella" + +#: src/lib/menu.sh +msgid "Open files with the apps" +msgstr "Apri i file con le app" + +#: src/lib/menu.sh +msgid "Size" +msgstr "Dimensione" + +#: src/lib/menu.sh +msgid "Sound" +msgstr "Audio" + +#: src/lib/menu.sh +msgid "Graphics" +msgstr "Grafica" + +#: src/lib/menu.sh +msgid "Signing in" +msgstr "Accesso" + +#: src/lib/menu.sh +msgid "Wait for the second MFA prompt" +msgstr "Attendi la seconda richiesta MFA" + +#: src/lib/menu.sh +msgid "Refresh the apps every day" +msgstr "Aggiorna le app ogni giorno" diff --git a/po/ja.po b/po/ja.po new file mode 100644 index 0000000..8890285 --- /dev/null +++ b/po/ja.po @@ -0,0 +1,561 @@ +# Japanese translation for rdfeed. +# Copyright (C) 2026 Felitendo +# This file is distributed under the same license as rdfeed. +# +msgid "" +msgstr "" +"Project-Id-Version: rdfeed 1.0.0\n" +"Report-Msgid-Bugs-To: https://github.com/LoonixTools/rdfeed/issues\n" +"POT-Creation-Date: 2026-10-02 10:29+0200\n" +"PO-Revision-Date: 2026-10-02 09:30+0200\n" +"Last-Translator: Felitendo\n" +"Language-Team: Japanese\n" +"Language: ja\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" + +#: src/rdfeed:79 src/rdfeed:103 +#, sh-printf-format +msgid "Could not write to %s." +msgstr "%s ใซๆ›ธใ่พผใ‚ใพใ›ใ‚“ใงใ—ใŸใ€‚" + +#: src/rdfeed:82 +#, sh-printf-format +msgid "Signing in to %sโ€ฆ" +msgstr "%s ใซใ‚ตใ‚คใƒณใ‚คใƒณใ—ใฆใ„ใพใ™โ€ฆ" + +#: src/rdfeed:100 +msgid "" +"The password could not be saved in the keyring, so each app asks for it." +msgstr "" +"ใƒ‘ใ‚นใƒฏใƒผใƒ‰ใ‚’ใ‚ญใƒผใƒชใƒณใ‚ฐใซไฟๅญ˜ใงใใชใ‹ใฃใŸใŸใ‚ใ€ใ‚ขใƒ—ใƒชใ”ใจใซๅ…ฅๅŠ›ใ‚’ๆฑ‚ใ‚ใพใ™ใ€‚" + +#: src/rdfeed:111 +#, sh-printf-format +msgid "1 app from %s is now in your app menu." +msgstr "%s ใฎใ‚ขใƒ—ใƒช 1 ๅ€‹ใŒใ‚ขใƒ—ใƒชใƒกใƒ‹ใƒฅใƒผใซ่ฟฝๅŠ ใ•ใ‚Œใพใ—ใŸใ€‚" + +#: src/rdfeed:113 +#, sh-printf-format +msgid "%d apps from %s are now in your app menu." +msgstr "%d ๅ€‹ใฎใ‚ขใƒ—ใƒช๏ผˆ%s๏ผ‰ใŒใ‚ขใƒ—ใƒชใƒกใƒ‹ใƒฅใƒผใซ่ฟฝๅŠ ใ•ใ‚Œใพใ—ใŸใ€‚" + +#: src/rdfeed:129 +msgid "" +"Your IT department has the address. A work e-mail address often works too." +msgstr "" +"ใ‚ขใƒ‰ใƒฌใ‚นใฏ IT ้ƒจ้–€ใŒ็Ÿฅใฃใฆใ„ใพใ™ใ€‚ไป•ไบ‹็”จใฎใƒกใƒผใƒซใ‚ขใƒ‰ใƒฌใ‚นใงใ‚‚ไฝฟใˆใ‚‹ใ“ใจใŒใ‚ˆใ" +"ใ‚ใ‚Šใพใ™ใ€‚" + +#: src/rdfeed:130 +msgid "Feed address, server or e-mail:" +msgstr "ใƒ•ใ‚ฃใƒผใƒ‰ใฎใ‚ขใƒ‰ใƒฌใ‚นใ€ใ‚ตใƒผใƒใƒผใ€ใพใŸใฏใƒกใƒผใƒซ:" + +#: src/rdfeed:137 +#, sh-printf-format +msgid "No feed is registered for %s. Ask your IT department for the address." +msgstr "" +"%s ใซใฏใƒ•ใ‚ฃใƒผใƒ‰ใŒ็™ป้Œฒใ•ใ‚Œใฆใ„ใพใ›ใ‚“ใ€‚IT ้ƒจ้–€ใซใ‚ขใƒ‰ใƒฌใ‚นใ‚’็ขบ่ชใ—ใฆใใ ใ•ใ„ใ€‚" + +#: src/rdfeed:142 src/rdfeed:156 +msgid "User name (DOMAIN\\name or name@domain):" +msgstr "ใƒฆใƒผใ‚ถใƒผๅ๏ผˆDOMAIN\\name ใพใŸใฏ name@domain๏ผ‰:" + +#: src/rdfeed:146 src/rdfeed:159 +msgid "Password:" +msgstr "ใƒ‘ใ‚นใƒฏใƒผใƒ‰:" + +#: src/rdfeed:178 src/rdfeed:236 +#, sh-printf-format +msgid "No workspace yet. Add one with `%s add`." +msgstr "ใƒฏใƒผใ‚ฏใ‚นใƒšใƒผใ‚นใฏใพใ ใ‚ใ‚Šใพใ›ใ‚“ใ€‚`%s add` ใง่ฟฝๅŠ ใ—ใฆใใ ใ•ใ„ใ€‚" + +#: src/rdfeed:187 src/lib/launch.sh:220 +#, sh-printf-format +msgid "No password saved for %s. Sign in again in rdfeed." +msgstr "" +"%s ใฎใƒ‘ใ‚นใƒฏใƒผใƒ‰ใŒไฟๅญ˜ใ•ใ‚Œใฆใ„ใพใ›ใ‚“ใ€‚rdfeed ใงใ‚‚ใ†ไธ€ๅบฆใ‚ตใ‚คใƒณใ‚คใƒณใ—ใฆใใ ใ•" +"ใ„ใ€‚" + +#: src/rdfeed:191 src/lib/launch.sh:149 +#, sh-printf-format +msgid "Password for %s:" +msgstr "%s ใฎใƒ‘ใ‚นใƒฏใƒผใƒ‰:" + +#: src/rdfeed:195 +#, sh-printf-format +msgid "Refreshing %sโ€ฆ" +msgstr "%s ใ‚’ๆ›ดๆ–ฐใ—ใฆใ„ใพใ™โ€ฆ" + +#: src/rdfeed:205 +msgid "The password no longer works. Sign in again in rdfeed." +msgstr "" +"ใƒ‘ใ‚นใƒฏใƒผใƒ‰ใŒไฝฟใˆใชใใชใ‚Šใพใ—ใŸใ€‚rdfeed ใงใ‚‚ใ†ไธ€ๅบฆใ‚ตใ‚คใƒณใ‚คใƒณใ—ใฆใใ ใ•ใ„ใ€‚" + +#: src/rdfeed:214 +#, sh-printf-format +msgid "%s: %d apps" +msgstr "%s: ใ‚ขใƒ—ใƒช %d ๅ€‹" + +#: src/rdfeed:222 +#, sh-printf-format +msgid "Which workspace? See `%s list` for the names." +msgstr "ใฉใฎใƒฏใƒผใ‚ฏใ‚นใƒšใƒผใ‚นใงใ™ใ‹๏ผŸๅๅ‰ใฏ `%s list` ใง็ขบ่ชใงใใพใ™ใ€‚" + +#: src/rdfeed:229 +#, sh-printf-format +msgid "%s and its apps are removed." +msgstr "%s ใจใใฎใ‚ขใƒ—ใƒชใ‚’ๅ‰Š้™คใ—ใพใ—ใŸใ€‚" + +#: src/rdfeed:253 +#, sh-printf-format +msgid "Which app? See `%s list`." +msgstr "ใฉใฎใ‚ขใƒ—ใƒชใงใ™ใ‹๏ผŸ`%s list` ใ‚’ๅ‚็…งใ—ใฆใใ ใ•ใ„ใ€‚" + +#: src/rdfeed:268 src/lib/launch.sh:199 +#, sh-printf-format +msgid "There is no app %s. See `%s list`." +msgstr "ใ‚ขใƒ—ใƒช %s ใฏใ‚ใ‚Šใพใ›ใ‚“ใ€‚`%s list` ใ‚’ๅ‚็…งใ—ใฆใใ ใ•ใ„ใ€‚" + +#: src/rdfeed:270 +#, sh-printf-format +msgid "Several workspaces have %s. Write it as WORKSPACE/APP, see `%s list`." +msgstr "" +"่ค‡ๆ•ฐใฎใƒฏใƒผใ‚ฏใ‚นใƒšใƒผใ‚นใซ %s ใŒใ‚ใ‚Šใพใ™ใ€‚ใƒฏใƒผใ‚ฏใ‚นใƒšใƒผใ‚น/ใ‚ขใƒ—ใƒช ใฎๅฝขใงๆŒ‡ๅฎšใ—ใฆใ" +"ใ ใ•ใ„๏ผˆ`%s list` ใ‚’ๅ‚็…ง๏ผ‰ใ€‚" + +#: src/rdfeed:278 +msgid "Usage: rdfeed [command]" +msgstr "ไฝฟใ„ๆ–น: rdfeed [ใ‚ณใƒžใƒณใƒ‰]" + +#: src/rdfeed:280 +msgid "Commands:" +msgstr "ใ‚ณใƒžใƒณใƒ‰:" + +#: src/rdfeed:281 +msgid "Add the workspace of your company" +msgstr "ไผš็คพใฎใƒฏใƒผใ‚ฏใ‚นใƒšใƒผใ‚นใ‚’่ฟฝๅŠ ใ™ใ‚‹" + +#: src/rdfeed:282 +msgid "Load the apps again" +msgstr "ใ‚ขใƒ—ใƒชใ‚’่ชญใฟ่พผใฟ็›ดใ™" + +#: src/rdfeed:283 +msgid "List the workspaces and their apps" +msgstr "ใƒฏใƒผใ‚ฏใ‚นใƒšใƒผใ‚นใจใใฎใ‚ขใƒ—ใƒชใ‚’ไธ€่ฆง่กจ็คบใ™ใ‚‹" + +#: src/rdfeed:284 +msgid "Start an app, or open a file with it" +msgstr "ใ‚ขใƒ—ใƒชใ‚’่ตทๅ‹•ใ™ใ‚‹ใ€ใพใŸใฏใƒ•ใ‚กใ‚คใƒซใ‚’้–‹ใ" + +#: src/rdfeed:285 +msgid "Remove a workspace and its apps" +msgstr "ใƒฏใƒผใ‚ฏใ‚นใƒšใƒผใ‚นใจใใฎใ‚ขใƒ—ใƒชใ‚’ๅ‰Š้™คใ™ใ‚‹" + +#: src/rdfeed:286 +msgid "Show this help" +msgstr "ใ“ใฎใƒ˜ใƒซใƒ—ใ‚’่กจ็คบ" + +#: src/rdfeed:287 +msgid "Show the version" +msgstr "ใƒใƒผใ‚ธใƒงใƒณใ‚’่กจ็คบ" + +#: src/rdfeed:289 +msgid "Without a command an interactive menu is shown." +msgstr "ใ‚ณใƒžใƒณใƒ‰ใชใ—ใง่ตทๅ‹•ใ™ใ‚‹ใจใ€ๅฏพ่ฉฑๅผใƒกใƒ‹ใƒฅใƒผใŒ่กจ็คบใ•ใ‚Œใพใ™ใ€‚" + +#: src/rdfeed:313 +#, sh-printf-format +msgid "Unknown command: %s" +msgstr "ไธๆ˜Žใชใ‚ณใƒžใƒณใƒ‰: %s" + +#: src/lib/common.sh:174 src/lib/menu.sh:251 +msgid "never" +msgstr "ใชใ—" + +#: src/lib/common.sh:184 +msgid "just now" +msgstr "ใŸใฃใŸไปŠ" + +#: src/lib/common.sh:186 +msgid "1 minute ago" +msgstr "1 ๅˆ†ๅ‰" + +#: src/lib/common.sh:189 +#, sh-printf-format +msgid "%d minutes ago" +msgstr "%d ๅˆ†ๅ‰" + +#: src/lib/common.sh:191 +msgid "1 hour ago" +msgstr "1 ๆ™‚้–“ๅ‰" + +#: src/lib/common.sh:194 +#, sh-printf-format +msgid "%d hours ago" +msgstr "%d ๆ™‚้–“ๅ‰" + +#: src/lib/common.sh:196 +msgid "1 day ago" +msgstr "1 ๆ—ฅๅ‰" + +#: src/lib/common.sh:199 +#, sh-printf-format +msgid "%d days ago" +msgstr "%d ๆ—ฅๅ‰" + +#: src/lib/launch.sh:169 +msgid "" +"Your password has expired. Change it on Windows or on the RD Web page, then " +"sign in again in rdfeed." +msgstr "" +"ใƒ‘ใ‚นใƒฏใƒผใƒ‰ใฎๆœ‰ๅŠนๆœŸ้™ใŒๅˆ‡ใ‚Œใฆใ„ใพใ™ใ€‚Windows ใ‹ RD Web ใฎใƒšใƒผใ‚ธใงๅค‰ๆ›ดใ—ใฆใ‹" +"ใ‚‰ใ€rdfeed ใงใ‚‚ใ†ไธ€ๅบฆใ‚ตใ‚คใƒณใ‚คใƒณใ—ใฆใใ ใ•ใ„ใ€‚" + +#: src/lib/launch.sh:171 +msgid "Your account is locked. Ask your IT department." +msgstr "ใ‚ขใ‚ซใ‚ฆใƒณใƒˆใŒใƒญใƒƒใ‚ฏใ•ใ‚Œใฆใ„ใพใ™ใ€‚IT ้ƒจ้–€ใซๅ•ใ„ๅˆใ‚ใ›ใฆใใ ใ•ใ„ใ€‚" + +#: src/lib/launch.sh:173 +msgid "The server did not accept the password. Sign in again in rdfeed." +msgstr "" +"ใ‚ตใƒผใƒใƒผใŒใƒ‘ใ‚นใƒฏใƒผใƒ‰ใ‚’ๅ—ใ‘ไป˜ใ‘ใพใ›ใ‚“ใงใ—ใŸใ€‚rdfeed ใงใ‚‚ใ†ไธ€ๅบฆใ‚ตใ‚คใƒณใ‚คใƒณใ—ใฆใ" +"ใ ใ•ใ„ใ€‚" + +#: src/lib/launch.sh:175 +msgid "Could not reach the server. Check the internet connection." +msgstr "ใ‚ตใƒผใƒใƒผใซๆŽฅ็ถšใงใใพใ›ใ‚“ใงใ—ใŸใ€‚ใ‚คใƒณใ‚ฟใƒผใƒใƒƒใƒˆๆŽฅ็ถšใ‚’็ขบ่ชใ—ใฆใใ ใ•ใ„ใ€‚" + +#: src/lib/launch.sh:178 +#, sh-printf-format +msgid "FreeRDP stopped: %s" +msgstr "FreeRDP ใŒๅœๆญขใ—ใพใ—ใŸ: %s" + +#: src/lib/launch.sh:205 src/lib/launch.sh:206 +#, sh-printf-format +msgid "File not found: %s" +msgstr "ใƒ•ใ‚กใ‚คใƒซใŒ่ฆ‹ใคใ‹ใ‚Šใพใ›ใ‚“: %s" + +#: src/lib/launch.sh:212 src/lib/launch.sh:213 +msgid "" +"FreeRDP 3 is missing. Install it to start the apps (the package is called " +"freerdp)." +msgstr "" +"FreeRDP 3 ใŒใ‚ใ‚Šใพใ›ใ‚“ใ€‚ใ‚ขใƒ—ใƒชใ‚’่ตทๅ‹•ใ™ใ‚‹ใซใฏใ‚คใƒณใ‚นใƒˆใƒผใƒซใ—ใฆใใ ใ•ใ„๏ผˆใƒ‘ใƒƒ" +"ใ‚ฑใƒผใ‚ธๅใฏ freerdp๏ผ‰ใ€‚" + +#: src/lib/launch.sh:277 +#, sh-printf-format +msgid "Opening %s in %s" +msgstr "%s ใ‚’ %s ใง้–‹ใ„ใฆใ„ใพใ™" + +#: src/lib/launch.sh:278 +#, sh-printf-format +msgid "If it does not open by itself, open it in the app from the drive %s." +msgstr "่‡ชๅ‹•ใง้–‹ใ‹ใชใ„ๅ ดๅˆใฏใ€ใ‚ขใƒ—ใƒชใงใƒ‰ใƒฉใ‚คใƒ– %s ใ‹ใ‚‰้–‹ใ„ใฆใใ ใ•ใ„ใ€‚" + +#: src/lib/launch.sh:280 +#, sh-printf-format +msgid "Starting %s" +msgstr "%s ใ‚’่ตทๅ‹•ใ—ใฆใ„ใพใ™" + +#: src/lib/launch.sh:281 +msgid "" +"If your company uses MFA, confirm the sign-in in your authenticator app." +msgstr "" +"ไผš็คพใŒ MFA ใ‚’ไฝฟใฃใฆใ„ใ‚‹ๅ ดๅˆใฏใ€่ช่จผใ‚ขใƒ—ใƒชใงใ‚ตใ‚คใƒณใ‚คใƒณใ‚’ๆ‰ฟ่ชใ—ใฆใใ ใ•ใ„ใ€‚" + +#: src/lib/launch.sh:290 +msgid "A second sign-in is coming" +msgstr "2 ๅ›ž็›ฎใฎใ‚ตใ‚คใƒณใ‚คใƒณใŒๆฅใพใ™" + +#: src/lib/launch.sh:291 +#, sh-printf-format +msgid "" +"Confirm the second prompt in your authenticator app too. It comes in about " +"%d seconds." +msgstr "่ช่จผใ‚ขใƒ—ใƒชใง 2 ๅ›ž็›ฎใฎ็ขบ่ชใ‚‚ๆ‰ฟ่ชใ—ใฆใใ ใ•ใ„ใ€‚็ด„ %d ็ง’ๅพŒใซๅฑŠใใพใ™ใ€‚" + +#: src/lib/launch.sh:301 +#, sh-printf-format +msgid "%s could not start" +msgstr "%s ใ‚’่ตทๅ‹•ใงใใพใ›ใ‚“ใงใ—ใŸ" + +#: src/lib/launch.sh:303 +#, sh-printf-format +msgid "Log: %s" +msgstr "ใƒญใ‚ฐ: %s" + +#. TRANSLATORS: the letter after "[" is the key for yes. y works too. +#: src/lib/menu.sh:93 +msgid "[y/N]" +msgstr "[y/N]" + +#: src/lib/menu.sh:163 +msgid "1 app" +msgstr "ใ‚ขใƒ—ใƒช 1 ๅ€‹" + +#: src/lib/menu.sh:165 +#, sh-printf-format +msgid "%d apps" +msgstr "ใ‚ขใƒ—ใƒช %d ๅ€‹" + +#: src/lib/menu.sh:190 +msgid "No workspace yet. Add the one of your company with [1]." +msgstr "" +"ใƒฏใƒผใ‚ฏใ‚นใƒšใƒผใ‚นใฏใพใ ใ‚ใ‚Šใพใ›ใ‚“ใ€‚[1] ใงไผš็คพใฎใƒฏใƒผใ‚ฏใ‚นใƒšใƒผใ‚นใ‚’่ฟฝๅŠ ใ—ใฆใใ ใ•" +"ใ„ใ€‚" + +#: src/lib/menu.sh:233 +msgid "off" +msgstr "ใ‚ชใƒ•" + +#: src/lib/menu.sh:234 +msgid "home folder" +msgstr "ใƒ›ใƒผใƒ ใƒ•ใ‚ฉใƒซใƒ€ใƒผ" + +#: src/lib/menu.sh:237 +msgid "Documents" +msgstr "ใƒ‰ใ‚ญใƒฅใƒกใƒณใƒˆ" + +#: src/lib/menu.sh:238 +msgid "Downloads" +msgstr "ใƒ€ใ‚ฆใƒณใƒญใƒผใƒ‰" + +#: src/lib/menu.sh:243 +msgid "not found" +msgstr "่ฆ‹ใคใ‹ใ‚Šใพใ›ใ‚“" + +#: src/lib/menu.sh:246 +#, sh-printf-format +msgid "automatic (%d %%)" +msgstr "่‡ชๅ‹• (%d %%)" + +#: src/lib/menu.sh:248 +msgid "classic" +msgstr "ใ‚ฏใƒฉใ‚ทใƒƒใ‚ฏ" + +#: src/lib/menu.sh:249 +msgid "GFX" +msgstr "GFX" + +#: src/lib/menu.sh:250 +msgid "always" +msgstr "ๅธธใซ" + +#: src/lib/menu.sh:252 +msgid "automatic" +msgstr "่‡ชๅ‹•" + +#: src/lib/menu.sh:261 +msgid "" +"The apps only see the files on the server. Pick a folder to open and save " +"files of this computer in them." +msgstr "" +"ใ‚ขใƒ—ใƒชใฏใ‚ตใƒผใƒใƒผไธŠใฎใƒ•ใ‚กใ‚คใƒซใ—ใ‹่ฆ‹ใˆใพใ›ใ‚“ใ€‚ใƒ•ใ‚ฉใƒซใƒ€ใƒผใ‚’้ธใถใจใ€ใ“ใฎใ‚ณใƒณ" +"ใƒ”ใƒฅใƒผใ‚ฟใƒผใฎใƒ•ใ‚กใ‚คใƒซใ‚’้–‹ใ„ใŸใ‚Šไฟๅญ˜ใ—ใŸใ‚Šใงใใพใ™ใ€‚" + +#: src/lib/menu.sh:262 +msgid "" +"The apps can open and save files in this folder. It shows up as a drive in " +"them." +msgstr "" +"ใ‚ขใƒ—ใƒชใฏใ“ใฎใƒ•ใ‚ฉใƒซใƒ€ใƒผใฎใƒ•ใ‚กใ‚คใƒซใ‚’้–‹ใ„ใŸใ‚Šไฟๅญ˜ใ—ใŸใ‚Šใงใใพใ™ใ€‚ใ‚ขใƒ—ใƒชๅ†…ใงใฏใƒ‰" +"ใƒฉใ‚คใƒ–ใจใ—ใฆ่กจ็คบใ•ใ‚Œใพใ™ใ€‚" + +#: src/lib/menu.sh:263 +msgid "" +"Offers the apps under \"Open with\" for their file types, for example Word " +"for .docx. The folder of the file is shared with the app while it is open." +msgstr "" +"ใƒ•ใ‚กใ‚คใƒซใฎ็จฎ้กžใ”ใจใซใ€Œใ‚ขใƒ—ใƒชใ‚ฑใƒผใ‚ทใƒงใƒณใง้–‹ใใ€ใซใ‚ขใƒ—ใƒชใ‚’่กจ็คบใ—ใพใ™" +"๏ผˆไพ‹: .docx ใชใ‚‰ Word๏ผ‰ใ€‚ใƒ•ใ‚กใ‚คใƒซใ‚’้–‹ใ„ใฆใ„ใ‚‹้–“ใ€ใใฎใƒ•ใ‚ฉใƒซใƒ€ใƒผใŒๅ…ฑๆœ‰ใ•ใ‚Œใพ" +"ใ™ใ€‚" + +#: src/lib/menu.sh:264 +msgid "" +"How big the apps are drawn. Automatic follows the scale of your desktop, so " +"they are sharp and the right size." +msgstr "" +"ใ‚ขใƒ—ใƒชใ‚’ๆใๅคงใใ•ใงใ™ใ€‚่‡ชๅ‹•ใงใฏใƒ‡ใ‚นใ‚ฏใƒˆใƒƒใƒ—ใฎๆ‹กๅคง็އใซๅˆใ‚ใ›ใ‚‹ใฎใงใ€ใใฃใใ‚Š" +"ใจ้ฉๅˆ‡ใชๅคงใใ•ใซใชใ‚Šใพใ™ใ€‚" + +#: src/lib/menu.sh:265 +msgid "Plays the sound of the apps here, for calls and videos." +msgstr "ใ‚ขใƒ—ใƒชใฎ้Ÿณใ‚’ใ“ใฎใ‚ณใƒณใƒ”ใƒฅใƒผใ‚ฟใƒผใงๅ†็”Ÿใ—ใพใ™ใ€‚้€š่ฉฑใ‚„ๅ‹•็”ปใซใ€‚" + +#: src/lib/menu.sh:266 +msgid "" +"Paints every window right. But some servers end the session right after " +"signing in, then go back to GFX." +msgstr "ใ™ในใฆใฎใ‚ฆใ‚ฃใƒณใƒ‰ใ‚ฆใ‚’ๆญฃใ—ใๆใใพใ™ใ€‚ใŸใ ใ—ใ‚ตใ‚คใƒณใ‚คใƒณ็›ดๅพŒใซใ‚ปใƒƒใ‚ทใƒงใƒณใ‚’ๅˆ‡ใ‚‹ใ‚ตใƒผใƒใƒผใ‚‚ใ‚ใ‚Šใ€ใใฎๅ ดๅˆใฏ GFX ใซๆˆปใ—ใฆใใ ใ•ใ„ใ€‚" + +#: src/lib/menu.sh:267 +msgid "" +"Works with every server. FreeRDP may not repaint a window after it was " +"minimized or resized." +msgstr "ใฉใฎใ‚ตใƒผใƒใƒผใงใ‚‚ๅ‹•ใใพใ™ใ€‚ๆœ€ๅฐๅŒ–ใ‚„ใ‚ตใ‚คใ‚บๅค‰ๆ›ดใฎๅพŒใ€FreeRDP ใŒใ‚ฆใ‚ฃใƒณใƒ‰ใ‚ฆใ‚’ๆใ็›ดใ•ใชใ„ใ“ใจใŒใ‚ใ‚Šใพใ™ใ€‚" + +#: src/lib/menu.sh:268 +msgid "" +"Always waits before the second sign-in. For when the second prompt does not " +"come with automatic." +msgstr "" +"2 ๅ›ž็›ฎใฎใ‚ตใ‚คใƒณใ‚คใƒณใฎๅ‰ใซๅฟ…ใšๅพ…ใกใพใ™ใ€‚่‡ชๅ‹•ใง 2 ๅ›ž็›ฎใฎ็ขบ่ชใŒๆฅใชใ„ใจใใซใ€‚" + +#: src/lib/menu.sh:269 +msgid "Never waits. Right when your company has no MFA." +msgstr "ๅพ…ใกใพใ›ใ‚“ใ€‚ไผš็คพใŒ MFA ใ‚’ไฝฟใฃใฆใ„ใชใ„ใจใๅ‘ใ‘ใงใ™ใ€‚" + +#: src/lib/menu.sh:270 +msgid "" +"Many servers with MFA need a second sign-in, and its prompt only comes after " +"a short wait. Automatic waits when the first sign-in took a while." +msgstr "" +"MFA ใ‚’ไฝฟใ†ๅคšใใฎใ‚ตใƒผใƒใƒผใงใฏ 2 ๅ›ž็›ฎใฎใ‚ตใ‚คใƒณใ‚คใƒณใŒๅฟ…่ฆใงใ€ใใฎ็ขบ่ชใฏๅฐ‘ใ—ๅพ…ใŸใช" +"ใ„ใจๅฑŠใใพใ›ใ‚“ใ€‚่‡ชๅ‹•ใงใฏใ€1 ๅ›ž็›ฎใซๆ™‚้–“ใŒใ‹ใ‹ใฃใŸใจใใซๅพ…ใกใพใ™ใ€‚" + +#: src/lib/menu.sh:273 +msgid "" +"Loads the list of apps again once a day, so new apps show up by themselves." +msgstr "" +"1 ๆ—ฅ 1 ๅ›žใ‚ขใƒ—ใƒชใฎไธ€่ฆงใ‚’่ชญใฟ่พผใฟ็›ดใ™ใฎใงใ€ๆ–ฐใ—ใ„ใ‚ขใƒ—ใƒชใŒ่‡ชๅ‹•ใง็พใ‚Œใพใ™ใ€‚" + +#: src/lib/menu.sh:275 src/lib/menu.sh:330 +msgid "Needs a systemd user session." +msgstr "systemd ใฎใƒฆใƒผใ‚ถใƒผใ‚ปใƒƒใ‚ทใƒงใƒณใŒๅฟ…่ฆใงใ™ใ€‚" + +#: src/lib/menu.sh:322 src/lib/menu.sh:325 +msgid "Could not save the setting." +msgstr "่จญๅฎšใ‚’ไฟๅญ˜ใงใใพใ›ใ‚“ใงใ—ใŸใ€‚" + +#: src/lib/menu.sh:371 src/lib/menu.sh:609 +msgid "Settings" +msgstr "่จญๅฎš" + +#: src/lib/menu.sh:372 +msgid "โ†‘โ†“ select โ†โ†’ or Space: change q: back" +msgstr "โ†‘โ†“ ้ธๆŠž โ†โ†’ ใพใŸใฏใ‚นใƒšใƒผใ‚น: ๅค‰ๆ›ด q: ๆˆปใ‚‹" + +#: src/lib/menu.sh:373 +msgid "ON" +msgstr "ใ‚ชใƒณ" + +#: src/lib/menu.sh:374 +msgid "OFF" +msgstr "ใ‚ชใƒ•" + +#: src/lib/menu.sh:456 src/lib/menu.sh:608 +msgid "Workspaces" +msgstr "ใƒฏใƒผใ‚ฏใ‚นใƒšใƒผใ‚น" + +#. TRANSLATORS: keep the letters, they are the keys. +#: src/lib/menu.sh:458 +msgid "" +"โ†‘โ†“ select r: refresh s: sign in again d: remove a: add q: back" +msgstr "โ†‘โ†“ ้ธๆŠž r: ๆ›ดๆ–ฐ s: ๅ†ใ‚ตใ‚คใƒณใ‚คใƒณ d: ๅ‰Š้™ค a: ่ฟฝๅŠ  q: ๆˆปใ‚‹" + +#: src/lib/menu.sh:459 +msgid "No workspace yet. Press a to add one." +msgstr "ใƒฏใƒผใ‚ฏใ‚นใƒšใƒผใ‚นใฏใพใ ใ‚ใ‚Šใพใ›ใ‚“ใ€‚a ใง่ฟฝๅŠ ใงใใพใ™ใ€‚" + +#: src/lib/menu.sh:484 +#, sh-printf-format +msgid "refreshed %s" +msgstr "%sใซๆ›ดๆ–ฐ" + +#: src/lib/menu.sh:512 +#, sh-printf-format +msgid "Remove \"%s\" and its apps?" +msgstr "ใ€Œ%sใ€ใจใใฎใ‚ขใƒ—ใƒชใ‚’ๅ‰Š้™คใ—ใพใ™ใ‹๏ผŸ" + +#: src/lib/menu.sh:536 src/lib/menu.sh:607 +msgid "Start an app" +msgstr "ใ‚ขใƒ—ใƒชใ‚’่ตทๅ‹•ใ™ใ‚‹" + +#: src/lib/menu.sh:537 +msgid "โ†‘โ†“ select Enter: start q: back" +msgstr "โ†‘โ†“ ้ธๆŠž Enter: ่ตทๅ‹• q: ๆˆปใ‚‹" + +#: src/lib/menu.sh:538 +msgid "No apps yet. Add a workspace first." +msgstr "ใ‚ขใƒ—ใƒชใฏใพใ ใ‚ใ‚Šใพใ›ใ‚“ใ€‚ๅ…ˆใซใƒฏใƒผใ‚ฏใ‚นใƒšใƒผใ‚นใ‚’่ฟฝๅŠ ใ—ใฆใใ ใ•ใ„ใ€‚" + +#: src/lib/menu.sh:579 +#, sh-printf-format +msgid "%s is starting. It opens in a window of its own." +msgstr "%s ใ‚’่ตทๅ‹•ใ—ใฆใ„ใพใ™ใ€‚ๅฐ‚็”จใฎใ‚ฆใ‚ฃใƒณใƒ‰ใ‚ฆใง้–‹ใใพใ™ใ€‚" + +#: src/lib/menu.sh:606 +msgid "Add a workspace" +msgstr "ใƒฏใƒผใ‚ฏใ‚นใƒšใƒผใ‚นใ‚’่ฟฝๅŠ " + +#: src/lib/menu.sh:610 +msgid "Quit" +msgstr "็ต‚ไบ†" + +#: src/lib/workspace.sh:163 +msgid "The server did not accept the user name or password." +msgstr "ใ‚ตใƒผใƒใƒผใŒใƒฆใƒผใ‚ถใƒผๅใพใŸใฏใƒ‘ใ‚นใƒฏใƒผใƒ‰ใ‚’ๅ—ใ‘ไป˜ใ‘ใพใ›ใ‚“ใงใ—ใŸใ€‚" + +#: src/lib/workspace.sh:164 +#, sh-printf-format +msgid "Could not reach the server: %s" +msgstr "ใ‚ตใƒผใƒใƒผใซๆŽฅ็ถšใงใใพใ›ใ‚“ใงใ—ใŸ: %s" + +#: src/lib/workspace.sh:165 +#, sh-printf-format +msgid "There is no RD Web feed at %s." +msgstr "%s ใซ RD Web ใฎใƒ•ใ‚ฃใƒผใƒ‰ใฏใ‚ใ‚Šใพใ›ใ‚“ใ€‚" + +#: src/lib/workspace.sh:166 +#, sh-printf-format +msgid "Could not load the apps: %s" +msgstr "ใ‚ขใƒ—ใƒชใ‚’่ชญใฟ่พผใ‚ใพใ›ใ‚“ใงใ—ใŸ: %s" + +#: src/lib/workspace.sh:282 +#, sh-printf-format +msgid "Remote desktop of %s" +msgstr "%s ใฎใƒชใƒขใƒผใƒˆใƒ‡ใ‚นใ‚ฏใƒˆใƒƒใƒ—" + +#: src/lib/workspace.sh:284 +#, sh-printf-format +msgid "%s from %s" +msgstr "%s๏ผˆ%s๏ผ‰" + +#: src/lib/menu.sh +msgid "Apps" +msgstr "ใ‚ขใƒ—ใƒช" + +#: src/lib/menu.sh +msgid "Share a folder" +msgstr "ใƒ•ใ‚ฉใƒซใƒ€ใƒผใ‚’ๅ…ฑๆœ‰" + +#: src/lib/menu.sh +msgid "Open files with the apps" +msgstr "ใƒ•ใ‚กใ‚คใƒซใ‚’ใ‚ขใƒ—ใƒชใง้–‹ใ" + +#: src/lib/menu.sh +msgid "Size" +msgstr "ใ‚ตใ‚คใ‚บ" + +#: src/lib/menu.sh +msgid "Sound" +msgstr "้Ÿณๅฃฐ" + +#: src/lib/menu.sh +msgid "Graphics" +msgstr "ใ‚ฐใƒฉใƒ•ใ‚ฃใƒƒใ‚ฏ" + +#: src/lib/menu.sh +msgid "Signing in" +msgstr "ใ‚ตใ‚คใƒณใ‚คใƒณ" + +#: src/lib/menu.sh +msgid "Wait for the second MFA prompt" +msgstr "2 ๅ›ž็›ฎใฎ MFA ็ขบ่ชใ‚’ๅพ…ใค" + +#: src/lib/menu.sh +msgid "Refresh the apps every day" +msgstr "ใ‚ขใƒ—ใƒชใ‚’ๆฏŽๆ—ฅๆ›ดๆ–ฐ" diff --git a/po/ko.po b/po/ko.po new file mode 100644 index 0000000..3f73148 --- /dev/null +++ b/po/ko.po @@ -0,0 +1,547 @@ +# Korean translation for rdfeed. +# Copyright (C) 2026 Felitendo +# This file is distributed under the same license as rdfeed. +# +msgid "" +msgstr "" +"Project-Id-Version: rdfeed 1.0.0\n" +"Report-Msgid-Bugs-To: https://github.com/LoonixTools/rdfeed/issues\n" +"POT-Creation-Date: 2026-10-02 10:29+0200\n" +"PO-Revision-Date: 2026-10-02 09:30+0200\n" +"Last-Translator: Felitendo\n" +"Language-Team: Korean\n" +"Language: ko\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" + +#: src/rdfeed:79 src/rdfeed:103 +#, sh-printf-format +msgid "Could not write to %s." +msgstr "%s์— ์“ฐ์ง€ ๋ชปํ–ˆ์Šต๋‹ˆ๋‹ค." + +#: src/rdfeed:82 +#, sh-printf-format +msgid "Signing in to %sโ€ฆ" +msgstr "%s์— ๋กœ๊ทธ์ธํ•˜๋Š” ์ค‘โ€ฆ" + +#: src/rdfeed:100 +msgid "" +"The password could not be saved in the keyring, so each app asks for it." +msgstr "๋น„๋ฐ€๋ฒˆํ˜ธ๋ฅผ ํ‚ค๋ง์— ์ €์žฅํ•˜์ง€ ๋ชปํ•ด ์•ฑ๋งˆ๋‹ค ๋น„๋ฐ€๋ฒˆํ˜ธ๋ฅผ ๋ฌป์Šต๋‹ˆ๋‹ค." + +#: src/rdfeed:111 +#, sh-printf-format +msgid "1 app from %s is now in your app menu." +msgstr "%s์˜ ์•ฑ 1๊ฐœ๊ฐ€ ์•ฑ ๋ฉ”๋‰ด์— ์ถ”๊ฐ€๋˜์—ˆ์Šต๋‹ˆ๋‹ค." + +#: src/rdfeed:113 +#, sh-printf-format +msgid "%d apps from %s are now in your app menu." +msgstr "์•ฑ %d๊ฐœ(%s)๊ฐ€ ์•ฑ ๋ฉ”๋‰ด์— ์ถ”๊ฐ€๋˜์—ˆ์Šต๋‹ˆ๋‹ค." + +#: src/rdfeed:129 +msgid "" +"Your IT department has the address. A work e-mail address often works too." +msgstr "" +"์ฃผ์†Œ๋Š” IT ๋ถ€์„œ์—์„œ ์•Œ๋ ค ์ค๋‹ˆ๋‹ค. ํšŒ์‚ฌ ์ด๋ฉ”์ผ ์ฃผ์†Œ๋กœ๋„ ๋˜๋Š” ๊ฒฝ์šฐ๊ฐ€ ๋งŽ์Šต๋‹ˆ๋‹ค." + +#: src/rdfeed:130 +msgid "Feed address, server or e-mail:" +msgstr "ํ”ผ๋“œ ์ฃผ์†Œ, ์„œ๋ฒ„ ๋˜๋Š” ์ด๋ฉ”์ผ:" + +#: src/rdfeed:137 +#, sh-printf-format +msgid "No feed is registered for %s. Ask your IT department for the address." +msgstr "%s์— ๋“ฑ๋ก๋œ ํ”ผ๋“œ๊ฐ€ ์—†์Šต๋‹ˆ๋‹ค. IT ๋ถ€์„œ์— ์ฃผ์†Œ๋ฅผ ๋ฌผ์–ด๋ณด์„ธ์š”." + +#: src/rdfeed:142 src/rdfeed:156 +msgid "User name (DOMAIN\\name or name@domain):" +msgstr "์‚ฌ์šฉ์ž ์ด๋ฆ„ (DOMAIN\\name ๋˜๋Š” name@domain):" + +#: src/rdfeed:146 src/rdfeed:159 +msgid "Password:" +msgstr "๋น„๋ฐ€๋ฒˆํ˜ธ:" + +#: src/rdfeed:178 src/rdfeed:236 +#, sh-printf-format +msgid "No workspace yet. Add one with `%s add`." +msgstr "์•„์ง ์ž‘์—… ๊ณต๊ฐ„์ด ์—†์Šต๋‹ˆ๋‹ค. `%s add`๋กœ ์ถ”๊ฐ€ํ•˜์„ธ์š”." + +#: src/rdfeed:187 src/lib/launch.sh:220 +#, sh-printf-format +msgid "No password saved for %s. Sign in again in rdfeed." +msgstr "%s์˜ ๋น„๋ฐ€๋ฒˆํ˜ธ๊ฐ€ ์ €์žฅ๋˜์–ด ์žˆ์ง€ ์•Š์Šต๋‹ˆ๋‹ค. rdfeed์—์„œ ๋‹ค์‹œ ๋กœ๊ทธ์ธํ•˜์„ธ์š”." + +#: src/rdfeed:191 src/lib/launch.sh:149 +#, sh-printf-format +msgid "Password for %s:" +msgstr "%s ๋น„๋ฐ€๋ฒˆํ˜ธ:" + +#: src/rdfeed:195 +#, sh-printf-format +msgid "Refreshing %sโ€ฆ" +msgstr "%s ์ƒˆ๋กœ ๊ณ ์น˜๋Š” ์ค‘โ€ฆ" + +#: src/rdfeed:205 +msgid "The password no longer works. Sign in again in rdfeed." +msgstr "๋น„๋ฐ€๋ฒˆํ˜ธ๊ฐ€ ๋” ์ด์ƒ ๋งž์ง€ ์•Š์Šต๋‹ˆ๋‹ค. rdfeed์—์„œ ๋‹ค์‹œ ๋กœ๊ทธ์ธํ•˜์„ธ์š”." + +#: src/rdfeed:214 +#, sh-printf-format +msgid "%s: %d apps" +msgstr "%s: ์•ฑ %d๊ฐœ" + +#: src/rdfeed:222 +#, sh-printf-format +msgid "Which workspace? See `%s list` for the names." +msgstr "์–ด๋А ์ž‘์—… ๊ณต๊ฐ„์ธ๊ฐ€์š”? ์ด๋ฆ„์€ `%s list`์—์„œ ๋ณผ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค." + +#: src/rdfeed:229 +#, sh-printf-format +msgid "%s and its apps are removed." +msgstr "%s์™€(๊ณผ) ํ•ด๋‹น ์•ฑ์„ ์‚ญ์ œํ–ˆ์Šต๋‹ˆ๋‹ค." + +#: src/rdfeed:253 +#, sh-printf-format +msgid "Which app? See `%s list`." +msgstr "์–ด๋А ์•ฑ์ธ๊ฐ€์š”? `%s list`๋ฅผ ๋ณด์„ธ์š”." + +#: src/rdfeed:268 src/lib/launch.sh:199 +#, sh-printf-format +msgid "There is no app %s. See `%s list`." +msgstr "%s ์•ฑ์ด ์—†์Šต๋‹ˆ๋‹ค. `%s list`๋ฅผ ๋ณด์„ธ์š”." + +#: src/rdfeed:270 +#, sh-printf-format +msgid "Several workspaces have %s. Write it as WORKSPACE/APP, see `%s list`." +msgstr "" +"์—ฌ๋Ÿฌ ์ž‘์—… ๊ณต๊ฐ„์— %s์ด(๊ฐ€) ์žˆ์Šต๋‹ˆ๋‹ค. ์ž‘์—…๊ณต๊ฐ„/์•ฑ ํ˜•์‹์œผ๋กœ ์“ฐ์„ธ์š”. `%s list`๋ฅผ " +"๋ณด์„ธ์š”." + +#: src/rdfeed:278 +msgid "Usage: rdfeed [command]" +msgstr "์‚ฌ์šฉ๋ฒ•: rdfeed [๋ช…๋ น]" + +#: src/rdfeed:280 +msgid "Commands:" +msgstr "๋ช…๋ น:" + +#: src/rdfeed:281 +msgid "Add the workspace of your company" +msgstr "ํšŒ์‚ฌ์˜ ์ž‘์—… ๊ณต๊ฐ„ ์ถ”๊ฐ€" + +#: src/rdfeed:282 +msgid "Load the apps again" +msgstr "์•ฑ ๋‹ค์‹œ ๋ถˆ๋Ÿฌ์˜ค๊ธฐ" + +#: src/rdfeed:283 +msgid "List the workspaces and their apps" +msgstr "์ž‘์—… ๊ณต๊ฐ„๊ณผ ์•ฑ ๋ชฉ๋ก ๋ณด๊ธฐ" + +#: src/rdfeed:284 +msgid "Start an app, or open a file with it" +msgstr "์•ฑ ์‹œ์ž‘ ๋˜๋Š” ํŒŒ์ผ ์—ด๊ธฐ" + +#: src/rdfeed:285 +msgid "Remove a workspace and its apps" +msgstr "์ž‘์—… ๊ณต๊ฐ„๊ณผ ์•ฑ ์‚ญ์ œ" + +#: src/rdfeed:286 +msgid "Show this help" +msgstr "์ด ๋„์›€๋ง ๋ณด๊ธฐ" + +#: src/rdfeed:287 +msgid "Show the version" +msgstr "๋ฒ„์ „ ๋ณด๊ธฐ" + +#: src/rdfeed:289 +msgid "Without a command an interactive menu is shown." +msgstr "๋ช…๋ น ์—†์ด ์‹คํ–‰ํ•˜๋ฉด ๋Œ€ํ™”ํ˜• ๋ฉ”๋‰ด๊ฐ€ ๋‚˜์˜ต๋‹ˆ๋‹ค." + +#: src/rdfeed:313 +#, sh-printf-format +msgid "Unknown command: %s" +msgstr "์•Œ ์ˆ˜ ์—†๋Š” ๋ช…๋ น: %s" + +#: src/lib/common.sh:174 src/lib/menu.sh:251 +msgid "never" +msgstr "์—†์Œ" + +#: src/lib/common.sh:184 +msgid "just now" +msgstr "๋ฐฉ๊ธˆ" + +#: src/lib/common.sh:186 +msgid "1 minute ago" +msgstr "1๋ถ„ ์ „" + +#: src/lib/common.sh:189 +#, sh-printf-format +msgid "%d minutes ago" +msgstr "%d๋ถ„ ์ „" + +#: src/lib/common.sh:191 +msgid "1 hour ago" +msgstr "1์‹œ๊ฐ„ ์ „" + +#: src/lib/common.sh:194 +#, sh-printf-format +msgid "%d hours ago" +msgstr "%d์‹œ๊ฐ„ ์ „" + +#: src/lib/common.sh:196 +msgid "1 day ago" +msgstr "1์ผ ์ „" + +#: src/lib/common.sh:199 +#, sh-printf-format +msgid "%d days ago" +msgstr "%d์ผ ์ „" + +#: src/lib/launch.sh:169 +msgid "" +"Your password has expired. Change it on Windows or on the RD Web page, then " +"sign in again in rdfeed." +msgstr "" +"๋น„๋ฐ€๋ฒˆํ˜ธ๊ฐ€ ๋งŒ๋ฃŒ๋˜์—ˆ์Šต๋‹ˆ๋‹ค. Windows๋‚˜ RD Web ํŽ˜์ด์ง€์—์„œ ๋ฐ”๊พผ ๋‹ค์Œ rdfeed์—์„œ " +"๋‹ค์‹œ ๋กœ๊ทธ์ธํ•˜์„ธ์š”." + +#: src/lib/launch.sh:171 +msgid "Your account is locked. Ask your IT department." +msgstr "๊ณ„์ •์ด ์ž ๊ฒผ์Šต๋‹ˆ๋‹ค. IT ๋ถ€์„œ์— ๋ฌธ์˜ํ•˜์„ธ์š”." + +#: src/lib/launch.sh:173 +msgid "The server did not accept the password. Sign in again in rdfeed." +msgstr "์„œ๋ฒ„๊ฐ€ ๋น„๋ฐ€๋ฒˆํ˜ธ๋ฅผ ๋ฐ›์•„๋“ค์ด์ง€ ์•Š์•˜์Šต๋‹ˆ๋‹ค. rdfeed์—์„œ ๋‹ค์‹œ ๋กœ๊ทธ์ธํ•˜์„ธ์š”." + +#: src/lib/launch.sh:175 +msgid "Could not reach the server. Check the internet connection." +msgstr "์„œ๋ฒ„์— ์—ฐ๊ฒฐํ•˜์ง€ ๋ชปํ–ˆ์Šต๋‹ˆ๋‹ค. ์ธํ„ฐ๋„ท ์—ฐ๊ฒฐ์„ ํ™•์ธํ•˜์„ธ์š”." + +#: src/lib/launch.sh:178 +#, sh-printf-format +msgid "FreeRDP stopped: %s" +msgstr "FreeRDP๊ฐ€ ๋ฉˆ์ท„์Šต๋‹ˆ๋‹ค: %s" + +#: src/lib/launch.sh:205 src/lib/launch.sh:206 +#, sh-printf-format +msgid "File not found: %s" +msgstr "ํŒŒ์ผ์„ ์ฐพ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค: %s" + +#: src/lib/launch.sh:212 src/lib/launch.sh:213 +msgid "" +"FreeRDP 3 is missing. Install it to start the apps (the package is called " +"freerdp)." +msgstr "" +"FreeRDP 3์ด ์—†์Šต๋‹ˆ๋‹ค. ์•ฑ์„ ์‹œ์ž‘ํ•˜๋ ค๋ฉด ์„ค์น˜ํ•˜์„ธ์š”(ํŒจํ‚ค์ง€ ์ด๋ฆ„์€ freerdp)." + +#: src/lib/launch.sh:277 +#, sh-printf-format +msgid "Opening %s in %s" +msgstr "%s ํŒŒ์ผ์„ %s(์œผ)๋กœ ์—ฌ๋Š” ์ค‘" + +#: src/lib/launch.sh:278 +#, sh-printf-format +msgid "If it does not open by itself, open it in the app from the drive %s." +msgstr "์ €์ ˆ๋กœ ์—ด๋ฆฌ์ง€ ์•Š์œผ๋ฉด ์•ฑ์—์„œ %s ๋“œ๋ผ์ด๋ธŒ์˜ ํŒŒ์ผ์„ ์—ฌ์„ธ์š”." + +#: src/lib/launch.sh:280 +#, sh-printf-format +msgid "Starting %s" +msgstr "%s ์‹œ์ž‘ํ•˜๋Š” ์ค‘" + +#: src/lib/launch.sh:281 +msgid "" +"If your company uses MFA, confirm the sign-in in your authenticator app." +msgstr "ํšŒ์‚ฌ์—์„œ MFA๋ฅผ ์“ด๋‹ค๋ฉด ์ธ์ฆ ์•ฑ์—์„œ ๋กœ๊ทธ์ธ์„ ์Šน์ธํ•˜์„ธ์š”." + +#: src/lib/launch.sh:290 +msgid "A second sign-in is coming" +msgstr "๋‘ ๋ฒˆ์งธ ๋กœ๊ทธ์ธ์ด ์˜ต๋‹ˆ๋‹ค" + +#: src/lib/launch.sh:291 +#, sh-printf-format +msgid "" +"Confirm the second prompt in your authenticator app too. It comes in about " +"%d seconds." +msgstr "์ธ์ฆ ์•ฑ์—์„œ ๋‘ ๋ฒˆ์งธ ์š”์ฒญ๋„ ์Šน์ธํ•˜์„ธ์š”. ์•ฝ %d์ดˆ ๋’ค์— ์˜ต๋‹ˆ๋‹ค." + +#: src/lib/launch.sh:301 +#, sh-printf-format +msgid "%s could not start" +msgstr "%s์„(๋ฅผ) ์‹œ์ž‘ํ•˜์ง€ ๋ชปํ–ˆ์Šต๋‹ˆ๋‹ค" + +#: src/lib/launch.sh:303 +#, sh-printf-format +msgid "Log: %s" +msgstr "๋กœ๊ทธ: %s" + +#. TRANSLATORS: the letter after "[" is the key for yes. y works too. +#: src/lib/menu.sh:93 +msgid "[y/N]" +msgstr "[y/N]" + +#: src/lib/menu.sh:163 +msgid "1 app" +msgstr "์•ฑ 1๊ฐœ" + +#: src/lib/menu.sh:165 +#, sh-printf-format +msgid "%d apps" +msgstr "์•ฑ %d๊ฐœ" + +#: src/lib/menu.sh:190 +msgid "No workspace yet. Add the one of your company with [1]." +msgstr "์•„์ง ์ž‘์—… ๊ณต๊ฐ„์ด ์—†์Šต๋‹ˆ๋‹ค. [1]๋กœ ํšŒ์‚ฌ์˜ ์ž‘์—… ๊ณต๊ฐ„์„ ์ถ”๊ฐ€ํ•˜์„ธ์š”." + +#: src/lib/menu.sh:233 +msgid "off" +msgstr "๊บผ์ง" + +#: src/lib/menu.sh:234 +msgid "home folder" +msgstr "ํ™ˆ ํด๋”" + +#: src/lib/menu.sh:237 +msgid "Documents" +msgstr "๋ฌธ์„œ" + +#: src/lib/menu.sh:238 +msgid "Downloads" +msgstr "๋‹ค์šด๋กœ๋“œ" + +#: src/lib/menu.sh:243 +msgid "not found" +msgstr "์ฐพ์„ ์ˆ˜ ์—†์Œ" + +#: src/lib/menu.sh:246 +#, sh-printf-format +msgid "automatic (%d %%)" +msgstr "์ž๋™ (%d%%)" + +#: src/lib/menu.sh:248 +msgid "classic" +msgstr "ํด๋ž˜์‹" + +#: src/lib/menu.sh:249 +msgid "GFX" +msgstr "GFX" + +#: src/lib/menu.sh:250 +msgid "always" +msgstr "ํ•ญ์ƒ" + +#: src/lib/menu.sh:252 +msgid "automatic" +msgstr "์ž๋™" + +#: src/lib/menu.sh:261 +msgid "" +"The apps only see the files on the server. Pick a folder to open and save " +"files of this computer in them." +msgstr "" +"์•ฑ์€ ์„œ๋ฒ„์˜ ํŒŒ์ผ๋งŒ ๋ด…๋‹ˆ๋‹ค. ํด๋”๋ฅผ ๊ณ ๋ฅด๋ฉด ์ด ์ปดํ“จํ„ฐ์˜ ํŒŒ์ผ์„ ์—ด๊ณ  ์ €์žฅํ•  ์ˆ˜ ์žˆ" +"์Šต๋‹ˆ๋‹ค." + +#: src/lib/menu.sh:262 +msgid "" +"The apps can open and save files in this folder. It shows up as a drive in " +"them." +msgstr "" +"์•ฑ์ด ์ด ํด๋”์˜ ํŒŒ์ผ์„ ์—ด๊ณ  ์ €์žฅํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ์•ฑ์—์„œ๋Š” ๋“œ๋ผ์ด๋ธŒ๋กœ ๋ณด์ž…๋‹ˆ๋‹ค." + +#: src/lib/menu.sh:263 +msgid "" +"Offers the apps under \"Open with\" for their file types, for example Word " +"for .docx. The folder of the file is shared with the app while it is open." +msgstr "" +"ํŒŒ์ผ ํ˜•์‹์— ๋งž๊ฒŒ \"์—ฐ๊ฒฐ ํ”„๋กœ๊ทธ๋žจ\"์— ์•ฑ์„ ํ‘œ์‹œํ•ฉ๋‹ˆ๋‹ค(์˜ˆ: .docx๋Š” Word). ํŒŒ์ผ" +"์ด ์—ด๋ ค ์žˆ๋Š” ๋™์•ˆ ํ•ด๋‹น ํด๋”๊ฐ€ ๊ณต์œ ๋ฉ๋‹ˆ๋‹ค." + +#: src/lib/menu.sh:264 +msgid "" +"How big the apps are drawn. Automatic follows the scale of your desktop, so " +"they are sharp and the right size." +msgstr "" +"์•ฑ์„ ๊ทธ๋ฆฌ๋Š” ํฌ๊ธฐ์ž…๋‹ˆ๋‹ค. ์ž๋™์€ ๋ฐ์Šคํฌํ†ฑ ๋ฐฐ์œจ์„ ๋”ฐ๋ฅด๋ฏ€๋กœ ์„ ๋ช…ํ•˜๊ณ  ์•Œ๋งž์€ ํฌ๊ธฐ" +"๋กœ ๋ณด์ž…๋‹ˆ๋‹ค." + +#: src/lib/menu.sh:265 +msgid "Plays the sound of the apps here, for calls and videos." +msgstr "์•ฑ์˜ ์†Œ๋ฆฌ๋ฅผ ์ด ์ปดํ“จํ„ฐ์—์„œ ์žฌ์ƒํ•ฉ๋‹ˆ๋‹ค. ํ†ตํ™”์™€ ๋™์˜์ƒ์— ํ•„์š”ํ•ฉ๋‹ˆ๋‹ค." + +#: src/lib/menu.sh:266 +msgid "" +"Paints every window right. But some servers end the session right after " +"signing in, then go back to GFX." +msgstr "๋ชจ๋“  ์ฐฝ์„ ์˜ฌ๋ฐ”๋ฅด๊ฒŒ ๊ทธ๋ฆฝ๋‹ˆ๋‹ค. ํ•˜์ง€๋งŒ ๋กœ๊ทธ์ธ ์งํ›„ ์„ธ์…˜์„ ๋Š๋Š” ์„œ๋ฒ„๋„ ์žˆ์œผ๋‹ˆ ๊ทธ๋•Œ๋Š” GFX๋กœ ๋Œ์•„๊ฐ€์„ธ์š”." + +#: src/lib/menu.sh:267 +msgid "" +"Works with every server. FreeRDP may not repaint a window after it was " +"minimized or resized." +msgstr "๋ชจ๋“  ์„œ๋ฒ„์—์„œ ๋ฉ๋‹ˆ๋‹ค. ์ตœ์†Œํ™”ํ•˜๊ฑฐ๋‚˜ ํฌ๊ธฐ๋ฅผ ๋ฐ”๊พผ ์ฐฝ์„ FreeRDP๊ฐ€ ๋‹ค์‹œ ๊ทธ๋ฆฌ์ง€ ์•Š์„ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค." + +#: src/lib/menu.sh:268 +msgid "" +"Always waits before the second sign-in. For when the second prompt does not " +"come with automatic." +msgstr "" +"๋‘ ๋ฒˆ์งธ ๋กœ๊ทธ์ธ ์ „์— ํ•ญ์ƒ ๊ธฐ๋‹ค๋ฆฝ๋‹ˆ๋‹ค. ์ž๋™์ผ ๋•Œ ๋‘ ๋ฒˆ์งธ ์š”์ฒญ์ด ์˜ค์ง€ ์•Š์œผ๋ฉด ์“ฐ" +"์„ธ์š”." + +#: src/lib/menu.sh:269 +msgid "Never waits. Right when your company has no MFA." +msgstr "๊ธฐ๋‹ค๋ฆฌ์ง€ ์•Š์Šต๋‹ˆ๋‹ค. ํšŒ์‚ฌ์—์„œ MFA๋ฅผ ์“ฐ์ง€ ์•Š์„ ๋•Œ ์•Œ๋งž์Šต๋‹ˆ๋‹ค." + +#: src/lib/menu.sh:270 +msgid "" +"Many servers with MFA need a second sign-in, and its prompt only comes after " +"a short wait. Automatic waits when the first sign-in took a while." +msgstr "" +"MFA๋ฅผ ์“ฐ๋Š” ๋งŽ์€ ์„œ๋ฒ„๋Š” ๋‘ ๋ฒˆ์งธ ๋กœ๊ทธ์ธ์ด ํ•„์š”ํ•˜๊ณ , ๊ทธ ์š”์ฒญ์€ ์ž ์‹œ ๊ธฐ๋‹ค๋ฆฐ ๋’ค์—" +"์•ผ ์˜ต๋‹ˆ๋‹ค. ์ž๋™์€ ์ฒซ ๋กœ๊ทธ์ธ์ด ์˜ค๋ž˜ ๊ฑธ๋ ธ์„ ๋•Œ ๊ธฐ๋‹ค๋ฆฝ๋‹ˆ๋‹ค." + +#: src/lib/menu.sh:273 +msgid "" +"Loads the list of apps again once a day, so new apps show up by themselves." +msgstr "ํ•˜๋ฃจ์— ํ•œ ๋ฒˆ ์•ฑ ๋ชฉ๋ก์„ ๋‹ค์‹œ ๋ถˆ๋Ÿฌ์˜ค๋ฏ€๋กœ ์ƒˆ ์•ฑ์ด ์ €์ ˆ๋กœ ๋‚˜ํƒ€๋‚ฉ๋‹ˆ๋‹ค." + +#: src/lib/menu.sh:275 src/lib/menu.sh:330 +msgid "Needs a systemd user session." +msgstr "systemd ์‚ฌ์šฉ์ž ์„ธ์…˜์ด ํ•„์š”ํ•ฉ๋‹ˆ๋‹ค." + +#: src/lib/menu.sh:322 src/lib/menu.sh:325 +msgid "Could not save the setting." +msgstr "์„ค์ •์„ ์ €์žฅํ•˜์ง€ ๋ชปํ–ˆ์Šต๋‹ˆ๋‹ค." + +#: src/lib/menu.sh:371 src/lib/menu.sh:609 +msgid "Settings" +msgstr "์„ค์ •" + +#: src/lib/menu.sh:372 +msgid "โ†‘โ†“ select โ†โ†’ or Space: change q: back" +msgstr "โ†‘โ†“ ์„ ํƒ โ†โ†’ ๋˜๋Š” ์ŠคํŽ˜์ด์Šค: ๋ณ€๊ฒฝ q: ๋’ค๋กœ" + +#: src/lib/menu.sh:373 +msgid "ON" +msgstr "์ผœ์ง" + +#: src/lib/menu.sh:374 +msgid "OFF" +msgstr "๊บผ์ง" + +#: src/lib/menu.sh:456 src/lib/menu.sh:608 +msgid "Workspaces" +msgstr "์ž‘์—… ๊ณต๊ฐ„" + +#. TRANSLATORS: keep the letters, they are the keys. +#: src/lib/menu.sh:458 +msgid "" +"โ†‘โ†“ select r: refresh s: sign in again d: remove a: add q: back" +msgstr "โ†‘โ†“ ์„ ํƒ r: ์ƒˆ๋กœ ๊ณ ์นจ s: ๋‹ค์‹œ ๋กœ๊ทธ์ธ d: ์‚ญ์ œ a: ์ถ”๊ฐ€ q: ๋’ค๋กœ" + +#: src/lib/menu.sh:459 +msgid "No workspace yet. Press a to add one." +msgstr "์•„์ง ์ž‘์—… ๊ณต๊ฐ„์ด ์—†์Šต๋‹ˆ๋‹ค. a๋ฅผ ๋ˆŒ๋Ÿฌ ์ถ”๊ฐ€ํ•˜์„ธ์š”." + +#: src/lib/menu.sh:484 +#, sh-printf-format +msgid "refreshed %s" +msgstr "%s ์ƒˆ๋กœ ๊ณ ์นจ" + +#: src/lib/menu.sh:512 +#, sh-printf-format +msgid "Remove \"%s\" and its apps?" +msgstr "\"%s\"์™€(๊ณผ) ํ•ด๋‹น ์•ฑ์„ ์‚ญ์ œํ• ๊นŒ์š”?" + +#: src/lib/menu.sh:536 src/lib/menu.sh:607 +msgid "Start an app" +msgstr "์•ฑ ์‹œ์ž‘" + +#: src/lib/menu.sh:537 +msgid "โ†‘โ†“ select Enter: start q: back" +msgstr "โ†‘โ†“ ์„ ํƒ Enter: ์‹œ์ž‘ q: ๋’ค๋กœ" + +#: src/lib/menu.sh:538 +msgid "No apps yet. Add a workspace first." +msgstr "์•„์ง ์•ฑ์ด ์—†์Šต๋‹ˆ๋‹ค. ๋จผ์ € ์ž‘์—… ๊ณต๊ฐ„์„ ์ถ”๊ฐ€ํ•˜์„ธ์š”." + +#: src/lib/menu.sh:579 +#, sh-printf-format +msgid "%s is starting. It opens in a window of its own." +msgstr "%s์„(๋ฅผ) ์‹œ์ž‘ํ•˜๋Š” ์ค‘์ž…๋‹ˆ๋‹ค. ๋ณ„๋„ ์ฐฝ์œผ๋กœ ์—ด๋ฆฝ๋‹ˆ๋‹ค." + +#: src/lib/menu.sh:606 +msgid "Add a workspace" +msgstr "์ž‘์—… ๊ณต๊ฐ„ ์ถ”๊ฐ€" + +#: src/lib/menu.sh:610 +msgid "Quit" +msgstr "๋๋‚ด๊ธฐ" + +#: src/lib/workspace.sh:163 +msgid "The server did not accept the user name or password." +msgstr "์„œ๋ฒ„๊ฐ€ ์‚ฌ์šฉ์ž ์ด๋ฆ„์ด๋‚˜ ๋น„๋ฐ€๋ฒˆํ˜ธ๋ฅผ ๋ฐ›์•„๋“ค์ด์ง€ ์•Š์•˜์Šต๋‹ˆ๋‹ค." + +#: src/lib/workspace.sh:164 +#, sh-printf-format +msgid "Could not reach the server: %s" +msgstr "์„œ๋ฒ„์— ์—ฐ๊ฒฐํ•˜์ง€ ๋ชปํ–ˆ์Šต๋‹ˆ๋‹ค: %s" + +#: src/lib/workspace.sh:165 +#, sh-printf-format +msgid "There is no RD Web feed at %s." +msgstr "%s์— RD Web ํ”ผ๋“œ๊ฐ€ ์—†์Šต๋‹ˆ๋‹ค." + +#: src/lib/workspace.sh:166 +#, sh-printf-format +msgid "Could not load the apps: %s" +msgstr "์•ฑ์„ ๋ถˆ๋Ÿฌ์˜ค์ง€ ๋ชปํ–ˆ์Šต๋‹ˆ๋‹ค: %s" + +#: src/lib/workspace.sh:282 +#, sh-printf-format +msgid "Remote desktop of %s" +msgstr "%s ์›๊ฒฉ ๋ฐ์Šคํฌํ†ฑ" + +#: src/lib/workspace.sh:284 +#, sh-printf-format +msgid "%s from %s" +msgstr "%s (%s)" + +#: src/lib/menu.sh +msgid "Apps" +msgstr "์•ฑ" + +#: src/lib/menu.sh +msgid "Share a folder" +msgstr "ํด๋” ๊ณต์œ " + +#: src/lib/menu.sh +msgid "Open files with the apps" +msgstr "์•ฑ์œผ๋กœ ํŒŒ์ผ ์—ด๊ธฐ" + +#: src/lib/menu.sh +msgid "Size" +msgstr "ํฌ๊ธฐ" + +#: src/lib/menu.sh +msgid "Sound" +msgstr "์†Œ๋ฆฌ" + +#: src/lib/menu.sh +msgid "Graphics" +msgstr "๊ทธ๋ž˜ํ”ฝ" + +#: src/lib/menu.sh +msgid "Signing in" +msgstr "๋กœ๊ทธ์ธ" + +#: src/lib/menu.sh +msgid "Wait for the second MFA prompt" +msgstr "๋‘ ๋ฒˆ์งธ MFA ์š”์ฒญ ๊ธฐ๋‹ค๋ฆฌ๊ธฐ" + +#: src/lib/menu.sh +msgid "Refresh the apps every day" +msgstr "๋งค์ผ ์•ฑ ์ƒˆ๋กœ ๊ณ ์นจ" diff --git a/po/nl.po b/po/nl.po new file mode 100644 index 0000000..8762785 --- /dev/null +++ b/po/nl.po @@ -0,0 +1,562 @@ +# Dutch translation for rdfeed. +# Copyright (C) 2026 Felitendo +# This file is distributed under the same license as rdfeed. +# +msgid "" +msgstr "" +"Project-Id-Version: rdfeed 1.0.0\n" +"Report-Msgid-Bugs-To: https://github.com/LoonixTools/rdfeed/issues\n" +"POT-Creation-Date: 2026-10-02 10:29+0200\n" +"PO-Revision-Date: 2026-10-02 09:30+0200\n" +"Last-Translator: Felitendo\n" +"Language-Team: Dutch\n" +"Language: nl\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" + +#: src/rdfeed:79 src/rdfeed:103 +#, sh-printf-format +msgid "Could not write to %s." +msgstr "Kon niet schrijven naar %s." + +#: src/rdfeed:82 +#, sh-printf-format +msgid "Signing in to %sโ€ฆ" +msgstr "Aanmelden bij %sโ€ฆ" + +#: src/rdfeed:100 +msgid "" +"The password could not be saved in the keyring, so each app asks for it." +msgstr "" +"Het wachtwoord kon niet in de sleutelbos worden opgeslagen, dus elke app " +"vraagt erom." + +#: src/rdfeed:111 +#, sh-printf-format +msgid "1 app from %s is now in your app menu." +msgstr "1 app van %s staat nu in je app-menu." + +#: src/rdfeed:113 +#, sh-printf-format +msgid "%d apps from %s are now in your app menu." +msgstr "%d apps van %s staan nu in je app-menu." + +#: src/rdfeed:129 +msgid "" +"Your IT department has the address. A work e-mail address often works too." +msgstr "Je IT-afdeling heeft het adres. Vaak werkt je werk-e-mailadres ook." + +#: src/rdfeed:130 +msgid "Feed address, server or e-mail:" +msgstr "Feedadres, server of e-mail:" + +#: src/rdfeed:137 +#, sh-printf-format +msgid "No feed is registered for %s. Ask your IT department for the address." +msgstr "" +"Er is geen feed geregistreerd voor %s. Vraag je IT-afdeling om het adres." + +#: src/rdfeed:142 src/rdfeed:156 +msgid "User name (DOMAIN\\name or name@domain):" +msgstr "Gebruikersnaam (DOMEIN\\naam of naam@domein):" + +#: src/rdfeed:146 src/rdfeed:159 +msgid "Password:" +msgstr "Wachtwoord:" + +#: src/rdfeed:178 src/rdfeed:236 +#, sh-printf-format +msgid "No workspace yet. Add one with `%s add`." +msgstr "Nog geen werkruimte. Voeg er een toe met `%s add`." + +#: src/rdfeed:187 src/lib/launch.sh:220 +#, sh-printf-format +msgid "No password saved for %s. Sign in again in rdfeed." +msgstr "Geen wachtwoord opgeslagen voor %s. Meld je opnieuw aan in rdfeed." + +#: src/rdfeed:191 src/lib/launch.sh:149 +#, sh-printf-format +msgid "Password for %s:" +msgstr "Wachtwoord voor %s:" + +#: src/rdfeed:195 +#, sh-printf-format +msgid "Refreshing %sโ€ฆ" +msgstr "%s vernieuwenโ€ฆ" + +#: src/rdfeed:205 +msgid "The password no longer works. Sign in again in rdfeed." +msgstr "Het wachtwoord werkt niet meer. Meld je opnieuw aan in rdfeed." + +#: src/rdfeed:214 +#, sh-printf-format +msgid "%s: %d apps" +msgstr "%s: %d apps" + +#: src/rdfeed:222 +#, sh-printf-format +msgid "Which workspace? See `%s list` for the names." +msgstr "Welke werkruimte? De namen staan in `%s list`." + +#: src/rdfeed:229 +#, sh-printf-format +msgid "%s and its apps are removed." +msgstr "%s en de apps ervan zijn verwijderd." + +#: src/rdfeed:253 +#, sh-printf-format +msgid "Which app? See `%s list`." +msgstr "Welke app? Zie `%s list`." + +#: src/rdfeed:268 src/lib/launch.sh:199 +#, sh-printf-format +msgid "There is no app %s. See `%s list`." +msgstr "Er is geen app %s. Zie `%s list`." + +#: src/rdfeed:270 +#, sh-printf-format +msgid "Several workspaces have %s. Write it as WORKSPACE/APP, see `%s list`." +msgstr "" +"Meerdere werkruimtes hebben %s. Schrijf het als WERKRUIMTE/APP, zie `%s " +"list`." + +#: src/rdfeed:278 +msgid "Usage: rdfeed [command]" +msgstr "Gebruik: rdfeed [opdracht]" + +#: src/rdfeed:280 +msgid "Commands:" +msgstr "Opdrachten:" + +#: src/rdfeed:281 +msgid "Add the workspace of your company" +msgstr "De werkruimte van je bedrijf toevoegen" + +#: src/rdfeed:282 +msgid "Load the apps again" +msgstr "De apps opnieuw laden" + +#: src/rdfeed:283 +msgid "List the workspaces and their apps" +msgstr "Werkruimtes en hun apps tonen" + +#: src/rdfeed:284 +msgid "Start an app, or open a file with it" +msgstr "Een app starten, of er een bestand mee openen" + +#: src/rdfeed:285 +msgid "Remove a workspace and its apps" +msgstr "Een werkruimte en de apps ervan verwijderen" + +#: src/rdfeed:286 +msgid "Show this help" +msgstr "Deze hulp tonen" + +#: src/rdfeed:287 +msgid "Show the version" +msgstr "De versie tonen" + +#: src/rdfeed:289 +msgid "Without a command an interactive menu is shown." +msgstr "Zonder opdracht verschijnt een interactief menu." + +#: src/rdfeed:313 +#, sh-printf-format +msgid "Unknown command: %s" +msgstr "Onbekende opdracht: %s" + +#: src/lib/common.sh:174 src/lib/menu.sh:251 +msgid "never" +msgstr "nooit" + +#: src/lib/common.sh:184 +msgid "just now" +msgstr "zojuist" + +#: src/lib/common.sh:186 +msgid "1 minute ago" +msgstr "1 minuut geleden" + +#: src/lib/common.sh:189 +#, sh-printf-format +msgid "%d minutes ago" +msgstr "%d minuten geleden" + +#: src/lib/common.sh:191 +msgid "1 hour ago" +msgstr "1 uur geleden" + +#: src/lib/common.sh:194 +#, sh-printf-format +msgid "%d hours ago" +msgstr "%d uur geleden" + +#: src/lib/common.sh:196 +msgid "1 day ago" +msgstr "1 dag geleden" + +#: src/lib/common.sh:199 +#, sh-printf-format +msgid "%d days ago" +msgstr "%d dagen geleden" + +#: src/lib/launch.sh:169 +msgid "" +"Your password has expired. Change it on Windows or on the RD Web page, then " +"sign in again in rdfeed." +msgstr "" +"Je wachtwoord is verlopen. Wijzig het in Windows of op de RD Web-pagina en " +"meld je daarna opnieuw aan in rdfeed." + +#: src/lib/launch.sh:171 +msgid "Your account is locked. Ask your IT department." +msgstr "Je account is geblokkeerd. Vraag het je IT-afdeling." + +#: src/lib/launch.sh:173 +msgid "The server did not accept the password. Sign in again in rdfeed." +msgstr "" +"De server heeft het wachtwoord niet geaccepteerd. Meld je opnieuw aan in " +"rdfeed." + +#: src/lib/launch.sh:175 +msgid "Could not reach the server. Check the internet connection." +msgstr "Kon de server niet bereiken. Controleer de internetverbinding." + +#: src/lib/launch.sh:178 +#, sh-printf-format +msgid "FreeRDP stopped: %s" +msgstr "FreeRDP is gestopt: %s" + +#: src/lib/launch.sh:205 src/lib/launch.sh:206 +#, sh-printf-format +msgid "File not found: %s" +msgstr "Bestand niet gevonden: %s" + +#: src/lib/launch.sh:212 src/lib/launch.sh:213 +msgid "" +"FreeRDP 3 is missing. Install it to start the apps (the package is called " +"freerdp)." +msgstr "" +"FreeRDP 3 ontbreekt. Installeer het om de apps te starten (het pakket heet " +"freerdp)." + +#: src/lib/launch.sh:277 +#, sh-printf-format +msgid "Opening %s in %s" +msgstr "%s wordt geopend in %s" + +#: src/lib/launch.sh:278 +#, sh-printf-format +msgid "If it does not open by itself, open it in the app from the drive %s." +msgstr "" +"Gaat het niet vanzelf open, open het dan in de app vanaf het station %s." + +#: src/lib/launch.sh:280 +#, sh-printf-format +msgid "Starting %s" +msgstr "%s wordt gestart" + +#: src/lib/launch.sh:281 +msgid "" +"If your company uses MFA, confirm the sign-in in your authenticator app." +msgstr "" +"Gebruikt je bedrijf MFA, bevestig de aanmelding dan in je authenticator-app." + +#: src/lib/launch.sh:290 +msgid "A second sign-in is coming" +msgstr "Er komt een tweede aanmelding" + +#: src/lib/launch.sh:291 +#, sh-printf-format +msgid "" +"Confirm the second prompt in your authenticator app too. It comes in about " +"%d seconds." +msgstr "" +"Bevestig ook het tweede verzoek in je authenticator-app. Het komt over " +"ongeveer %d seconden." + +#: src/lib/launch.sh:301 +#, sh-printf-format +msgid "%s could not start" +msgstr "%s kon niet starten" + +#: src/lib/launch.sh:303 +#, sh-printf-format +msgid "Log: %s" +msgstr "Logboek: %s" + +#. TRANSLATORS: the letter after "[" is the key for yes. y works too. +#: src/lib/menu.sh:93 +msgid "[y/N]" +msgstr "[j/N]" + +#: src/lib/menu.sh:163 +msgid "1 app" +msgstr "1 app" + +#: src/lib/menu.sh:165 +#, sh-printf-format +msgid "%d apps" +msgstr "%d apps" + +#: src/lib/menu.sh:190 +msgid "No workspace yet. Add the one of your company with [1]." +msgstr "Nog geen werkruimte. Voeg die van je bedrijf toe met [1]." + +#: src/lib/menu.sh:233 +msgid "off" +msgstr "uit" + +#: src/lib/menu.sh:234 +msgid "home folder" +msgstr "thuismap" + +#: src/lib/menu.sh:237 +msgid "Documents" +msgstr "Documenten" + +#: src/lib/menu.sh:238 +msgid "Downloads" +msgstr "Downloads" + +#: src/lib/menu.sh:243 +msgid "not found" +msgstr "niet gevonden" + +#: src/lib/menu.sh:246 +#, sh-printf-format +msgid "automatic (%d %%)" +msgstr "automatisch (%d %%)" + +#: src/lib/menu.sh:248 +msgid "classic" +msgstr "klassiek" + +#: src/lib/menu.sh:249 +msgid "GFX" +msgstr "GFX" + +#: src/lib/menu.sh:250 +msgid "always" +msgstr "altijd" + +#: src/lib/menu.sh:252 +msgid "automatic" +msgstr "automatisch" + +#: src/lib/menu.sh:261 +msgid "" +"The apps only see the files on the server. Pick a folder to open and save " +"files of this computer in them." +msgstr "" +"De apps zien alleen de bestanden op de server. Kies een map, dan kunnen ze " +"bestanden van deze computer openen en opslaan." + +#: src/lib/menu.sh:262 +msgid "" +"The apps can open and save files in this folder. It shows up as a drive in " +"them." +msgstr "" +"De apps kunnen in deze map bestanden openen en opslaan. Hij verschijnt " +"daarin als station." + +#: src/lib/menu.sh:263 +msgid "" +"Offers the apps under \"Open with\" for their file types, for example Word " +"for .docx. The folder of the file is shared with the app while it is open." +msgstr "" +"Biedt de apps aan onder \"Openen met\" voor hun bestandstypen, bijvoorbeeld " +"Word voor .docx. De map van het bestand wordt gedeeld zolang het open is." + +#: src/lib/menu.sh:264 +msgid "" +"How big the apps are drawn. Automatic follows the scale of your desktop, so " +"they are sharp and the right size." +msgstr "" +"Hoe groot de apps worden getekend. Automatisch volgt de schaal van je " +"bureaublad, zo zijn ze scherp en goed van formaat." + +#: src/lib/menu.sh:265 +msgid "Plays the sound of the apps here, for calls and videos." +msgstr "Speelt het geluid van de apps hier af, voor gesprekken en video's." + +#: src/lib/menu.sh:266 +msgid "" +"Paints every window right. But some servers end the session right after " +"signing in, then go back to GFX." +msgstr "Tekent elk venster goed. Maar sommige servers beรซindigen de sessie direct na het aanmelden; ga dan terug naar GFX." + +#: src/lib/menu.sh:267 +msgid "" +"Works with every server. FreeRDP may not repaint a window after it was " +"minimized or resized." +msgstr "Werkt met elke server. FreeRDP tekent een venster na minimaliseren of van formaat wijzigen misschien niet opnieuw." + +#: src/lib/menu.sh:268 +msgid "" +"Always waits before the second sign-in. For when the second prompt does not " +"come with automatic." +msgstr "" +"Wacht altijd voor de tweede aanmelding. Voor als het tweede verzoek met " +"automatisch niet komt." + +#: src/lib/menu.sh:269 +msgid "Never waits. Right when your company has no MFA." +msgstr "Wacht nooit. Goed als je bedrijf geen MFA gebruikt." + +#: src/lib/menu.sh:270 +msgid "" +"Many servers with MFA need a second sign-in, and its prompt only comes after " +"a short wait. Automatic waits when the first sign-in took a while." +msgstr "" +"Veel servers met MFA vragen een tweede aanmelding, en het verzoek daarvoor " +"komt pas na even wachten. Automatisch wacht als de eerste aanmelding even " +"duurde." + +#: src/lib/menu.sh:273 +msgid "" +"Loads the list of apps again once a day, so new apps show up by themselves." +msgstr "" +"Laadt de lijst met apps รฉรฉn keer per dag opnieuw, zo verschijnen nieuwe apps " +"vanzelf." + +#: src/lib/menu.sh:275 src/lib/menu.sh:330 +msgid "Needs a systemd user session." +msgstr "Vereist een systemd-gebruikerssessie." + +#: src/lib/menu.sh:322 src/lib/menu.sh:325 +msgid "Could not save the setting." +msgstr "Kon de instelling niet opslaan." + +#: src/lib/menu.sh:371 src/lib/menu.sh:609 +msgid "Settings" +msgstr "Instellingen" + +#: src/lib/menu.sh:372 +msgid "โ†‘โ†“ select โ†โ†’ or Space: change q: back" +msgstr "โ†‘โ†“ kiezen โ†โ†’ of Spatie: wijzigen q: terug" + +#: src/lib/menu.sh:373 +msgid "ON" +msgstr "AAN" + +#: src/lib/menu.sh:374 +msgid "OFF" +msgstr "UIT" + +#: src/lib/menu.sh:456 src/lib/menu.sh:608 +msgid "Workspaces" +msgstr "Werkruimtes" + +#. TRANSLATORS: keep the letters, they are the keys. +#: src/lib/menu.sh:458 +msgid "" +"โ†‘โ†“ select r: refresh s: sign in again d: remove a: add q: back" +msgstr "" +"โ†‘โ†“ kiezen r: vernieuwen s: opnieuw aanmelden d: verwijderen a: " +"toevoegen q: terug" + +#: src/lib/menu.sh:459 +msgid "No workspace yet. Press a to add one." +msgstr "Nog geen werkruimte. Druk op a om er een toe te voegen." + +#: src/lib/menu.sh:484 +#, sh-printf-format +msgid "refreshed %s" +msgstr "vernieuwd %s" + +#: src/lib/menu.sh:512 +#, sh-printf-format +msgid "Remove \"%s\" and its apps?" +msgstr "\"%s\" en de apps ervan verwijderen?" + +#: src/lib/menu.sh:536 src/lib/menu.sh:607 +msgid "Start an app" +msgstr "Een app starten" + +#: src/lib/menu.sh:537 +msgid "โ†‘โ†“ select Enter: start q: back" +msgstr "โ†‘โ†“ kiezen Enter: starten q: terug" + +#: src/lib/menu.sh:538 +msgid "No apps yet. Add a workspace first." +msgstr "Nog geen apps. Voeg eerst een werkruimte toe." + +#: src/lib/menu.sh:579 +#, sh-printf-format +msgid "%s is starting. It opens in a window of its own." +msgstr "%s wordt gestart. Het opent in een eigen venster." + +#: src/lib/menu.sh:606 +msgid "Add a workspace" +msgstr "Werkruimte toevoegen" + +#: src/lib/menu.sh:610 +msgid "Quit" +msgstr "Afsluiten" + +#: src/lib/workspace.sh:163 +msgid "The server did not accept the user name or password." +msgstr "De server heeft de gebruikersnaam of het wachtwoord niet geaccepteerd." + +#: src/lib/workspace.sh:164 +#, sh-printf-format +msgid "Could not reach the server: %s" +msgstr "Kon de server niet bereiken: %s" + +#: src/lib/workspace.sh:165 +#, sh-printf-format +msgid "There is no RD Web feed at %s." +msgstr "Er is geen RD Web-feed op %s." + +#: src/lib/workspace.sh:166 +#, sh-printf-format +msgid "Could not load the apps: %s" +msgstr "Kon de apps niet laden: %s" + +#: src/lib/workspace.sh:282 +#, sh-printf-format +msgid "Remote desktop of %s" +msgstr "Extern bureaublad van %s" + +#: src/lib/workspace.sh:284 +#, sh-printf-format +msgid "%s from %s" +msgstr "%s van %s" + +#: src/lib/menu.sh +msgid "Apps" +msgstr "Apps" + +#: src/lib/menu.sh +msgid "Share a folder" +msgstr "Map delen" + +#: src/lib/menu.sh +msgid "Open files with the apps" +msgstr "Bestanden openen met de apps" + +#: src/lib/menu.sh +msgid "Size" +msgstr "Grootte" + +#: src/lib/menu.sh +msgid "Sound" +msgstr "Geluid" + +#: src/lib/menu.sh +msgid "Graphics" +msgstr "Weergave" + +#: src/lib/menu.sh +msgid "Signing in" +msgstr "Aanmelden" + +#: src/lib/menu.sh +msgid "Wait for the second MFA prompt" +msgstr "Wachten op tweede MFA-verzoek" + +#: src/lib/menu.sh +msgid "Refresh the apps every day" +msgstr "Apps elke dag vernieuwen" diff --git a/po/pl.po b/po/pl.po new file mode 100644 index 0000000..965f2d3 --- /dev/null +++ b/po/pl.po @@ -0,0 +1,555 @@ +# Polish translation for rdfeed. +# Copyright (C) 2026 Felitendo +# This file is distributed under the same license as rdfeed. +# +msgid "" +msgstr "" +"Project-Id-Version: rdfeed 1.0.0\n" +"Report-Msgid-Bugs-To: https://github.com/LoonixTools/rdfeed/issues\n" +"POT-Creation-Date: 2026-10-02 10:29+0200\n" +"PO-Revision-Date: 2026-10-02 09:30+0200\n" +"Last-Translator: Felitendo\n" +"Language-Team: Polish\n" +"Language: pl\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" + +#: src/rdfeed:79 src/rdfeed:103 +#, sh-printf-format +msgid "Could not write to %s." +msgstr "Nie udaล‚o siฤ™ zapisaฤ‡ w %s." + +#: src/rdfeed:82 +#, sh-printf-format +msgid "Signing in to %sโ€ฆ" +msgstr "Logowanie do %sโ€ฆ" + +#: src/rdfeed:100 +msgid "" +"The password could not be saved in the keyring, so each app asks for it." +msgstr "" +"Nie udaล‚o siฤ™ zapisaฤ‡ hasล‚a w pฤ™ku kluczy, wiฤ™c kaลผda aplikacja o nie zapyta." + +#: src/rdfeed:111 +#, sh-printf-format +msgid "1 app from %s is now in your app menu." +msgstr "1 aplikacja z %s jest teraz w menu aplikacji." + +#: src/rdfeed:113 +#, sh-printf-format +msgid "%d apps from %s are now in your app menu." +msgstr "Dodano aplikacje (%d) z %s do menu aplikacji." + +#: src/rdfeed:129 +msgid "" +"Your IT department has the address. A work e-mail address often works too." +msgstr "Adres ma twรณj dziaล‚ IT. Czฤ™sto dziaล‚a teลผ sล‚uลผbowy adres e-mail." + +#: src/rdfeed:130 +msgid "Feed address, server or e-mail:" +msgstr "Adres kanaล‚u, serwer lub e-mail:" + +#: src/rdfeed:137 +#, sh-printf-format +msgid "No feed is registered for %s. Ask your IT department for the address." +msgstr "Dla %s nie zarejestrowano kanaล‚u. Zapytaj dziaล‚ IT o adres." + +#: src/rdfeed:142 src/rdfeed:156 +msgid "User name (DOMAIN\\name or name@domain):" +msgstr "Nazwa uลผytkownika (DOMENA\\nazwa lub nazwa@domena):" + +#: src/rdfeed:146 src/rdfeed:159 +msgid "Password:" +msgstr "Hasล‚o:" + +#: src/rdfeed:178 src/rdfeed:236 +#, sh-printf-format +msgid "No workspace yet. Add one with `%s add`." +msgstr "Nie ma jeszcze obszaru roboczego. Dodaj go poleceniem `%s add`." + +#: src/rdfeed:187 src/lib/launch.sh:220 +#, sh-printf-format +msgid "No password saved for %s. Sign in again in rdfeed." +msgstr "Brak zapisanego hasล‚a dla %s. Zaloguj siฤ™ ponownie w rdfeed." + +#: src/rdfeed:191 src/lib/launch.sh:149 +#, sh-printf-format +msgid "Password for %s:" +msgstr "Hasล‚o dla %s:" + +#: src/rdfeed:195 +#, sh-printf-format +msgid "Refreshing %sโ€ฆ" +msgstr "Odล›wieลผanie %sโ€ฆ" + +#: src/rdfeed:205 +msgid "The password no longer works. Sign in again in rdfeed." +msgstr "Hasล‚o juลผ nie dziaล‚a. Zaloguj siฤ™ ponownie w rdfeed." + +#: src/rdfeed:214 +#, sh-printf-format +msgid "%s: %d apps" +msgstr "%s: aplikacje: %d" + +#: src/rdfeed:222 +#, sh-printf-format +msgid "Which workspace? See `%s list` for the names." +msgstr "Ktรณry obszar roboczy? Nazwy pokazuje `%s list`." + +#: src/rdfeed:229 +#, sh-printf-format +msgid "%s and its apps are removed." +msgstr "Usuniฤ™to %s i jego aplikacje." + +#: src/rdfeed:253 +#, sh-printf-format +msgid "Which app? See `%s list`." +msgstr "Ktรณra aplikacja? Zobacz `%s list`." + +#: src/rdfeed:268 src/lib/launch.sh:199 +#, sh-printf-format +msgid "There is no app %s. See `%s list`." +msgstr "Nie ma aplikacji %s. Zobacz `%s list`." + +#: src/rdfeed:270 +#, sh-printf-format +msgid "Several workspaces have %s. Write it as WORKSPACE/APP, see `%s list`." +msgstr "" +"Kilka obszarรณw roboczych ma %s. Wpisz to jako OBSZAR/APLIKACJA, zobacz `%s " +"list`." + +#: src/rdfeed:278 +msgid "Usage: rdfeed [command]" +msgstr "Uลผycie: rdfeed [polecenie]" + +#: src/rdfeed:280 +msgid "Commands:" +msgstr "Polecenia:" + +#: src/rdfeed:281 +msgid "Add the workspace of your company" +msgstr "Dodaj obszar roboczy swojej firmy" + +#: src/rdfeed:282 +msgid "Load the apps again" +msgstr "Wczytaj aplikacje ponownie" + +#: src/rdfeed:283 +msgid "List the workspaces and their apps" +msgstr "Wyล›wietl obszary robocze i ich aplikacje" + +#: src/rdfeed:284 +msgid "Start an app, or open a file with it" +msgstr "Uruchom aplikacjฤ™ lub otwรณrz niฤ… plik" + +#: src/rdfeed:285 +msgid "Remove a workspace and its apps" +msgstr "Usuล„ obszar roboczy i jego aplikacje" + +#: src/rdfeed:286 +msgid "Show this help" +msgstr "Pokaลผ tฤ™ pomoc" + +#: src/rdfeed:287 +msgid "Show the version" +msgstr "Pokaลผ wersjฤ™" + +#: src/rdfeed:289 +msgid "Without a command an interactive menu is shown." +msgstr "Bez polecenia wyล›wietla siฤ™ interaktywne menu." + +#: src/rdfeed:313 +#, sh-printf-format +msgid "Unknown command: %s" +msgstr "Nieznane polecenie: %s" + +#: src/lib/common.sh:174 src/lib/menu.sh:251 +msgid "never" +msgstr "nigdy" + +#: src/lib/common.sh:184 +msgid "just now" +msgstr "przed chwilฤ…" + +#: src/lib/common.sh:186 +msgid "1 minute ago" +msgstr "1 minutฤ™ temu" + +#: src/lib/common.sh:189 +#, sh-printf-format +msgid "%d minutes ago" +msgstr "%d min temu" + +#: src/lib/common.sh:191 +msgid "1 hour ago" +msgstr "1 godzinฤ™ temu" + +#: src/lib/common.sh:194 +#, sh-printf-format +msgid "%d hours ago" +msgstr "%d godz. temu" + +#: src/lib/common.sh:196 +msgid "1 day ago" +msgstr "1 dzieล„ temu" + +#: src/lib/common.sh:199 +#, sh-printf-format +msgid "%d days ago" +msgstr "%d dni temu" + +#: src/lib/launch.sh:169 +msgid "" +"Your password has expired. Change it on Windows or on the RD Web page, then " +"sign in again in rdfeed." +msgstr "" +"Hasล‚o wygasล‚o. Zmieล„ je w Windows lub na stronie RD Web, a potem zaloguj siฤ™ " +"ponownie w rdfeed." + +#: src/lib/launch.sh:171 +msgid "Your account is locked. Ask your IT department." +msgstr "Konto jest zablokowane. Zapytaj dziaล‚ IT." + +#: src/lib/launch.sh:173 +msgid "The server did not accept the password. Sign in again in rdfeed." +msgstr "Serwer nie przyjฤ…ล‚ hasล‚a. Zaloguj siฤ™ ponownie w rdfeed." + +#: src/lib/launch.sh:175 +msgid "Could not reach the server. Check the internet connection." +msgstr "Nie udaล‚o siฤ™ poล‚ฤ…czyฤ‡ z serwerem. Sprawdลบ poล‚ฤ…czenie z internetem." + +#: src/lib/launch.sh:178 +#, sh-printf-format +msgid "FreeRDP stopped: %s" +msgstr "FreeRDP zatrzymaล‚ siฤ™: %s" + +#: src/lib/launch.sh:205 src/lib/launch.sh:206 +#, sh-printf-format +msgid "File not found: %s" +msgstr "Nie znaleziono pliku: %s" + +#: src/lib/launch.sh:212 src/lib/launch.sh:213 +msgid "" +"FreeRDP 3 is missing. Install it to start the apps (the package is called " +"freerdp)." +msgstr "" +"Brakuje FreeRDP 3. Zainstaluj go, aby uruchamiaฤ‡ aplikacje (pakiet nazywa " +"siฤ™ freerdp)." + +#: src/lib/launch.sh:277 +#, sh-printf-format +msgid "Opening %s in %s" +msgstr "Otwieranie %s w %s" + +#: src/lib/launch.sh:278 +#, sh-printf-format +msgid "If it does not open by itself, open it in the app from the drive %s." +msgstr "Jeล›li nie otworzy siฤ™ sam, otwรณrz go w aplikacji z dysku %s." + +#: src/lib/launch.sh:280 +#, sh-printf-format +msgid "Starting %s" +msgstr "Uruchamianie: %s" + +#: src/lib/launch.sh:281 +msgid "" +"If your company uses MFA, confirm the sign-in in your authenticator app." +msgstr "" +"Jeล›li firma uลผywa MFA, potwierdลบ logowanie w aplikacji uwierzytelniajฤ…cej." + +#: src/lib/launch.sh:290 +msgid "A second sign-in is coming" +msgstr "Nadchodzi drugie logowanie" + +#: src/lib/launch.sh:291 +#, sh-printf-format +msgid "" +"Confirm the second prompt in your authenticator app too. It comes in about " +"%d seconds." +msgstr "" +"Potwierdลบ teลผ drugie ลผฤ…danie w aplikacji uwierzytelniajฤ…cej. Przyjdzie za " +"okoล‚o %d s." + +#: src/lib/launch.sh:301 +#, sh-printf-format +msgid "%s could not start" +msgstr "Nie udaล‚o siฤ™ uruchomiฤ‡: %s" + +#: src/lib/launch.sh:303 +#, sh-printf-format +msgid "Log: %s" +msgstr "Dziennik: %s" + +#. TRANSLATORS: the letter after "[" is the key for yes. y works too. +#: src/lib/menu.sh:93 +msgid "[y/N]" +msgstr "[t/N]" + +#: src/lib/menu.sh:163 +msgid "1 app" +msgstr "1 aplikacja" + +#: src/lib/menu.sh:165 +#, sh-printf-format +msgid "%d apps" +msgstr "aplikacje: %d" + +#: src/lib/menu.sh:190 +msgid "No workspace yet. Add the one of your company with [1]." +msgstr "Nie ma jeszcze obszaru roboczego. Dodaj obszar swojej firmy przez [1]." + +#: src/lib/menu.sh:233 +msgid "off" +msgstr "wyล‚." + +#: src/lib/menu.sh:234 +msgid "home folder" +msgstr "folder domowy" + +#: src/lib/menu.sh:237 +msgid "Documents" +msgstr "Dokumenty" + +#: src/lib/menu.sh:238 +msgid "Downloads" +msgstr "Pobrane" + +#: src/lib/menu.sh:243 +msgid "not found" +msgstr "nie znaleziono" + +#: src/lib/menu.sh:246 +#, sh-printf-format +msgid "automatic (%d %%)" +msgstr "automatyczny (%d %%)" + +#: src/lib/menu.sh:248 +msgid "classic" +msgstr "klasyczny" + +#: src/lib/menu.sh:249 +msgid "GFX" +msgstr "GFX" + +#: src/lib/menu.sh:250 +msgid "always" +msgstr "zawsze" + +#: src/lib/menu.sh:252 +msgid "automatic" +msgstr "automatycznie" + +#: src/lib/menu.sh:261 +msgid "" +"The apps only see the files on the server. Pick a folder to open and save " +"files of this computer in them." +msgstr "" +"Aplikacje widzฤ… tylko pliki na serwerze. Wybierz folder, aby mogล‚y otwieraฤ‡ " +"i zapisywaฤ‡ pliki z tego komputera." + +#: src/lib/menu.sh:262 +msgid "" +"The apps can open and save files in this folder. It shows up as a drive in " +"them." +msgstr "" +"Aplikacje mogฤ… otwieraฤ‡ i zapisywaฤ‡ pliki w tym folderze. Widzฤ… go jako dysk." + +#: src/lib/menu.sh:263 +msgid "" +"Offers the apps under \"Open with\" for their file types, for example Word " +"for .docx. The folder of the file is shared with the app while it is open." +msgstr "" +"Pokazuje aplikacje w โ€žOtwรณrz za pomocฤ…โ€ dla ich typรณw plikรณw, na przykล‚ad " +"Word dla .docx. Folder pliku jest udostฤ™pniany, dopรณki plik jest otwarty." + +#: src/lib/menu.sh:264 +msgid "" +"How big the apps are drawn. Automatic follows the scale of your desktop, so " +"they are sharp and the right size." +msgstr "" +"Jak duลผe sฤ… rysowane aplikacje. Automatyczny podฤ…ลผa za skalฤ… pulpitu, wiฤ™c " +"sฤ… ostre i majฤ… wล‚aล›ciwy rozmiar." + +#: src/lib/menu.sh:265 +msgid "Plays the sound of the apps here, for calls and videos." +msgstr "Odtwarza tutaj dลบwiฤ™k aplikacji, do rozmรณw i filmรณw." + +#: src/lib/menu.sh:266 +msgid "" +"Paints every window right. But some servers end the session right after " +"signing in, then go back to GFX." +msgstr "Rysuje kaลผde okno poprawnie. Ale niektรณre serwery koล„czฤ… sesjฤ™ zaraz po zalogowaniu, wtedy wrรณฤ‡ do GFX." + +#: src/lib/menu.sh:267 +msgid "" +"Works with every server. FreeRDP may not repaint a window after it was " +"minimized or resized." +msgstr "Dziaล‚a z kaลผdym serwerem. FreeRDP moลผe nie odrysowaฤ‡ okna po zminimalizowaniu lub zmianie rozmiaru." + +#: src/lib/menu.sh:268 +msgid "" +"Always waits before the second sign-in. For when the second prompt does not " +"come with automatic." +msgstr "" +"Zawsze czeka przed drugim logowaniem. Gdy przy ustawieniu automatycznym " +"drugie ลผฤ…danie nie przychodzi." + +#: src/lib/menu.sh:269 +msgid "Never waits. Right when your company has no MFA." +msgstr "Nigdy nie czeka. Wล‚aล›ciwe, gdy firma nie uลผywa MFA." + +#: src/lib/menu.sh:270 +msgid "" +"Many servers with MFA need a second sign-in, and its prompt only comes after " +"a short wait. Automatic waits when the first sign-in took a while." +msgstr "" +"Wiele serwerรณw z MFA wymaga drugiego logowania, a jego ลผฤ…danie przychodzi " +"dopiero po chwili. Automatycznie czeka, gdy pierwsze logowanie trochฤ™ trwaล‚o." + +#: src/lib/menu.sh:273 +msgid "" +"Loads the list of apps again once a day, so new apps show up by themselves." +msgstr "" +"Wczytuje listฤ™ aplikacji raz dziennie, wiฤ™c nowe aplikacje pojawiajฤ… siฤ™ " +"same." + +#: src/lib/menu.sh:275 src/lib/menu.sh:330 +msgid "Needs a systemd user session." +msgstr "Wymaga sesji uลผytkownika systemd." + +#: src/lib/menu.sh:322 src/lib/menu.sh:325 +msgid "Could not save the setting." +msgstr "Nie udaล‚o siฤ™ zapisaฤ‡ ustawienia." + +#: src/lib/menu.sh:371 src/lib/menu.sh:609 +msgid "Settings" +msgstr "Ustawienia" + +#: src/lib/menu.sh:372 +msgid "โ†‘โ†“ select โ†โ†’ or Space: change q: back" +msgstr "โ†‘โ†“ wybierz โ†โ†’ lub Spacja: zmieล„ q: wstecz" + +#: src/lib/menu.sh:373 +msgid "ON" +msgstr "Wล." + +#: src/lib/menu.sh:374 +msgid "OFF" +msgstr "WYล." + +#: src/lib/menu.sh:456 src/lib/menu.sh:608 +msgid "Workspaces" +msgstr "Obszary robocze" + +#. TRANSLATORS: keep the letters, they are the keys. +#: src/lib/menu.sh:458 +msgid "" +"โ†‘โ†“ select r: refresh s: sign in again d: remove a: add q: back" +msgstr "" +"โ†‘โ†“ wybierz r: odล›wieลผ s: zaloguj ponownie d: usuล„ a: dodaj q: " +"wstecz" + +#: src/lib/menu.sh:459 +msgid "No workspace yet. Press a to add one." +msgstr "Nie ma jeszcze obszaru roboczego. Naciล›nij a, aby dodaฤ‡." + +#: src/lib/menu.sh:484 +#, sh-printf-format +msgid "refreshed %s" +msgstr "odล›wieลผono %s" + +#: src/lib/menu.sh:512 +#, sh-printf-format +msgid "Remove \"%s\" and its apps?" +msgstr "Usunฤ…ฤ‡ โ€ž%sโ€ i jego aplikacje?" + +#: src/lib/menu.sh:536 src/lib/menu.sh:607 +msgid "Start an app" +msgstr "Uruchom aplikacjฤ™" + +#: src/lib/menu.sh:537 +msgid "โ†‘โ†“ select Enter: start q: back" +msgstr "โ†‘โ†“ wybierz Enter: uruchom q: wstecz" + +#: src/lib/menu.sh:538 +msgid "No apps yet. Add a workspace first." +msgstr "Nie ma jeszcze aplikacji. Najpierw dodaj obszar roboczy." + +#: src/lib/menu.sh:579 +#, sh-printf-format +msgid "%s is starting. It opens in a window of its own." +msgstr "Uruchamianie: %s. Otworzy siฤ™ we wล‚asnym oknie." + +#: src/lib/menu.sh:606 +msgid "Add a workspace" +msgstr "Dodaj obszar roboczy" + +#: src/lib/menu.sh:610 +msgid "Quit" +msgstr "Zakoล„cz" + +#: src/lib/workspace.sh:163 +msgid "The server did not accept the user name or password." +msgstr "Serwer nie przyjฤ…ล‚ nazwy uลผytkownika lub hasล‚a." + +#: src/lib/workspace.sh:164 +#, sh-printf-format +msgid "Could not reach the server: %s" +msgstr "Nie udaล‚o siฤ™ poล‚ฤ…czyฤ‡ z serwerem: %s" + +#: src/lib/workspace.sh:165 +#, sh-printf-format +msgid "There is no RD Web feed at %s." +msgstr "Pod adresem %s nie ma kanaล‚u RD Web." + +#: src/lib/workspace.sh:166 +#, sh-printf-format +msgid "Could not load the apps: %s" +msgstr "Nie udaล‚o siฤ™ wczytaฤ‡ aplikacji: %s" + +#: src/lib/workspace.sh:282 +#, sh-printf-format +msgid "Remote desktop of %s" +msgstr "Pulpit zdalny %s" + +#: src/lib/workspace.sh:284 +#, sh-printf-format +msgid "%s from %s" +msgstr "%s z %s" + +#: src/lib/menu.sh +msgid "Apps" +msgstr "Aplikacje" + +#: src/lib/menu.sh +msgid "Share a folder" +msgstr "Udostฤ™pnij folder" + +#: src/lib/menu.sh +msgid "Open files with the apps" +msgstr "Otwieraj pliki w aplikacjach" + +#: src/lib/menu.sh +msgid "Size" +msgstr "Rozmiar" + +#: src/lib/menu.sh +msgid "Sound" +msgstr "Dลบwiฤ™k" + +#: src/lib/menu.sh +msgid "Graphics" +msgstr "Grafika" + +#: src/lib/menu.sh +msgid "Signing in" +msgstr "Logowanie" + +#: src/lib/menu.sh +msgid "Wait for the second MFA prompt" +msgstr "Czekaj na drugie ลผฤ…danie MFA" + +#: src/lib/menu.sh +msgid "Refresh the apps every day" +msgstr "Odล›wieลผaj aplikacje codziennie" diff --git a/po/pt_BR.po b/po/pt_BR.po new file mode 100644 index 0000000..f974dfa --- /dev/null +++ b/po/pt_BR.po @@ -0,0 +1,561 @@ +# Brazilian Portuguese translation for rdfeed. +# Copyright (C) 2026 Felitendo +# This file is distributed under the same license as rdfeed. +# +msgid "" +msgstr "" +"Project-Id-Version: rdfeed 1.0.0\n" +"Report-Msgid-Bugs-To: https://github.com/LoonixTools/rdfeed/issues\n" +"POT-Creation-Date: 2026-10-02 10:29+0200\n" +"PO-Revision-Date: 2026-10-02 09:30+0200\n" +"Last-Translator: Felitendo\n" +"Language-Team: Brazilian Portuguese\n" +"Language: pt_BR\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" + +#: src/rdfeed:79 src/rdfeed:103 +#, sh-printf-format +msgid "Could not write to %s." +msgstr "Nรฃo foi possรญvel gravar em %s." + +#: src/rdfeed:82 +#, sh-printf-format +msgid "Signing in to %sโ€ฆ" +msgstr "Entrando em %sโ€ฆ" + +#: src/rdfeed:100 +msgid "" +"The password could not be saved in the keyring, so each app asks for it." +msgstr "" +"Nรฃo foi possรญvel salvar a senha no chaveiro, entรฃo cada app vai pedi-la." + +#: src/rdfeed:111 +#, sh-printf-format +msgid "1 app from %s is now in your app menu." +msgstr "1 app de %s agora estรก no seu menu de aplicativos." + +#: src/rdfeed:113 +#, sh-printf-format +msgid "%d apps from %s are now in your app menu." +msgstr "%d apps de %s agora estรฃo no seu menu de aplicativos." + +#: src/rdfeed:129 +msgid "" +"Your IT department has the address. A work e-mail address often works too." +msgstr "" +"O seu departamento de TI tem o endereรงo. Muitas vezes o e-mail do trabalho " +"tambรฉm funciona." + +#: src/rdfeed:130 +msgid "Feed address, server or e-mail:" +msgstr "Endereรงo do feed, servidor ou e-mail:" + +#: src/rdfeed:137 +#, sh-printf-format +msgid "No feed is registered for %s. Ask your IT department for the address." +msgstr "" +"Nenhum feed estรก registrado para %s. Peรงa o endereรงo ao seu departamento de " +"TI." + +#: src/rdfeed:142 src/rdfeed:156 +msgid "User name (DOMAIN\\name or name@domain):" +msgstr "Nome de usuรกrio (DOMรNIO\\nome ou nome@domรญnio):" + +#: src/rdfeed:146 src/rdfeed:159 +msgid "Password:" +msgstr "Senha:" + +#: src/rdfeed:178 src/rdfeed:236 +#, sh-printf-format +msgid "No workspace yet. Add one with `%s add`." +msgstr "Ainda nรฃo hรก espaรงo de trabalho. Adicione um com `%s add`." + +#: src/rdfeed:187 src/lib/launch.sh:220 +#, sh-printf-format +msgid "No password saved for %s. Sign in again in rdfeed." +msgstr "Nenhuma senha salva para %s. Entre de novo no rdfeed." + +#: src/rdfeed:191 src/lib/launch.sh:149 +#, sh-printf-format +msgid "Password for %s:" +msgstr "Senha de %s:" + +#: src/rdfeed:195 +#, sh-printf-format +msgid "Refreshing %sโ€ฆ" +msgstr "Atualizando %sโ€ฆ" + +#: src/rdfeed:205 +msgid "The password no longer works. Sign in again in rdfeed." +msgstr "A senha nรฃo funciona mais. Entre de novo no rdfeed." + +#: src/rdfeed:214 +#, sh-printf-format +msgid "%s: %d apps" +msgstr "%s: %d apps" + +#: src/rdfeed:222 +#, sh-printf-format +msgid "Which workspace? See `%s list` for the names." +msgstr "Qual espaรงo de trabalho? Os nomes estรฃo em `%s list`." + +#: src/rdfeed:229 +#, sh-printf-format +msgid "%s and its apps are removed." +msgstr "%s e os seus apps foram removidos." + +#: src/rdfeed:253 +#, sh-printf-format +msgid "Which app? See `%s list`." +msgstr "Qual app? Veja `%s list`." + +#: src/rdfeed:268 src/lib/launch.sh:199 +#, sh-printf-format +msgid "There is no app %s. See `%s list`." +msgstr "Nรฃo existe o app %s. Veja `%s list`." + +#: src/rdfeed:270 +#, sh-printf-format +msgid "Several workspaces have %s. Write it as WORKSPACE/APP, see `%s list`." +msgstr "" +"Vรกrios espaรงos de trabalho tรชm %s. Escreva como ESPAร‡O/APP, veja `%s list`." + +#: src/rdfeed:278 +msgid "Usage: rdfeed [command]" +msgstr "Uso: rdfeed [comando]" + +#: src/rdfeed:280 +msgid "Commands:" +msgstr "Comandos:" + +#: src/rdfeed:281 +msgid "Add the workspace of your company" +msgstr "Adicionar o espaรงo de trabalho da sua empresa" + +#: src/rdfeed:282 +msgid "Load the apps again" +msgstr "Carregar os apps de novo" + +#: src/rdfeed:283 +msgid "List the workspaces and their apps" +msgstr "Listar os espaรงos de trabalho e seus apps" + +#: src/rdfeed:284 +msgid "Start an app, or open a file with it" +msgstr "Abrir um app, ou um arquivo com ele" + +#: src/rdfeed:285 +msgid "Remove a workspace and its apps" +msgstr "Remover um espaรงo de trabalho e seus apps" + +#: src/rdfeed:286 +msgid "Show this help" +msgstr "Mostrar esta ajuda" + +#: src/rdfeed:287 +msgid "Show the version" +msgstr "Mostrar a versรฃo" + +#: src/rdfeed:289 +msgid "Without a command an interactive menu is shown." +msgstr "Sem comando, um menu interativo รฉ exibido." + +#: src/rdfeed:313 +#, sh-printf-format +msgid "Unknown command: %s" +msgstr "Comando desconhecido: %s" + +#: src/lib/common.sh:174 src/lib/menu.sh:251 +msgid "never" +msgstr "nunca" + +#: src/lib/common.sh:184 +msgid "just now" +msgstr "agora mesmo" + +#: src/lib/common.sh:186 +msgid "1 minute ago" +msgstr "hรก 1 minuto" + +#: src/lib/common.sh:189 +#, sh-printf-format +msgid "%d minutes ago" +msgstr "hรก %d minutos" + +#: src/lib/common.sh:191 +msgid "1 hour ago" +msgstr "hรก 1 hora" + +#: src/lib/common.sh:194 +#, sh-printf-format +msgid "%d hours ago" +msgstr "hรก %d horas" + +#: src/lib/common.sh:196 +msgid "1 day ago" +msgstr "hรก 1 dia" + +#: src/lib/common.sh:199 +#, sh-printf-format +msgid "%d days ago" +msgstr "hรก %d dias" + +#: src/lib/launch.sh:169 +msgid "" +"Your password has expired. Change it on Windows or on the RD Web page, then " +"sign in again in rdfeed." +msgstr "" +"A sua senha expirou. Mude-a no Windows ou na pรกgina do RD Web e depois entre " +"de novo no rdfeed." + +#: src/lib/launch.sh:171 +msgid "Your account is locked. Ask your IT department." +msgstr "A sua conta estรก bloqueada. Fale com o seu departamento de TI." + +#: src/lib/launch.sh:173 +msgid "The server did not accept the password. Sign in again in rdfeed." +msgstr "O servidor nรฃo aceitou a senha. Entre de novo no rdfeed." + +#: src/lib/launch.sh:175 +msgid "Could not reach the server. Check the internet connection." +msgstr "" +"Nรฃo foi possรญvel alcanรงar o servidor. Verifique a conexรฃo com a internet." + +#: src/lib/launch.sh:178 +#, sh-printf-format +msgid "FreeRDP stopped: %s" +msgstr "O FreeRDP parou: %s" + +#: src/lib/launch.sh:205 src/lib/launch.sh:206 +#, sh-printf-format +msgid "File not found: %s" +msgstr "Arquivo nรฃo encontrado: %s" + +#: src/lib/launch.sh:212 src/lib/launch.sh:213 +msgid "" +"FreeRDP 3 is missing. Install it to start the apps (the package is called " +"freerdp)." +msgstr "" +"O FreeRDP 3 nรฃo estรก instalado. Instale-o para abrir os apps (o pacote se " +"chama freerdp)." + +#: src/lib/launch.sh:277 +#, sh-printf-format +msgid "Opening %s in %s" +msgstr "Abrindo %s no %s" + +#: src/lib/launch.sh:278 +#, sh-printf-format +msgid "If it does not open by itself, open it in the app from the drive %s." +msgstr "Se nรฃo abrir sozinho, abra-o no app pela unidade %s." + +#: src/lib/launch.sh:280 +#, sh-printf-format +msgid "Starting %s" +msgstr "Abrindo %s" + +#: src/lib/launch.sh:281 +msgid "" +"If your company uses MFA, confirm the sign-in in your authenticator app." +msgstr "Se a sua empresa usa MFA, confirme a entrada no seu app autenticador." + +#: src/lib/launch.sh:290 +msgid "A second sign-in is coming" +msgstr "Vem uma segunda entrada" + +#: src/lib/launch.sh:291 +#, sh-printf-format +msgid "" +"Confirm the second prompt in your authenticator app too. It comes in about " +"%d seconds." +msgstr "" +"Confirme tambรฉm o segundo pedido no seu app autenticador. Ele chega em cerca " +"de %d segundos." + +#: src/lib/launch.sh:301 +#, sh-printf-format +msgid "%s could not start" +msgstr "Nรฃo foi possรญvel abrir %s" + +#: src/lib/launch.sh:303 +#, sh-printf-format +msgid "Log: %s" +msgstr "Log: %s" + +#. TRANSLATORS: the letter after "[" is the key for yes. y works too. +#: src/lib/menu.sh:93 +msgid "[y/N]" +msgstr "[s/N]" + +#: src/lib/menu.sh:163 +msgid "1 app" +msgstr "1 app" + +#: src/lib/menu.sh:165 +#, sh-printf-format +msgid "%d apps" +msgstr "%d apps" + +#: src/lib/menu.sh:190 +msgid "No workspace yet. Add the one of your company with [1]." +msgstr "Ainda nรฃo hรก espaรงo de trabalho. Adicione o da sua empresa com [1]." + +#: src/lib/menu.sh:233 +msgid "off" +msgstr "desativado" + +#: src/lib/menu.sh:234 +msgid "home folder" +msgstr "pasta pessoal" + +#: src/lib/menu.sh:237 +msgid "Documents" +msgstr "Documentos" + +#: src/lib/menu.sh:238 +msgid "Downloads" +msgstr "Downloads" + +#: src/lib/menu.sh:243 +msgid "not found" +msgstr "nรฃo encontrada" + +#: src/lib/menu.sh:246 +#, sh-printf-format +msgid "automatic (%d %%)" +msgstr "automรกtico (%d %%)" + +#: src/lib/menu.sh:248 +msgid "classic" +msgstr "clรกssico" + +#: src/lib/menu.sh:249 +msgid "GFX" +msgstr "GFX" + +#: src/lib/menu.sh:250 +msgid "always" +msgstr "sempre" + +#: src/lib/menu.sh:252 +msgid "automatic" +msgstr "automรกtico" + +#: src/lib/menu.sh:261 +msgid "" +"The apps only see the files on the server. Pick a folder to open and save " +"files of this computer in them." +msgstr "" +"Os apps sรณ veem os arquivos do servidor. Escolha uma pasta para que possam " +"abrir e salvar arquivos deste computador." + +#: src/lib/menu.sh:262 +msgid "" +"The apps can open and save files in this folder. It shows up as a drive in " +"them." +msgstr "" +"Os apps podem abrir e salvar arquivos nesta pasta. Ela aparece neles como " +"uma unidade." + +#: src/lib/menu.sh:263 +msgid "" +"Offers the apps under \"Open with\" for their file types, for example Word " +"for .docx. The folder of the file is shared with the app while it is open." +msgstr "" +"Oferece os apps em \"Abrir com\" para seus tipos de arquivo, por exemplo o " +"Word para .docx. A pasta do arquivo fica compartilhada enquanto ele estรก " +"aberto." + +#: src/lib/menu.sh:264 +msgid "" +"How big the apps are drawn. Automatic follows the scale of your desktop, so " +"they are sharp and the right size." +msgstr "" +"O tamanho em que os apps sรฃo desenhados. Automรกtico segue a escala da sua " +"รกrea de trabalho, assim ficam nรญtidos e do tamanho certo." + +#: src/lib/menu.sh:265 +msgid "Plays the sound of the apps here, for calls and videos." +msgstr "Toca aqui o som dos apps, para chamadas e vรญdeos." + +#: src/lib/menu.sh:266 +msgid "" +"Paints every window right. But some servers end the session right after " +"signing in, then go back to GFX." +msgstr "Desenha cada janela direito. Mas alguns servidores encerram a sessรฃo logo apรณs a entrada; nesse caso, volte ao GFX." + +#: src/lib/menu.sh:267 +msgid "" +"Works with every server. FreeRDP may not repaint a window after it was " +"minimized or resized." +msgstr "Funciona com qualquer servidor. O FreeRDP pode nรฃo redesenhar uma janela depois de minimizada ou redimensionada." + +#: src/lib/menu.sh:268 +msgid "" +"Always waits before the second sign-in. For when the second prompt does not " +"come with automatic." +msgstr "" +"Sempre espera antes da segunda entrada. Para quando, no automรกtico, o " +"segundo pedido nรฃo chega." + +#: src/lib/menu.sh:269 +msgid "Never waits. Right when your company has no MFA." +msgstr "Nunca espera. Certo quando a sua empresa nรฃo usa MFA." + +#: src/lib/menu.sh:270 +msgid "" +"Many servers with MFA need a second sign-in, and its prompt only comes after " +"a short wait. Automatic waits when the first sign-in took a while." +msgstr "" +"Muitos servidores com MFA pedem uma segunda entrada, e o pedido sรณ chega " +"depois de uma breve espera. Automรกtico espera quando a primeira entrada " +"demorou." + +#: src/lib/menu.sh:273 +msgid "" +"Loads the list of apps again once a day, so new apps show up by themselves." +msgstr "" +"Carrega a lista de apps de novo uma vez por dia, assim os novos aparecem " +"sozinhos." + +#: src/lib/menu.sh:275 src/lib/menu.sh:330 +msgid "Needs a systemd user session." +msgstr "Precisa de uma sessรฃo de usuรกrio do systemd." + +#: src/lib/menu.sh:322 src/lib/menu.sh:325 +msgid "Could not save the setting." +msgstr "Nรฃo foi possรญvel salvar a configuraรงรฃo." + +#: src/lib/menu.sh:371 src/lib/menu.sh:609 +msgid "Settings" +msgstr "Configuraรงรตes" + +#: src/lib/menu.sh:372 +msgid "โ†‘โ†“ select โ†โ†’ or Space: change q: back" +msgstr "โ†‘โ†“ escolher โ†โ†’ ou Espaรงo: mudar q: voltar" + +#: src/lib/menu.sh:373 +msgid "ON" +msgstr "ATIVADO" + +#: src/lib/menu.sh:374 +msgid "OFF" +msgstr "DESATIVADO" + +#: src/lib/menu.sh:456 src/lib/menu.sh:608 +msgid "Workspaces" +msgstr "Espaรงos de trabalho" + +#. TRANSLATORS: keep the letters, they are the keys. +#: src/lib/menu.sh:458 +msgid "" +"โ†‘โ†“ select r: refresh s: sign in again d: remove a: add q: back" +msgstr "" +"โ†‘โ†“ escolher r: atualizar s: entrar de novo d: remover a: adicionar " +"q: voltar" + +#: src/lib/menu.sh:459 +msgid "No workspace yet. Press a to add one." +msgstr "Ainda nรฃo hรก espaรงo de trabalho. Aperte a para adicionar um." + +#: src/lib/menu.sh:484 +#, sh-printf-format +msgid "refreshed %s" +msgstr "atualizado %s" + +#: src/lib/menu.sh:512 +#, sh-printf-format +msgid "Remove \"%s\" and its apps?" +msgstr "Remover \"%s\" e seus apps?" + +#: src/lib/menu.sh:536 src/lib/menu.sh:607 +msgid "Start an app" +msgstr "Abrir um app" + +#: src/lib/menu.sh:537 +msgid "โ†‘โ†“ select Enter: start q: back" +msgstr "โ†‘โ†“ escolher Enter: abrir q: voltar" + +#: src/lib/menu.sh:538 +msgid "No apps yet. Add a workspace first." +msgstr "Ainda nรฃo hรก apps. Adicione primeiro um espaรงo de trabalho." + +#: src/lib/menu.sh:579 +#, sh-printf-format +msgid "%s is starting. It opens in a window of its own." +msgstr "%s estรก abrindo. Ele abre numa janela prรณpria." + +#: src/lib/menu.sh:606 +msgid "Add a workspace" +msgstr "Adicionar um espaรงo de trabalho" + +#: src/lib/menu.sh:610 +msgid "Quit" +msgstr "Sair" + +#: src/lib/workspace.sh:163 +msgid "The server did not accept the user name or password." +msgstr "O servidor nรฃo aceitou o nome de usuรกrio ou a senha." + +#: src/lib/workspace.sh:164 +#, sh-printf-format +msgid "Could not reach the server: %s" +msgstr "Nรฃo foi possรญvel alcanรงar o servidor: %s" + +#: src/lib/workspace.sh:165 +#, sh-printf-format +msgid "There is no RD Web feed at %s." +msgstr "Nรฃo hรก feed do RD Web em %s." + +#: src/lib/workspace.sh:166 +#, sh-printf-format +msgid "Could not load the apps: %s" +msgstr "Nรฃo foi possรญvel carregar os apps: %s" + +#: src/lib/workspace.sh:282 +#, sh-printf-format +msgid "Remote desktop of %s" +msgstr "รrea de trabalho remota de %s" + +#: src/lib/workspace.sh:284 +#, sh-printf-format +msgid "%s from %s" +msgstr "%s de %s" + +#: src/lib/menu.sh +msgid "Apps" +msgstr "Apps" + +#: src/lib/menu.sh +msgid "Share a folder" +msgstr "Compartilhar uma pasta" + +#: src/lib/menu.sh +msgid "Open files with the apps" +msgstr "Abrir arquivos com os apps" + +#: src/lib/menu.sh +msgid "Size" +msgstr "Tamanho" + +#: src/lib/menu.sh +msgid "Sound" +msgstr "Som" + +#: src/lib/menu.sh +msgid "Graphics" +msgstr "Grรกficos" + +#: src/lib/menu.sh +msgid "Signing in" +msgstr "Acesso" + +#: src/lib/menu.sh +msgid "Wait for the second MFA prompt" +msgstr "Esperar o segundo pedido de MFA" + +#: src/lib/menu.sh +msgid "Refresh the apps every day" +msgstr "Atualizar os apps todo dia" diff --git a/po/rdfeed.pot b/po/rdfeed.pot new file mode 100644 index 0000000..4642cf9 --- /dev/null +++ b/po/rdfeed.pot @@ -0,0 +1,531 @@ +# Translation template for rdfeed. +# Copyright (C) 2026 Felitendo +# This file is distributed under the same license as rdfeed. +# +#, fuzzy +msgid "" +msgstr "" +"Project-Id-Version: rdfeed 1.0.0\n" +"Report-Msgid-Bugs-To: https://github.com/LoonixTools/rdfeed/issues\n" +"POT-Creation-Date: 2026-10-02 10:29+0200\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: FULL NAME \n" +"Language-Team: LANGUAGE \n" +"Language: \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" + +#: src/rdfeed:79 src/rdfeed:103 +#, sh-printf-format +msgid "Could not write to %s." +msgstr "" + +#: src/rdfeed:82 +#, sh-printf-format +msgid "Signing in to %sโ€ฆ" +msgstr "" + +#: src/rdfeed:100 +msgid "" +"The password could not be saved in the keyring, so each app asks for it." +msgstr "" + +#: src/rdfeed:111 +#, sh-printf-format +msgid "1 app from %s is now in your app menu." +msgstr "" + +#: src/rdfeed:113 +#, sh-printf-format +msgid "%d apps from %s are now in your app menu." +msgstr "" + +#: src/rdfeed:129 +msgid "" +"Your IT department has the address. A work e-mail address often works too." +msgstr "" + +#: src/rdfeed:130 +msgid "Feed address, server or e-mail:" +msgstr "" + +#: src/rdfeed:137 +#, sh-printf-format +msgid "No feed is registered for %s. Ask your IT department for the address." +msgstr "" + +#: src/rdfeed:142 src/rdfeed:156 +msgid "User name (DOMAIN\\name or name@domain):" +msgstr "" + +#: src/rdfeed:146 src/rdfeed:159 +msgid "Password:" +msgstr "" + +#: src/rdfeed:178 src/rdfeed:236 +#, sh-printf-format +msgid "No workspace yet. Add one with `%s add`." +msgstr "" + +#: src/rdfeed:187 src/lib/launch.sh:220 +#, sh-printf-format +msgid "No password saved for %s. Sign in again in rdfeed." +msgstr "" + +#: src/rdfeed:191 src/lib/launch.sh:149 +#, sh-printf-format +msgid "Password for %s:" +msgstr "" + +#: src/rdfeed:195 +#, sh-printf-format +msgid "Refreshing %sโ€ฆ" +msgstr "" + +#: src/rdfeed:205 +msgid "The password no longer works. Sign in again in rdfeed." +msgstr "" + +#: src/rdfeed:214 +#, sh-printf-format +msgid "%s: %d apps" +msgstr "" + +#: src/rdfeed:222 +#, sh-printf-format +msgid "Which workspace? See `%s list` for the names." +msgstr "" + +#: src/rdfeed:229 +#, sh-printf-format +msgid "%s and its apps are removed." +msgstr "" + +#: src/rdfeed:253 +#, sh-printf-format +msgid "Which app? See `%s list`." +msgstr "" + +#: src/rdfeed:268 src/lib/launch.sh:199 +#, sh-printf-format +msgid "There is no app %s. See `%s list`." +msgstr "" + +#: src/rdfeed:270 +#, sh-printf-format +msgid "Several workspaces have %s. Write it as WORKSPACE/APP, see `%s list`." +msgstr "" + +#: src/rdfeed:278 +msgid "Usage: rdfeed [command]" +msgstr "" + +#: src/rdfeed:280 +msgid "Commands:" +msgstr "" + +#: src/rdfeed:281 +msgid "Add the workspace of your company" +msgstr "" + +#: src/rdfeed:282 +msgid "Load the apps again" +msgstr "" + +#: src/rdfeed:283 +msgid "List the workspaces and their apps" +msgstr "" + +#: src/rdfeed:284 +msgid "Start an app, or open a file with it" +msgstr "" + +#: src/rdfeed:285 +msgid "Remove a workspace and its apps" +msgstr "" + +#: src/rdfeed:286 +msgid "Show this help" +msgstr "" + +#: src/rdfeed:287 +msgid "Show the version" +msgstr "" + +#: src/rdfeed:289 +msgid "Without a command an interactive menu is shown." +msgstr "" + +#: src/rdfeed:313 +#, sh-printf-format +msgid "Unknown command: %s" +msgstr "" + +#: src/lib/common.sh:174 src/lib/menu.sh:251 +msgid "never" +msgstr "" + +#: src/lib/common.sh:184 +msgid "just now" +msgstr "" + +#: src/lib/common.sh:186 +msgid "1 minute ago" +msgstr "" + +#: src/lib/common.sh:189 +#, sh-printf-format +msgid "%d minutes ago" +msgstr "" + +#: src/lib/common.sh:191 +msgid "1 hour ago" +msgstr "" + +#: src/lib/common.sh:194 +#, sh-printf-format +msgid "%d hours ago" +msgstr "" + +#: src/lib/common.sh:196 +msgid "1 day ago" +msgstr "" + +#: src/lib/common.sh:199 +#, sh-printf-format +msgid "%d days ago" +msgstr "" + +#: src/lib/launch.sh:169 +msgid "" +"Your password has expired. Change it on Windows or on the RD Web page, then " +"sign in again in rdfeed." +msgstr "" + +#: src/lib/launch.sh:171 +msgid "Your account is locked. Ask your IT department." +msgstr "" + +#: src/lib/launch.sh:173 +msgid "The server did not accept the password. Sign in again in rdfeed." +msgstr "" + +#: src/lib/launch.sh:175 +msgid "Could not reach the server. Check the internet connection." +msgstr "" + +#: src/lib/launch.sh:178 +#, sh-printf-format +msgid "FreeRDP stopped: %s" +msgstr "" + +#: src/lib/launch.sh:205 src/lib/launch.sh:206 +#, sh-printf-format +msgid "File not found: %s" +msgstr "" + +#: src/lib/launch.sh:212 src/lib/launch.sh:213 +msgid "" +"FreeRDP 3 is missing. Install it to start the apps (the package is called " +"freerdp)." +msgstr "" + +#: src/lib/launch.sh:277 +#, sh-printf-format +msgid "Opening %s in %s" +msgstr "" + +#: src/lib/launch.sh:278 +#, sh-printf-format +msgid "If it does not open by itself, open it in the app from the drive %s." +msgstr "" + +#: src/lib/launch.sh:280 +#, sh-printf-format +msgid "Starting %s" +msgstr "" + +#: src/lib/launch.sh:281 +msgid "" +"If your company uses MFA, confirm the sign-in in your authenticator app." +msgstr "" + +#: src/lib/launch.sh:290 +msgid "A second sign-in is coming" +msgstr "" + +#: src/lib/launch.sh:291 +#, sh-printf-format +msgid "" +"Confirm the second prompt in your authenticator app too. It comes in about " +"%d seconds." +msgstr "" + +#: src/lib/launch.sh:301 +#, sh-printf-format +msgid "%s could not start" +msgstr "" + +#: src/lib/launch.sh:303 +#, sh-printf-format +msgid "Log: %s" +msgstr "" + +#. TRANSLATORS: the letter after "[" is the key for yes. y works too. +#: src/lib/menu.sh:93 +msgid "[y/N]" +msgstr "" + +#: src/lib/menu.sh:163 +msgid "1 app" +msgstr "" + +#: src/lib/menu.sh:165 +#, sh-printf-format +msgid "%d apps" +msgstr "" + +#: src/lib/menu.sh:190 +msgid "No workspace yet. Add the one of your company with [1]." +msgstr "" + +#: src/lib/menu.sh:233 +msgid "off" +msgstr "" + +#: src/lib/menu.sh:234 +msgid "home folder" +msgstr "" + +#: src/lib/menu.sh:237 +msgid "Documents" +msgstr "" + +#: src/lib/menu.sh:238 +msgid "Downloads" +msgstr "" + +#: src/lib/menu.sh:243 +msgid "not found" +msgstr "" + +#: src/lib/menu.sh:246 +#, sh-printf-format +msgid "automatic (%d %%)" +msgstr "" + +#: src/lib/menu.sh:248 +msgid "classic" +msgstr "" + +#: src/lib/menu.sh:249 +msgid "GFX" +msgstr "" + +#: src/lib/menu.sh:250 +msgid "always" +msgstr "" + +#: src/lib/menu.sh:252 +msgid "automatic" +msgstr "" + +#: src/lib/menu.sh:261 +msgid "" +"The apps only see the files on the server. Pick a folder to open and save " +"files of this computer in them." +msgstr "" + +#: src/lib/menu.sh:262 +msgid "" +"The apps can open and save files in this folder. It shows up as a drive in " +"them." +msgstr "" + +#: src/lib/menu.sh:263 +msgid "" +"Offers the apps under \"Open with\" for their file types, for example Word " +"for .docx. The folder of the file is shared with the app while it is open." +msgstr "" + +#: src/lib/menu.sh:264 +msgid "" +"How big the apps are drawn. Automatic follows the scale of your desktop, so " +"they are sharp and the right size." +msgstr "" + +#: src/lib/menu.sh:265 +msgid "Plays the sound of the apps here, for calls and videos." +msgstr "" + +#: src/lib/menu.sh:266 +msgid "" +"Paints every window right. But some servers end the session right after " +"signing in, then go back to GFX." +msgstr "" + +#: src/lib/menu.sh:267 +msgid "" +"Works with every server. FreeRDP may not repaint a window after it was " +"minimized or resized." +msgstr "" + +#: src/lib/menu.sh:268 +msgid "" +"Always waits before the second sign-in. For when the second prompt does not " +"come with automatic." +msgstr "" + +#: src/lib/menu.sh:269 +msgid "Never waits. Right when your company has no MFA." +msgstr "" + +#: src/lib/menu.sh:270 +msgid "" +"Many servers with MFA need a second sign-in, and its prompt only comes after " +"a short wait. Automatic waits when the first sign-in took a while." +msgstr "" + +#: src/lib/menu.sh:273 +msgid "" +"Loads the list of apps again once a day, so new apps show up by themselves." +msgstr "" + +#: src/lib/menu.sh:275 src/lib/menu.sh:330 +msgid "Needs a systemd user session." +msgstr "" + +#: src/lib/menu.sh:322 src/lib/menu.sh:325 +msgid "Could not save the setting." +msgstr "" + +#: src/lib/menu.sh:371 src/lib/menu.sh:609 +msgid "Settings" +msgstr "" + +#: src/lib/menu.sh:372 +msgid "โ†‘โ†“ select โ†โ†’ or Space: change q: back" +msgstr "" + +#: src/lib/menu.sh:373 +msgid "ON" +msgstr "" + +#: src/lib/menu.sh:374 +msgid "OFF" +msgstr "" + +#: src/lib/menu.sh:456 src/lib/menu.sh:608 +msgid "Workspaces" +msgstr "" + +#. TRANSLATORS: keep the letters, they are the keys. +#: src/lib/menu.sh:458 +msgid "" +"โ†‘โ†“ select r: refresh s: sign in again d: remove a: add q: back" +msgstr "" + +#: src/lib/menu.sh:459 +msgid "No workspace yet. Press a to add one." +msgstr "" + +#: src/lib/menu.sh:484 +#, sh-printf-format +msgid "refreshed %s" +msgstr "" + +#: src/lib/menu.sh:512 +#, sh-printf-format +msgid "Remove \"%s\" and its apps?" +msgstr "" + +#: src/lib/menu.sh:536 src/lib/menu.sh:607 +msgid "Start an app" +msgstr "" + +#: src/lib/menu.sh:537 +msgid "โ†‘โ†“ select Enter: start q: back" +msgstr "" + +#: src/lib/menu.sh:538 +msgid "No apps yet. Add a workspace first." +msgstr "" + +#: src/lib/menu.sh:579 +#, sh-printf-format +msgid "%s is starting. It opens in a window of its own." +msgstr "" + +#: src/lib/menu.sh:606 +msgid "Add a workspace" +msgstr "" + +#: src/lib/menu.sh:610 +msgid "Quit" +msgstr "" + +#: src/lib/workspace.sh:163 +msgid "The server did not accept the user name or password." +msgstr "" + +#: src/lib/workspace.sh:164 +#, sh-printf-format +msgid "Could not reach the server: %s" +msgstr "" + +#: src/lib/workspace.sh:165 +#, sh-printf-format +msgid "There is no RD Web feed at %s." +msgstr "" + +#: src/lib/workspace.sh:166 +#, sh-printf-format +msgid "Could not load the apps: %s" +msgstr "" + +#: src/lib/workspace.sh:282 +#, sh-printf-format +msgid "Remote desktop of %s" +msgstr "" + +#: src/lib/workspace.sh:284 +#, sh-printf-format +msgid "%s from %s" +msgstr "" + +#: src/lib/menu.sh +msgid "Apps" +msgstr "" + +#: src/lib/menu.sh +msgid "Share a folder" +msgstr "" + +#: src/lib/menu.sh +msgid "Open files with the apps" +msgstr "" + +#: src/lib/menu.sh +msgid "Size" +msgstr "" + +#: src/lib/menu.sh +msgid "Sound" +msgstr "" + +#: src/lib/menu.sh +msgid "Graphics" +msgstr "" + +#: src/lib/menu.sh +msgid "Signing in" +msgstr "" + +#: src/lib/menu.sh +msgid "Wait for the second MFA prompt" +msgstr "" + +#: src/lib/menu.sh +msgid "Refresh the apps every day" +msgstr "" diff --git a/po/ru.po b/po/ru.po new file mode 100644 index 0000000..4cdadcf --- /dev/null +++ b/po/ru.po @@ -0,0 +1,558 @@ +# Russian translation for rdfeed. +# Copyright (C) 2026 Felitendo +# This file is distributed under the same license as rdfeed. +# +msgid "" +msgstr "" +"Project-Id-Version: rdfeed 1.0.0\n" +"Report-Msgid-Bugs-To: https://github.com/LoonixTools/rdfeed/issues\n" +"POT-Creation-Date: 2026-10-02 10:29+0200\n" +"PO-Revision-Date: 2026-10-02 09:30+0200\n" +"Last-Translator: Felitendo\n" +"Language-Team: Russian\n" +"Language: ru\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" + +#: src/rdfeed:79 src/rdfeed:103 +#, sh-printf-format +msgid "Could not write to %s." +msgstr "ะะต ัƒะดะฐะปะพััŒ ะทะฐะฟะธัะฐั‚ัŒ ะฒ %s." + +#: src/rdfeed:82 +#, sh-printf-format +msgid "Signing in to %sโ€ฆ" +msgstr "ะ’ั…ะพะด ะฝะฐ %sโ€ฆ" + +#: src/rdfeed:100 +msgid "" +"The password could not be saved in the keyring, so each app asks for it." +msgstr "" +"ะะต ัƒะดะฐะปะพััŒ ัะพั…ั€ะฐะฝะธั‚ัŒ ะฟะฐั€ะพะปัŒ ะฒ ัะฒัะทะบะต ะบะปัŽั‡ะตะน, ะฟะพัั‚ะพะผัƒ ะบะฐะถะดะพะต ะฟั€ะธะปะพะถะตะฝะธะต ะฑัƒะดะตั‚ " +"ะตะณะพ ะทะฐะฟั€ะฐัˆะธะฒะฐั‚ัŒ." + +#: src/rdfeed:111 +#, sh-printf-format +msgid "1 app from %s is now in your app menu." +msgstr "1 ะฟั€ะธะปะพะถะตะฝะธะต ะธะท %s ั‚ะตะฟะตั€ัŒ ะฒ ะผะตะฝัŽ ะฟั€ะธะปะพะถะตะฝะธะน." + +#: src/rdfeed:113 +#, sh-printf-format +msgid "%d apps from %s are now in your app menu." +msgstr "ะŸั€ะธะปะพะถะตะฝะธะน: %d (ะธะท %s). ะขะตะฟะตั€ัŒ ะพะฝะธ ะฒ ะผะตะฝัŽ ะฟั€ะธะปะพะถะตะฝะธะน." + +#: src/rdfeed:129 +msgid "" +"Your IT department has the address. A work e-mail address often works too." +msgstr "" +"ะะดั€ะตั ะตัั‚ัŒ ัƒ ะฒะฐัˆะตะณะพ ะ˜ะข-ะพั‚ะดะตะปะฐ. ะงะฐัั‚ะพ ะฟะพะดั…ะพะดะธั‚ ะธ ั€ะฐะฑะพั‡ะธะน ะฐะดั€ะตั ัะปะตะบั‚ั€ะพะฝะฝะพะน " +"ะฟะพั‡ั‚ั‹." + +#: src/rdfeed:130 +msgid "Feed address, server or e-mail:" +msgstr "ะะดั€ะตั ั„ะธะดะฐ, ัะตั€ะฒะตั€ ะธะปะธ e-mail:" + +#: src/rdfeed:137 +#, sh-printf-format +msgid "No feed is registered for %s. Ask your IT department for the address." +msgstr "ะ”ะปั %s ะฝะต ะทะฐั€ะตะณะธัั‚ั€ะธั€ะพะฒะฐะฝ ั„ะธะด. ะกะฟั€ะพัะธั‚ะต ะฐะดั€ะตั ัƒ ะ˜ะข-ะพั‚ะดะตะปะฐ." + +#: src/rdfeed:142 src/rdfeed:156 +msgid "User name (DOMAIN\\name or name@domain):" +msgstr "ะ˜ะผั ะฟะพะปัŒะทะพะฒะฐั‚ะตะปั (ะ”ะžะœะ•ะ\\ะธะผั ะธะปะธ ะธะผั@ะดะพะผะตะฝ):" + +#: src/rdfeed:146 src/rdfeed:159 +msgid "Password:" +msgstr "ะŸะฐั€ะพะปัŒ:" + +#: src/rdfeed:178 src/rdfeed:236 +#, sh-printf-format +msgid "No workspace yet. Add one with `%s add`." +msgstr "ะ ะฐะฑะพั‡ะธั… ะพะฑะปะฐัั‚ะตะน ะฟะพะบะฐ ะฝะตั‚. ะ”ะพะฑะฐะฒัŒั‚ะต ะตั‘ ะบะพะผะฐะฝะดะพะน `%s add`." + +#: src/rdfeed:187 src/lib/launch.sh:220 +#, sh-printf-format +msgid "No password saved for %s. Sign in again in rdfeed." +msgstr "ะ”ะปั %s ะฝะต ัะพั…ั€ะฐะฝั‘ะฝ ะฟะฐั€ะพะปัŒ. ะ’ะพะนะดะธั‚ะต ัะฝะพะฒะฐ ะฒ rdfeed." + +#: src/rdfeed:191 src/lib/launch.sh:149 +#, sh-printf-format +msgid "Password for %s:" +msgstr "ะŸะฐั€ะพะปัŒ ะดะปั %s:" + +#: src/rdfeed:195 +#, sh-printf-format +msgid "Refreshing %sโ€ฆ" +msgstr "ะžะฑะฝะพะฒะปะตะฝะธะต %sโ€ฆ" + +#: src/rdfeed:205 +msgid "The password no longer works. Sign in again in rdfeed." +msgstr "ะŸะฐั€ะพะปัŒ ะฑะพะปัŒัˆะต ะฝะต ะฟะพะดั…ะพะดะธั‚. ะ’ะพะนะดะธั‚ะต ัะฝะพะฒะฐ ะฒ rdfeed." + +#: src/rdfeed:214 +#, sh-printf-format +msgid "%s: %d apps" +msgstr "%s: ะฟั€ะธะปะพะถะตะฝะธะน: %d" + +#: src/rdfeed:222 +#, sh-printf-format +msgid "Which workspace? See `%s list` for the names." +msgstr "ะšะฐะบะฐั ั€ะฐะฑะพั‡ะฐั ะพะฑะปะฐัั‚ัŒ? ะะฐะทะฒะฐะฝะธั ะฟะพะบะฐะทั‹ะฒะฐะตั‚ `%s list`." + +#: src/rdfeed:229 +#, sh-printf-format +msgid "%s and its apps are removed." +msgstr "ะฃะดะฐะปะตะฝะพ: %s ะฒะผะตัั‚ะต ั ะฟั€ะธะปะพะถะตะฝะธัะผะธ." + +#: src/rdfeed:253 +#, sh-printf-format +msgid "Which app? See `%s list`." +msgstr "ะšะฐะบะพะต ะฟั€ะธะปะพะถะตะฝะธะต? ะกะผะพั‚ั€ะธั‚ะต `%s list`." + +#: src/rdfeed:268 src/lib/launch.sh:199 +#, sh-printf-format +msgid "There is no app %s. See `%s list`." +msgstr "ะŸั€ะธะปะพะถะตะฝะธั %s ะฝะตั‚. ะกะผะพั‚ั€ะธั‚ะต `%s list`." + +#: src/rdfeed:270 +#, sh-printf-format +msgid "Several workspaces have %s. Write it as WORKSPACE/APP, see `%s list`." +msgstr "" +"%s ะตัั‚ัŒ ะฒ ะฝะตัะบะพะปัŒะบะธั… ั€ะฐะฑะพั‡ะธั… ะพะฑะปะฐัั‚ัั…. ะฃะบะฐะถะธั‚ะต ะฒ ะฒะธะดะต ะžะ‘ะ›ะะกะขะฌ/ะŸะ ะ˜ะ›ะžะ–ะ•ะะ˜ะ•, " +"ัะผะพั‚ั€ะธั‚ะต `%s list`." + +#: src/rdfeed:278 +msgid "Usage: rdfeed [command]" +msgstr "ะ˜ัะฟะพะปัŒะทะพะฒะฐะฝะธะต: rdfeed [ะบะพะผะฐะฝะดะฐ]" + +#: src/rdfeed:280 +msgid "Commands:" +msgstr "ะšะพะผะฐะฝะดั‹:" + +#: src/rdfeed:281 +msgid "Add the workspace of your company" +msgstr "ะ”ะพะฑะฐะฒะธั‚ัŒ ั€ะฐะฑะพั‡ัƒัŽ ะพะฑะปะฐัั‚ัŒ ะฒะฐัˆะตะน ะบะพะผะฟะฐะฝะธะธ" + +#: src/rdfeed:282 +msgid "Load the apps again" +msgstr "ะ—ะฐะณั€ัƒะทะธั‚ัŒ ะฟั€ะธะปะพะถะตะฝะธั ะทะฐะฝะพะฒะพ" + +#: src/rdfeed:283 +msgid "List the workspaces and their apps" +msgstr "ะŸะพะบะฐะทะฐั‚ัŒ ั€ะฐะฑะพั‡ะธะต ะพะฑะปะฐัั‚ะธ ะธ ะธั… ะฟั€ะธะปะพะถะตะฝะธั" + +#: src/rdfeed:284 +msgid "Start an app, or open a file with it" +msgstr "ะ—ะฐะฟัƒัั‚ะธั‚ัŒ ะฟั€ะธะปะพะถะตะฝะธะต ะธะปะธ ะพั‚ะบั€ั‹ั‚ัŒ ะฒ ะฝั‘ะผ ั„ะฐะนะป" + +#: src/rdfeed:285 +msgid "Remove a workspace and its apps" +msgstr "ะฃะดะฐะปะธั‚ัŒ ั€ะฐะฑะพั‡ัƒัŽ ะพะฑะปะฐัั‚ัŒ ะธ ะตั‘ ะฟั€ะธะปะพะถะตะฝะธั" + +#: src/rdfeed:286 +msgid "Show this help" +msgstr "ะŸะพะบะฐะทะฐั‚ัŒ ัั‚ัƒ ัะฟั€ะฐะฒะบัƒ" + +#: src/rdfeed:287 +msgid "Show the version" +msgstr "ะŸะพะบะฐะทะฐั‚ัŒ ะฒะตั€ัะธัŽ" + +#: src/rdfeed:289 +msgid "Without a command an interactive menu is shown." +msgstr "ะ‘ะตะท ะบะพะผะฐะฝะดั‹ ะพั‚ะบั€ั‹ะฒะฐะตั‚ัั ะธะฝั‚ะตั€ะฐะบั‚ะธะฒะฝะพะต ะผะตะฝัŽ." + +#: src/rdfeed:313 +#, sh-printf-format +msgid "Unknown command: %s" +msgstr "ะะตะธะทะฒะตัั‚ะฝะฐั ะบะพะผะฐะฝะดะฐ: %s" + +#: src/lib/common.sh:174 src/lib/menu.sh:251 +msgid "never" +msgstr "ะฝะธะบะพะณะดะฐ" + +#: src/lib/common.sh:184 +msgid "just now" +msgstr "ั‚ะพะปัŒะบะพ ั‡ั‚ะพ" + +#: src/lib/common.sh:186 +msgid "1 minute ago" +msgstr "1 ะผะธะฝัƒั‚ัƒ ะฝะฐะทะฐะด" + +#: src/lib/common.sh:189 +#, sh-printf-format +msgid "%d minutes ago" +msgstr "%d ะผะธะฝ ะฝะฐะทะฐะด" + +#: src/lib/common.sh:191 +msgid "1 hour ago" +msgstr "1 ั‡ะฐั ะฝะฐะทะฐะด" + +#: src/lib/common.sh:194 +#, sh-printf-format +msgid "%d hours ago" +msgstr "%d ั‡ ะฝะฐะทะฐะด" + +#: src/lib/common.sh:196 +msgid "1 day ago" +msgstr "1 ะดะตะฝัŒ ะฝะฐะทะฐะด" + +#: src/lib/common.sh:199 +#, sh-printf-format +msgid "%d days ago" +msgstr "%d ะดะฝ. ะฝะฐะทะฐะด" + +#: src/lib/launch.sh:169 +msgid "" +"Your password has expired. Change it on Windows or on the RD Web page, then " +"sign in again in rdfeed." +msgstr "" +"ะกั€ะพะบ ะดะตะนัั‚ะฒะธั ะฟะฐั€ะพะปั ะธัั‚ั‘ะบ. ะกะผะตะฝะธั‚ะต ะตะณะพ ะฒ Windows ะธะปะธ ะฝะฐ ัั‚ั€ะฐะฝะธั†ะต RD Web, " +"ะทะฐั‚ะตะผ ัะฝะพะฒะฐ ะฒะพะนะดะธั‚ะต ะฒ rdfeed." + +#: src/lib/launch.sh:171 +msgid "Your account is locked. Ask your IT department." +msgstr "ะฃั‡ั‘ั‚ะฝะฐั ะทะฐะฟะธััŒ ะทะฐะฑะปะพะบะธั€ะพะฒะฐะฝะฐ. ะžะฑั€ะฐั‚ะธั‚ะตััŒ ะฒ ะ˜ะข-ะพั‚ะดะตะป." + +#: src/lib/launch.sh:173 +msgid "The server did not accept the password. Sign in again in rdfeed." +msgstr "ะกะตั€ะฒะตั€ ะฝะต ะฟั€ะธะฝัะป ะฟะฐั€ะพะปัŒ. ะ’ะพะนะดะธั‚ะต ัะฝะพะฒะฐ ะฒ rdfeed." + +#: src/lib/launch.sh:175 +msgid "Could not reach the server. Check the internet connection." +msgstr "ะะต ัƒะดะฐะปะพััŒ ัะฒัะทะฐั‚ัŒัั ั ัะตั€ะฒะตั€ะพะผ. ะŸั€ะพะฒะตั€ัŒั‚ะต ะฟะพะดะบะปัŽั‡ะตะฝะธะต ะบ ะธะฝั‚ะตั€ะฝะตั‚ัƒ." + +#: src/lib/launch.sh:178 +#, sh-printf-format +msgid "FreeRDP stopped: %s" +msgstr "FreeRDP ะพัั‚ะฐะฝะพะฒะธะปัั: %s" + +#: src/lib/launch.sh:205 src/lib/launch.sh:206 +#, sh-printf-format +msgid "File not found: %s" +msgstr "ะคะฐะนะป ะฝะต ะฝะฐะนะดะตะฝ: %s" + +#: src/lib/launch.sh:212 src/lib/launch.sh:213 +msgid "" +"FreeRDP 3 is missing. Install it to start the apps (the package is called " +"freerdp)." +msgstr "" +"ะะตั‚ FreeRDP 3. ะฃัั‚ะฐะฝะพะฒะธั‚ะต ะตะณะพ, ั‡ั‚ะพะฑั‹ ะทะฐะฟัƒัะบะฐั‚ัŒ ะฟั€ะธะปะพะถะตะฝะธั (ะฟะฐะบะตั‚ ะฝะฐะทั‹ะฒะฐะตั‚ัั " +"freerdp)." + +#: src/lib/launch.sh:277 +#, sh-printf-format +msgid "Opening %s in %s" +msgstr "ะžั‚ะบั€ั‹ั‚ะธะต %s ะฒ %s" + +#: src/lib/launch.sh:278 +#, sh-printf-format +msgid "If it does not open by itself, open it in the app from the drive %s." +msgstr "ะ•ัะปะธ ั„ะฐะนะป ะฝะต ะพั‚ะบั€ะพะตั‚ัั ัะฐะผ, ะพั‚ะบั€ะพะนั‚ะต ะตะณะพ ะฒ ะฟั€ะธะปะพะถะตะฝะธะธ ั ะดะธัะบะฐ %s." + +#: src/lib/launch.sh:280 +#, sh-printf-format +msgid "Starting %s" +msgstr "ะ—ะฐะฟัƒัะบ: %s" + +#: src/lib/launch.sh:281 +msgid "" +"If your company uses MFA, confirm the sign-in in your authenticator app." +msgstr "" +"ะ•ัะปะธ ะฒะฐัˆะฐ ะบะพะผะฟะฐะฝะธั ะธัะฟะพะปัŒะทัƒะตั‚ MFA, ะฟะพะดั‚ะฒะตั€ะดะธั‚ะต ะฒั…ะพะด ะฒ ะฟั€ะธะปะพะถะตะฝะธะธ-" +"ะฐัƒั‚ะตะฝั‚ะธั„ะธะบะฐั‚ะพั€ะต." + +#: src/lib/launch.sh:290 +msgid "A second sign-in is coming" +msgstr "ะ‘ัƒะดะตั‚ ะฒั‚ะพั€ะพะน ะฒั…ะพะด" + +#: src/lib/launch.sh:291 +#, sh-printf-format +msgid "" +"Confirm the second prompt in your authenticator app too. It comes in about " +"%d seconds." +msgstr "" +"ะŸะพะดั‚ะฒะตั€ะดะธั‚ะต ะธ ะฒั‚ะพั€ะพะน ะทะฐะฟั€ะพั ะฒ ะฟั€ะธะปะพะถะตะฝะธะธ-ะฐัƒั‚ะตะฝั‚ะธั„ะธะบะฐั‚ะพั€ะต. ะžะฝ ะฟั€ะธะดั‘ั‚ ะฟั€ะธะผะตั€ะฝะพ " +"ั‡ะตั€ะตะท %d ั." + +#: src/lib/launch.sh:301 +#, sh-printf-format +msgid "%s could not start" +msgstr "ะะต ัƒะดะฐะปะพััŒ ะทะฐะฟัƒัั‚ะธั‚ัŒ: %s" + +#: src/lib/launch.sh:303 +#, sh-printf-format +msgid "Log: %s" +msgstr "ะ–ัƒั€ะฝะฐะป: %s" + +#. TRANSLATORS: the letter after "[" is the key for yes. y works too. +#: src/lib/menu.sh:93 +msgid "[y/N]" +msgstr "[ะด/ะ]" + +#: src/lib/menu.sh:163 +msgid "1 app" +msgstr "1 ะฟั€ะธะปะพะถะตะฝะธะต" + +#: src/lib/menu.sh:165 +#, sh-printf-format +msgid "%d apps" +msgstr "ะฟั€ะธะปะพะถะตะฝะธะน: %d" + +#: src/lib/menu.sh:190 +msgid "No workspace yet. Add the one of your company with [1]." +msgstr "ะ ะฐะฑะพั‡ะธั… ะพะฑะปะฐัั‚ะตะน ะฟะพะบะฐ ะฝะตั‚. ะ”ะพะฑะฐะฒัŒั‚ะต ะพะฑะปะฐัั‚ัŒ ะฒะฐัˆะตะน ะบะพะผะฟะฐะฝะธะธ ั‡ะตั€ะตะท [1]." + +#: src/lib/menu.sh:233 +msgid "off" +msgstr "ะฒั‹ะบะป." + +#: src/lib/menu.sh:234 +msgid "home folder" +msgstr "ะดะพะผะฐัˆะฝัั ะฟะฐะฟะบะฐ" + +#: src/lib/menu.sh:237 +msgid "Documents" +msgstr "ะ”ะพะบัƒะผะตะฝั‚ั‹" + +#: src/lib/menu.sh:238 +msgid "Downloads" +msgstr "ะ—ะฐะณั€ัƒะทะบะธ" + +#: src/lib/menu.sh:243 +msgid "not found" +msgstr "ะฝะต ะฝะฐะนะดะตะฝะฐ" + +#: src/lib/menu.sh:246 +#, sh-printf-format +msgid "automatic (%d %%)" +msgstr "ะฐะฒั‚ะพะผะฐั‚ะธั‡ะตัะบะธ (%d %%)" + +#: src/lib/menu.sh:248 +msgid "classic" +msgstr "ะบะปะฐััะธั‡ะตัะบะธะน" + +#: src/lib/menu.sh:249 +msgid "GFX" +msgstr "GFX" + +#: src/lib/menu.sh:250 +msgid "always" +msgstr "ะฒัะตะณะดะฐ" + +#: src/lib/menu.sh:252 +msgid "automatic" +msgstr "ะฐะฒั‚ะพะผะฐั‚ะธั‡ะตัะบะธ" + +#: src/lib/menu.sh:261 +msgid "" +"The apps only see the files on the server. Pick a folder to open and save " +"files of this computer in them." +msgstr "" +"ะŸั€ะธะปะพะถะตะฝะธั ะฒะธะดัั‚ ั‚ะพะปัŒะบะพ ั„ะฐะนะปั‹ ะฝะฐ ัะตั€ะฒะตั€ะต. ะ’ั‹ะฑะตั€ะธั‚ะต ะฟะฐะฟะบัƒ, ั‡ั‚ะพะฑั‹ ะพะฝะธ ะผะพะณะปะธ " +"ะพั‚ะบั€ั‹ะฒะฐั‚ัŒ ะธ ัะพั…ั€ะฐะฝัั‚ัŒ ั„ะฐะนะปั‹ ัั‚ะพะณะพ ะบะพะผะฟัŒัŽั‚ะตั€ะฐ." + +#: src/lib/menu.sh:262 +msgid "" +"The apps can open and save files in this folder. It shows up as a drive in " +"them." +msgstr "" +"ะŸั€ะธะปะพะถะตะฝะธั ะผะพะณัƒั‚ ะพั‚ะบั€ั‹ะฒะฐั‚ัŒ ะธ ัะพั…ั€ะฐะฝัั‚ัŒ ั„ะฐะนะปั‹ ะฒ ัั‚ะพะน ะฟะฐะฟะบะต. ะ’ ะฝะธั… ะพะฝะฐ ะฒะธะดะฝะฐ " +"ะบะฐะบ ะดะธัะบ." + +#: src/lib/menu.sh:263 +msgid "" +"Offers the apps under \"Open with\" for their file types, for example Word " +"for .docx. The folder of the file is shared with the app while it is open." +msgstr "" +"ะŸั€ะตะดะปะฐะณะฐะตั‚ ะฟั€ะธะปะพะถะตะฝะธั ะฒ ยซะžั‚ะบั€ั‹ั‚ัŒ ั ะฟะพะผะพั‰ัŒัŽยป ะดะปั ะธั… ั‚ะธะฟะพะฒ ั„ะฐะนะปะพะฒ, ะฝะฐะฟั€ะธะผะตั€ " +"Word ะดะปั .docx. ะŸะฐะฟะบะฐ ั„ะฐะนะปะฐ ะพั‚ะบั€ั‹ั‚ะฐ ะดะปั ัะตั€ะฒะตั€ะฐ, ะฟะพะบะฐ ั„ะฐะนะป ะพั‚ะบั€ั‹ั‚." + +#: src/lib/menu.sh:264 +msgid "" +"How big the apps are drawn. Automatic follows the scale of your desktop, so " +"they are sharp and the right size." +msgstr "" +"ะะฐัะบะพะปัŒะบะพ ะบั€ัƒะฟะฝะพ ั€ะธััƒัŽั‚ัั ะฟั€ะธะปะพะถะตะฝะธั. ยซะะฒั‚ะพะผะฐั‚ะธั‡ะตัะบะธยป ัะปะตะดัƒะตั‚ ะผะฐััˆั‚ะฐะฑัƒ " +"ั€ะฐะฑะพั‡ะตะณะพ ัั‚ะพะปะฐ, ั‚ะฐะบ ะพะฝะธ ั‡ั‘ั‚ะบะธะต ะธ ะฝัƒะถะฝะพะณะพ ั€ะฐะทะผะตั€ะฐ." + +#: src/lib/menu.sh:265 +msgid "Plays the sound of the apps here, for calls and videos." +msgstr "ะ’ะพัะฟั€ะพะธะทะฒะพะดะธั‚ ะทะฒัƒะบ ะฟั€ะธะปะพะถะตะฝะธะน ะทะดะตััŒ, ะดะปั ะทะฒะพะฝะบะพะฒ ะธ ะฒะธะดะตะพ." + +#: src/lib/menu.sh:266 +msgid "" +"Paints every window right. But some servers end the session right after " +"signing in, then go back to GFX." +msgstr "ะŸั€ะฐะฒะธะปัŒะฝะพ ั€ะธััƒะตั‚ ะบะฐะถะดะพะต ะพะบะฝะพ. ะะพ ะฝะตะบะพั‚ะพั€ั‹ะต ัะตั€ะฒะตั€ั‹ ะพะฑั€ั‹ะฒะฐัŽั‚ ัะตะฐะฝั ัั€ะฐะทัƒ ะฟะพัะปะต ะฒั…ะพะดะฐ, ั‚ะพะณะดะฐ ะฒะตั€ะฝะธั‚ะตััŒ ะบ GFX." + +#: src/lib/menu.sh:267 +msgid "" +"Works with every server. FreeRDP may not repaint a window after it was " +"minimized or resized." +msgstr "ะ ะฐะฑะพั‚ะฐะตั‚ ั ะปัŽะฑั‹ะผ ัะตั€ะฒะตั€ะพะผ. FreeRDP ะผะพะถะตั‚ ะฝะต ะฟะตั€ะตั€ะธัะพะฒะฐั‚ัŒ ะพะบะฝะพ ะฟะพัะปะต ัะฒะพั€ะฐั‡ะธะฒะฐะฝะธั ะธะปะธ ะธะทะผะตะฝะตะฝะธั ั€ะฐะทะผะตั€ะฐ." + +#: src/lib/menu.sh:268 +msgid "" +"Always waits before the second sign-in. For when the second prompt does not " +"come with automatic." +msgstr "" +"ะ’ัะตะณะดะฐ ะถะดั‘ั‚ ะฟะตั€ะตะด ะฒั‚ะพั€ั‹ะผ ะฒั…ะพะดะพะผ. ะ•ัะปะธ ั ยซะฐะฒั‚ะพะผะฐั‚ะธั‡ะตัะบะธยป ะฒั‚ะพั€ะพะน ะทะฐะฟั€ะพั ะฝะต " +"ะฟั€ะธั…ะพะดะธั‚." + +#: src/lib/menu.sh:269 +msgid "Never waits. Right when your company has no MFA." +msgstr "ะะธะบะพะณะดะฐ ะฝะต ะถะดั‘ั‚. ะŸะพะดั…ะพะดะธั‚, ะตัะปะธ ะฒะฐัˆะฐ ะบะพะผะฟะฐะฝะธั ะฝะต ะธัะฟะพะปัŒะทัƒะตั‚ MFA." + +#: src/lib/menu.sh:270 +msgid "" +"Many servers with MFA need a second sign-in, and its prompt only comes after " +"a short wait. Automatic waits when the first sign-in took a while." +msgstr "" +"ะœะฝะพะณะธะผ ัะตั€ะฒะตั€ะฐะผ ั MFA ะฝัƒะถะตะฝ ะฒั‚ะพั€ะพะน ะฒั…ะพะด, ะธ ะตะณะพ ะทะฐะฟั€ะพั ะฟั€ะธั…ะพะดะธั‚ ั‚ะพะปัŒะบะพ ะฟะพัะปะต " +"ะบะพั€ะพั‚ะบะพะน ะฟะฐัƒะทั‹. ยซะะฒั‚ะพะผะฐั‚ะธั‡ะตัะบะธยป ะถะดั‘ั‚, ะตัะปะธ ะฟะตั€ะฒั‹ะน ะฒั…ะพะด ะทะฐะฝัะป ะฒั€ะตะผั." + +#: src/lib/menu.sh:273 +msgid "" +"Loads the list of apps again once a day, so new apps show up by themselves." +msgstr "" +"ะ ะฐะท ะฒ ะดะตะฝัŒ ะทะฐะฝะพะฒะพ ะทะฐะณั€ัƒะถะฐะตั‚ ัะฟะธัะพะบ ะฟั€ะธะปะพะถะตะฝะธะน, ั‚ะฐะบ ะฝะพะฒั‹ะต ะฟะพัะฒะปััŽั‚ัั ัะฐะผะธ." + +#: src/lib/menu.sh:275 src/lib/menu.sh:330 +msgid "Needs a systemd user session." +msgstr "ะัƒะถะตะฝ ะฟะพะปัŒะทะพะฒะฐั‚ะตะปัŒัะบะธะน ัะตะฐะฝั systemd." + +#: src/lib/menu.sh:322 src/lib/menu.sh:325 +msgid "Could not save the setting." +msgstr "ะะต ัƒะดะฐะปะพััŒ ัะพั…ั€ะฐะฝะธั‚ัŒ ะฝะฐัั‚ั€ะพะนะบัƒ." + +#: src/lib/menu.sh:371 src/lib/menu.sh:609 +msgid "Settings" +msgstr "ะะฐัั‚ั€ะพะนะบะธ" + +#: src/lib/menu.sh:372 +msgid "โ†‘โ†“ select โ†โ†’ or Space: change q: back" +msgstr "โ†‘โ†“ ะฒั‹ะฑะพั€ โ†โ†’ ะธะปะธ ะŸั€ะพะฑะตะป: ะธะทะผะตะฝะธั‚ัŒ q: ะฝะฐะทะฐะด" + +#: src/lib/menu.sh:373 +msgid "ON" +msgstr "ะ’ะšะ›" + +#: src/lib/menu.sh:374 +msgid "OFF" +msgstr "ะ’ะซะšะ›" + +#: src/lib/menu.sh:456 src/lib/menu.sh:608 +msgid "Workspaces" +msgstr "ะ ะฐะฑะพั‡ะธะต ะพะฑะปะฐัั‚ะธ" + +#. TRANSLATORS: keep the letters, they are the keys. +#: src/lib/menu.sh:458 +msgid "" +"โ†‘โ†“ select r: refresh s: sign in again d: remove a: add q: back" +msgstr "" +"โ†‘โ†“ ะฒั‹ะฑะพั€ r: ะพะฑะฝะพะฒะธั‚ัŒ s: ะฒะพะนั‚ะธ ัะฝะพะฒะฐ d: ัƒะดะฐะปะธั‚ัŒ a: ะดะพะฑะฐะฒะธั‚ัŒ q: ะฝะฐะทะฐะด" + +#: src/lib/menu.sh:459 +msgid "No workspace yet. Press a to add one." +msgstr "ะ ะฐะฑะพั‡ะธั… ะพะฑะปะฐัั‚ะตะน ะฟะพะบะฐ ะฝะตั‚. ะะฐะถะผะธั‚ะต a, ั‡ั‚ะพะฑั‹ ะดะพะฑะฐะฒะธั‚ัŒ." + +#: src/lib/menu.sh:484 +#, sh-printf-format +msgid "refreshed %s" +msgstr "ะพะฑะฝะพะฒะปะตะฝะพ %s" + +#: src/lib/menu.sh:512 +#, sh-printf-format +msgid "Remove \"%s\" and its apps?" +msgstr "ะฃะดะฐะปะธั‚ัŒ ยซ%sยป ะฒะผะตัั‚ะต ั ะฟั€ะธะปะพะถะตะฝะธัะผะธ?" + +#: src/lib/menu.sh:536 src/lib/menu.sh:607 +msgid "Start an app" +msgstr "ะ—ะฐะฟัƒัั‚ะธั‚ัŒ ะฟั€ะธะปะพะถะตะฝะธะต" + +#: src/lib/menu.sh:537 +msgid "โ†‘โ†“ select Enter: start q: back" +msgstr "โ†‘โ†“ ะฒั‹ะฑะพั€ Enter: ะทะฐะฟัƒัั‚ะธั‚ัŒ q: ะฝะฐะทะฐะด" + +#: src/lib/menu.sh:538 +msgid "No apps yet. Add a workspace first." +msgstr "ะŸั€ะธะปะพะถะตะฝะธะน ะฟะพะบะฐ ะฝะตั‚. ะกะฝะฐั‡ะฐะปะฐ ะดะพะฑะฐะฒัŒั‚ะต ั€ะฐะฑะพั‡ัƒัŽ ะพะฑะปะฐัั‚ัŒ." + +#: src/lib/menu.sh:579 +#, sh-printf-format +msgid "%s is starting. It opens in a window of its own." +msgstr "ะ—ะฐะฟัƒัะบ: %s. ะžั‚ะบั€ะพะตั‚ัั ะฒ ะพั‚ะดะตะปัŒะฝะพะผ ะพะบะฝะต." + +#: src/lib/menu.sh:606 +msgid "Add a workspace" +msgstr "ะ”ะพะฑะฐะฒะธั‚ัŒ ั€ะฐะฑะพั‡ัƒัŽ ะพะฑะปะฐัั‚ัŒ" + +#: src/lib/menu.sh:610 +msgid "Quit" +msgstr "ะ’ั‹ั…ะพะด" + +#: src/lib/workspace.sh:163 +msgid "The server did not accept the user name or password." +msgstr "ะกะตั€ะฒะตั€ ะฝะต ะฟั€ะธะฝัะป ะธะผั ะฟะพะปัŒะทะพะฒะฐั‚ะตะปั ะธะปะธ ะฟะฐั€ะพะปัŒ." + +#: src/lib/workspace.sh:164 +#, sh-printf-format +msgid "Could not reach the server: %s" +msgstr "ะะต ัƒะดะฐะปะพััŒ ัะฒัะทะฐั‚ัŒัั ั ัะตั€ะฒะตั€ะพะผ: %s" + +#: src/lib/workspace.sh:165 +#, sh-printf-format +msgid "There is no RD Web feed at %s." +msgstr "ะŸะพ ะฐะดั€ะตััƒ %s ะฝะตั‚ ั„ะธะดะฐ RD Web." + +#: src/lib/workspace.sh:166 +#, sh-printf-format +msgid "Could not load the apps: %s" +msgstr "ะะต ัƒะดะฐะปะพััŒ ะทะฐะณั€ัƒะทะธั‚ัŒ ะฟั€ะธะปะพะถะตะฝะธั: %s" + +#: src/lib/workspace.sh:282 +#, sh-printf-format +msgid "Remote desktop of %s" +msgstr "ะฃะดะฐะปั‘ะฝะฝั‹ะน ั€ะฐะฑะพั‡ะธะน ัั‚ะพะป %s" + +#: src/lib/workspace.sh:284 +#, sh-printf-format +msgid "%s from %s" +msgstr "%s ะธะท %s" + +#: src/lib/menu.sh +msgid "Apps" +msgstr "ะŸั€ะธะปะพะถะตะฝะธั" + +#: src/lib/menu.sh +msgid "Share a folder" +msgstr "ะžะฑั‰ะฐั ะฟะฐะฟะบะฐ" + +#: src/lib/menu.sh +msgid "Open files with the apps" +msgstr "ะžั‚ะบั€ั‹ะฒะฐั‚ัŒ ั„ะฐะนะปั‹ ะฒ ะฟั€ะธะปะพะถะตะฝะธัั…" + +#: src/lib/menu.sh +msgid "Size" +msgstr "ะ ะฐะทะผะตั€" + +#: src/lib/menu.sh +msgid "Sound" +msgstr "ะ—ะฒัƒะบ" + +#: src/lib/menu.sh +msgid "Graphics" +msgstr "ะ“ั€ะฐั„ะธะบะฐ" + +#: src/lib/menu.sh +msgid "Signing in" +msgstr "ะ’ั…ะพะด" + +#: src/lib/menu.sh +msgid "Wait for the second MFA prompt" +msgstr "ะ–ะดะฐั‚ัŒ ะฒั‚ะพั€ะพะน ะทะฐะฟั€ะพั MFA" + +#: src/lib/menu.sh +msgid "Refresh the apps every day" +msgstr "ะžะฑะฝะพะฒะปัั‚ัŒ ะฟั€ะธะปะพะถะตะฝะธั ะบะฐะถะดั‹ะน ะดะตะฝัŒ" diff --git a/po/tr.po b/po/tr.po new file mode 100644 index 0000000..641ba55 --- /dev/null +++ b/po/tr.po @@ -0,0 +1,554 @@ +# Turkish translation for rdfeed. +# Copyright (C) 2026 Felitendo +# This file is distributed under the same license as rdfeed. +# +msgid "" +msgstr "" +"Project-Id-Version: rdfeed 1.0.0\n" +"Report-Msgid-Bugs-To: https://github.com/LoonixTools/rdfeed/issues\n" +"POT-Creation-Date: 2026-10-02 10:29+0200\n" +"PO-Revision-Date: 2026-10-02 09:30+0200\n" +"Last-Translator: Felitendo\n" +"Language-Team: Turkish\n" +"Language: tr\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" + +#: src/rdfeed:79 src/rdfeed:103 +#, sh-printf-format +msgid "Could not write to %s." +msgstr "%s konumuna yazฤฑlamadฤฑ." + +#: src/rdfeed:82 +#, sh-printf-format +msgid "Signing in to %sโ€ฆ" +msgstr "%s รผzerinde oturum aรงฤฑlฤฑyorโ€ฆ" + +#: src/rdfeed:100 +msgid "" +"The password could not be saved in the keyring, so each app asks for it." +msgstr "" +"Parola anahtarlฤฑฤŸa kaydedilemedi, bu yรผzden her uygulama parolayฤฑ soracak." + +#: src/rdfeed:111 +#, sh-printf-format +msgid "1 app from %s is now in your app menu." +msgstr "%s kaynaฤŸฤฑndan 1 uygulama artฤฑk uygulama menรผnรผzde." + +#: src/rdfeed:113 +#, sh-printf-format +msgid "%d apps from %s are now in your app menu." +msgstr "%d uygulama (%s) artฤฑk uygulama menรผnรผzde." + +#: src/rdfeed:129 +msgid "" +"Your IT department has the address. A work e-mail address often works too." +msgstr "Adres BT bรถlรผmรผnรผzde. ร‡oฤŸu zaman iลŸ e-posta adresiniz de iลŸe yarar." + +#: src/rdfeed:130 +msgid "Feed address, server or e-mail:" +msgstr "AkฤฑลŸ adresi, sunucu ya da e-posta:" + +#: src/rdfeed:137 +#, sh-printf-format +msgid "No feed is registered for %s. Ask your IT department for the address." +msgstr "%s iรงin kayฤฑtlฤฑ bir akฤฑลŸ yok. Adresi BT bรถlรผmรผnรผze sorun." + +#: src/rdfeed:142 src/rdfeed:156 +msgid "User name (DOMAIN\\name or name@domain):" +msgstr "Kullanฤฑcฤฑ adฤฑ (ALAN\\ad ya da ad@alan):" + +#: src/rdfeed:146 src/rdfeed:159 +msgid "Password:" +msgstr "Parola:" + +#: src/rdfeed:178 src/rdfeed:236 +#, sh-printf-format +msgid "No workspace yet. Add one with `%s add`." +msgstr "Henรผz รงalฤฑลŸma alanฤฑ yok. `%s add` ile bir tane ekleyin." + +#: src/rdfeed:187 src/lib/launch.sh:220 +#, sh-printf-format +msgid "No password saved for %s. Sign in again in rdfeed." +msgstr "%s iรงin kayฤฑtlฤฑ parola yok. rdfeed iรงinde yeniden oturum aรงฤฑn." + +#: src/rdfeed:191 src/lib/launch.sh:149 +#, sh-printf-format +msgid "Password for %s:" +msgstr "%s parolasฤฑ:" + +#: src/rdfeed:195 +#, sh-printf-format +msgid "Refreshing %sโ€ฆ" +msgstr "%s yenileniyorโ€ฆ" + +#: src/rdfeed:205 +msgid "The password no longer works. Sign in again in rdfeed." +msgstr "Parola artฤฑk รงalฤฑลŸmฤฑyor. rdfeed iรงinde yeniden oturum aรงฤฑn." + +#: src/rdfeed:214 +#, sh-printf-format +msgid "%s: %d apps" +msgstr "%s: %d uygulama" + +#: src/rdfeed:222 +#, sh-printf-format +msgid "Which workspace? See `%s list` for the names." +msgstr "Hangi รงalฤฑลŸma alanฤฑ? Adlarฤฑ `%s list` gรถsterir." + +#: src/rdfeed:229 +#, sh-printf-format +msgid "%s and its apps are removed." +msgstr "%s ve uygulamalarฤฑ kaldฤฑrฤฑldฤฑ." + +#: src/rdfeed:253 +#, sh-printf-format +msgid "Which app? See `%s list`." +msgstr "Hangi uygulama? `%s list` komutuna bakฤฑn." + +#: src/rdfeed:268 src/lib/launch.sh:199 +#, sh-printf-format +msgid "There is no app %s. See `%s list`." +msgstr "%s diye bir uygulama yok. `%s list` komutuna bakฤฑn." + +#: src/rdfeed:270 +#, sh-printf-format +msgid "Several workspaces have %s. Write it as WORKSPACE/APP, see `%s list`." +msgstr "" +"%s birden รงok รงalฤฑลŸma alanฤฑnda var. ร‡ALIลžMA_ALANI/UYGULAMA biรงiminde yazฤฑn, " +"`%s list` komutuna bakฤฑn." + +#: src/rdfeed:278 +msgid "Usage: rdfeed [command]" +msgstr "Kullanฤฑm: rdfeed [komut]" + +#: src/rdfeed:280 +msgid "Commands:" +msgstr "Komutlar:" + +#: src/rdfeed:281 +msgid "Add the workspace of your company" +msgstr "ลžirketinizin รงalฤฑลŸma alanฤฑnฤฑ ekle" + +#: src/rdfeed:282 +msgid "Load the apps again" +msgstr "Uygulamalarฤฑ yeniden yรผkle" + +#: src/rdfeed:283 +msgid "List the workspaces and their apps" +msgstr "ร‡alฤฑลŸma alanlarฤฑnฤฑ ve uygulamalarฤฑnฤฑ listele" + +#: src/rdfeed:284 +msgid "Start an app, or open a file with it" +msgstr "Bir uygulama baลŸlat ya da onunla bir dosya aรง" + +#: src/rdfeed:285 +msgid "Remove a workspace and its apps" +msgstr "Bir รงalฤฑลŸma alanฤฑnฤฑ ve uygulamalarฤฑnฤฑ kaldฤฑr" + +#: src/rdfeed:286 +msgid "Show this help" +msgstr "Bu yardฤฑmฤฑ gรถster" + +#: src/rdfeed:287 +msgid "Show the version" +msgstr "Sรผrรผmรผ gรถster" + +#: src/rdfeed:289 +msgid "Without a command an interactive menu is shown." +msgstr "Komut verilmezse etkileลŸimli bir menรผ gรถsterilir." + +#: src/rdfeed:313 +#, sh-printf-format +msgid "Unknown command: %s" +msgstr "Bilinmeyen komut: %s" + +#: src/lib/common.sh:174 src/lib/menu.sh:251 +msgid "never" +msgstr "hiรงbir zaman" + +#: src/lib/common.sh:184 +msgid "just now" +msgstr "az รถnce" + +#: src/lib/common.sh:186 +msgid "1 minute ago" +msgstr "1 dakika รถnce" + +#: src/lib/common.sh:189 +#, sh-printf-format +msgid "%d minutes ago" +msgstr "%d dakika รถnce" + +#: src/lib/common.sh:191 +msgid "1 hour ago" +msgstr "1 saat รถnce" + +#: src/lib/common.sh:194 +#, sh-printf-format +msgid "%d hours ago" +msgstr "%d saat รถnce" + +#: src/lib/common.sh:196 +msgid "1 day ago" +msgstr "1 gรผn รถnce" + +#: src/lib/common.sh:199 +#, sh-printf-format +msgid "%d days ago" +msgstr "%d gรผn รถnce" + +#: src/lib/launch.sh:169 +msgid "" +"Your password has expired. Change it on Windows or on the RD Web page, then " +"sign in again in rdfeed." +msgstr "" +"Parolanฤฑzฤฑn sรผresi doldu. Windows'ta ya da RD Web sayfasฤฑnda deฤŸiลŸtirin, " +"sonra rdfeed iรงinde yeniden oturum aรงฤฑn." + +#: src/lib/launch.sh:171 +msgid "Your account is locked. Ask your IT department." +msgstr "Hesabฤฑnฤฑz kilitli. BT bรถlรผmรผnรผze sorun." + +#: src/lib/launch.sh:173 +msgid "The server did not accept the password. Sign in again in rdfeed." +msgstr "Sunucu parolayฤฑ kabul etmedi. rdfeed iรงinde yeniden oturum aรงฤฑn." + +#: src/lib/launch.sh:175 +msgid "Could not reach the server. Check the internet connection." +msgstr "Sunucuya ulaลŸฤฑlamadฤฑ. ฤฐnternet baฤŸlantฤฑsฤฑnฤฑ kontrol edin." + +#: src/lib/launch.sh:178 +#, sh-printf-format +msgid "FreeRDP stopped: %s" +msgstr "FreeRDP durdu: %s" + +#: src/lib/launch.sh:205 src/lib/launch.sh:206 +#, sh-printf-format +msgid "File not found: %s" +msgstr "Dosya bulunamadฤฑ: %s" + +#: src/lib/launch.sh:212 src/lib/launch.sh:213 +msgid "" +"FreeRDP 3 is missing. Install it to start the apps (the package is called " +"freerdp)." +msgstr "" +"FreeRDP 3 eksik. Uygulamalarฤฑ baลŸlatmak iรงin kurun (paketin adฤฑ freerdp)." + +#: src/lib/launch.sh:277 +#, sh-printf-format +msgid "Opening %s in %s" +msgstr "%s, %s ile aรงฤฑlฤฑyor" + +#: src/lib/launch.sh:278 +#, sh-printf-format +msgid "If it does not open by itself, open it in the app from the drive %s." +msgstr "KendiliฤŸinden aรงฤฑlmazsa uygulamada %s sรผrรผcรผsรผnden aรงฤฑn." + +#: src/lib/launch.sh:280 +#, sh-printf-format +msgid "Starting %s" +msgstr "%s baลŸlatฤฑlฤฑyor" + +#: src/lib/launch.sh:281 +msgid "" +"If your company uses MFA, confirm the sign-in in your authenticator app." +msgstr "" +"ลžirketiniz MFA kullanฤฑyorsa doฤŸrulama uygulamanฤฑzda oturum aรงmayฤฑ onaylayฤฑn." + +#: src/lib/launch.sh:290 +msgid "A second sign-in is coming" +msgstr "ฤฐkinci bir oturum aรงma geliyor" + +#: src/lib/launch.sh:291 +#, sh-printf-format +msgid "" +"Confirm the second prompt in your authenticator app too. It comes in about " +"%d seconds." +msgstr "" +"ฤฐkinci isteฤŸi de doฤŸrulama uygulamanฤฑzda onaylayฤฑn. YaklaลŸฤฑk %d saniye " +"iรงinde gelir." + +#: src/lib/launch.sh:301 +#, sh-printf-format +msgid "%s could not start" +msgstr "%s baลŸlatฤฑlamadฤฑ" + +#: src/lib/launch.sh:303 +#, sh-printf-format +msgid "Log: %s" +msgstr "Gรผnlรผk: %s" + +#. TRANSLATORS: the letter after "[" is the key for yes. y works too. +#: src/lib/menu.sh:93 +msgid "[y/N]" +msgstr "[e/H]" + +#: src/lib/menu.sh:163 +msgid "1 app" +msgstr "1 uygulama" + +#: src/lib/menu.sh:165 +#, sh-printf-format +msgid "%d apps" +msgstr "%d uygulama" + +#: src/lib/menu.sh:190 +msgid "No workspace yet. Add the one of your company with [1]." +msgstr "Henรผz รงalฤฑลŸma alanฤฑ yok. ลžirketinizinkini [1] ile ekleyin." + +#: src/lib/menu.sh:233 +msgid "off" +msgstr "kapalฤฑ" + +#: src/lib/menu.sh:234 +msgid "home folder" +msgstr "ev klasรถrรผ" + +#: src/lib/menu.sh:237 +msgid "Documents" +msgstr "Belgeler" + +#: src/lib/menu.sh:238 +msgid "Downloads" +msgstr "ฤฐndirilenler" + +#: src/lib/menu.sh:243 +msgid "not found" +msgstr "bulunamadฤฑ" + +#: src/lib/menu.sh:246 +#, sh-printf-format +msgid "automatic (%d %%)" +msgstr "otomatik (%%%d)" + +#: src/lib/menu.sh:248 +msgid "classic" +msgstr "klasik" + +#: src/lib/menu.sh:249 +msgid "GFX" +msgstr "GFX" + +#: src/lib/menu.sh:250 +msgid "always" +msgstr "her zaman" + +#: src/lib/menu.sh:252 +msgid "automatic" +msgstr "otomatik" + +#: src/lib/menu.sh:261 +msgid "" +"The apps only see the files on the server. Pick a folder to open and save " +"files of this computer in them." +msgstr "" +"Uygulamalar yalnฤฑzca sunucudaki dosyalarฤฑ gรถrรผr. Bu bilgisayarฤฑn dosyalarฤฑnฤฑ " +"aรงฤฑp kaydedebilmeleri iรงin bir klasรถr seรงin." + +#: src/lib/menu.sh:262 +msgid "" +"The apps can open and save files in this folder. It shows up as a drive in " +"them." +msgstr "" +"Uygulamalar bu klasรถrde dosya aรงฤฑp kaydedebilir. Klasรถr onlarda bir sรผrรผcรผ " +"olarak gรถrรผnรผr." + +#: src/lib/menu.sh:263 +msgid "" +"Offers the apps under \"Open with\" for their file types, for example Word " +"for .docx. The folder of the file is shared with the app while it is open." +msgstr "" +"Uygulamalarฤฑ dosya tรผrleri iรงin \"Birlikte aรง\" menรผsรผnde sunar, " +"รถrneฤŸin .docx iรงin Word. Dosya aรงฤฑk olduฤŸu sรผrece klasรถrรผ paylaลŸฤฑlฤฑr." + +#: src/lib/menu.sh:264 +msgid "" +"How big the apps are drawn. Automatic follows the scale of your desktop, so " +"they are sharp and the right size." +msgstr "" +"Uygulamalarฤฑn ne kadar bรผyรผk รงizileceฤŸi. Otomatik, masaรผstรผnรผzรผn รถlรงeฤŸini " +"izler; bรถylece keskin ve doฤŸru boyutta olurlar." + +#: src/lib/menu.sh:265 +msgid "Plays the sound of the apps here, for calls and videos." +msgstr "Uygulamalarฤฑn sesini burada รงalar; aramalar ve videolar iรงin." + +#: src/lib/menu.sh:266 +msgid "" +"Paints every window right. But some servers end the session right after " +"signing in, then go back to GFX." +msgstr "Her pencereyi doฤŸru รงizer. Ama bazฤฑ sunucular oturum aรงar aรงmaz oturumu kapatฤฑr, o zaman GFX'e geri dรถnรผn." + +#: src/lib/menu.sh:267 +msgid "" +"Works with every server. FreeRDP may not repaint a window after it was " +"minimized or resized." +msgstr "Her sunucuyla รงalฤฑลŸฤฑr. FreeRDP kรผรงรผltรผlen ya da boyutu deฤŸiลŸen bir pencereyi yeniden รงizmeyebilir." + +#: src/lib/menu.sh:268 +msgid "" +"Always waits before the second sign-in. For when the second prompt does not " +"come with automatic." +msgstr "" +"ฤฐkinci oturum aรงmadan รถnce hep bekler. Otomatikte ikinci istek gelmiyorsa " +"kullanฤฑn." + +#: src/lib/menu.sh:269 +msgid "Never waits. Right when your company has no MFA." +msgstr "Hiรง beklemez. ลžirketiniz MFA kullanmฤฑyorsa doฤŸru seรงim." + +#: src/lib/menu.sh:270 +msgid "" +"Many servers with MFA need a second sign-in, and its prompt only comes after " +"a short wait. Automatic waits when the first sign-in took a while." +msgstr "" +"MFA kullanan birรงok sunucu ikinci bir oturum aรงma ister ve isteฤŸi ancak kฤฑsa " +"bir beklemeden sonra gelir. Otomatik, ilk oturum aรงma biraz sรผrdรผyse bekler." + +#: src/lib/menu.sh:273 +msgid "" +"Loads the list of apps again once a day, so new apps show up by themselves." +msgstr "" +"Uygulama listesini gรผnde bir kez yeniden yรผkler; bรถylece yeni uygulamalar " +"kendiliฤŸinden gรถrรผnรผr." + +#: src/lib/menu.sh:275 src/lib/menu.sh:330 +msgid "Needs a systemd user session." +msgstr "Bir systemd kullanฤฑcฤฑ oturumu gerekir." + +#: src/lib/menu.sh:322 src/lib/menu.sh:325 +msgid "Could not save the setting." +msgstr "Ayar kaydedilemedi." + +#: src/lib/menu.sh:371 src/lib/menu.sh:609 +msgid "Settings" +msgstr "Ayarlar" + +#: src/lib/menu.sh:372 +msgid "โ†‘โ†“ select โ†โ†’ or Space: change q: back" +msgstr "โ†‘โ†“ seรง โ†โ†’ ya da BoลŸluk: deฤŸiลŸtir q: geri" + +#: src/lib/menu.sh:373 +msgid "ON" +msgstr "Aร‡IK" + +#: src/lib/menu.sh:374 +msgid "OFF" +msgstr "KAPALI" + +#: src/lib/menu.sh:456 src/lib/menu.sh:608 +msgid "Workspaces" +msgstr "ร‡alฤฑลŸma alanlarฤฑ" + +#. TRANSLATORS: keep the letters, they are the keys. +#: src/lib/menu.sh:458 +msgid "" +"โ†‘โ†“ select r: refresh s: sign in again d: remove a: add q: back" +msgstr "" +"โ†‘โ†“ seรง r: yenile s: yeniden oturum aรง d: kaldฤฑr a: ekle q: geri" + +#: src/lib/menu.sh:459 +msgid "No workspace yet. Press a to add one." +msgstr "Henรผz รงalฤฑลŸma alanฤฑ yok. Eklemek iรงin a tuลŸuna basฤฑn." + +#: src/lib/menu.sh:484 +#, sh-printf-format +msgid "refreshed %s" +msgstr "%s yenilendi" + +#: src/lib/menu.sh:512 +#, sh-printf-format +msgid "Remove \"%s\" and its apps?" +msgstr "\"%s\" ve uygulamalarฤฑ kaldฤฑrฤฑlsฤฑn mฤฑ?" + +#: src/lib/menu.sh:536 src/lib/menu.sh:607 +msgid "Start an app" +msgstr "Bir uygulama baลŸlat" + +#: src/lib/menu.sh:537 +msgid "โ†‘โ†“ select Enter: start q: back" +msgstr "โ†‘โ†“ seรง Enter: baลŸlat q: geri" + +#: src/lib/menu.sh:538 +msgid "No apps yet. Add a workspace first." +msgstr "Henรผz uygulama yok. ร–nce bir รงalฤฑลŸma alanฤฑ ekleyin." + +#: src/lib/menu.sh:579 +#, sh-printf-format +msgid "%s is starting. It opens in a window of its own." +msgstr "%s baลŸlatฤฑlฤฑyor. Kendi penceresinde aรงฤฑlฤฑr." + +#: src/lib/menu.sh:606 +msgid "Add a workspace" +msgstr "ร‡alฤฑลŸma alanฤฑ ekle" + +#: src/lib/menu.sh:610 +msgid "Quit" +msgstr "ร‡ฤฑk" + +#: src/lib/workspace.sh:163 +msgid "The server did not accept the user name or password." +msgstr "Sunucu kullanฤฑcฤฑ adฤฑnฤฑ ya da parolayฤฑ kabul etmedi." + +#: src/lib/workspace.sh:164 +#, sh-printf-format +msgid "Could not reach the server: %s" +msgstr "Sunucuya ulaลŸฤฑlamadฤฑ: %s" + +#: src/lib/workspace.sh:165 +#, sh-printf-format +msgid "There is no RD Web feed at %s." +msgstr "%s adresinde RD Web akฤฑลŸฤฑ yok." + +#: src/lib/workspace.sh:166 +#, sh-printf-format +msgid "Could not load the apps: %s" +msgstr "Uygulamalar yรผklenemedi: %s" + +#: src/lib/workspace.sh:282 +#, sh-printf-format +msgid "Remote desktop of %s" +msgstr "%s uzak masaรผstรผ" + +#: src/lib/workspace.sh:284 +#, sh-printf-format +msgid "%s from %s" +msgstr "%s (%s)" + +#: src/lib/menu.sh +msgid "Apps" +msgstr "Uygulamalar" + +#: src/lib/menu.sh +msgid "Share a folder" +msgstr "Klasรถr paylaลŸ" + +#: src/lib/menu.sh +msgid "Open files with the apps" +msgstr "Dosyalarฤฑ uygulamalarla aรง" + +#: src/lib/menu.sh +msgid "Size" +msgstr "Boyut" + +#: src/lib/menu.sh +msgid "Sound" +msgstr "Ses" + +#: src/lib/menu.sh +msgid "Graphics" +msgstr "Grafik" + +#: src/lib/menu.sh +msgid "Signing in" +msgstr "Oturum aรงma" + +#: src/lib/menu.sh +msgid "Wait for the second MFA prompt" +msgstr "ฤฐkinci MFA isteฤŸini bekle" + +#: src/lib/menu.sh +msgid "Refresh the apps every day" +msgstr "Uygulamalarฤฑ her gรผn yenile" diff --git a/po/uk.po b/po/uk.po new file mode 100644 index 0000000..28e2ab2 --- /dev/null +++ b/po/uk.po @@ -0,0 +1,559 @@ +# Ukrainian translation for rdfeed. +# Copyright (C) 2026 Felitendo +# This file is distributed under the same license as rdfeed. +# +msgid "" +msgstr "" +"Project-Id-Version: rdfeed 1.0.0\n" +"Report-Msgid-Bugs-To: https://github.com/LoonixTools/rdfeed/issues\n" +"POT-Creation-Date: 2026-10-02 10:29+0200\n" +"PO-Revision-Date: 2026-10-02 09:30+0200\n" +"Last-Translator: Felitendo\n" +"Language-Team: Ukrainian\n" +"Language: uk\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" + +#: src/rdfeed:79 src/rdfeed:103 +#, sh-printf-format +msgid "Could not write to %s." +msgstr "ะะต ะฒะดะฐะปะพัั ะทะฐะฟะธัะฐั‚ะธ ะฒ %s." + +#: src/rdfeed:82 +#, sh-printf-format +msgid "Signing in to %sโ€ฆ" +msgstr "ะ’ั…ั–ะด ะฝะฐ %sโ€ฆ" + +#: src/rdfeed:100 +msgid "" +"The password could not be saved in the keyring, so each app asks for it." +msgstr "" +"ะะต ะฒะดะฐะปะพัั ะทะฑะตั€ะตะณั‚ะธ ะฟะฐั€ะพะปัŒ ัƒ ะฒ'ัะทั†ั– ะบะปัŽั‡ั–ะฒ, ั‚ะพะถ ะบะพะถะตะฝ ะทะฐัั‚ะพััƒะฝะพะบ ะฟะธั‚ะฐั‚ะธะผะต " +"ะนะพะณะพ." + +#: src/rdfeed:111 +#, sh-printf-format +msgid "1 app from %s is now in your app menu." +msgstr "1 ะทะฐัั‚ะพััƒะฝะพะบ ั–ะท %s ั‚ะตะฟะตั€ ัƒ ะผะตะฝัŽ ะทะฐัั‚ะพััƒะฝะบั–ะฒ." + +#: src/rdfeed:113 +#, sh-printf-format +msgid "%d apps from %s are now in your app menu." +msgstr "ะ—ะฐัั‚ะพััƒะฝะบั–ะฒ: %d (ั–ะท %s). ะขะตะฟะตั€ ะฒะพะฝะธ ะฒ ะผะตะฝัŽ ะทะฐัั‚ะพััƒะฝะบั–ะฒ." + +#: src/rdfeed:129 +msgid "" +"Your IT department has the address. A work e-mail address often works too." +msgstr "" +"ะะดั€ะตััƒ ะผะฐั” ะฒะฐัˆ ะ†ะข-ะฒั–ะดะดั–ะป. ะงะฐัั‚ะพ ะฟั–ะดั…ะพะดะธั‚ัŒ ั– ั€ะพะฑะพั‡ะฐ ะฐะดั€ะตัะฐ ะตะปะตะบั‚ั€ะพะฝะฝะพั— ะฟะพัˆั‚ะธ." + +#: src/rdfeed:130 +msgid "Feed address, server or e-mail:" +msgstr "ะะดั€ะตัะฐ ั„ั–ะดะฐ, ัะตั€ะฒะตั€ ะฐะฑะพ e-mail:" + +#: src/rdfeed:137 +#, sh-printf-format +msgid "No feed is registered for %s. Ask your IT department for the address." +msgstr "ะ”ะปั %s ะฝะต ะทะฐั€ะตั”ัั‚ั€ะพะฒะฐะฝะพ ั„ั–ะด. ะ—ะฐะฟะธั‚ะฐะนั‚ะต ะฐะดั€ะตััƒ ะฒ ะ†ะข-ะฒั–ะดะดั–ะปั–." + +#: src/rdfeed:142 src/rdfeed:156 +msgid "User name (DOMAIN\\name or name@domain):" +msgstr "ะ†ะผ'ั ะบะพั€ะธัั‚ัƒะฒะฐั‡ะฐ (ะ”ะžะœะ•ะ\\ั–ะผ'ั ะฐะฑะพ ั–ะผ'ั@ะดะพะผะตะฝ):" + +#: src/rdfeed:146 src/rdfeed:159 +msgid "Password:" +msgstr "ะŸะฐั€ะพะปัŒ:" + +#: src/rdfeed:178 src/rdfeed:236 +#, sh-printf-format +msgid "No workspace yet. Add one with `%s add`." +msgstr "ะ ะพะฑะพั‡ะธั… ะฟั€ะพัั‚ะพั€ั–ะฒ ั‰ะต ะฝะตะผะฐั”. ะ”ะพะดะฐะนั‚ะต ะนะพะณะพ ะบะพะผะฐะฝะดะพัŽ `%s add`." + +#: src/rdfeed:187 src/lib/launch.sh:220 +#, sh-printf-format +msgid "No password saved for %s. Sign in again in rdfeed." +msgstr "ะ”ะปั %s ะฝะต ะทะฑะตั€ะตะถะตะฝะพ ะฟะฐั€ะพะปัŒ. ะฃะฒั–ะนะดั–ั‚ัŒ ะทะฝะพะฒัƒ ะฒ rdfeed." + +#: src/rdfeed:191 src/lib/launch.sh:149 +#, sh-printf-format +msgid "Password for %s:" +msgstr "ะŸะฐั€ะพะปัŒ ะดะปั %s:" + +#: src/rdfeed:195 +#, sh-printf-format +msgid "Refreshing %sโ€ฆ" +msgstr "ะžะฝะพะฒะปะตะฝะฝั %sโ€ฆ" + +#: src/rdfeed:205 +msgid "The password no longer works. Sign in again in rdfeed." +msgstr "ะŸะฐั€ะพะปัŒ ะฑั–ะปัŒัˆะต ะฝะต ะฟั–ะดั…ะพะดะธั‚ัŒ. ะฃะฒั–ะนะดั–ั‚ัŒ ะทะฝะพะฒัƒ ะฒ rdfeed." + +#: src/rdfeed:214 +#, sh-printf-format +msgid "%s: %d apps" +msgstr "%s: ะทะฐัั‚ะพััƒะฝะบั–ะฒ: %d" + +#: src/rdfeed:222 +#, sh-printf-format +msgid "Which workspace? See `%s list` for the names." +msgstr "ะฏะบะธะน ั€ะพะฑะพั‡ะธะน ะฟั€ะพัั‚ั–ั€? ะะฐะทะฒะธ ะฟะพะบะฐะทัƒั” `%s list`." + +#: src/rdfeed:229 +#, sh-printf-format +msgid "%s and its apps are removed." +msgstr "ะ’ะธะดะฐะปะตะฝะพ: %s ั€ะฐะทะพะผ ั–ะท ะทะฐัั‚ะพััƒะฝะบะฐะผะธ." + +#: src/rdfeed:253 +#, sh-printf-format +msgid "Which app? See `%s list`." +msgstr "ะฏะบะธะน ะทะฐัั‚ะพััƒะฝะพะบ? ะ”ะธะฒั–ั‚ัŒัั `%s list`." + +#: src/rdfeed:268 src/lib/launch.sh:199 +#, sh-printf-format +msgid "There is no app %s. See `%s list`." +msgstr "ะ—ะฐัั‚ะพััƒะฝะบัƒ %s ะฝะตะผะฐั”. ะ”ะธะฒั–ั‚ัŒัั `%s list`." + +#: src/rdfeed:270 +#, sh-printf-format +msgid "Several workspaces have %s. Write it as WORKSPACE/APP, see `%s list`." +msgstr "" +"%s ั” ะฒ ะบั–ะปัŒะบะพั… ั€ะพะฑะพั‡ะธั… ะฟั€ะพัั‚ะพั€ะฐั…. ะ’ะบะฐะถั–ั‚ัŒ ัƒ ะฒะธะณะปัะดั– ะŸะ ะžะกะขะ†ะ /ะ—ะะกะขะžะกะฃะะžะš, " +"ะดะธะฒั–ั‚ัŒัั `%s list`." + +#: src/rdfeed:278 +msgid "Usage: rdfeed [command]" +msgstr "ะ’ะธะบะพั€ะธัั‚ะฐะฝะฝั: rdfeed [ะบะพะผะฐะฝะดะฐ]" + +#: src/rdfeed:280 +msgid "Commands:" +msgstr "ะšะพะผะฐะฝะดะธ:" + +#: src/rdfeed:281 +msgid "Add the workspace of your company" +msgstr "ะ”ะพะดะฐั‚ะธ ั€ะพะฑะพั‡ะธะน ะฟั€ะพัั‚ั–ั€ ะฒะฐัˆะพั— ะบะพะผะฟะฐะฝั–ั—" + +#: src/rdfeed:282 +msgid "Load the apps again" +msgstr "ะ—ะฐะฒะฐะฝั‚ะฐะถะธั‚ะธ ะทะฐัั‚ะพััƒะฝะบะธ ะทะฝะพะฒัƒ" + +#: src/rdfeed:283 +msgid "List the workspaces and their apps" +msgstr "ะŸะพะบะฐะทะฐั‚ะธ ั€ะพะฑะพั‡ั– ะฟั€ะพัั‚ะพั€ะธ ั‚ะฐ ั—ั…ะฝั– ะทะฐัั‚ะพััƒะฝะบะธ" + +#: src/rdfeed:284 +msgid "Start an app, or open a file with it" +msgstr "ะ—ะฐะฟัƒัั‚ะธั‚ะธ ะทะฐัั‚ะพััƒะฝะพะบ ะฐะฑะพ ะฒั–ะดะบั€ะธั‚ะธ ะฒ ะฝัŒะพะผัƒ ั„ะฐะนะป" + +#: src/rdfeed:285 +msgid "Remove a workspace and its apps" +msgstr "ะ’ะธะดะฐะปะธั‚ะธ ั€ะพะฑะพั‡ะธะน ะฟั€ะพัั‚ั–ั€ ั– ะนะพะณะพ ะทะฐัั‚ะพััƒะฝะบะธ" + +#: src/rdfeed:286 +msgid "Show this help" +msgstr "ะŸะพะบะฐะทะฐั‚ะธ ั†ัŽ ะดะพะฒั–ะดะบัƒ" + +#: src/rdfeed:287 +msgid "Show the version" +msgstr "ะŸะพะบะฐะทะฐั‚ะธ ะฒะตั€ัั–ัŽ" + +#: src/rdfeed:289 +msgid "Without a command an interactive menu is shown." +msgstr "ะ‘ะตะท ะบะพะผะฐะฝะดะธ ะฒั–ะดะบั€ะธะฒะฐั”ั‚ัŒัั ั–ะฝั‚ะตั€ะฐะบั‚ะธะฒะฝะต ะผะตะฝัŽ." + +#: src/rdfeed:313 +#, sh-printf-format +msgid "Unknown command: %s" +msgstr "ะะตะฒั–ะดะพะผะฐ ะบะพะผะฐะฝะดะฐ: %s" + +#: src/lib/common.sh:174 src/lib/menu.sh:251 +msgid "never" +msgstr "ะฝั–ะบะพะปะธ" + +#: src/lib/common.sh:184 +msgid "just now" +msgstr "ั‰ะพะนะฝะพ" + +#: src/lib/common.sh:186 +msgid "1 minute ago" +msgstr "1 ั…ะฒะธะปะธะฝัƒ ั‚ะพะผัƒ" + +#: src/lib/common.sh:189 +#, sh-printf-format +msgid "%d minutes ago" +msgstr "%d ั…ะฒ ั‚ะพะผัƒ" + +#: src/lib/common.sh:191 +msgid "1 hour ago" +msgstr "1 ะณะพะดะธะฝัƒ ั‚ะพะผัƒ" + +#: src/lib/common.sh:194 +#, sh-printf-format +msgid "%d hours ago" +msgstr "%d ะณะพะด ั‚ะพะผัƒ" + +#: src/lib/common.sh:196 +msgid "1 day ago" +msgstr "1 ะดะตะฝัŒ ั‚ะพะผัƒ" + +#: src/lib/common.sh:199 +#, sh-printf-format +msgid "%d days ago" +msgstr "%d ะดะฝ. ั‚ะพะผัƒ" + +#: src/lib/launch.sh:169 +msgid "" +"Your password has expired. Change it on Windows or on the RD Web page, then " +"sign in again in rdfeed." +msgstr "" +"ะขะตั€ะผั–ะฝ ะดั–ั— ะฟะฐั€ะพะปั ะผะธะฝัƒะฒ. ะ—ะผั–ะฝั–ั‚ัŒ ะนะพะณะพ ัƒ Windows ะฐะฑะพ ะฝะฐ ัั‚ะพั€ั–ะฝั†ั– RD Web, " +"ะฟะพั‚ั–ะผ ะทะฝะพะฒัƒ ัƒะฒั–ะนะดั–ั‚ัŒ ัƒ rdfeed." + +#: src/lib/launch.sh:171 +msgid "Your account is locked. Ask your IT department." +msgstr "ะžะฑะปั–ะบะพะฒะธะน ะทะฐะฟะธั ะทะฐะฑะปะพะบะพะฒะฐะฝะพ. ะ—ะฒะตั€ะฝั–ั‚ัŒัั ะดะพ ะ†ะข-ะฒั–ะดะดั–ะปัƒ." + +#: src/lib/launch.sh:173 +msgid "The server did not accept the password. Sign in again in rdfeed." +msgstr "ะกะตั€ะฒะตั€ ะฝะต ะฟั€ะธะนะฝัะฒ ะฟะฐั€ะพะปัŒ. ะฃะฒั–ะนะดั–ั‚ัŒ ะทะฝะพะฒัƒ ะฒ rdfeed." + +#: src/lib/launch.sh:175 +msgid "Could not reach the server. Check the internet connection." +msgstr "ะะต ะฒะดะฐะปะพัั ะทะฒ'ัะทะฐั‚ะธัั ั–ะท ัะตั€ะฒะตั€ะพะผ. ะŸะตั€ะตะฒั–ั€ั‚ะต ะฟั–ะดะบะปัŽั‡ะตะฝะฝั ะดะพ ั–ะฝั‚ะตั€ะฝะตั‚ัƒ." + +#: src/lib/launch.sh:178 +#, sh-printf-format +msgid "FreeRDP stopped: %s" +msgstr "FreeRDP ะทัƒะฟะธะฝะธะฒัั: %s" + +#: src/lib/launch.sh:205 src/lib/launch.sh:206 +#, sh-printf-format +msgid "File not found: %s" +msgstr "ะคะฐะนะป ะฝะต ะทะฝะฐะนะดะตะฝะพ: %s" + +#: src/lib/launch.sh:212 src/lib/launch.sh:213 +msgid "" +"FreeRDP 3 is missing. Install it to start the apps (the package is called " +"freerdp)." +msgstr "" +"ะะตะผะฐั” FreeRDP 3. ะฃัั‚ะฐะฝะพะฒั–ั‚ัŒ ะนะพะณะพ, ั‰ะพะฑ ะทะฐะฟัƒัะบะฐั‚ะธ ะทะฐัั‚ะพััƒะฝะบะธ (ะฟะฐะบะตั‚ " +"ะฝะฐะทะธะฒะฐั”ั‚ัŒัั freerdp)." + +#: src/lib/launch.sh:277 +#, sh-printf-format +msgid "Opening %s in %s" +msgstr "ะ’ั–ะดะบั€ะธั‚ั‚ั %s ัƒ %s" + +#: src/lib/launch.sh:278 +#, sh-printf-format +msgid "If it does not open by itself, open it in the app from the drive %s." +msgstr "ะฏะบั‰ะพ ั„ะฐะนะป ะฝะต ะฒั–ะดะบั€ะธั”ั‚ัŒัั ัะฐะผ, ะฒั–ะดะบั€ะธะนั‚ะต ะนะพะณะพ ะฒ ะทะฐัั‚ะพััƒะฝะบัƒ ะท ะดะธัะบะฐ %s." + +#: src/lib/launch.sh:280 +#, sh-printf-format +msgid "Starting %s" +msgstr "ะ—ะฐะฟัƒัะบ: %s" + +#: src/lib/launch.sh:281 +msgid "" +"If your company uses MFA, confirm the sign-in in your authenticator app." +msgstr "" +"ะฏะบั‰ะพ ะฒะฐัˆะฐ ะบะพะผะฟะฐะฝั–ั ะฒะธะบะพั€ะธัั‚ะพะฒัƒั” MFA, ะฟั–ะดั‚ะฒะตั€ะดัŒั‚ะต ะฒั…ั–ะด ัƒ ะทะฐัั‚ะพััƒะฝะบัƒ " +"ะฐะฒั‚ะตะฝั‚ะธั„ั–ะบะฐั†ั–ั—." + +#: src/lib/launch.sh:290 +msgid "A second sign-in is coming" +msgstr "ะ‘ัƒะดะต ะดั€ัƒะณะธะน ะฒั…ั–ะด" + +#: src/lib/launch.sh:291 +#, sh-printf-format +msgid "" +"Confirm the second prompt in your authenticator app too. It comes in about " +"%d seconds." +msgstr "" +"ะŸั–ะดั‚ะฒะตั€ะดัŒั‚ะต ั– ะดั€ัƒะณะธะน ะทะฐะฟะธั‚ ัƒ ะทะฐัั‚ะพััƒะฝะบัƒ ะฐะฒั‚ะตะฝั‚ะธั„ั–ะบะฐั†ั–ั—. ะ’ั–ะฝ ะฝะฐะดั–ะนะดะต " +"ะฟั€ะธะฑะปะธะทะฝะพ ะทะฐ %d ั." + +#: src/lib/launch.sh:301 +#, sh-printf-format +msgid "%s could not start" +msgstr "ะะต ะฒะดะฐะปะพัั ะทะฐะฟัƒัั‚ะธั‚ะธ: %s" + +#: src/lib/launch.sh:303 +#, sh-printf-format +msgid "Log: %s" +msgstr "ะ–ัƒั€ะฝะฐะป: %s" + +#. TRANSLATORS: the letter after "[" is the key for yes. y works too. +#: src/lib/menu.sh:93 +msgid "[y/N]" +msgstr "[ั‚/ะ]" + +#: src/lib/menu.sh:163 +msgid "1 app" +msgstr "1 ะทะฐัั‚ะพััƒะฝะพะบ" + +#: src/lib/menu.sh:165 +#, sh-printf-format +msgid "%d apps" +msgstr "ะทะฐัั‚ะพััƒะฝะบั–ะฒ: %d" + +#: src/lib/menu.sh:190 +msgid "No workspace yet. Add the one of your company with [1]." +msgstr "ะ ะพะฑะพั‡ะธั… ะฟั€ะพัั‚ะพั€ั–ะฒ ั‰ะต ะฝะตะผะฐั”. ะ”ะพะดะฐะนั‚ะต ะฟั€ะพัั‚ั–ั€ ะฒะฐัˆะพั— ะบะพะผะฟะฐะฝั–ั— ั‡ะตั€ะตะท [1]." + +#: src/lib/menu.sh:233 +msgid "off" +msgstr "ะฒะธะผะบ." + +#: src/lib/menu.sh:234 +msgid "home folder" +msgstr "ะดะพะผะฐัˆะฝั ั‚ะตะบะฐ" + +#: src/lib/menu.sh:237 +msgid "Documents" +msgstr "ะ”ะพะบัƒะผะตะฝั‚ะธ" + +#: src/lib/menu.sh:238 +msgid "Downloads" +msgstr "ะ—ะฐะฒะฐะฝั‚ะฐะถะตะฝะฝั" + +#: src/lib/menu.sh:243 +msgid "not found" +msgstr "ะฝะต ะทะฝะฐะนะดะตะฝะพ" + +#: src/lib/menu.sh:246 +#, sh-printf-format +msgid "automatic (%d %%)" +msgstr "ะฐะฒั‚ะพะผะฐั‚ะธั‡ะฝะพ (%d %%)" + +#: src/lib/menu.sh:248 +msgid "classic" +msgstr "ะบะปะฐัะธั‡ะฝะธะน" + +#: src/lib/menu.sh:249 +msgid "GFX" +msgstr "GFX" + +#: src/lib/menu.sh:250 +msgid "always" +msgstr "ะทะฐะฒะถะดะธ" + +#: src/lib/menu.sh:252 +msgid "automatic" +msgstr "ะฐะฒั‚ะพะผะฐั‚ะธั‡ะฝะพ" + +#: src/lib/menu.sh:261 +msgid "" +"The apps only see the files on the server. Pick a folder to open and save " +"files of this computer in them." +msgstr "" +"ะ—ะฐัั‚ะพััƒะฝะบะธ ะฑะฐั‡ะฐั‚ัŒ ะปะธัˆะต ั„ะฐะนะปะธ ะฝะฐ ัะตั€ะฒะตั€ั–. ะ’ะธะฑะตั€ั–ั‚ัŒ ั‚ะตะบัƒ, ั‰ะพะฑ ะฒะพะฝะธ ะผะพะณะปะธ " +"ะฒั–ะดะบั€ะธะฒะฐั‚ะธ ะน ะทะฑะตั€ั–ะณะฐั‚ะธ ั„ะฐะนะปะธ ั†ัŒะพะณะพ ะบะพะผะฟ'ัŽั‚ะตั€ะฐ." + +#: src/lib/menu.sh:262 +msgid "" +"The apps can open and save files in this folder. It shows up as a drive in " +"them." +msgstr "" +"ะ—ะฐัั‚ะพััƒะฝะบะธ ะผะพะถัƒั‚ัŒ ะฒั–ะดะบั€ะธะฒะฐั‚ะธ ะน ะทะฑะตั€ั–ะณะฐั‚ะธ ั„ะฐะนะปะธ ะฒ ั†ั–ะน ั‚ะตั†ั–. ะฃ ะฝะธั… ะฒะพะฝะฐ ะฒะธะดะฝะฐ " +"ัะบ ะดะธัะบ." + +#: src/lib/menu.sh:263 +msgid "" +"Offers the apps under \"Open with\" for their file types, for example Word " +"for .docx. The folder of the file is shared with the app while it is open." +msgstr "" +"ะŸั€ะพะฟะพะฝัƒั” ะทะฐัั‚ะพััƒะฝะบะธ ะฒ ยซะ’ั–ะดะบั€ะธั‚ะธ ะทะฐ ะดะพะฟะพะผะพะณะพัŽยป ะดะปั ั—ั…ะฝั–ั… ั‚ะธะฟั–ะฒ ั„ะฐะนะปั–ะฒ, " +"ะฝะฐะฟั€ะธะบะปะฐะด Word ะดะปั .docx. ะขะตะบะฐ ั„ะฐะนะปัƒ ะฒั–ะดะบั€ะธั‚ะฐ ะดะปั ัะตั€ะฒะตั€ะฐ, ะดะพะบะธ ั„ะฐะนะป " +"ะฒั–ะดะบั€ะธั‚ะธะน." + +#: src/lib/menu.sh:264 +msgid "" +"How big the apps are drawn. Automatic follows the scale of your desktop, so " +"they are sharp and the right size." +msgstr "" +"ะะฐัะบั–ะปัŒะบะธ ะฒะตะปะธะบะธะผะธ ะผะฐะปัŽัŽั‚ัŒัั ะทะฐัั‚ะพััƒะฝะบะธ. ยซะะฒั‚ะพะผะฐั‚ะธั‡ะฝะพยป ัั‚ะตะถะธั‚ัŒ ะทะฐ ะผะฐััˆั‚ะฐะฑะพะผ " +"ัั‚ั–ะปัŒะฝะธั†ั–, ั‚ะพะถ ะฒะพะฝะธ ั‡ั–ั‚ะบั– ะน ะฟะพั‚ั€ั–ะฑะฝะพะณะพ ั€ะพะทะผั–ั€ัƒ." + +#: src/lib/menu.sh:265 +msgid "Plays the sound of the apps here, for calls and videos." +msgstr "ะ’ั–ะดั‚ะฒะพั€ัŽั” ะทะฒัƒะบ ะทะฐัั‚ะพััƒะฝะบั–ะฒ ั‚ัƒั‚, ะดะปั ะดะทะฒั–ะฝะบั–ะฒ ั– ะฒั–ะดะตะพ." + +#: src/lib/menu.sh:266 +msgid "" +"Paints every window right. But some servers end the session right after " +"signing in, then go back to GFX." +msgstr "ะŸั€ะฐะฒะธะปัŒะฝะพ ะผะฐะปัŽั” ะบะพะถะฝะต ะฒั–ะบะฝะพ. ะะปะต ะดะตัะบั– ัะตั€ะฒะตั€ะธ ะพะฑั€ะธะฒะฐัŽั‚ัŒ ัะตะฐะฝั ะพะดั€ะฐะทัƒ ะฟั–ัะปั ะฒั…ะพะดัƒ, ั‚ะพะดั– ะฟะพะฒะตั€ะฝั–ั‚ัŒัั ะดะพ GFX." + +#: src/lib/menu.sh:267 +msgid "" +"Works with every server. FreeRDP may not repaint a window after it was " +"minimized or resized." +msgstr "ะŸั€ะฐั†ัŽั” ะท ะฑัƒะดัŒ-ัะบะธะผ ัะตั€ะฒะตั€ะพะผ. FreeRDP ะผะพะถะต ะฝะต ะฟะตั€ะตะผะฐะปัŽะฒะฐั‚ะธ ะฒั–ะบะฝะพ ะฟั–ัะปั ะทะณะพั€ั‚ะฐะฝะฝั ั‡ะธ ะทะผั–ะฝะธ ั€ะพะทะผั–ั€ัƒ." + +#: src/lib/menu.sh:268 +msgid "" +"Always waits before the second sign-in. For when the second prompt does not " +"come with automatic." +msgstr "" +"ะ—ะฐะฒะถะดะธ ั‡ะตะบะฐั” ะฟะตั€ะตะด ะดั€ัƒะณะธะผ ะฒั…ะพะดะพะผ. ะฏะบั‰ะพ ะท ยซะฐะฒั‚ะพะผะฐั‚ะธั‡ะฝะพยป ะดั€ัƒะณะธะน ะทะฐะฟะธั‚ ะฝะต " +"ะฝะฐะดั…ะพะดะธั‚ัŒ." + +#: src/lib/menu.sh:269 +msgid "Never waits. Right when your company has no MFA." +msgstr "ะั–ะบะพะปะธ ะฝะต ั‡ะตะบะฐั”. ะŸั–ะดั…ะพะดะธั‚ัŒ, ัะบั‰ะพ ะฒะฐัˆะฐ ะบะพะผะฟะฐะฝั–ั ะฝะต ะฒะธะบะพั€ะธัั‚ะพะฒัƒั” MFA." + +#: src/lib/menu.sh:270 +msgid "" +"Many servers with MFA need a second sign-in, and its prompt only comes after " +"a short wait. Automatic waits when the first sign-in took a while." +msgstr "" +"ะ‘ะฐะณะฐั‚ัŒะพะผ ัะตั€ะฒะตั€ะฐะผ ะท MFA ะฟะพั‚ั€ั–ะฑะตะฝ ะดั€ัƒะณะธะน ะฒั…ั–ะด, ั– ะนะพะณะพ ะทะฐะฟะธั‚ ะฝะฐะดั…ะพะดะธั‚ัŒ ะปะธัˆะต " +"ะฟั–ัะปั ะบะพั€ะพั‚ะบะพั— ะฟะฐัƒะทะธ. ยซะะฒั‚ะพะผะฐั‚ะธั‡ะฝะพยป ั‡ะตะบะฐั”, ัะบั‰ะพ ะฟะตั€ัˆะธะน ะฒั…ั–ะด ั‚ั€ะธะฒะฐะฒ ัะบะธะนััŒ " +"ั‡ะฐั." + +#: src/lib/menu.sh:273 +msgid "" +"Loads the list of apps again once a day, so new apps show up by themselves." +msgstr "" +"ะ ะฐะท ะฝะฐ ะดะตะฝัŒ ะทะฐะฝะพะฒะพ ะทะฐะฒะฐะฝั‚ะฐะถัƒั” ัะฟะธัะพะบ ะทะฐัั‚ะพััƒะฝะบั–ะฒ, ั‚ะพะถ ะฝะพะฒั– ะท'ัะฒะปััŽั‚ัŒัั ัะฐะผั–." + +#: src/lib/menu.sh:275 src/lib/menu.sh:330 +msgid "Needs a systemd user session." +msgstr "ะŸะพั‚ั€ั–ะฑะตะฝ ะบะพั€ะธัั‚ัƒะฒะฐั†ัŒะบะธะน ัะตะฐะฝั systemd." + +#: src/lib/menu.sh:322 src/lib/menu.sh:325 +msgid "Could not save the setting." +msgstr "ะะต ะฒะดะฐะปะพัั ะทะฑะตั€ะตะณั‚ะธ ะฝะฐะปะฐัˆั‚ัƒะฒะฐะฝะฝั." + +#: src/lib/menu.sh:371 src/lib/menu.sh:609 +msgid "Settings" +msgstr "ะะฐะปะฐัˆั‚ัƒะฒะฐะฝะฝั" + +#: src/lib/menu.sh:372 +msgid "โ†‘โ†“ select โ†โ†’ or Space: change q: back" +msgstr "โ†‘โ†“ ะฒะธะฑั–ั€ โ†โ†’ ะฐะฑะพ ะŸั€ะพะฑั–ะป: ะทะผั–ะฝะธั‚ะธ q: ะฝะฐะทะฐะด" + +#: src/lib/menu.sh:373 +msgid "ON" +msgstr "ะฃะ’ะ†ะœะš" + +#: src/lib/menu.sh:374 +msgid "OFF" +msgstr "ะ’ะ˜ะœะš" + +#: src/lib/menu.sh:456 src/lib/menu.sh:608 +msgid "Workspaces" +msgstr "ะ ะพะฑะพั‡ั– ะฟั€ะพัั‚ะพั€ะธ" + +#. TRANSLATORS: keep the letters, they are the keys. +#: src/lib/menu.sh:458 +msgid "" +"โ†‘โ†“ select r: refresh s: sign in again d: remove a: add q: back" +msgstr "" +"โ†‘โ†“ ะฒะธะฑั–ั€ r: ะพะฝะพะฒะธั‚ะธ s: ัƒะฒั–ะนั‚ะธ ะทะฝะพะฒัƒ d: ะฒะธะดะฐะปะธั‚ะธ a: ะดะพะดะฐั‚ะธ q: ะฝะฐะทะฐะด" + +#: src/lib/menu.sh:459 +msgid "No workspace yet. Press a to add one." +msgstr "ะ ะพะฑะพั‡ะธั… ะฟั€ะพัั‚ะพั€ั–ะฒ ั‰ะต ะฝะตะผะฐั”. ะะฐั‚ะธัะฝั–ั‚ัŒ a, ั‰ะพะฑ ะดะพะดะฐั‚ะธ." + +#: src/lib/menu.sh:484 +#, sh-printf-format +msgid "refreshed %s" +msgstr "ะพะฝะพะฒะปะตะฝะพ %s" + +#: src/lib/menu.sh:512 +#, sh-printf-format +msgid "Remove \"%s\" and its apps?" +msgstr "ะ’ะธะดะฐะปะธั‚ะธ ยซ%sยป ั€ะฐะทะพะผ ั–ะท ะทะฐัั‚ะพััƒะฝะบะฐะผะธ?" + +#: src/lib/menu.sh:536 src/lib/menu.sh:607 +msgid "Start an app" +msgstr "ะ—ะฐะฟัƒัั‚ะธั‚ะธ ะทะฐัั‚ะพััƒะฝะพะบ" + +#: src/lib/menu.sh:537 +msgid "โ†‘โ†“ select Enter: start q: back" +msgstr "โ†‘โ†“ ะฒะธะฑั–ั€ Enter: ะทะฐะฟัƒัั‚ะธั‚ะธ q: ะฝะฐะทะฐะด" + +#: src/lib/menu.sh:538 +msgid "No apps yet. Add a workspace first." +msgstr "ะ—ะฐัั‚ะพััƒะฝะบั–ะฒ ั‰ะต ะฝะตะผะฐั”. ะกะฟะตั€ัˆัƒ ะดะพะดะฐะนั‚ะต ั€ะพะฑะพั‡ะธะน ะฟั€ะพัั‚ั–ั€." + +#: src/lib/menu.sh:579 +#, sh-printf-format +msgid "%s is starting. It opens in a window of its own." +msgstr "ะ—ะฐะฟัƒัะบ: %s. ะ’ั–ะดะบั€ะธั”ั‚ัŒัั ะฒ ะพะบั€ะตะผะพะผัƒ ะฒั–ะบะฝั–." + +#: src/lib/menu.sh:606 +msgid "Add a workspace" +msgstr "ะ”ะพะดะฐั‚ะธ ั€ะพะฑะพั‡ะธะน ะฟั€ะพัั‚ั–ั€" + +#: src/lib/menu.sh:610 +msgid "Quit" +msgstr "ะ’ะธะนั‚ะธ" + +#: src/lib/workspace.sh:163 +msgid "The server did not accept the user name or password." +msgstr "ะกะตั€ะฒะตั€ ะฝะต ะฟั€ะธะนะฝัะฒ ั–ะผ'ั ะบะพั€ะธัั‚ัƒะฒะฐั‡ะฐ ะฐะฑะพ ะฟะฐั€ะพะปัŒ." + +#: src/lib/workspace.sh:164 +#, sh-printf-format +msgid "Could not reach the server: %s" +msgstr "ะะต ะฒะดะฐะปะพัั ะทะฒ'ัะทะฐั‚ะธัั ั–ะท ัะตั€ะฒะตั€ะพะผ: %s" + +#: src/lib/workspace.sh:165 +#, sh-printf-format +msgid "There is no RD Web feed at %s." +msgstr "ะ—ะฐ ะฐะดั€ะตัะพัŽ %s ะฝะตะผะฐั” ั„ั–ะดะฐ RD Web." + +#: src/lib/workspace.sh:166 +#, sh-printf-format +msgid "Could not load the apps: %s" +msgstr "ะะต ะฒะดะฐะปะพัั ะทะฐะฒะฐะฝั‚ะฐะถะธั‚ะธ ะทะฐัั‚ะพััƒะฝะบะธ: %s" + +#: src/lib/workspace.sh:282 +#, sh-printf-format +msgid "Remote desktop of %s" +msgstr "ะ’ั–ะดะดะฐะปะตะฝะฐ ัั‚ั–ะปัŒะฝะธั†ั %s" + +#: src/lib/workspace.sh:284 +#, sh-printf-format +msgid "%s from %s" +msgstr "%s ั–ะท %s" + +#: src/lib/menu.sh +msgid "Apps" +msgstr "ะ—ะฐัั‚ะพััƒะฝะบะธ" + +#: src/lib/menu.sh +msgid "Share a folder" +msgstr "ะกะฟั–ะปัŒะฝะฐ ั‚ะตะบะฐ" + +#: src/lib/menu.sh +msgid "Open files with the apps" +msgstr "ะ’ั–ะดะบั€ะธะฒะฐั‚ะธ ั„ะฐะนะปะธ ะฒ ะทะฐัั‚ะพััƒะฝะบะฐั…" + +#: src/lib/menu.sh +msgid "Size" +msgstr "ะ ะพะทะผั–ั€" + +#: src/lib/menu.sh +msgid "Sound" +msgstr "ะ—ะฒัƒะบ" + +#: src/lib/menu.sh +msgid "Graphics" +msgstr "ะ“ั€ะฐั„ั–ะบะฐ" + +#: src/lib/menu.sh +msgid "Signing in" +msgstr "ะ’ั…ั–ะด" + +#: src/lib/menu.sh +msgid "Wait for the second MFA prompt" +msgstr "ะงะตะบะฐั‚ะธ ะฝะฐ ะดั€ัƒะณะธะน ะทะฐะฟะธั‚ MFA" + +#: src/lib/menu.sh +msgid "Refresh the apps every day" +msgstr "ะžะฝะพะฒะปัŽะฒะฐั‚ะธ ะทะฐัั‚ะพััƒะฝะบะธ ั‰ะพะดะฝั" diff --git a/po/update-pot.sh b/po/update-pot.sh new file mode 100755 index 0000000..fccd1b1 --- /dev/null +++ b/po/update-pot.sh @@ -0,0 +1,34 @@ +#!/usr/bin/env bash +# +# Collects every translatable string into po/rdfeed.pot and brings the +# translations up to date with it. +# +# The settings labels and headings live in an array in menu.sh and reach +# gettext at run time, so xgettext cannot see them; they are pulled out here +# first. + +set -euo pipefail + +cd -- "$(dirname -- "${BASH_SOURCE[0]}")/.." +tmp="$(mktemp -d)" +trap 'rm -rf -- "$tmp"' EXIT + +version="$(make -s version)" + +# The labels, as calls xgettext understands, pointing back at menu.sh. +awk -F'|' '/^\t"(user|entries|timer)\|/ { sub(/"$/, "", $5); print "rf_msg \"" $5 "\"" } + /^\t"group\|/ { sub(/"$/, "", $2); print "rf_msg \"" $2 "\"" }' src/lib/menu.sh > "$tmp/settings.sh" + +xgettext --from-code=UTF-8 --add-comments=TRANSLATORS --package-name=rdfeed --package-version="$version" \ + --msgid-bugs-address=https://github.com/LoonixTools/rdfeed/issues \ + -L Shell -k --keyword=rf_msg --keyword=rf_msg_into:2 --keyword=rf_msg_in:2 \ + -o po/rdfeed.pot src/rdfeed src/lib/*.sh "$tmp/settings.sh" +sed -i "s|$tmp/settings.sh:[0-9]*|src/lib/menu.sh|g" po/rdfeed.pot +sed -i -e '1i # Translation template for rdfeed.\n# Copyright (C) 2026 Felitendo\n# This file is distributed under the same license as rdfeed.' -e '1,4d' \ + po/rdfeed.pot + +for po in po/*.po; do + [[ -e $po ]] || continue + msgmerge --quiet --update --backup=none --no-fuzzy-matching "$po" po/rdfeed.pot +done +echo "po/rdfeed.pot: $(grep -c '^msgid' po/rdfeed.pot) strings" diff --git a/po/zh_CN.po b/po/zh_CN.po new file mode 100644 index 0000000..bd762c4 --- /dev/null +++ b/po/zh_CN.po @@ -0,0 +1,537 @@ +# Simplified Chinese translation for rdfeed. +# Copyright (C) 2026 Felitendo +# This file is distributed under the same license as rdfeed. +# +msgid "" +msgstr "" +"Project-Id-Version: rdfeed 1.0.0\n" +"Report-Msgid-Bugs-To: https://github.com/LoonixTools/rdfeed/issues\n" +"POT-Creation-Date: 2026-10-02 10:29+0200\n" +"PO-Revision-Date: 2026-10-02 09:30+0200\n" +"Last-Translator: Felitendo\n" +"Language-Team: Simplified Chinese\n" +"Language: zh_CN\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" + +#: src/rdfeed:79 src/rdfeed:103 +#, sh-printf-format +msgid "Could not write to %s." +msgstr "ๆ— ๆณ•ๅ†™ๅ…ฅ %sใ€‚" + +#: src/rdfeed:82 +#, sh-printf-format +msgid "Signing in to %sโ€ฆ" +msgstr "ๆญฃๅœจ็™ปๅฝ• %sโ€ฆ" + +#: src/rdfeed:100 +msgid "" +"The password could not be saved in the keyring, so each app asks for it." +msgstr "ๆ— ๆณ•ๆŠŠๅฏ†็ ไฟๅญ˜ๅˆฐๅฏ†้’ฅ็Žฏ๏ผŒๅ› ๆญคๆฏไธชๅบ”็”จ้ƒฝไผš่ฏข้—ฎๅฏ†็ ใ€‚" + +#: src/rdfeed:111 +#, sh-printf-format +msgid "1 app from %s is now in your app menu." +msgstr "ๆฅ่‡ช %s ็š„ 1 ไธชๅบ”็”จๅทฒๅŠ ๅ…ฅๅบ”็”จ่œๅ•ใ€‚" + +#: src/rdfeed:113 +#, sh-printf-format +msgid "%d apps from %s are now in your app menu." +msgstr "%d ไธชๅบ”็”จ๏ผˆๆฅ่‡ช %s๏ผ‰ๅทฒๅŠ ๅ…ฅๅบ”็”จ่œๅ•ใ€‚" + +#: src/rdfeed:129 +msgid "" +"Your IT department has the address. A work e-mail address often works too." +msgstr "ๅœฐๅ€ๅฏไปฅๅ‘ไฝ ็š„ IT ้ƒจ้—จ็ดขๅ–ใ€‚ๅพˆๅคšๆ—ถๅ€™ๅทฅไฝœ้‚ฎ็ฎฑๅœฐๅ€ไนŸๅฏไปฅใ€‚" + +#: src/rdfeed:130 +msgid "Feed address, server or e-mail:" +msgstr "่ฎข้˜…ๆบๅœฐๅ€ใ€ๆœๅŠกๅ™จๆˆ–้‚ฎ็ฎฑ๏ผš" + +#: src/rdfeed:137 +#, sh-printf-format +msgid "No feed is registered for %s. Ask your IT department for the address." +msgstr "%s ๆฒกๆœ‰็™ป่ฎฐ่ฎข้˜…ๆบใ€‚่ฏทๅ‘ IT ้ƒจ้—จ่ฏข้—ฎๅœฐๅ€ใ€‚" + +#: src/rdfeed:142 src/rdfeed:156 +msgid "User name (DOMAIN\\name or name@domain):" +msgstr "็”จๆˆทๅ๏ผˆๅŸŸ\\็”จๆˆทๅ ๆˆ– ็”จๆˆทๅ@ๅŸŸ๏ผ‰๏ผš" + +#: src/rdfeed:146 src/rdfeed:159 +msgid "Password:" +msgstr "ๅฏ†็ ๏ผš" + +#: src/rdfeed:178 src/rdfeed:236 +#, sh-printf-format +msgid "No workspace yet. Add one with `%s add`." +msgstr "่ฟ˜ๆฒกๆœ‰ๅทฅไฝœๅŒบใ€‚็”จ `%s add` ๆทปๅŠ ไธ€ไธชใ€‚" + +#: src/rdfeed:187 src/lib/launch.sh:220 +#, sh-printf-format +msgid "No password saved for %s. Sign in again in rdfeed." +msgstr "ๆฒกๆœ‰ไฟๅญ˜ %s ็š„ๅฏ†็ ใ€‚่ฏทๅœจ rdfeed ไธญ้‡ๆ–ฐ็™ปๅฝ•ใ€‚" + +#: src/rdfeed:191 src/lib/launch.sh:149 +#, sh-printf-format +msgid "Password for %s:" +msgstr "%s ็š„ๅฏ†็ ๏ผš" + +#: src/rdfeed:195 +#, sh-printf-format +msgid "Refreshing %sโ€ฆ" +msgstr "ๆญฃๅœจๅˆทๆ–ฐ %sโ€ฆ" + +#: src/rdfeed:205 +msgid "The password no longer works. Sign in again in rdfeed." +msgstr "ๅฏ†็ ๅทฒๅคฑๆ•ˆใ€‚่ฏทๅœจ rdfeed ไธญ้‡ๆ–ฐ็™ปๅฝ•ใ€‚" + +#: src/rdfeed:214 +#, sh-printf-format +msgid "%s: %d apps" +msgstr "%s๏ผš%d ไธชๅบ”็”จ" + +#: src/rdfeed:222 +#, sh-printf-format +msgid "Which workspace? See `%s list` for the names." +msgstr "ๅ“ชไธชๅทฅไฝœๅŒบ๏ผŸๅ็งฐ่ง `%s list`ใ€‚" + +#: src/rdfeed:229 +#, sh-printf-format +msgid "%s and its apps are removed." +msgstr "ๅทฒ็งป้™ค %s ๅŠๅ…ถๅบ”็”จใ€‚" + +#: src/rdfeed:253 +#, sh-printf-format +msgid "Which app? See `%s list`." +msgstr "ๅ“ชไธชๅบ”็”จ๏ผŸ่ง `%s list`ใ€‚" + +#: src/rdfeed:268 src/lib/launch.sh:199 +#, sh-printf-format +msgid "There is no app %s. See `%s list`." +msgstr "ๆฒกๆœ‰ๅบ”็”จ %sใ€‚่ง `%s list`ใ€‚" + +#: src/rdfeed:270 +#, sh-printf-format +msgid "Several workspaces have %s. Write it as WORKSPACE/APP, see `%s list`." +msgstr "ๅคšไธชๅทฅไฝœๅŒบ้ƒฝๆœ‰ %sใ€‚่ฏทๅ†™ๆˆ ๅทฅไฝœๅŒบ/ๅบ”็”จ๏ผŒ่ง `%s list`ใ€‚" + +#: src/rdfeed:278 +msgid "Usage: rdfeed [command]" +msgstr "็”จๆณ•๏ผšrdfeed [ๅ‘ฝไปค]" + +#: src/rdfeed:280 +msgid "Commands:" +msgstr "ๅ‘ฝไปค๏ผš" + +#: src/rdfeed:281 +msgid "Add the workspace of your company" +msgstr "ๆทปๅŠ ไฝ ๅ…ฌๅธ็š„ๅทฅไฝœๅŒบ" + +#: src/rdfeed:282 +msgid "Load the apps again" +msgstr "้‡ๆ–ฐๅŠ ่ฝฝๅบ”็”จ" + +#: src/rdfeed:283 +msgid "List the workspaces and their apps" +msgstr "ๅˆ—ๅ‡บๅทฅไฝœๅŒบๅŠๅ…ถๅบ”็”จ" + +#: src/rdfeed:284 +msgid "Start an app, or open a file with it" +msgstr "ๅฏๅŠจๅบ”็”จ๏ผŒๆˆ–็”จๅฎƒๆ‰“ๅผ€ๆ–‡ไปถ" + +#: src/rdfeed:285 +msgid "Remove a workspace and its apps" +msgstr "็งป้™คๅทฅไฝœๅŒบๅŠๅ…ถๅบ”็”จ" + +#: src/rdfeed:286 +msgid "Show this help" +msgstr "ๆ˜พ็คบๆญคๅธฎๅŠฉ" + +#: src/rdfeed:287 +msgid "Show the version" +msgstr "ๆ˜พ็คบ็‰ˆๆœฌ" + +#: src/rdfeed:289 +msgid "Without a command an interactive menu is shown." +msgstr "ไธๅธฆๅ‘ฝไปคๆ—ถๆ˜พ็คบไบคไบ’ๅผ่œๅ•ใ€‚" + +#: src/rdfeed:313 +#, sh-printf-format +msgid "Unknown command: %s" +msgstr "ๆœช็Ÿฅๅ‘ฝไปค๏ผš%s" + +#: src/lib/common.sh:174 src/lib/menu.sh:251 +msgid "never" +msgstr "ไปŽๆœช" + +#: src/lib/common.sh:184 +msgid "just now" +msgstr "ๅˆšๅˆš" + +#: src/lib/common.sh:186 +msgid "1 minute ago" +msgstr "1 ๅˆ†้’Ÿๅ‰" + +#: src/lib/common.sh:189 +#, sh-printf-format +msgid "%d minutes ago" +msgstr "%d ๅˆ†้’Ÿๅ‰" + +#: src/lib/common.sh:191 +msgid "1 hour ago" +msgstr "1 ๅฐๆ—ถๅ‰" + +#: src/lib/common.sh:194 +#, sh-printf-format +msgid "%d hours ago" +msgstr "%d ๅฐๆ—ถๅ‰" + +#: src/lib/common.sh:196 +msgid "1 day ago" +msgstr "1 ๅคฉๅ‰" + +#: src/lib/common.sh:199 +#, sh-printf-format +msgid "%d days ago" +msgstr "%d ๅคฉๅ‰" + +#: src/lib/launch.sh:169 +msgid "" +"Your password has expired. Change it on Windows or on the RD Web page, then " +"sign in again in rdfeed." +msgstr "" +"ไฝ ็š„ๅฏ†็ ๅทฒ่ฟ‡ๆœŸใ€‚่ฏทๅœจ Windows ๆˆ– RD Web ้กต้ขไธŠไฟฎๆ”น๏ผŒ็„ถๅŽๅœจ rdfeed ไธญ้‡ๆ–ฐ็™ปๅฝ•ใ€‚" + +#: src/lib/launch.sh:171 +msgid "Your account is locked. Ask your IT department." +msgstr "ไฝ ็š„่ดฆๆˆทๅทฒ่ขซ้”ๅฎšใ€‚่ฏท่”็ณป IT ้ƒจ้—จใ€‚" + +#: src/lib/launch.sh:173 +msgid "The server did not accept the password. Sign in again in rdfeed." +msgstr "ๆœๅŠกๅ™จๆฒกๆœ‰ๆŽฅๅ—ๅฏ†็ ใ€‚่ฏทๅœจ rdfeed ไธญ้‡ๆ–ฐ็™ปๅฝ•ใ€‚" + +#: src/lib/launch.sh:175 +msgid "Could not reach the server. Check the internet connection." +msgstr "ๆ— ๆณ•่ฟžๆŽฅๆœๅŠกๅ™จใ€‚่ฏทๆฃ€ๆŸฅ็ฝ‘็ปœ่ฟžๆŽฅใ€‚" + +#: src/lib/launch.sh:178 +#, sh-printf-format +msgid "FreeRDP stopped: %s" +msgstr "FreeRDP ๅทฒๅœๆญข๏ผš%s" + +#: src/lib/launch.sh:205 src/lib/launch.sh:206 +#, sh-printf-format +msgid "File not found: %s" +msgstr "ๆ‰พไธๅˆฐๆ–‡ไปถ๏ผš%s" + +#: src/lib/launch.sh:212 src/lib/launch.sh:213 +msgid "" +"FreeRDP 3 is missing. Install it to start the apps (the package is called " +"freerdp)." +msgstr "็ผบๅฐ‘ FreeRDP 3ใ€‚่ฏทๅฎ‰่ฃ…ๅฎƒไปฅๅฏๅŠจๅบ”็”จ๏ผˆ่ฝฏไปถๅŒ…ๅไธบ freerdp๏ผ‰ใ€‚" + +#: src/lib/launch.sh:277 +#, sh-printf-format +msgid "Opening %s in %s" +msgstr "ๆญฃๅœจๆ‰“ๅผ€ %s๏ผˆไฝฟ็”จ %s๏ผ‰" + +#: src/lib/launch.sh:278 +#, sh-printf-format +msgid "If it does not open by itself, open it in the app from the drive %s." +msgstr "ๅฆ‚ๆžœๆฒกๆœ‰่‡ชๅŠจๆ‰“ๅผ€๏ผŒ่ฏทๅœจๅบ”็”จไธญไปŽ้ฉฑๅŠจๅ™จ %s ๆ‰“ๅผ€ๅฎƒใ€‚" + +#: src/lib/launch.sh:280 +#, sh-printf-format +msgid "Starting %s" +msgstr "ๆญฃๅœจๅฏๅŠจ %s" + +#: src/lib/launch.sh:281 +msgid "" +"If your company uses MFA, confirm the sign-in in your authenticator app." +msgstr "ๅฆ‚ๆžœไฝ ็š„ๅ…ฌๅธไฝฟ็”จ MFA๏ผŒ่ฏทๅœจ่บซไปฝ้ชŒ่ฏๅ™จๅบ”็”จไธญ็กฎ่ฎค็™ปๅฝ•ใ€‚" + +#: src/lib/launch.sh:290 +msgid "A second sign-in is coming" +msgstr "ๅณๅฐ†่ฟ›่กŒ็ฌฌไบŒๆฌก็™ปๅฝ•" + +#: src/lib/launch.sh:291 +#, sh-printf-format +msgid "" +"Confirm the second prompt in your authenticator app too. It comes in about " +"%d seconds." +msgstr "ไนŸ่ฏทๅœจ่บซไปฝ้ชŒ่ฏๅ™จๅบ”็”จไธญ็กฎ่ฎค็ฌฌไบŒๆฌก่ฏทๆฑ‚ใ€‚ๅฎƒๅคง็บฆไผšๅœจ %d ็ง’ๅŽๅˆฐ่พพใ€‚" + +#: src/lib/launch.sh:301 +#, sh-printf-format +msgid "%s could not start" +msgstr "ๆ— ๆณ•ๅฏๅŠจ %s" + +#: src/lib/launch.sh:303 +#, sh-printf-format +msgid "Log: %s" +msgstr "ๆ—ฅๅฟ—๏ผš%s" + +#. TRANSLATORS: the letter after "[" is the key for yes. y works too. +#: src/lib/menu.sh:93 +msgid "[y/N]" +msgstr "[y/N]" + +#: src/lib/menu.sh:163 +msgid "1 app" +msgstr "1 ไธชๅบ”็”จ" + +#: src/lib/menu.sh:165 +#, sh-printf-format +msgid "%d apps" +msgstr "%d ไธชๅบ”็”จ" + +#: src/lib/menu.sh:190 +msgid "No workspace yet. Add the one of your company with [1]." +msgstr "่ฟ˜ๆฒกๆœ‰ๅทฅไฝœๅŒบใ€‚ๆŒ‰ [1] ๆทปๅŠ ไฝ ๅ…ฌๅธ็š„ๅทฅไฝœๅŒบใ€‚" + +#: src/lib/menu.sh:233 +msgid "off" +msgstr "ๅ…ณ" + +#: src/lib/menu.sh:234 +msgid "home folder" +msgstr "ไธปๆ–‡ไปถๅคน" + +#: src/lib/menu.sh:237 +msgid "Documents" +msgstr "ๆ–‡ๆกฃ" + +#: src/lib/menu.sh:238 +msgid "Downloads" +msgstr "ไธ‹่ฝฝ" + +#: src/lib/menu.sh:243 +msgid "not found" +msgstr "ๆœชๆ‰พๅˆฐ" + +#: src/lib/menu.sh:246 +#, sh-printf-format +msgid "automatic (%d %%)" +msgstr "่‡ชๅŠจ๏ผˆ%d%%๏ผ‰" + +#: src/lib/menu.sh:248 +msgid "classic" +msgstr "็ปๅ…ธ" + +#: src/lib/menu.sh:249 +msgid "GFX" +msgstr "GFX" + +#: src/lib/menu.sh:250 +msgid "always" +msgstr "ๆ€ปๆ˜ฏ" + +#: src/lib/menu.sh:252 +msgid "automatic" +msgstr "่‡ชๅŠจ" + +#: src/lib/menu.sh:261 +msgid "" +"The apps only see the files on the server. Pick a folder to open and save " +"files of this computer in them." +msgstr "" +"ๅบ”็”จๅช่ƒฝ็œ‹ๅˆฐๆœๅŠกๅ™จไธŠ็š„ๆ–‡ไปถใ€‚้€‰ไธ€ไธชๆ–‡ไปถๅคน๏ผŒๅฎƒไปฌๅฐฑ่ƒฝๆ‰“ๅผ€ๅ’Œไฟๅญ˜่ฟ™ๅฐ็”ต่„‘ไธŠ็š„ๆ–‡" +"ไปถใ€‚" + +#: src/lib/menu.sh:262 +msgid "" +"The apps can open and save files in this folder. It shows up as a drive in " +"them." +msgstr "ๅบ”็”จๅฏไปฅๅœจ่ฟ™ไธชๆ–‡ไปถๅคน้‡Œๆ‰“ๅผ€ๅ’Œไฟๅญ˜ๆ–‡ไปถใ€‚ๅฎƒๅœจๅบ”็”จไธญๆ˜พ็คบไธบไธ€ไธช้ฉฑๅŠจๅ™จใ€‚" + +#: src/lib/menu.sh:263 +msgid "" +"Offers the apps under \"Open with\" for their file types, for example Word " +"for .docx. The folder of the file is shared with the app while it is open." +msgstr "" +"ๅœจโ€œๆ‰“ๅผ€ๆ–นๅผโ€ไธญๆŒ‰ๆ–‡ไปถ็ฑปๅž‹ๆไพ›่ฟ™ไบ›ๅบ”็”จ๏ผŒไพ‹ๅฆ‚็”จ Word ๆ‰“ๅผ€ .docxใ€‚ๆ–‡ไปถๆ‰“ๅผ€ๆœŸ้—ด๏ผŒ" +"ๅฎƒๆ‰€ๅœจ็š„ๆ–‡ไปถๅคนไผš่ขซๅ…ฑไบซใ€‚" + +#: src/lib/menu.sh:264 +msgid "" +"How big the apps are drawn. Automatic follows the scale of your desktop, so " +"they are sharp and the right size." +msgstr "ๅบ”็”จ็ป˜ๅˆถ็š„ๅคงๅฐใ€‚โ€œ่‡ชๅŠจโ€ไผš่ทŸ้šๆกŒ้ข็š„็ผฉๆ”พ๏ผŒ่ฟ™ๆ ทๅบ”็”จๆธ…ๆ™ฐไธ”ๅคงๅฐๅˆ้€‚ใ€‚" + +#: src/lib/menu.sh:265 +msgid "Plays the sound of the apps here, for calls and videos." +msgstr "ๅœจ่ฟ™ๅฐ็”ต่„‘ไธŠๆ’ญๆ”พๅบ”็”จ็š„ๅฃฐ้Ÿณ๏ผŒ็”จไบŽ้€š่ฏๅ’Œ่ง†้ข‘ใ€‚" + +#: src/lib/menu.sh:266 +msgid "" +"Paints every window right. But some servers end the session right after " +"signing in, then go back to GFX." +msgstr "่ƒฝๆญฃ็กฎ็ป˜ๅˆถๆฏไธช็ช—ๅฃใ€‚ไฝ†ๆœ‰ไบ›ๆœๅŠกๅ™จไผšๅœจ็™ปๅฝ•ๅŽ็ซ‹ๅณ็ป“ๆŸไผš่ฏ๏ผŒ่ฟ™ๆ—ถ่ฏทๆ”นๅ›ž GFXใ€‚" + +#: src/lib/menu.sh:267 +msgid "" +"Works with every server. FreeRDP may not repaint a window after it was " +"minimized or resized." +msgstr "้€‚็”จไบŽๆ‰€ๆœ‰ๆœๅŠกๅ™จใ€‚็ช—ๅฃๆœ€ๅฐๅŒ–ๆˆ–่ฐƒๆ•ดๅคงๅฐๅŽ๏ผŒFreeRDP ๅฏ่ƒฝไธไผš้‡็ป˜ใ€‚" + +#: src/lib/menu.sh:268 +msgid "" +"Always waits before the second sign-in. For when the second prompt does not " +"come with automatic." +msgstr "็ฌฌไบŒๆฌก็™ปๅฝ•ๅ‰ๆ€ปๆ˜ฏ็ญ‰ๅพ…ใ€‚้€‚็”จไบŽ้€‰โ€œ่‡ชๅŠจโ€ๆ—ถ็ฌฌไบŒๆฌก่ฏทๆฑ‚ๆฒกๆœ‰ๅ‡บ็Žฐ็š„ๆƒ…ๅ†ตใ€‚" + +#: src/lib/menu.sh:269 +msgid "Never waits. Right when your company has no MFA." +msgstr "ไปŽไธ็ญ‰ๅพ…ใ€‚้€‚ๅˆๅ…ฌๅธไธไฝฟ็”จ MFA ็š„ๆƒ…ๅ†ตใ€‚" + +#: src/lib/menu.sh:270 +msgid "" +"Many servers with MFA need a second sign-in, and its prompt only comes after " +"a short wait. Automatic waits when the first sign-in took a while." +msgstr "" +"่ฎธๅคšไฝฟ็”จ MFA ็š„ๆœๅŠกๅ™จ้œ€่ฆ็ฌฌไบŒๆฌก็™ปๅฝ•๏ผŒ่€Œๅฎƒ็š„่ฏทๆฑ‚่ฆ็จ็ญ‰ไธ€ไธ‹ๆ‰ไผšๅ‡บ็Žฐใ€‚โ€œ่‡ชๅŠจโ€ไผšๅœจ" +"็ฌฌไธ€ๆฌก็™ปๅฝ•่€—ๆ—ถ่พƒ้•ฟๆ—ถ็ญ‰ๅพ…ใ€‚" + +#: src/lib/menu.sh:273 +msgid "" +"Loads the list of apps again once a day, so new apps show up by themselves." +msgstr "ๆฏๅคฉ้‡ๆ–ฐๅŠ ่ฝฝไธ€ๆฌกๅบ”็”จๅˆ—่กจ๏ผŒๆ–ฐๅบ”็”จไผš่‡ชๅŠจๅ‡บ็Žฐใ€‚" + +#: src/lib/menu.sh:275 src/lib/menu.sh:330 +msgid "Needs a systemd user session." +msgstr "้œ€่ฆ systemd ็”จๆˆทไผš่ฏใ€‚" + +#: src/lib/menu.sh:322 src/lib/menu.sh:325 +msgid "Could not save the setting." +msgstr "ๆ— ๆณ•ไฟๅญ˜่ฎพ็ฝฎใ€‚" + +#: src/lib/menu.sh:371 src/lib/menu.sh:609 +msgid "Settings" +msgstr "่ฎพ็ฝฎ" + +#: src/lib/menu.sh:372 +msgid "โ†‘โ†“ select โ†โ†’ or Space: change q: back" +msgstr "โ†‘โ†“ ้€‰ๆ‹ฉ โ†โ†’ ๆˆ–็ฉบๆ ผ๏ผšๆ›ดๆ”น q๏ผš่ฟ”ๅ›ž" + +#: src/lib/menu.sh:373 +msgid "ON" +msgstr "ๅผ€ๅฏ" + +#: src/lib/menu.sh:374 +msgid "OFF" +msgstr "ๅ…ณ้—ญ" + +#: src/lib/menu.sh:456 src/lib/menu.sh:608 +msgid "Workspaces" +msgstr "ๅทฅไฝœๅŒบ" + +#. TRANSLATORS: keep the letters, they are the keys. +#: src/lib/menu.sh:458 +msgid "" +"โ†‘โ†“ select r: refresh s: sign in again d: remove a: add q: back" +msgstr "โ†‘โ†“ ้€‰ๆ‹ฉ r๏ผšๅˆทๆ–ฐ s๏ผš้‡ๆ–ฐ็™ปๅฝ• d๏ผš็งป้™ค a๏ผšๆทปๅŠ  q๏ผš่ฟ”ๅ›ž" + +#: src/lib/menu.sh:459 +msgid "No workspace yet. Press a to add one." +msgstr "่ฟ˜ๆฒกๆœ‰ๅทฅไฝœๅŒบใ€‚ๆŒ‰ a ๆทปๅŠ ไธ€ไธชใ€‚" + +#: src/lib/menu.sh:484 +#, sh-printf-format +msgid "refreshed %s" +msgstr "%sๅˆทๆ–ฐ" + +#: src/lib/menu.sh:512 +#, sh-printf-format +msgid "Remove \"%s\" and its apps?" +msgstr "็งป้™คโ€œ%sโ€ๅŠๅ…ถๅบ”็”จ๏ผŸ" + +#: src/lib/menu.sh:536 src/lib/menu.sh:607 +msgid "Start an app" +msgstr "ๅฏๅŠจๅบ”็”จ" + +#: src/lib/menu.sh:537 +msgid "โ†‘โ†“ select Enter: start q: back" +msgstr "โ†‘โ†“ ้€‰ๆ‹ฉ Enter๏ผšๅฏๅŠจ q๏ผš่ฟ”ๅ›ž" + +#: src/lib/menu.sh:538 +msgid "No apps yet. Add a workspace first." +msgstr "่ฟ˜ๆฒกๆœ‰ๅบ”็”จใ€‚่ฏทๅ…ˆๆทปๅŠ ๅทฅไฝœๅŒบใ€‚" + +#: src/lib/menu.sh:579 +#, sh-printf-format +msgid "%s is starting. It opens in a window of its own." +msgstr "ๆญฃๅœจๅฏๅŠจ %sใ€‚ๅฎƒไผšๅœจๅ•็‹ฌ็š„็ช—ๅฃไธญๆ‰“ๅผ€ใ€‚" + +#: src/lib/menu.sh:606 +msgid "Add a workspace" +msgstr "ๆทปๅŠ ๅทฅไฝœๅŒบ" + +#: src/lib/menu.sh:610 +msgid "Quit" +msgstr "้€€ๅ‡บ" + +#: src/lib/workspace.sh:163 +msgid "The server did not accept the user name or password." +msgstr "ๆœๅŠกๅ™จๆฒกๆœ‰ๆŽฅๅ—็”จๆˆทๅๆˆ–ๅฏ†็ ใ€‚" + +#: src/lib/workspace.sh:164 +#, sh-printf-format +msgid "Could not reach the server: %s" +msgstr "ๆ— ๆณ•่ฟžๆŽฅๆœๅŠกๅ™จ๏ผš%s" + +#: src/lib/workspace.sh:165 +#, sh-printf-format +msgid "There is no RD Web feed at %s." +msgstr "%s ๆฒกๆœ‰ RD Web ่ฎข้˜…ๆบใ€‚" + +#: src/lib/workspace.sh:166 +#, sh-printf-format +msgid "Could not load the apps: %s" +msgstr "ๆ— ๆณ•ๅŠ ่ฝฝๅบ”็”จ๏ผš%s" + +#: src/lib/workspace.sh:282 +#, sh-printf-format +msgid "Remote desktop of %s" +msgstr "%s ็š„่ฟœ็จ‹ๆกŒ้ข" + +#: src/lib/workspace.sh:284 +#, sh-printf-format +msgid "%s from %s" +msgstr "%s๏ผˆๆฅ่‡ช %s๏ผ‰" + +#: src/lib/menu.sh +msgid "Apps" +msgstr "ๅบ”็”จ" + +#: src/lib/menu.sh +msgid "Share a folder" +msgstr "ๅ…ฑไบซๆ–‡ไปถๅคน" + +#: src/lib/menu.sh +msgid "Open files with the apps" +msgstr "็”จ่ฟ™ไบ›ๅบ”็”จๆ‰“ๅผ€ๆ–‡ไปถ" + +#: src/lib/menu.sh +msgid "Size" +msgstr "ๅคงๅฐ" + +#: src/lib/menu.sh +msgid "Sound" +msgstr "ๅฃฐ้Ÿณ" + +#: src/lib/menu.sh +msgid "Graphics" +msgstr "ๅ›พๅฝข" + +#: src/lib/menu.sh +msgid "Signing in" +msgstr "็™ปๅฝ•" + +#: src/lib/menu.sh +msgid "Wait for the second MFA prompt" +msgstr "็ญ‰ๅพ…็ฌฌไบŒๆฌก MFA ่ฏทๆฑ‚" + +#: src/lib/menu.sh +msgid "Refresh the apps every day" +msgstr "ๆฏๅคฉๅˆทๆ–ฐๅบ”็”จ" diff --git a/res/rdfeed.svg b/res/rdfeed.svg new file mode 100644 index 0000000..2b1d9d4 --- /dev/null +++ b/res/rdfeed.svg @@ -0,0 +1,68 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/res/screenshots/menu.png b/res/screenshots/menu.png new file mode 100644 index 0000000..21077f8 Binary files /dev/null and b/res/screenshots/menu.png differ diff --git a/res/screenshots/settings.png b/res/screenshots/settings.png new file mode 100644 index 0000000..6d477df Binary files /dev/null and b/res/screenshots/settings.png differ diff --git a/res/systemd/rdfeed-refresh.service b/res/systemd/rdfeed-refresh.service new file mode 100644 index 0000000..2d9ae06 --- /dev/null +++ b/res/systemd/rdfeed-refresh.service @@ -0,0 +1,8 @@ +[Unit] +Description=Load the apps of the rdfeed workspaces again +Documentation=man:rdfeed(1) +After=network-online.target + +[Service] +Type=oneshot +ExecStart=@BINDIR@/rdfeed refresh --quiet diff --git a/res/systemd/rdfeed-refresh.timer b/res/systemd/rdfeed-refresh.timer new file mode 100644 index 0000000..45f4a3e --- /dev/null +++ b/res/systemd/rdfeed-refresh.timer @@ -0,0 +1,11 @@ +[Unit] +Description=Load the apps of the rdfeed workspaces again every day +Documentation=man:rdfeed(1) + +[Timer] +OnCalendar=daily +RandomizedDelaySec=1h +Persistent=true + +[Install] +WantedBy=timers.target diff --git a/src/fetch/rdfeed-fetch b/src/fetch/rdfeed-fetch new file mode 100755 index 0000000..957632b --- /dev/null +++ b/src/fetch/rdfeed-fetch @@ -0,0 +1,617 @@ +#!/usr/bin/env python3 +# +# rdfeed-fetch: sign in to an RD Web Access feed and download its apps +# +# The feed is the one Windows subscribes to under "RemoteApp and Desktop +# Connections". Signing in takes NTLM, which curl no longer always has, so it +# is done here with nothing but the Python standard library. +# +# rdfeed-fetch --url URL --user USER [--domain DOMAIN] --out DIR +# +# The password comes on stdin. DIR gets feed.xml, apps.tsv (app, type, title, +# file types), rdp/.rdp and icons/.png. Progress goes to stdout, one +# tab-separated line each: +# +# workspace the name the company gave the feed +# domain the Windows domain the server signed us in to +# app +# skip <title> <reason> +# +# Exit codes: 0 done, 10 user name or password wrong, 11 server not reached, +# 12 no feed at that address, 2 bad arguments. +# +# Copyright (C) 2026 Felitendo +# SPDX-License-Identifier: GPL-3.0-or-later + +import argparse +import base64 +import hmac +import html.parser +import http.client +import os +import re +import socket +import ssl +import struct +import sys +import time +import unicodedata +import urllib.parse +import xml.etree.ElementTree as ET +import zlib + +EXIT_AUTH = 10 +EXIT_NETWORK = 11 +EXIT_NOFEED = 12 + +# WebFeedLogin.aspx hands back a forms ticket in its body, and the Windows +# client returns it as a cookie. The cookie name is not documented and differs +# between versions, so it goes out under every name in use. IIS reads its own. +FEED_AUTH_COOKIES = ("TSWAFeedAuthCookie", ".ASPXAUTH", "TSWAFeedAuth") + +PNG_SIGNATURE = b"\x89PNG\r\n\x1a\n" + +# The file types of the usual apps, for feeds that list none. Windows only +# offers "Open with" for the types the administrator entered, and most never +# enter any. Matched as whole words against the program, the title and the +# alias of an app. +KNOWN_TYPES = [ + (("winword", "word"), "docx docm doc dotx dotm dot rtf odt"), + (("excel",), "xlsx xlsm xlsb xls xltx xltm csv ods"), + (("powerpnt", "powerpoint"), "pptx pptm ppt potx potm ppsx pps odp"), + (("onenote",), "one"), + (("outlook",), "msg"), + (("visio",), "vsdx vsd"), + (("msaccess", "access"), "accdb mdb"), + (("winproj", "project"), "mpp"), + (("mspub", "publisher"), "pub"), + (("acrobat", "acrord32", "acrord", "pdf24", "foxit"), "pdf"), + (("trueview", "dwgviewr", "autocad", "acad"), "dwg dxf"), + (("notepad",), "txt log"), +] + + +class FeedError(Exception): + code = EXIT_NOFEED + + +class AuthError(FeedError): + code = EXIT_AUTH + + +# ---------------------------------------------------------------- NTLMv2 (MS-NLMP) + +def md4(data): + """hashlib has no MD4 any more on OpenSSL 3, and NTLM needs it.""" + def rol(v, s): + v &= 0xFFFFFFFF + return ((v << s) | (v >> (32 - s))) & 0xFFFFFFFF + + msg = bytearray(data) + b"\x80" + while len(msg) % 64 != 56: + msg.append(0) + msg += struct.pack("<Q", (8 * len(data)) & 0xFFFFFFFFFFFFFFFF) + a, b, c, d = 0x67452301, 0xEFCDAB89, 0x98BADCFE, 0x10325476 + for off in range(0, len(msg), 64): + x = struct.unpack("<16I", msg[off:off + 64]) + aa, bb, cc, dd = a, b, c, d + for i in (0, 4, 8, 12): + a = rol(a + ((b & c) | (~b & d)) + x[i], 3) + d = rol(d + ((a & b) | (~a & c)) + x[i + 1], 7) + c = rol(c + ((d & a) | (~d & b)) + x[i + 2], 11) + b = rol(b + ((c & d) | (~c & a)) + x[i + 3], 19) + for i in (0, 1, 2, 3): + a = rol(a + ((b & c) | (b & d) | (c & d)) + x[i] + 0x5A827999, 3) + d = rol(d + ((a & b) | (a & c) | (b & c)) + x[i + 4] + 0x5A827999, 5) + c = rol(c + ((d & a) | (d & b) | (a & b)) + x[i + 8] + 0x5A827999, 9) + b = rol(b + ((c & d) | (c & a) | (d & a)) + x[i + 12] + 0x5A827999, 13) + for i in (0, 2, 1, 3): + a = rol(a + (b ^ c ^ d) + x[i] + 0x6ED9EBA1, 3) + d = rol(d + (a ^ b ^ c) + x[i + 8] + 0x6ED9EBA1, 9) + c = rol(c + (d ^ a ^ b) + x[i + 4] + 0x6ED9EBA1, 11) + b = rol(b + (c ^ d ^ a) + x[i + 12] + 0x6ED9EBA1, 15) + a, b = (a + aa) & 0xFFFFFFFF, (b + bb) & 0xFFFFFFFF + c, d = (c + cc) & 0xFFFFFFFF, (d + dd) & 0xFFFFFFFF + return struct.pack("<4I", a, b, c, d) + + +# UNICODE | REQUEST_TARGET | NTLM | ALWAYS_SIGN | EXTENDED_SESSIONSECURITY | +# TARGET_INFO | VERSION | 128 | 56 +NTLM_FLAGS = (0x00000001 | 0x00000004 | 0x00000200 | 0x00008000 | 0x00080000 + | 0x00800000 | 0x02000000 | 0x20000000 | 0x80000000) +NTLM_VERSION = bytes([10, 0, 0x61, 0x4A, 0, 0, 0, 15]) + + +def ntlm_negotiate(): + return (b"NTLMSSP\x00" + struct.pack("<II", 1, NTLM_FLAGS | 0x00000002) + + struct.pack("<HHI", 0, 0, 40) * 2 + NTLM_VERSION) + + +def ntlm_av_pairs(info): + pairs, i = {}, 0 + while i + 4 <= len(info): + av_id, length = struct.unpack("<HH", info[i:i + 4]) + i += 4 + if av_id == 0: + break + pairs[av_id] = info[i:i + length] + i += length + return pairs + + +def ntowfv2(user, domain, password): + return hmac.new(md4(password.encode("utf-16-le")), + (user.upper() + domain).encode("utf-16-le"), "md5").digest() + + +def ntlmv2_response(key, server_challenge, client_challenge, timestamp, target_info): + temp = (b"\x01\x01" + b"\x00" * 6 + timestamp + client_challenge + b"\x00" * 4 + + target_info + b"\x00" * 4) + return hmac.new(key, server_challenge + temp, "md5").digest() + temp + + +def ntlm_authenticate(challenge, user, domain, password): + """The AUTHENTICATE message. domain None takes the server's domain.""" + if challenge[:8] != b"NTLMSSP\x00" or struct.unpack("<I", challenge[8:12])[0] != 2: + raise FeedError("invalid NTLM challenge") + server_flags = struct.unpack("<I", challenge[20:24])[0] + server_challenge = challenge[24:32] + info_len, _, info_off = struct.unpack("<HHI", challenge[40:48]) + target_info = challenge[info_off:info_off + info_len] + av = ntlm_av_pairs(target_info) + if domain is None: + domain = av.get(2, b"").decode("utf-16-le") + + key = ntowfv2(user, domain, password) + client_challenge = os.urandom(8) + timestamp = av.get(7) or struct.pack("<Q", int((time.time() + 11644473600) * 10_000_000)) + nt_response = ntlmv2_response(key, server_challenge, client_challenge, timestamp, target_info) + # With a server timestamp the LM response is left empty (MS-NLMP 3.1.5.1.2). + if 7 in av: + lm_response = b"\x00" * 24 + else: + lm_response = hmac.new(key, server_challenge + client_challenge, "md5").digest() + client_challenge + + workstation = socket.gethostname().split(".")[0].upper() + fields = [lm_response, nt_response, domain.encode("utf-16-le"), user.encode("utf-16-le"), + workstation.encode("utf-16-le"), b""] + offset, secbufs, payload = 72, b"", b"" + for field in fields: + secbufs += struct.pack("<HHI", len(field), len(field), offset) + payload += field + offset += len(field) + message = (b"NTLMSSP\x00" + struct.pack("<I", 3) + secbufs + + struct.pack("<I", server_flags & NTLM_FLAGS) + NTLM_VERSION + payload) + return message, domain + + +# ---------------------------------------------------------------- HTTP + +class HiddenInputs(html.parser.HTMLParser): + def __init__(self): + super().__init__() + self.fields, self.action = {}, None + + def handle_starttag(self, tag, attrs): + a = dict(attrs) + if tag == "form" and self.action is None: + self.action = a.get("action") + elif tag == "input" and (a.get("type") or "").lower() == "hidden" and a.get("name"): + self.fields[a["name"]] = a.get("value") or "" + + +class Session: + """One keep-alive connection with cookies. NTLM signs in the connection, + not the request, so the handshake has to stay on one socket.""" + + def __init__(self, url, user, domain, password, timeout=30): + parts = urllib.parse.urlsplit(url) + self.scheme = parts.scheme + self.host = parts.hostname + self.port = parts.port + self.user, self.domain, self.password = user, domain, password + self.detected_domain = domain + self.timeout = timeout + self.cookies = {"TSWAFeatureCheckCookie": "true"} + self.conn = None + self.forms_tried = False + + def close(self): + if self.conn: + self.conn.close() + self.conn = None + + def _connect(self): + if self.scheme == "http": + return http.client.HTTPConnection(self.host, self.port, timeout=self.timeout) + return http.client.HTTPSConnection(self.host, self.port, timeout=self.timeout, + context=ssl.create_default_context()) + + def path(self, url, base="/"): + parts = urllib.parse.urlsplit(urllib.parse.urljoin(f"{self.scheme}://{self.host}{base}", url)) + if parts.hostname and parts.hostname.lower() != self.host.lower(): + raise FeedError(f"redirected to another server: {parts.hostname}") + return urllib.parse.urlunsplit(("", "", parts.path or "/", parts.query, "")) + + def send(self, method, path, body=None, headers=None): + hdrs = {"User-Agent": "TSWorkspace/2.0", "Accept": "*/*", "Connection": "keep-alive"} + if self.cookies: + hdrs["Cookie"] = "; ".join(f"{k}={v}" for k, v in self.cookies.items()) + hdrs.update(headers or {}) + for attempt in range(2): + if self.conn is None: + self.conn = self._connect() + try: + self.conn.request(method, path, body=body, headers=hdrs) + resp = self.conn.getresponse() + data = resp.read() + break + except (http.client.HTTPException, OSError): + self.close() + if attempt: + raise + for name, value in resp.getheaders(): + if name.lower() == "set-cookie": + cname, _, rest = value.partition("=") + cvalue = rest.split(";", 1)[0] + if cvalue and "01-jan-1970" not in value.lower(): + self.cookies[cname.strip()] = cvalue + else: + self.cookies.pop(cname.strip(), None) + if (resp.getheader("Connection") or "").lower() == "close": + self.close() + return resp, data + + def ntlm(self, method, path, body, headers): + auth = {"Authorization": "NTLM " + base64.b64encode(ntlm_negotiate()).decode()} + resp, data = self.send(method, path, body, {**(headers or {}), **auth}) + challenge = next((v[5:] for k, v in resp.getheaders() + if k.lower() == "www-authenticate" and v.startswith("NTLM ")), None) + if resp.status != 401 or not challenge: + return resp, data + message, self.detected_domain = ntlm_authenticate( + base64.b64decode(challenge), self.user, self.domain, self.password) + auth = {"Authorization": "NTLM " + base64.b64encode(message).decode()} + return self.send(method, path, body, {**(headers or {}), **auth}) + + def fetch(self, url, method="GET", body=None, headers=None): + path = original = self.path(url) + token_retry = True + for _ in range(10): + resp, data = self.send(method, path, body, headers) + if resp.status == 401: + offered = " ".join(v.lower() for k, v in resp.getheaders() + if k.lower() == "www-authenticate") + if "ntlm" in offered: + resp, data = self.ntlm(method, path, body, headers) + if resp.status == 401: + raise AuthError("the server did not accept the user name or password") + content_type = (resp.getheader("Content-Type") or "").lower() + if resp.status == 200 and content_type.startswith("application/x-msts-webfeed-login"): + token = data.decode("ascii", "replace").strip() + for name in FEED_AUTH_COOKIES: + self.cookies[name] = token + if path != original and token_retry: + path, token_retry = original, False + continue + location = resp.getheader("Location") + if resp.status in (301, 302, 303, 307, 308) and location: + path = self.path(location, path) + if resp.status not in (307, 308): + method, body, headers = "GET", None, None + continue + return resp, data, path + raise FeedError("too many redirects") + + def forms_login(self, login_path): + """Some servers keep the .rdp files behind the RD Web form login.""" + resp, data, path = self.fetch(login_path) + parser = HiddenInputs() + parser.feed(data.decode("utf-8", "replace")) + fields = dict(parser.fields) + user = self.user if "@" in self.user else f"{self.detected_domain}\\{self.user}" + fields.update({"DomainUserName": user, "UserPass": self.password, + "MachineType": "private", "isUtf8": "1"}) + fields.setdefault("flags", "0") + action = self.path(parser.action or path, path) + resp, data, path = self.fetch(action, "POST", urllib.parse.urlencode(fields), + {"Content-Type": "application/x-www-form-urlencoded"}) + if "login.aspx" in path.lower(): + raise AuthError("the RD Web form login failed") + + def download(self, url): + resp, data, path = self.fetch(url) + if "login.aspx" in path.lower() and not self.forms_tried: + self.forms_tried = True + self.forms_login(path) + resp, data, path = self.fetch(url) + if resp.status != 200: + raise FeedError(f"HTTP {resp.status}") + return data + + +# ---------------------------------------------------------------- The feed + +def local_tag(el): + return el.tag.rsplit("}", 1)[-1] + + +def make_slug(text, taken): + # รค โ†’ a, รฉ โ†’ e: the marks go, the letters stay. + text = "".join(c for c in unicodedata.normalize("NFKD", text) if not unicodedata.combining(c)) + base = re.sub(r"[^a-z0-9]+", "-", text.lower()).strip("-")[:40].strip("-") or "app" + slug, n = base, 2 + while slug in taken: + slug, n = f"{base}-{n}", n + 1 + taken.add(slug) + return slug + + +def common_suffix(names): + """The end all aliases share, like "-RemoteApps-CmsRdsh", from a dash on.""" + if len(names) < 2: + return "" + suffix = os.path.commonprefix([n[::-1] for n in names])[::-1] + cut = suffix.find("-") + suffix = suffix[cut:] if cut >= 0 else "" + return suffix if all(len(n) > len(suffix) for n in names) else "" + + +def parse_feed(data): + """The workspace name and its apps. Raises FeedError if this is no feed.""" + try: + root = ET.fromstring(data) + except ET.ParseError: + raise FeedError("the server sent no feed") + if local_tag(root) != "ResourceCollection": + raise FeedError("the server sent no feed") + name = "" + apps = [] + for el in root.iter(): + tag = local_tag(el) + if tag == "Publisher" and not name: + name = (el.get("Name") or "").strip() + if tag != "Resource": + continue + rtype = el.get("Type") or "RemoteApp" + if rtype not in ("RemoteApp", "Desktop"): + continue + title = (el.get("Title") or "").strip() + alias = el.get("Alias") or title or el.get("ID") or "app" + icons, rdp_url, types = [], None, [] + for sub in el.iter(): + stag = local_tag(sub) + if stag == "FileExtension" and sub.get("Name"): + ext = sub.get("Name").strip().lstrip(".").lower() + if re.fullmatch(r"[a-z0-9_+-]+", ext) and ext not in types: + types.append(ext) + elif stag.startswith("Icon") and sub.get("FileURL"): + dims = re.findall(r"\d+", sub.get("Dimensions") or stag) or ["0"] + icons.append({"url": sub.get("FileURL"), "type": (sub.get("FileType") or "").lower(), + "size": int(dims[0])}) + elif stag == "ResourceFile" and sub.get("URL") and rdp_url is None: + if (sub.get("FileExtension") or ".rdp").lower() == ".rdp": + rdp_url = sub.get("URL") + if rdp_url: + apps.append({"alias": alias, "title": title or alias, "type": rtype, + "rdp": rdp_url, "icons": icons, "types": types}) + suffix = common_suffix([a["alias"] for a in apps]) + taken = set() + for app in apps: + app["slug"] = make_slug(app["alias"][:len(app["alias"]) - len(suffix)], taken) + return name, apps + + +def known_types(*names): + """The file types of a well-known app, from its program, title or alias.""" + text = " ".join(names).lower() + for keys, types in KNOWN_TYPES: + if any(re.search(rf"(^|[^a-z0-9]){re.escape(k)}($|[^a-z0-9])", text) for k in keys): + return types.split() + return [] + + +# ---------------------------------------------------------------- .rdp files + +def decode_rdp(data): + if data.startswith(b"\xff\xfe") or data.startswith(b"\xfe\xff"): + return data.decode("utf-16") + if b"\x00" in data[:16]: + return data.decode("utf-16-le") + try: + return data.decode("utf-8-sig") + except UnicodeDecodeError: + return data.decode("latin-1") + + +def rdp_set(text, name, kind, value): + pattern = re.compile(rf"^{re.escape(name)}:[isb]:[^\r\n]*", re.IGNORECASE | re.MULTILINE) + line = f"{name}:{kind}:{value}" + if pattern.search(text): + return pattern.sub(line, text) + return text.rstrip("\r\n") + "\r\n" + line + "\r\n" + + +def rdp_del(text, name): + return re.sub(rf"^{re.escape(name)}:[isb]:[^\r\n]*\r?\n?", "", text, flags=re.IGNORECASE | re.MULTILINE) + + +def sanitize_rdp(text): + # One sign-in for the gateway and the session, and never a prompt: started + # from the app menu, there is no terminal to ask in. + text = rdp_set(text, "promptcredentialonce", "i", "1") + text = rdp_set(text, "prompt for credentials on client", "i", "0") + # Drive letters mean nothing here, and FreeRDP reads "*" as every mount, + # camera or USB device of this computer. Sharing a folder is a setting. + for name in ("drivestoredirect", "devicestoredirect", "camerastoredirect", "usbdevicestoredirect"): + text = rdp_del(text, name) + return text + + +# ---------------------------------------------------------------- Icons + +def png_encode(width, height, rows): + """RGBA rows, top down, as a PNG.""" + def chunk(kind, payload): + return (struct.pack(">I", len(payload)) + kind + payload + + struct.pack(">I", zlib.crc32(kind + payload) & 0xFFFFFFFF)) + raw = b"".join(b"\x00" + bytes(row) for row in rows) + return (PNG_SIGNATURE + chunk(b"IHDR", struct.pack(">IIBBBBB", width, height, 8, 6, 0, 0, 0)) + + chunk(b"IDAT", zlib.compress(raw, 9)) + chunk(b"IEND", b"")) + + +def dib_to_png(dib): + """One bitmap frame of an .ico (BITMAPINFOHEADER, 1 to 32 bits) as a PNG.""" + header, width, height2, _, bpp, compression = struct.unpack("<IiiHHI", dib[:20]) + height = abs(height2) // 2 + if width <= 0 or height <= 0 or compression not in (0, 3) or bpp not in (1, 4, 8, 24, 32): + return None + off = header + (12 if compression == 3 else 0) + palette = [] + if bpp <= 8: + count = struct.unpack("<I", dib[32:36])[0] or (1 << bpp) + for i in range(count): + b, g, r = dib[off + 4 * i:off + 4 * i + 3] + palette.append((r, g, b)) + off += 4 * count + stride = ((width * bpp + 31) // 32) * 4 + pixels = dib[off:off + stride * height] + mask_stride = ((width + 31) // 32) * 4 + mask = dib[off + stride * height:off + stride * height + mask_stride * height] + alpha = bpp == 32 and any(pixels[i] for i in range(3, len(pixels), 4)) + + rows = [] + for y in range(height): + src = height - 1 - y + row = pixels[src * stride:(src + 1) * stride] + mrow = mask[src * mask_stride:(src + 1) * mask_stride] + out = bytearray() + for x in range(width): + if bpp == 32: + b, g, r, a = row[4 * x:4 * x + 4] + if not alpha: + a = 255 + elif bpp == 24: + b, g, r = row[3 * x:3 * x + 3] + a = 255 + else: + bit = x * bpp + index = (row[bit // 8] >> (8 - bpp - bit % 8)) & ((1 << bpp) - 1) + r, g, b = palette[index] if index < len(palette) else (0, 0, 0) + a = 255 + if not alpha and x // 8 < len(mrow) and (mrow[x // 8] >> (7 - x % 8)) & 1: + a = 0 + out += bytes((r, g, b, a)) + rows.append(out) + return png_encode(width, height, rows) + + +def ico_to_png(data): + """The biggest frame of an .ico, as a PNG. None if there is none to read.""" + if data[:4] != b"\x00\x00\x01\x00": + return None + count = struct.unpack("<H", data[4:6])[0] + entries = [] + for i in range(count): + entry = data[6 + 16 * i:22 + 16 * i] + if len(entry) < 16: + break + w, h, _, _, _, bpp, size, offset = struct.unpack("<BBBBHHII", entry) + entries.append(((w or 256) * (h or 256), bpp, size, offset)) + for _, _, size, offset in sorted(entries, reverse=True): + frame = data[offset:offset + size] + if frame[:8] == PNG_SIGNATURE: + return frame + try: + png = dib_to_png(frame) + except (struct.error, ValueError, IndexError): + png = None + if png: + return png + return None + + +def save_icon(session, app, dest): + candidates = [i for i in app["icons"] if i["type"] == "ico"] + candidates += sorted((i for i in app["icons"] if i["type"] != "ico"), + key=lambda i: i["size"], reverse=True) + for icon in candidates: + try: + data = session.download(icon["url"]) + except (FeedError, OSError, http.client.HTTPException): + continue + png = data if data[:8] == PNG_SIGNATURE else ico_to_png(data) + if png: + with open(dest, "wb") as f: + f.write(png) + return True + return False + + +# ---------------------------------------------------------------- Main + +def say(*fields): + print("\t".join(str(f).replace("\t", " ").replace("\n", " ") for f in fields), flush=True) + + +def run(args, password): + session = Session(args.url, args.user, args.domain, password, args.timeout) + resp, data, _ = session.fetch(args.url) + if resp.status != 200: + raise FeedError(f"HTTP {resp.status}") + name, apps = parse_feed(data) + + os.makedirs(os.path.join(args.out, "rdp"), exist_ok=True) + os.makedirs(os.path.join(args.out, "icons"), exist_ok=True) + with open(os.path.join(args.out, "feed.xml"), "wb") as f: + f.write(data) + say("workspace", name or session.host) + say("domain", session.detected_domain or "") + + done = [] + for app in apps: + try: + text = decode_rdp(session.download(app["rdp"])) + if "full address:s:" not in text.lower(): + raise FeedError("not an RDP file") + with open(os.path.join(args.out, "rdp", app["slug"] + ".rdp"), "w") as f: + f.write(sanitize_rdp(text)) + if not app["types"] and app["type"] == "RemoteApp": + program = re.search(r"^remoteapplicationprogram:s:(.*)$", text, re.IGNORECASE | re.MULTILINE) + app["types"] = known_types(program.group(1) if program else "", app["title"], app["alias"]) + save_icon(session, app, os.path.join(args.out, "icons", app["slug"] + ".png")) + except (FeedError, OSError, http.client.HTTPException) as e: + say("skip", app["title"], e) + continue + done.append(app) + say("app", app["slug"], app["type"], app["title"]) + + with open(os.path.join(args.out, "apps.tsv"), "w") as f: + for app in done: + f.write("\t".join((app["slug"], app["type"], app["title"].replace("\t", " "), + " ".join(app["types"]))) + "\n") + + +def main(argv): + parser = argparse.ArgumentParser(prog="rdfeed-fetch", description="Download the apps of an RD Web Access feed.") + parser.add_argument("--url", required=True) + parser.add_argument("--user", required=True) + parser.add_argument("--domain", help="Windows domain; without it the server's own is used") + parser.add_argument("--out", required=True, help="directory for the downloaded files") + parser.add_argument("--timeout", type=float, default=30) + args = parser.parse_args(argv) + + password = sys.stdin.readline().rstrip("\n") + try: + run(args, password) + except FeedError as e: + print(f"rdfeed-fetch: {e}", file=sys.stderr) + return e.code + except (OSError, http.client.HTTPException) as e: + print(f"rdfeed-fetch: {e}", file=sys.stderr) + return EXIT_NETWORK + return 0 + + +if __name__ == "__main__": + sys.exit(main(sys.argv[1:])) diff --git a/src/hook/rdfeed-hook.c b/src/hook/rdfeed-hook.c new file mode 100644 index 0000000..fb9ea44 --- /dev/null +++ b/src/hook/rdfeed-hook.c @@ -0,0 +1,146 @@ +/* + * rdfeed-hook: works around two FreeRDP problems, loaded with LD_PRELOAD. + * + * 1. The second MFA prompt. A connection broker sends the client on to the + * server that hosts the session, so FreeRDP opens a second tunnel through + * the RD Gateway right away. With MFA on the gateway (the Azure MFA NPS + * extension), that second sign-in needs a second prompt. When it comes + * less than about 10 seconds after the first one was confirmed, no prompt + * is sent and the gateway waits forever. Windows does not run into this. + * So a new connection to port 443 waits a little first. + * + * RDFEED_GW_WAIT auto (the default), always or never. auto waits only + * when the first sign-in took long enough for somebody to + * confirm a prompt. + * RDFEED_GW_DELAY how long to wait, in seconds (12). + * + * Before waiting it prints "[rdfeed] gateway-delay <seconds>" to stderr, + * so rdfeed can tell the user a second prompt is coming. + * + * 2. The graphics pipeline. FreeRDP 3 always turns GFX on, there is no switch + * to turn it off, and in RemoteApp mode xfreerdp does not repaint a window + * after it was minimized or resized. With RDFEED_NO_GFX set, every attempt + * to turn it on is turned into off, and the classic drawing is used. + * + * It also makes stdout line buffered, so FreeRDP's log reaches rdfeed as it + * happens. + * + * Copyright (C) 2026 Felitendo + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +#define _GNU_SOURCE +#include <dlfcn.h> +#include <netinet/in.h> +#include <stdio.h> +#include <stdlib.h> +#include <string.h> +#include <sys/socket.h> +#include <sys/types.h> +#include <time.h> +#include <unistd.h> + +/* A pause this long between two connections means a new tunnel, not the + * second channel of the same one or an IPv6 to IPv4 fallback. */ +#define NEW_TUNNEL_SECONDS 2.0 +/* A first sign-in that took this long had a person confirming a prompt. */ +#define MFA_SECONDS 2.5 + +static int (*real_connect)(int, const struct sockaddr *, socklen_t); +static int (*real_set_bool)(void *, int, int); + +static double first_connect = -1; +static double last_connect = -1; + +static double now(void) +{ + struct timespec ts; + clock_gettime(CLOCK_MONOTONIC, &ts); + return (double)ts.tv_sec + (double)ts.tv_nsec / 1e9; +} + +__attribute__((constructor)) static void init(void) +{ + setvbuf(stdout, NULL, _IOLBF, 0); +} + +/* RTLD_NEXT only finds libfreerdp3 when it was loaded globally, as it is in + * xfreerdp. Otherwise ask the library itself. */ +static void *freerdp_sym(const char *name) +{ + void *sym = dlsym(RTLD_NEXT, name); + if (!sym) { + void *lib = dlopen("libfreerdp3.so.3", RTLD_LAZY | RTLD_NOLOAD); + if (lib) + sym = dlsym(lib, name); + } + return sym; +} + +static long gfx_key(void) +{ + static long key = -2; + if (key == -2) { + key = -1; + if (getenv("RDFEED_NO_GFX")) { + ssize_t (*by_name)(const char *) = + (ssize_t (*)(const char *))freerdp_sym("freerdp_settings_get_key_for_name"); + if (by_name) + key = (long)by_name("FreeRDP_SupportGraphicsPipeline"); + } + } + return key; +} + +int freerdp_settings_set_bool(void *settings, int id, int val) +{ + if (!real_set_bool) + real_set_bool = (int (*)(void *, int, int))freerdp_sym("freerdp_settings_set_bool"); + if (!real_set_bool) + return 0; + if (val && id == gfx_key()) + val = 0; + return real_set_bool(settings, id, val); +} + +static int should_wait(double t) +{ + const char *mode = getenv("RDFEED_GW_WAIT"); + + if (last_connect < 0 || t - last_connect < NEW_TUNNEL_SECONDS) + return 0; + if (mode && strcmp(mode, "never") == 0) + return 0; + if (mode && strcmp(mode, "always") == 0) + return 1; + return t - first_connect >= MFA_SECONDS; +} + +int connect(int fd, const struct sockaddr *addr, socklen_t len) +{ + if (!real_connect) + real_connect = (int (*)(int, const struct sockaddr *, socklen_t))dlsym(RTLD_NEXT, "connect"); + + int port = -1; + if (addr && addr->sa_family == AF_INET) + port = ntohs(((const struct sockaddr_in *)addr)->sin_port); + else if (addr && addr->sa_family == AF_INET6) + port = ntohs(((const struct sockaddr_in6 *)addr)->sin6_port); + + if (port == 443) { + double t = now(); + if (first_connect < 0) + first_connect = t; + if (should_wait(t)) { + const char *env = getenv("RDFEED_GW_DELAY"); + unsigned left = env ? (unsigned)atoi(env) : 12; + fprintf(stderr, "[rdfeed] gateway-delay %u\n", left); + fflush(stderr); + while (left) + left = sleep(left); + t = now(); + } + last_connect = t; + } + return real_connect(fd, addr, len); +} diff --git a/src/lib/common.sh b/src/lib/common.sh new file mode 100644 index 0000000..7bc3ad2 --- /dev/null +++ b/src/lib/common.sh @@ -0,0 +1,202 @@ +# shellcheck shell=bash +# +# Paths, translations and output helpers. +# +# Everything rdfeed keeps is the user's own: a settings file and one file per +# workspace under ~/.config/rdfeed, the downloaded apps under +# ~/.local/share/rdfeed, and the passwords in the keyring. Nothing needs root. + +RF_VERSION="@VERSION@" +RF_NAME="rdfeed" +RF_PRETTY="rdfeed" + +RF_LIBDIR="${RF_LIBDIR:-@LIBDIR@}" +RF_LIBEXECDIR="${RF_LIBEXECDIR:-@LIBEXECDIR@}" +RF_LOCALEDIR="${RF_LOCALEDIR:-@LOCALEDIR@}" + +RF_FETCH="${RF_FETCH:-$RF_LIBEXECDIR/rdfeed-fetch}" +RF_HOOK="${RF_HOOK:-$RF_LIBEXECDIR/rdfeed-hook.so}" + +RF_XDG_CONFIG="${XDG_CONFIG_HOME:-$HOME/.config}" +RF_XDG_DATA="${XDG_DATA_HOME:-$HOME/.local/share}" +RF_XDG_CACHE="${XDG_CACHE_HOME:-$HOME/.cache}" + +RF_CONFDIR="${RF_XDG_CONFIG}/${RF_NAME}" +RF_CONFIG="${RF_CONFDIR}/config" +RF_WSDIR="${RF_CONFDIR}/workspaces" +RF_DATADIR="${RF_XDG_DATA}/${RF_NAME}" +RF_CACHEDIR="${RF_XDG_CACHE}/${RF_NAME}" +# For what only lives while an app runs: the .rdp copy that names a file. +RF_RUNDIR="${XDG_RUNTIME_DIR:-$RF_CACHEDIR}/${RF_NAME}" + +# Where the app menu looks: the entries, the folder for each workspace (a +# merged menu) and the folder's name and icon. +RF_APPDIR="${RF_XDG_DATA}/applications" +RF_MENUDIR="${RF_XDG_CONFIG}/menus/applications-merged" +RF_DIRDIR="${RF_XDG_DATA}/desktop-directories" + +RF_UNIT_TIMER="rdfeed-refresh.timer" + +# --------------------------------------------------------------------------- +# Translations +# --------------------------------------------------------------------------- + +export TEXTDOMAIN="rdfeed" +export TEXTDOMAINDIR="${RF_LOCALEDIR}" + +rf_ui_locale() { + local l="${RF_UI_LOCALE:-}" + + if [[ -z $l ]]; then + l="${LC_ALL:-}" + [[ -z $l ]] && l="${LC_MESSAGES:-}" + [[ -z $l ]] && l="${LANG:-}" + fi + + # systemd writes /etc/locale.conf and most distributions use it; Debian and + # Ubuntu keep the same LANG= line in /etc/default/locale instead. + if [[ -z $l ]]; then + local f + for f in /etc/locale.conf /etc/default/locale; do + [[ -r $f ]] || continue + l="$(sed -n 's/^LANG=//p' "$f" | tr -d '"' | head -n1)" + [[ -n $l ]] && break + done + fi + + printf '%s\n' "${l:-C}" +} + +# Every gettext lookup is a fork and the settings screen redraws a screenful of +# labels per keypress, so results are memoized. +declare -A RF_MSG_CACHE=() + +RF_MSG_RESULT='' + +# rf_msg_into <locale> <msgid> +# Plain lookup with the result in RF_MSG_RESULT and no printf formatting, for +# callers that would otherwise pay a fork per label per frame. +rf_msg_into() { + local locale="$1" msgid="$2" cachekey + cachekey="${locale}"$'\x1f'"${msgid}" + + if [[ -n ${RF_MSG_CACHE[$cachekey]+set} ]]; then + RF_MSG_RESULT="${RF_MSG_CACHE[$cachekey]}" + return 0 + fi + + RF_MSG_RESULT="$(LC_ALL="$locale" LANGUAGE="${locale%%.*}" gettext -- "$msgid" 2>/dev/null)" + [[ -n $RF_MSG_RESULT ]] || RF_MSG_RESULT="$msgid" + RF_MSG_CACHE[$cachekey]="$RF_MSG_RESULT" + return 0 +} + +# rf_msg_in <locale> <msgid> [printf args...] +rf_msg_in() { + local locale="$1" msgid="$2" + shift 2 + + rf_msg_into "$locale" "$msgid" + + # With no arguments the message is plain text, not a format string. Feeding + # it to printf anyway would turn a literal percent sign in a translation + # into an invalid conversion. + if (( $# == 0 )); then + printf '%s' "$RF_MSG_RESULT" + return + fi + + # shellcheck disable=SC2059 # the format string is the translated message + printf -- "$RF_MSG_RESULT" "$@" +} + +RF_LOCALE_CACHED='' + +# rf_msg <msgid> [printf args...] +rf_msg() { + [[ -n $RF_LOCALE_CACHED ]] || RF_LOCALE_CACHED="$(rf_ui_locale)" + rf_msg_in "$RF_LOCALE_CACHED" "$@" +} + +# --------------------------------------------------------------------------- +# Output +# --------------------------------------------------------------------------- + +# Decided once, while stdout is still whatever the process was started with: +# testing -t 1 at the point of use is wrong for anything called through $(...), +# which sees a pipe and would conclude nobody is watching. +RF_INTERACTIVE='' +[[ -t 1 ]] && RF_INTERACTIVE=1 + +if [[ -n $RF_INTERACTIVE && -z ${NO_COLOR:-} ]]; then + RF_C_RESET=$'\033[0m' + RF_C_BOLD=$'\033[1m' + RF_C_DIM=$'\033[2m' + RF_C_BLUE=$'\033[38;2;52;153;255m' + RF_C_GREEN=$'\033[32m' + RF_C_YELLOW=$'\033[33m' + RF_C_RED=$'\033[31m' +else + RF_C_RESET='' RF_C_BOLD='' RF_C_DIM='' RF_C_BLUE='' + RF_C_GREEN='' RF_C_YELLOW='' RF_C_RED='' +fi + +# What rf_ok, rf_bad and rf_note printed. The menu redraws straight after an +# action, which wipes the screen, so it shows these again under the new frame +# rather than holding everything up for a key press. +RF_UI_NOTICES=() + +rf_say() { printf '%s\n' "$*"; } +rf_head() { printf '\n%s%s%s\n\n' "$RF_C_BOLD$RF_C_BLUE" "$*" "$RF_C_RESET"; } +rf_ok() { RF_UI_NOTICES+=("$RF_C_GREENโœ”$RF_C_RESET $*"); printf '%sโœ”%s %s\n' "$RF_C_GREEN" "$RF_C_RESET" "$*"; } +rf_bad() { RF_UI_NOTICES+=("$RF_C_REDโœ˜$RF_C_RESET $*"); printf '%sโœ˜%s %s\n' "$RF_C_RED" "$RF_C_RESET" "$*" >&2; } +rf_note() { RF_UI_NOTICES+=("$RF_C_DIMโ€ข$RF_C_RESET $*"); printf '%sโ€ข%s %s\n' "$RF_C_DIM" "$RF_C_RESET" "$*"; } + +rf_have() { command -v "$1" > /dev/null 2>&1; } + +# rf_notify <summary> <body> [icon] +# A desktop notification, for what happens after the menu or the app menu +# started something and nobody watches a terminal. +rf_notify() { + rf_have notify-send || return 0 + notify-send -a "$RF_PRETTY" -i "${3:-rdfeed}" -- "$1" "$2" > /dev/null 2>&1 || true +} + +# Human-readable "x minutes ago" for a unix timestamp. 0 or empty yields the +# translated "never". +# +# Written with [[ ]] and a variable for each number on purpose: xgettext reads +# the < of an (( )) as a redirection and loses every string after it. +rf_time_ago() { + local ts="$1" now delta n + + if [[ ! $ts =~ ^[0-9]+$ || $ts -eq 0 ]]; then + rf_msg "never" + printf '\n' + return + fi + + now="$(date +%s)" + delta=$(( now - ts )) + [[ $delta -lt 0 ]] && delta=0 + + if [[ $delta -lt 60 ]]; then + rf_msg "just now" + elif [[ $delta -lt 120 ]]; then + rf_msg "1 minute ago" + elif [[ $delta -lt 3600 ]]; then + n=$(( delta / 60 )) + rf_msg "%d minutes ago" "$n" + elif [[ $delta -lt 7200 ]]; then + rf_msg "1 hour ago" + elif [[ $delta -lt 86400 ]]; then + n=$(( delta / 3600 )) + rf_msg "%d hours ago" "$n" + elif [[ $delta -lt 172800 ]]; then + rf_msg "1 day ago" + else + n=$(( delta / 86400 )) + rf_msg "%d days ago" "$n" + fi + printf '\n' +} diff --git a/src/lib/config.sh b/src/lib/config.sh new file mode 100644 index 0000000..3aee70f --- /dev/null +++ b/src/lib/config.sh @@ -0,0 +1,155 @@ +# shellcheck shell=bash +# +# Reading and writing the settings files. +# +# ~/.config/rdfeed/config holds how the apps run (a shared folder, the size, +# the graphics), and ~/.config/rdfeed/workspaces/<id> where a workspace comes +# from and who signs in to it. Neither is meant to be edited by hand: every +# option is in the menu. +# +# Both are parsed rather than sourced. One "Key=Value" per line, '#' comments. + +declare -A RF_KV_CACHE=() + +# _rf_kv_lookup <file> <Key> [default] +# Result in RF_KV_VALUE. Assigning rather than printing matters on the +# settings screen, which reads every key on every frame: a command +# substitution there is a fork, and forks are the whole cost of a redraw. +RF_KV_VALUE='' + +_rf_kv_lookup() { + local file="$1" key="$2" default="${3:-}" val='' line content + + RF_KV_VALUE="$default" + [[ -r $file ]] || return 0 + + if [[ -n ${RF_KV_CACHE[$file]+set} ]]; then + content="${RF_KV_CACHE[$file]}" + else + content="$(< "$file")" + RF_KV_CACHE[$file]="$content" + fi + + while IFS= read -r line; do + [[ $line == *"$key"* ]] || continue + [[ $line =~ ^[[:space:]]*"$key"[[:space:]]*=(.*)$ ]] || continue + val="${BASH_REMATCH[1]}" + done <<< "$content" + + val="${val#"${val%%[![:space:]]*}"}" + val="${val%"${val##*[![:space:]]}"}" + + [[ -n $val ]] && RF_KV_VALUE="$val" + return 0 +} + +rf_is_true() { + case "${1,,}" in + yes|y|true|1|on|enabled) return 0 ;; + *) return 1 ;; + esac +} + +# rf_kv_set <file> <Key> <Value> <header line> +rf_kv_set() { + local file="$1" key="$2" value="$3" header="$4" tmp + + if [[ ! -e $file ]]; then + mkdir -p "$(dirname "$file")" || return 1 + { + printf '# %s\n' "$header" + printf '#\n' + # shellcheck disable=SC2016 # the backticks are text + printf '# Written by `%s`. Nothing here needs editing by hand:\n' "$RF_NAME" + printf '# every option is in the menu.\n' + } > "$file" || return 1 + fi + [[ -w $file ]] || return 1 + + tmp="$(mktemp "${file}.XXXXXX")" || return 1 + chmod --reference="$file" "$tmp" 2>/dev/null || chmod 0644 "$tmp" + + if grep -qE "^[[:space:]]*#?[[:space:]]*${key}[[:space:]]*=" "$file"; then + awk -v key="$key" -v value="$value" ' + !done && $0 ~ "^[[:space:]]*#?[[:space:]]*" key "[[:space:]]*=" { + print key "=" value; done = 1; next + } + # drop any further occurrences so the file cannot grow duplicates + $0 ~ "^[[:space:]]*" key "[[:space:]]*=" { next } + { print } + ' "$file" > "$tmp" || { rm -f "$tmp"; return 1; } + else + cat "$file" > "$tmp" || { rm -f "$tmp"; return 1; } + printf '%s=%s\n' "$key" "$value" >> "$tmp" + fi + + mv -f "$tmp" "$file" + unset 'RF_KV_CACHE[$file]' +} + +# --------------------------------------------------------------------------- +# The settings +# --------------------------------------------------------------------------- + +rf_config_get() { + _rf_kv_lookup "$RF_CONFIG" "$@" + printf '%s\n' "$RF_KV_VALUE" +} + +rf_config_set() { + rf_kv_set "$RF_CONFIG" "$1" "$2" "$RF_PRETTY" +} + +# rf_config_load +# Everything starting an app needs, resolved once. +rf_config_load() { + RF_KV_CACHE=() + _rf_kv_lookup "$RF_CONFIG" Share off; CFG_SHARE="$RF_KV_VALUE" + _rf_kv_lookup "$RF_CONFIG" Scale auto; CFG_SCALE="$RF_KV_VALUE" + _rf_kv_lookup "$RF_CONFIG" Graphics gfx; CFG_GRAPHICS="$RF_KV_VALUE" + _rf_kv_lookup "$RF_CONFIG" MfaWait auto; CFG_MFAWAIT="$RF_KV_VALUE" + _rf_kv_lookup "$RF_CONFIG" Sound yes; CFG_SOUND=no; rf_is_true "$RF_KV_VALUE" && CFG_SOUND=yes + return 0 +} + +# --------------------------------------------------------------------------- +# Workspaces +# --------------------------------------------------------------------------- + +rf_ws_file() { printf '%s/%s\n' "$RF_WSDIR" "$1"; } + +# rf_ws_ids +# The workspaces, sorted, in RF_WS_IDS. +RF_WS_IDS=() + +rf_ws_ids() { + local f + RF_WS_IDS=() + [[ -d $RF_WSDIR ]] || return 0 + for f in "$RF_WSDIR"/*; do + [[ -f $f ]] || continue + RF_WS_IDS+=("${f##*/}") + done +} + +# rf_ws_load <id> +# Sets WS_URL, WS_NAME, WS_USER, WS_DOMAIN, WS_REFRESHED and WS_ACCOUNT, the +# name the way Windows writes it (DOMAIN\user, or user@domain as typed). +rf_ws_load() { + local file + file="$(rf_ws_file "$1")" + _rf_kv_lookup "$file" Url; WS_URL="$RF_KV_VALUE" + _rf_kv_lookup "$file" Name "$1"; WS_NAME="$RF_KV_VALUE" + _rf_kv_lookup "$file" User; WS_USER="$RF_KV_VALUE" + _rf_kv_lookup "$file" Domain; WS_DOMAIN="$RF_KV_VALUE" + _rf_kv_lookup "$file" Refreshed 0; WS_REFRESHED="$RF_KV_VALUE" + if [[ -n $WS_DOMAIN && $WS_USER != *@* ]]; then + WS_ACCOUNT="$WS_DOMAIN\\$WS_USER" + else + WS_ACCOUNT="$WS_USER" + fi +} + +rf_ws_set() { + rf_kv_set "$(rf_ws_file "$1")" "$2" "$3" "$RF_PRETTY workspace" +} diff --git a/src/lib/launch.sh b/src/lib/launch.sh new file mode 100644 index 0000000..2ab94bd --- /dev/null +++ b/src/lib/launch.sh @@ -0,0 +1,307 @@ +# shellcheck shell=bash +# +# Starting an app. +# +# Each app runs in xfreerdp in RemoteApp mode: only its own windows show, not +# a whole Windows desktop. The arguments, the password among them, go to +# FreeRDP on stdin (/args-from:stdin), never on its command line. +# +# A file from this computer (an "Open with", or `rdfeed run APP FILE`) gets +# there the way Windows does it: its folder is shared as a drive, and the app +# is started with the path on that drive, \\tsclient\<folder>\<file>. +# +# rdfeed-hook.so goes into FreeRDP with LD_PRELOAD. It waits before the second +# gateway sign-in so a second MFA prompt is sent, and keeps the graphics +# pipeline off, which leaves windows unpainted in RemoteApp mode. See +# src/hook/rdfeed-hook.c. + +# How long the hook waits before the second sign-in, in seconds. +RF_GW_DELAY=12 + +# rf_freerdp +# The FreeRDP 3 client that can show RemoteApps (the X11 one), in RF_FREERDP. +# Arch and Debian call it xfreerdp3, Fedora xfreerdp. +RF_FREERDP='' + +rf_freerdp() { + RF_FREERDP='' + if rf_have xfreerdp3; then + RF_FREERDP=xfreerdp3 + elif rf_have xfreerdp && [[ $(xfreerdp /version 2>/dev/null) == *" 3."* ]]; then + RF_FREERDP=xfreerdp + fi + [[ -n $RF_FREERDP ]] +} + +# rf_kbd_layout +# The Windows id of the keyboard layout in use. Under Wayland, X11 programs +# often see "us" whatever the real layout is, so it is asked of the desktop. +rf_kbd_layout() { + local layout='' + if [[ -r $RF_XDG_CONFIG/kxkbrc ]]; then + layout="$(sed -n 's/^LayoutList=//p' "$RF_XDG_CONFIG/kxkbrc" | head -n1)" + fi + if [[ -z $layout && ${XDG_CURRENT_DESKTOP:-} == *GNOME* ]] && rf_have gsettings; then + layout="$(gsettings get org.gnome.desktop.input-sources sources 2>/dev/null \ + | sed -n "s/^[^']*'xkb', '\([a-z]*\).*/\1/p")" + fi + if [[ -z $layout ]] && rf_have localectl; then + layout="$(localectl status 2>/dev/null | sed -n 's/.*X11 Layout: *//p')" + fi + [[ -z $layout ]] && layout="${XKB_DEFAULT_LAYOUT:-}" + layout="${layout%%,*}" + case "${layout,,}" in + de|at) printf '0x00000407\n' ;; + ch) printf '0x00000807\n' ;; + us) printf '0x00000409\n' ;; + gb|uk) printf '0x00000809\n' ;; + fr) printf '0x0000040C\n' ;; + be) printf '0x0000080C\n' ;; + es) printf '0x0000040A\n' ;; + it) printf '0x00000410\n' ;; + nl) printf '0x00000413\n' ;; + pl) printf '0x00000415\n' ;; + pt) printf '0x00000816\n' ;; + br) printf '0x00000416\n' ;; + ru) printf '0x00000419\n' ;; + ua) printf '0x00000422\n' ;; + tr) printf '0x0000041F\n' ;; + jp) printf '0x00000411\n' ;; + kr) printf '0x00000412\n' ;; + se) printf '0x0000041D\n' ;; + no) printf '0x00000414\n' ;; + dk) printf '0x00000406\n' ;; + fi) printf '0x0000040B\n' ;; + cz) printf '0x00000405\n' ;; + sk) printf '0x0000041B\n' ;; + hu) printf '0x0000040E\n' ;; + *) return 1 ;; + esac +} + +# rf_auto_scale +# How much the desktop scales X11 programs that scale themselves, in percent, +# in RF_AUTO_SCALE. The apps are then drawn that much bigger on the server, so +# they come out sharp and the right size. +RF_AUTO_SCALE=100 + +rf_auto_scale() { + local s dpi + RF_AUTO_SCALE=100 + s="$(awk -F= '/^\[/ { section = $0 } section == "[Xwayland]" && $1 == "Scale" { print $2 }' \ + "$RF_XDG_CONFIG/kwinrc" 2>/dev/null | tail -n1)" + if [[ $s =~ ^[0-9]+(\.[0-9]+)?$ ]]; then + RF_AUTO_SCALE="$(awk -v s="$s" 'BEGIN { printf "%d", s * 100 + 0.5 }')" + elif rf_have xrdb; then + dpi="$(xrdb -query 2>/dev/null | awk '/^Xft\.dpi:/ { print $2 }')" + [[ $dpi =~ ^[0-9]+$ ]] && RF_AUTO_SCALE=$(( (dpi * 100 + 48) / 96 )) + fi + [[ $RF_AUTO_SCALE -lt 100 ]] && RF_AUTO_SCALE=100 + [[ $RF_AUTO_SCALE -gt 500 ]] && RF_AUTO_SCALE=500 + return 0 +} + +# rf_share_dir +# The folder the apps may see, from the Share setting. Nothing for off. +rf_share_dir() { + local dir='' + case "$CFG_SHARE" in + home) dir="$HOME" ;; + documents) rf_have xdg-user-dir && dir="$(xdg-user-dir DOCUMENTS)" ;; + downloads) rf_have xdg-user-dir && dir="$(xdg-user-dir DOWNLOAD)" ;; + esac + # xdg-user-dir answers with the home folder for a folder that is not set + # up. Sharing all of it then would be a surprise. + [[ $CFG_SHARE != home && ${dir%/} == "${HOME%/}" ]] && return 1 + [[ -n $dir && -d $dir ]] || return 1 + printf '%s\n' "$dir" +} + +# _rf_drive_name <dir> +# A name for a shared folder, as the app shows it: the folder's own name, +# without what Windows does not allow in one. +_rf_drive_name() { + local name="${1%/}" + name="${name##*/}" + name="${name//[\\\/:*?\"<>|,;]/_}" + printf '%s\n' "${name:-Linux}" +} + +# _rf_drive_path <dir> +# The folder as FreeRDP can take it. /drive splits at commas, so a folder with +# one in its path is shared through a link that has none. +_rf_drive_path() { + local dir="$1" link + if [[ $dir != *,* ]]; then + printf '%s\n' "$dir" + return + fi + link="$RF_RUNDIR/links/$(_rf_drive_name "$dir")" + link="${link//,/_}" + mkdir -p "${link%/*}" && ln -sfn "$dir" "$link" && printf '%s\n' "$link" +} + +# rf_ask_password <account> +# For when the keyring has none: in the terminal if there is one, else in a +# small window. +rf_ask_password() { + local prompt password + prompt="$(rf_msg "Password for %s:" "$1")" + if [[ -t 0 ]]; then + IFS= read -rsp " $prompt " password || return 1 + printf '\n' >&2 + elif rf_have kdialog; then + password="$(kdialog --title "$RF_PRETTY" --password "$prompt" 2>/dev/null)" || return 1 + elif rf_have zenity; then + password="$(zenity --password --title "$prompt" 2>/dev/null)" || return 1 + else + return 1 + fi + [[ -n $password ]] || return 1 + printf '%s' "$password" +} + +# rf_launch_error <log> +# Why FreeRDP gave up, in words, from what it logged. +rf_launch_error() { + local log="$1" last + if grep -qE 'ERRCONNECT_(PASSWORD_EXPIRED|PASSWORD_MUST_CHANGE|PASSWORD_CERTAINLY_EXPIRED)' "$log"; then + rf_msg "Your password has expired. Change it on Windows or on the RD Web page, then sign in again in rdfeed." + elif grep -qE 'ERRCONNECT_ACCOUNT_LOCKED_OUT' "$log"; then + rf_msg "Your account is locked. Ask your IT department." + elif grep -qE 'ERRCONNECT_(LOGON_FAILURE|WRONG_PASSWORD|AUTHENTICATION_FAILED)|STATUS_LOGON_FAILURE' "$log"; then + rf_msg "The server did not accept the password. Sign in again in rdfeed." + elif grep -qE 'ERRCONNECT_(DNS_NAME_NOT_FOUND|DNS_ERROR|CONNECT_FAILED|CONNECT_TRANSPORT_FAILED)' "$log"; then + rf_msg "Could not reach the server. Check the internet connection." + else + last="$(grep '\[ERROR\]' "$log" | tail -n1 | sed 's/^.*\] - \[[^]]*\]: *//')" + rf_msg "FreeRDP stopped: %s" "${last:-?}" + fi +} + +# rf_launch <id> <app> [file] +rf_launch() { + local id="$1" slug="$2" file="${3:-}" i title='' type='' icon rdp password log rcfile rc line kbd share + local drive='' remote='' temprdp='' dir name rel + local -a args env + + rf_ws_load "$id" + rf_ws_apps_load "$id" + for i in "${!RF_APP_SLUGS[@]}"; do + if [[ ${RF_APP_SLUGS[i]} == "$slug" ]]; then + title="${RF_APP_TITLES[i]}" type="${RF_APP_TYPES[i]}" + fi + done + rdp="$RF_DATADIR/$id/rdp/$slug.rdp" + icon="$RF_DATADIR/$id/icons/$slug.png" + [[ -f $icon ]] || icon=rdfeed + if [[ -z $title || ! -f $rdp ]]; then + rf_bad "$(rf_msg "There is no app %s. See \`%s list\`." "$id/$slug" "$RF_NAME")" + return 1 + fi + + if [[ -n $file ]]; then + file="$(realpath -e -- "$file" 2>/dev/null)" && [[ -f $file ]] || { + rf_bad "$(rf_msg "File not found: %s" "${3:-}")" + rf_notify "$title" "$(rf_msg "File not found: %s" "${3:-}")" dialog-error + return 1 + } + fi + + if ! rf_freerdp; then + rf_bad "$(rf_msg "FreeRDP 3 is missing. Install it to start the apps (the package is called freerdp).")" + rf_notify "$title" "$(rf_msg "FreeRDP 3 is missing. Install it to start the apps (the package is called freerdp).")" dialog-error + return 1 + fi + + password="$(rf_secret_get "$id" "$WS_ACCOUNT")" || password='' + if [[ -z $password ]]; then + password="$(rf_ask_password "$WS_ACCOUNT")" || { + rf_notify "$title" "$(rf_msg "No password saved for %s. Sign in again in rdfeed." "$WS_NAME")" dialog-error + return 1 + } + fi + + rf_config_load + args=("$rdp" "/u:$WS_USER") + [[ -n $WS_DOMAIN && $WS_USER != *@* ]] && args+=("/d:$WS_DOMAIN") + args+=("/p:$password" /cert:tofu +clipboard) + kbd="$(rf_kbd_layout)" && args+=("/kbd:layout:$kbd") + [[ $CFG_SOUND == yes ]] && args+=(/sound) + if [[ $CFG_SCALE == auto ]]; then + rf_auto_scale + else + RF_AUTO_SCALE="$CFG_SCALE" + fi + [[ $RF_AUTO_SCALE -gt 100 ]] && args+=("/scale-desktop:$RF_AUTO_SCALE") + if share="$(rf_share_dir)"; then + name="$(_rf_drive_name "$share")" + args+=("/drive:$name,$(_rf_drive_path "$share")") + # A file in the shared folder needs no drive of its own. + if [[ -n $file && $file == "${share%/}"/* ]]; then + drive="$name" + rel="${file#"${share%/}"/}" + remote="\\\\tsclient\\$name\\${rel//\//\\}" + fi + fi + if [[ -n $file && -z $remote ]]; then + dir="${file%/*}" + drive="$(_rf_drive_name "$dir")" + args+=("/drive:$drive,$(_rf_drive_path "${dir:-/}")") + remote="\\\\tsclient\\$drive\\${file##*/}" + fi + if [[ -n $remote ]]; then + # The app gets the file as its command line, in a copy of the .rdp. + mkdir -p "$RF_RUNDIR" + temprdp="$(mktemp "$RF_RUNDIR/$id--$slug.XXXXXX.rdp")" + RF_CMDLINE="\"$remote\"" awk ' + BEGIN { line = "remoteapplicationcmdline:s:" ENVIRON["RF_CMDLINE"] } + tolower($0) ~ /^remoteapplicationcmdline:s:/ { if (!done) print line "\r"; done = 1; next } + { print } + END { if (!done) print line "\r" } + ' "$rdp" > "$temprdp" + args[0]="$temprdp" + fi + [[ $type == Desktop ]] && args+=(/dynamic-resolution) + + env=(WLOG_LEVEL="${WLOG_LEVEL:-INFO}" RDFEED_GW_WAIT="$CFG_MFAWAIT" RDFEED_GW_DELAY="$RF_GW_DELAY") + [[ $CFG_GRAPHICS == gfx ]] || env+=(RDFEED_NO_GFX=1) + [[ -f $RF_HOOK ]] && env+=(LD_PRELOAD="$RF_HOOK${LD_PRELOAD:+:$LD_PRELOAD}") + + mkdir -p "$RF_CACHEDIR" + log="$RF_CACHEDIR/$id--$slug.log" + rcfile="$(mktemp "$RF_CACHEDIR/.exit.XXXXXX")" + : > "$log" + + if [[ -n $file ]]; then + rf_notify "$(rf_msg "Opening %s in %s" "${file##*/}" "$title")" \ + "$(rf_msg "If it does not open by itself, open it in the app from the drive %s." "$drive")" "$icon" + else + rf_notify "$(rf_msg "Starting %s" "$title")" \ + "$(rf_msg "If your company uses MFA, confirm the sign-in in your authenticator app.")" "$icon" + fi + + { + printf '%s\n' "${args[@]}" | env "${env[@]}" "$RF_FREERDP" /args-from:stdin 2>&1 + printf '%s\n' "${PIPESTATUS[1]}" > "$rcfile" + } | while IFS= read -r line; do + printf '%s\n' "$line" >> "$log" + if [[ $line == "[rdfeed] gateway-delay"* ]]; then + rf_notify "$(rf_msg "A second sign-in is coming")" \ + "$(rf_msg "Confirm the second prompt in your authenticator app too. It comes in about %d seconds." "$RF_GW_DELAY")" "$icon" + fi + done + rc="$(< "$rcfile")" + rm -f "$rcfile" + [[ -n $temprdp ]] && rm -f "$temprdp" + + # Below 128 the session ended the normal way: closed, logged off, or taken + # over by the next app of the same workspace. + if [[ $rc =~ ^[0-9]+$ ]] && (( rc >= 128 )) && ! grep -q ERRCONNECT_CONNECT_CANCELLED "$log"; then + rf_notify "$(rf_msg "%s could not start" "$title")" "$(rf_launch_error "$log")" dialog-error + rf_bad "$(rf_launch_error "$log")" + rf_note "$(rf_msg "Log: %s" "$log")" + return 1 + fi + return 0 +} diff --git a/src/lib/menu.sh b/src/lib/menu.sh new file mode 100644 index 0000000..9c10a10 --- /dev/null +++ b/src/lib/menu.sh @@ -0,0 +1,635 @@ +# shellcheck shell=bash +# +# The interactive front end. +# +# This is the whole configuration interface. There are files behind it, but no +# part of the program ever asks anybody to open one: the workspaces, the apps, +# a settings list. + +# Terminal mode. +# +# bash flips the terminal into non-canonical mode for each `read -sn1` and back +# out again in between. That gap matters: in canonical mode DEL is the ERASE +# character, so the line discipline eats it instead of delivering it, and a +# backspace typed while the interface was between reads simply vanishes. +# Holding non-canonical mode for the whole interface removes the gap. +RF_TERM_SAVED='' + +rf_ui_term_raw() { + rf_have stty || return 0 + [[ -t 0 ]] || return 0 + [[ -n $RF_TERM_SAVED ]] && return 0 + + RF_TERM_SAVED="$(stty -g 2>/dev/null)" || { RF_TERM_SAVED=''; return 0; } + stty -icanon -echo min 1 time 0 2>/dev/null || true +} + +rf_ui_term_restore() { + [[ -n $RF_TERM_SAVED ]] || return 0 + stty "$RF_TERM_SAVED" 2>/dev/null || true + RF_TERM_SAVED='' +} + +# Runs an action with the terminal handed back to normal line mode, so what it +# prompts for (a user name, a password) behaves the way a program expects. +rf_ui_cooked() { + rf_ui_term_restore + "$@" + local rc=$? + rf_ui_term_raw + return $rc +} + +# rf_read_key +# One keypress, resolved to a symbolic name. Arrow keys arrive as ESC [ A, so +# the tail of the sequence is consumed here rather than being mistaken for +# three separate presses. +rf_read_key() { + local k rest + + IFS= read -rsn1 k || return 1 + + case "$k" in + $'\e') + if IFS= read -rsn2 -t 0.05 rest; then + case "$rest" in + '[A') printf 'up\n' ;; + '[B') printf 'down\n' ;; + '[C') printf 'right\n' ;; + '[D') printf 'left\n' ;; + *) printf 'escape\n' ;; + esac + else + printf 'escape\n' + fi + ;; + ''|$'\r') printf 'enter\n' ;; + $'\x7f'|$'\b') printf 'backspace\n' ;; + ' ') printf 'space\n' ;; + *) printf '%s\n' "$k" ;; + esac +} + +# _rf_ui_take_notices +# The messages the last action left, as lines for under a frame, in +# RF_UI_NOTICE_TEXT. Each is shown once. +RF_UI_NOTICE_TEXT='' + +_rf_ui_take_notices() { + local n + RF_UI_NOTICE_TEXT='' + (( ${#RF_UI_NOTICES[@]} )) || return 0 + RF_UI_NOTICE_TEXT=$'\n' + for n in "${RF_UI_NOTICES[@]}"; do + RF_UI_NOTICE_TEXT+=" $n"$'\n' + done + RF_UI_NOTICES=() +} + +# rf_ui_confirm <question> +rf_ui_confirm() { + local key hint yes + # TRANSLATORS: the letter after "[" is the key for yes. y works too. + hint="$(rf_msg "[y/N]")" + yes="${hint:1:1}" + printf '\n %s %s ' "$1" "$hint" + key="$(rf_read_key)" || return 1 + printf '%s\n' "$key" + [[ ${key,,} == y || ${key,,} == "${yes,,}" ]] +} + +# _rf_width <text> +# The columns the text takes, into RF_WIDTH. ${#s} counts characters, but +# Chinese, Japanese and Korean ones take two columns each. +RF_WIDTH=0 + +_rf_width() { + local wide="${1//[^ใ€€-ใ€ฟใ€-ใƒฟใ€-ไถฟไธ€-้ฟฟ๊ฐ€-ํžฏ่ฑˆ-๏ซฟ๏ผ€-๏ฝ ]/}" + RF_WIDTH=$(( ${#1} + ${#wide} )) +} + +# _rf_pad <text> <columns> +# The text, padded with spaces to the columns, into RF_PADDED. printf's %-28s +# pads by bytes, so a label containing "รผ" would come out one column short. +RF_PADDED='' + +_rf_pad() { + local pad + _rf_width "$1" + pad=$(( $2 - RF_WIDTH )) + (( pad < 1 )) && pad=1 + printf -v RF_PADDED '%s%*s' "$1" "$pad" '' +} + +# _rf_wrap <width> <text> +# Word wrap, into RF_WRAP_LINES. +RF_WRAP_LINES=() + +_rf_wrap() { + local width="$1" word line='' used=0 c i + local -a words + RF_WRAP_LINES=() + read -r -a words <<< "$2" + for word in "${words[@]}"; do + _rf_width "$word" + if (( used > 0 && used + 1 + RF_WIDTH > width )); then + RF_WRAP_LINES+=("$line") + line='' used=0 + fi + (( used > 0 )) && line+=' ' used=$(( used + 1 )) + if (( used + RF_WIDTH <= width )); then + line+="$word" used=$(( used + RF_WIDTH )) + continue + fi + # Longer than a line: Chinese and Japanese have no spaces, so break + # between any two characters. + for (( i = 0; i < ${#word}; i++ )); do + c="${word:i:1}" + _rf_width "$c" + if (( used + RF_WIDTH > width )); then + RF_WRAP_LINES+=("$line") + line='' used=0 + fi + line+="$c" used=$(( used + RF_WIDTH )) + done + done + [[ -n $line ]] && RF_WRAP_LINES+=("$line") + return 0 +} + +# _rf_apps_label <count> +_rf_apps_label() { + if [[ $1 == 1 ]]; then + rf_msg "1 app" + else + rf_msg "%d apps" "$1" + fi +} + +# rf_start_detached <id> <app> +# Starts an app on its own, so it keeps running when the menu closes. +rf_start_detached() { + if rf_have setsid; then + setsid -f "$RF_SELF" run "$1/$2" > /dev/null 2>&1 < /dev/null + else + nohup "$RF_SELF" run "$1/$2" > /dev/null 2>&1 < /dev/null & + fi +} + +# --------------------------------------------------------------------------- +# Status +# --------------------------------------------------------------------------- + +# rf_ui_status +# The workspaces, for the head of the menu. +rf_ui_status() { + local id width=0 apps + + rf_ws_ids + if (( ${#RF_WS_IDS[@]} == 0 )); then + printf ' %s\n' "$(rf_msg "No workspace yet. Add the one of your company with [1].")" + return 0 + fi + for id in "${RF_WS_IDS[@]}"; do + rf_ws_load "$id" + _rf_width "$WS_NAME" + (( RF_WIDTH > width )) && width=$RF_WIDTH + done + for id in "${RF_WS_IDS[@]}"; do + rf_ws_load "$id" + rf_ws_apps_load "$id" + _rf_pad "$WS_NAME" $(( width + 3 )) + apps="$(_rf_apps_label "${#RF_APP_SLUGS[@]}")" + printf ' %s%-10s %s%s%s\n' "$RF_PADDED" "$apps" "$RF_C_DIM" "$WS_ACCOUNT" "$RF_C_RESET" + done +} + +# --------------------------------------------------------------------------- +# Settings +# --------------------------------------------------------------------------- +# Format: scope|Key|type|default|label-msgid|choices +# scope user (the settings file), entries (the settings file, and the app +# menu entries are written again), timer (the daily refresh, a +# systemd user timer), or group for a heading, with only the label +# after it +# type bool, or choice (steps through the choices) +RF_SETTINGS=( + "group|Apps" + "user|Share|choice|off|Share a folder|off,documents,downloads,home" + "entries|OpenWith|bool|yes|Open files with the apps" + "user|Scale|choice|auto|Size|auto,100,125,150,175,200" + "user|Sound|bool|yes|Sound" + "user|Graphics|choice|gfx|Graphics|gfx,classic" + "group|Signing in" + "user|MfaWait|choice|auto|Wait for the second MFA prompt|auto,always,never" + "timer|AutoRefresh|bool|yes|Refresh the apps every day" +) + +# rf_value_label <Key> <value> +# How a setting's value reads in the menu. +rf_value_label() { + local dir + case "$1:$2" in + Share:off) rf_msg "off" ;; + Share:home) rf_msg "home folder" ;; + Share:documents|Share:downloads) + case "$2" in + documents) rf_msg "Documents" ;; + *) rf_msg "Downloads" ;; + esac + if dir="$(CFG_SHARE="$2" rf_share_dir)"; then + printf ' %s(%s)%s' "$RF_C_DIM" "${dir/#"$HOME"/\~}" "$RF_C_RESET" + else + printf ' %s(%s)%s' "$RF_C_YELLOW" "$(rf_msg "not found")" "$RF_C_RESET" + fi + ;; + Scale:auto) rf_auto_scale; rf_msg "automatic (%d %%)" "$RF_AUTO_SCALE" ;; + Scale:*) printf '%s %%' "$2" ;; + Graphics:classic) rf_msg "classic" ;; + Graphics:*) rf_msg "GFX" ;; + MfaWait:always) rf_msg "always" ;; + MfaWait:never) rf_msg "never" ;; + MfaWait:*) rf_msg "automatic" ;; + *) printf '%s' "$2" ;; + esac +} + +# rf_setting_help <Key> <value> +# What a setting does, shown under the list for the selected one. +rf_setting_help() { + case "$1:$2" in + Share:off) rf_msg "The apps only see the files on the server. Pick a folder to open and save files of this computer in them." ;; + Share:*) rf_msg "The apps can open and save files in this folder. It shows up as a drive in them." ;; + OpenWith:*) rf_msg "Offers the apps under \"Open with\" for their file types, for example Word for .docx. The folder of the file is shared with the app while it is open." ;; + Scale:*) rf_msg "How big the apps are drawn. Automatic follows the scale of your desktop, so they are sharp and the right size." ;; + Sound:*) rf_msg "Plays the sound of the apps here, for calls and videos." ;; + Graphics:classic) rf_msg "Paints every window right. But some servers end the session right after signing in, then go back to GFX." ;; + Graphics:*) rf_msg "Works with every server. FreeRDP may not repaint a window after it was minimized or resized." ;; + MfaWait:always) rf_msg "Always waits before the second sign-in. For when the second prompt does not come with automatic." ;; + MfaWait:never) rf_msg "Never waits. Right when your company has no MFA." ;; + MfaWait:*) rf_msg "Many servers with MFA need a second sign-in, and its prompt only comes after a short wait. Automatic waits when the first sign-in took a while." ;; + AutoRefresh:*) + if rf_timer_available; then + rf_msg "Loads the list of apps again once a day, so new apps show up by themselves." + else + rf_msg "Needs a systemd user session." + fi + ;; + esac +} + +# _rf_setting_value <scope> <Key> <default> +# The current value, in RF_SETTING_VALUE. +RF_SETTING_VALUE='' + +_rf_setting_value() { + case "$1" in + user|entries) _rf_kv_lookup "$RF_CONFIG" "$2" "$3"; RF_SETTING_VALUE="$RF_KV_VALUE" ;; + timer) if rf_timer_enabled; then RF_SETTING_VALUE=yes; else RF_SETTING_VALUE=no; fi ;; + esac +} + +# _rf_step <current> <step> <choice>... +# The choice <step> (1 or -1) away from the current one, round at the ends. +_rf_step() { + local current="$1" step="$2" i + shift 2 + local -a all=("$@") + for i in "${!all[@]}"; do + if [[ ${all[i]} == "$current" ]]; then + printf '%s\n' "${all[(i + step + ${#all[@]}) % ${#all[@]}]}" + return + fi + done + printf '%s\n' "${all[0]}" +} + +# _rf_setting_change <scope> <Key> <type> <current> <choices> <step> +_rf_setting_change() { + local scope="$1" key="$2" type="$3" current="$4" options="$5" step="$6" next + local -a choices + + case "$type" in + bool) if rf_is_true "$current"; then next=no; else next=yes; fi ;; + choice) + IFS=',' read -r -a choices <<< "$options" + next="$(_rf_step "$current" "$step" "${choices[@]}")" + ;; + esac + + case "$scope" in + user) + rf_config_set "$key" "$next" || rf_bad "$(rf_msg "Could not save the setting.")" + ;; + entries) + rf_config_set "$key" "$next" || rf_bad "$(rf_msg "Could not save the setting.")" + rf_ws_entries_all + ;; + timer) + rf_config_set "$key" "$next" + rf_timer_set "$next" || rf_bad "$(rf_msg "Needs a systemd user session.")" + ;; + esac +} + +# rf_ui_settings +# A cursor list in groups, with what the selected setting does under it. The +# frame is assembled in memory and written once, and everything constant is +# resolved before the loop. +rf_ui_settings() { + local -a scopes=() keys=() types=() defaults=() labels=() widths=() choices=() values=() rows=() + local spec scope key type default label choice locale i j frame row pad dirty=1 cursor=0 shown + local width=0 wrap cols + + locale="$(rf_ui_locale)" + for spec in "${RF_SETTINGS[@]}"; do + IFS='|' read -r scope key type default label choice <<< "$spec" + # A heading has its label where the key would be. + [[ $scope == group ]] && label="$key" key='' + scopes+=("$scope"); keys+=("$key"); types+=("$type"); defaults+=("$default") + choices+=("$choice") + rf_msg_into "$locale" "$label" + labels+=("$RF_MSG_RESULT") + _rf_width "$RF_MSG_RESULT" + widths+=("$RF_WIDTH") + if [[ $scope != group ]]; then + rows+=($(( ${#keys[@]} - 1 ))) + (( RF_WIDTH > width )) && width=$RF_WIDTH + fi + done + local count=${#rows[@]} + + cols="$(tput cols 2>/dev/null)" || cols=80 + [[ $cols =~ ^[0-9]+$ ]] || cols=80 + wrap=$(( cols - 4 )) + (( wrap > 72 )) && wrap=72 + local rule + printf -v rule '%*s' "$wrap" '' + rule="${rule// /โ”€}" + + local title hint l_on l_off + rf_msg_into "$locale" "Settings"; title="$RF_MSG_RESULT" + rf_msg_into "$locale" "โ†‘โ†“ select โ†โ†’ or Space: change q: back"; hint="$RF_MSG_RESULT" + rf_msg_into "$locale" "ON"; l_on="$RF_MSG_RESULT" + rf_msg_into "$locale" "OFF"; l_off="$RF_MSG_RESULT" + + local clearseq + clearseq="$(clear 2>/dev/null)" || clearseq=$'\033[H\033[2J' + + while true; do + if (( dirty )); then + RF_KV_CACHE=() + for i in "${rows[@]}"; do + _rf_setting_value "${scopes[i]}" "${keys[i]}" "${defaults[i]}" + values[i]="$RF_SETTING_VALUE" + done + dirty=0 + fi + + frame="$clearseq"$'\n'"${RF_C_BOLD}${RF_C_BLUE} ${title}${RF_C_RESET}"$'\n' + + local selected=${rows[cursor]} marker before after + for i in "${!keys[@]}"; do + if [[ ${scopes[i]} == group ]]; then + frame+=$'\n'" ${RF_C_BOLD}${labels[i]}${RF_C_RESET}"$'\n' + continue + fi + if [[ ${types[i]} == bool ]]; then + if rf_is_true "${values[i]}"; then + shown="${RF_C_GREEN}${l_on}${RF_C_RESET}" + else + shown="${RF_C_DIM}${l_off}${RF_C_RESET}" + fi + else + shown="$(rf_value_label "${keys[i]}" "${values[i]}")" + fi + # Arrows on the selected choice: left and right step through it. + before=' ' after='' + if (( i == selected )) && [[ ${types[i]} != bool ]]; then + before="${RF_C_BLUE}โ—‚${RF_C_RESET} " after=" ${RF_C_BLUE}โ–ธ${RF_C_RESET}" + fi + pad=$(( width + 2 - widths[i] )) + if (( i == selected )); then marker="${RF_C_BLUE}โ–ธ${RF_C_RESET} "; else marker=' '; fi + printf -v row ' %s%s%*s%s%s%s' "$marker" "${labels[i]}" "$pad" '' "$before" "$shown$RF_C_RESET" "$after" + frame+="$row"$'\n' + done + + # What the selected setting does, in a box of fixed height so the + # screen does not jump while moving through the list. + _rf_wrap "$wrap" "$(rf_setting_help "${keys[selected]}" "${values[selected]}")" + frame+=$'\n'" ${RF_C_DIM}${rule}${RF_C_RESET}"$'\n' + for j in 0 1 2; do + frame+=" ${RF_WRAP_LINES[j]:-}"$'\n' + done + frame+=$'\n'" ${RF_C_DIM}${hint}${RF_C_RESET}"$'\n' + _rf_ui_take_notices + frame+="$RF_UI_NOTICE_TEXT" + printf '%s' "$frame" + + key="$(rf_read_key)" || return 0 + + case "$key" in + up|k) cursor=$(( (cursor - 1 + count) % count )) ;; + down|j) cursor=$(( (cursor + 1) % count )) ;; + space|right|l|left|h) + local step=1 + [[ $key == left || $key == h ]] && step=-1 + _rf_setting_change "${scopes[selected]}" "${keys[selected]}" "${types[selected]}" "${values[selected]}" "${choices[selected]}" "$step" + dirty=1 + ;; + q|Q|escape) return 0 ;; + *) ;; + esac + done +} + +# --------------------------------------------------------------------------- +# Workspaces +# --------------------------------------------------------------------------- + +rf_ui_workspaces() { + local key frame row i cursor=0 count locale width + local -a ids=() + + locale="$(rf_ui_locale)" + local title hint empty + rf_msg_into "$locale" "Workspaces"; title="$RF_MSG_RESULT" + # TRANSLATORS: keep the letters, they are the keys. + rf_msg_into "$locale" "โ†‘โ†“ select r: refresh s: sign in again d: remove a: add q: back"; hint="$RF_MSG_RESULT" + rf_msg_into "$locale" "No workspace yet. Press a to add one."; empty="$RF_MSG_RESULT" + + local clearseq + clearseq="$(clear 2>/dev/null)" || clearseq=$'\033[H\033[2J' + + while true; do + rf_ws_ids + ids=("${RF_WS_IDS[@]}") + count=${#ids[@]} + (( cursor >= count )) && cursor=$(( count > 0 ? count - 1 : 0 )) + + width=0 + for i in "${!ids[@]}"; do + rf_ws_load "${ids[i]}" + _rf_width "$WS_NAME" + (( RF_WIDTH > width )) && width=$RF_WIDTH + done + + frame="$clearseq"$'\n'"${RF_C_BOLD}${RF_C_BLUE} ${title}${RF_C_RESET}"$'\n\n' + (( count == 0 )) && frame+=" ${RF_C_DIM}${empty}${RF_C_RESET}"$'\n' + local marker apps when + for i in "${!ids[@]}"; do + rf_ws_load "${ids[i]}" + rf_ws_apps_load "${ids[i]}" + apps="$(_rf_apps_label "${#RF_APP_SLUGS[@]}")" + when="$(rf_msg "refreshed %s" "$(rf_time_ago "$WS_REFRESHED")")" + _rf_pad "$WS_NAME" $(( width + 3 )) + if (( i == cursor )); then marker="${RF_C_BLUE}โ–ธ${RF_C_RESET} "; else marker=' '; fi + printf -v row ' %s%s%-10s %s %s%s%s' "$marker" "$RF_PADDED" "$apps" "$WS_ACCOUNT" "$RF_C_DIM" "$when" "$RF_C_RESET" + frame+="$row"$'\n' + done + frame+=$'\n'" ${RF_C_DIM}${hint}${RF_C_RESET}"$'\n' + _rf_ui_take_notices + frame+="$RF_UI_NOTICE_TEXT" + printf '%s' "$frame" + + key="$(rf_read_key)" || return 0 + case "$key" in + up|k) (( count )) && cursor=$(( (cursor - 1 + count) % count )) ;; + down|j) (( count )) && cursor=$(( (cursor + 1) % count )) ;; + r|R) + (( count )) || continue + printf '\n' + rf_ui_cooked rf_do_refresh "${ids[cursor]}" + ;; + s|S) + (( count )) || continue + printf '\n' + rf_ui_cooked rf_do_signin_again "${ids[cursor]}" + ;; + d|D) + (( count )) || continue + rf_ws_load "${ids[cursor]}" + if rf_ui_confirm "$(rf_msg "Remove \"%s\" and its apps?" "$WS_NAME")"; then + rf_do_remove "${ids[cursor]}" > /dev/null + fi + ;; + a|A) + printf '\n' + rf_ui_cooked rf_do_add + ;; + q|Q|escape) return 0 ;; + *) ;; + esac + done +} + +# --------------------------------------------------------------------------- +# Apps +# --------------------------------------------------------------------------- + +rf_ui_apps() { + local key frame row i id cursor=0 count locale + local -a ws=() slugs=() titles=() + + locale="$(rf_ui_locale)" + local title hint empty + rf_msg_into "$locale" "Start an app"; title="$RF_MSG_RESULT" + rf_msg_into "$locale" "โ†‘โ†“ select Enter: start q: back"; hint="$RF_MSG_RESULT" + rf_msg_into "$locale" "No apps yet. Add a workspace first."; empty="$RF_MSG_RESULT" + + rf_ws_ids + for id in "${RF_WS_IDS[@]}"; do + rf_ws_apps_load "$id" + for i in "${!RF_APP_SLUGS[@]}"; do + ws+=("$id") slugs+=("${RF_APP_SLUGS[i]}") titles+=("${RF_APP_TITLES[i]}") + done + done + count=${#slugs[@]} + + local clearseq + clearseq="$(clear 2>/dev/null)" || clearseq=$'\033[H\033[2J' + + while true; do + frame="$clearseq"$'\n'"${RF_C_BOLD}${RF_C_BLUE} ${title}${RF_C_RESET}"$'\n' + (( count == 0 )) && frame+=$'\n'" ${RF_C_DIM}${empty}${RF_C_RESET}"$'\n' + local marker last='' + for i in "${!slugs[@]}"; do + # A heading for each workspace. + if [[ ${ws[i]} != "$last" ]]; then + rf_ws_load "${ws[i]}" + frame+=$'\n'" ${RF_C_BOLD}${WS_NAME}${RF_C_RESET}"$'\n' + last="${ws[i]}" + fi + if (( i == cursor )); then marker="${RF_C_BLUE}โ–ธ${RF_C_RESET} "; else marker=' '; fi + printf -v row ' %s%s' "$marker" "${titles[i]}" + frame+="$row"$'\n' + done + frame+=$'\n'" ${RF_C_DIM}${hint}${RF_C_RESET}"$'\n' + _rf_ui_take_notices + frame+="$RF_UI_NOTICE_TEXT" + printf '%s' "$frame" + + key="$(rf_read_key)" || return 0 + case "$key" in + up|k) (( count )) && cursor=$(( (cursor - 1 + count) % count )) ;; + down|j) (( count )) && cursor=$(( (cursor + 1) % count )) ;; + enter|space) + (( count )) || continue + rf_start_detached "${ws[cursor]}" "${slugs[cursor]}" + rf_ok "$(rf_msg "%s is starting. It opens in a window of its own." "${titles[cursor]}")" > /dev/null + ;; + q|Q|escape) return 0 ;; + *) ;; + esac + done +} + +# --------------------------------------------------------------------------- +# The menu +# --------------------------------------------------------------------------- + +rf_ui_menu() { + local choice + + if [[ ! -t 0 ]]; then + rf_do_help + return 1 + fi + rf_ui_term_raw + trap 'rf_ui_term_restore' EXIT INT TERM + + while true; do + clear 2>/dev/null || true + rf_head " $RF_PRETTY" + rf_ui_status + printf '\n' + printf ' [1] %s\n' "$(rf_msg "Add a workspace")" + printf ' [2] %s\n' "$(rf_msg "Start an app")" + printf ' [3] %s\n' "$(rf_msg "Workspaces")" + printf ' [4] %s\n' "$(rf_msg "Settings")" + printf ' [q] %s\n' "$(rf_msg "Quit")" + _rf_ui_take_notices + printf '%s' "$RF_UI_NOTICE_TEXT" + printf '\n > ' + + choice="$(rf_read_key)" || { + printf '\n'; rf_ui_term_restore; trap - EXIT INT TERM; return 0 + } + case "$choice" in + enter|space|up|down|left|right|escape) choice='' ;; + esac + printf '%s\n' "$choice" + + case "$choice" in + 1) printf '\n'; rf_ui_cooked rf_do_add ;; + 2) rf_ui_apps ;; + 3) rf_ui_workspaces ;; + 4) rf_ui_settings ;; + q|Q) rf_ui_term_restore; trap - EXIT INT TERM; return 0 ;; + # Anything else (Enter, arrow keys, stray characters) just + # redraws. Escape is deliberately not a quit key, so a mistyped + # arrow key cannot close the menu. + *) ;; + esac + done +} diff --git a/src/lib/workspace.sh b/src/lib/workspace.sh new file mode 100644 index 0000000..178516e --- /dev/null +++ b/src/lib/workspace.sh @@ -0,0 +1,389 @@ +# shellcheck shell=bash +# +# Workspaces: the feeds rdfeed is subscribed to, and the app menu entries they +# turn into. +# +# A workspace has an id made from its server name. Its settings are in +# ~/.config/rdfeed/workspaces/<id>, its apps in ~/.local/share/rdfeed/<id> +# (apps.tsv, rdp/<app>.rdp, icons/<app>.png), its password in the keyring. +# Each app gets ~/.local/share/applications/rdfeed-<id>--<app>.desktop, and +# the workspace a folder of its own in the app menu. With "Open files with +# the apps" on, the entries also name the file types, so file managers offer +# the apps under "Open with". + +# --------------------------------------------------------------------------- +# The keyring +# --------------------------------------------------------------------------- + +rf_secret_get() { + rf_have secret-tool || return 1 + secret-tool lookup service rdfeed workspace "$1" user "$2" 2>/dev/null +} + +# rf_secret_set <id> <account> <label> <password> +# secret-tool keeps all of stdin, a trailing newline too, so printf and not +# a here-string. printf is a builtin: the password shows in no process list. +rf_secret_set() { + rf_have secret-tool || return 1 + printf '%s' "$4" | secret-tool store --label="rdfeed: $3" service rdfeed workspace "$1" user "$2" \ + > /dev/null 2>&1 +} + +rf_secret_clear() { + rf_have secret-tool || return 0 + secret-tool clear service rdfeed workspace "$1" > /dev/null 2>&1 || true +} + +# --------------------------------------------------------------------------- +# Finding the feed +# --------------------------------------------------------------------------- + +# rf_discover <domain> +# Windows finds the feed for a work e-mail address in a TXT record of the +# domain, _msradc.<domain>. Whichever DNS tool is around asks for it. +rf_discover() { + local name="_msradc.$1" txt='' + rf_have resolvectl && txt="$(resolvectl query --legend=no -t TXT "$name" 2>/dev/null)" + [[ -z $txt ]] && rf_have dig && txt="$(dig +short TXT "$name" 2>/dev/null)" + [[ -z $txt ]] && rf_have host && txt="$(host -t TXT "$name" 2>/dev/null)" + [[ -z $txt ]] && rf_have drill && txt="$(drill TXT "$name" 2>/dev/null)" + [[ $txt =~ (https?://[^\"[:space:]]+) ]] || return 1 + printf '%s\n' "${BASH_REMATCH[1]}" +} + +# rf_feed_url <what was typed> +# A server name, an address or a work e-mail address, as the address of the +# feed, in RF_URL. 1: nothing typed, 2: no feed found for the e-mail address. +RF_URL='' + +rf_feed_url() { + local in="$1" scheme rest host path + in="${in#"${in%%[![:space:]]*}"}" + in="${in%"${in##*[![:space:]]}"}" + RF_URL='' + [[ -n $in ]] || return 1 + + if [[ $in != */* && $in == *@* ]]; then + RF_URL="$(rf_discover "${in#*@}")" || return 2 + return 0 + fi + + [[ $in == *://* ]] || in="https://$in" + scheme="${in%%://*}" + rest="${in#*://}" + host="${rest%%/*}" + path="${rest#"$host"}" + # The feed sits at the same place on every server. Whatever part of it + # was typed, the RD Web page included, leads there. + case "${path,,}" in + ''|/|/rdweb|/rdweb/|/rdweb/feed|/rdweb/feed/|/rdweb/pages*) path=/RDWeb/Feed/webfeed.aspx ;; + */webfeed) path+=.aspx ;; + esac + RF_URL="${scheme,,}://$host$path" +} + +# rf_ws_id_for <url> +# The workspace that has this address, or a new id made from the server name. +rf_ws_id_for() { + local url="$1" id base n=2 host + rf_ws_ids + for id in "${RF_WS_IDS[@]}"; do + _rf_kv_lookup "$(rf_ws_file "$id")" Url + [[ ${RF_KV_VALUE,,} == "${url,,}" ]] && { printf '%s\n' "$id"; return; } + done + host="${url#*://}" + host="${host%%/*}" + host="${host%%:*}" + base="${host,,}" + base="${base//[^a-z0-9]/-}" + while [[ $base == *--* ]]; do base="${base//--/-}"; done + base="${base#-}" + base="${base%-}" + [[ -n $base ]] || base=workspace + id="$base" + while [[ -e $(rf_ws_file "$id") ]]; do + id="$base-$n" + n=$(( n + 1 )) + done + printf '%s\n' "$id" +} + +# rf_split_user <typed> +# DOMAIN\name, name@domain or just a name, into RF_USER and RF_DOMAIN. The +# domain is "-" when the server's own should be taken, and empty for +# name@domain, which carries its domain. +rf_split_user() { + local in="$1" + if [[ $in == *\\* ]]; then + RF_DOMAIN="${in%%\\*}" + RF_USER="${in#*\\}" + elif [[ $in == *@* ]]; then + RF_DOMAIN='' + RF_USER="$in" + else + RF_DOMAIN='-' + RF_USER="$in" + fi +} + +# --------------------------------------------------------------------------- +# Downloading +# --------------------------------------------------------------------------- + +# rf_fetch <url> <user> <domain or -> <dir> <password> [quiet] +# Runs rdfeed-fetch into <dir>, printing each app as it arrives unless quiet. +# Leaves RF_FETCH_NAME, RF_FETCH_DOMAIN, RF_FETCH_COUNT and, when it failed, +# RF_FETCH_ERROR (the reason, already translated). +rf_fetch() { + local url="$1" user="$2" domain="$3" out="$4" password="$5" quiet="${6:-}" + local kind a b c rc=1 detail + local -a args=(--url "$url" --user "$user" --out "$out") + [[ $domain != - ]] && args+=(--domain "$domain") + + RF_FETCH_NAME='' RF_FETCH_DOMAIN='' RF_FETCH_COUNT=0 RF_FETCH_ERROR='' + while IFS=$'\t' read -r kind a b c; do + case "$kind" in + workspace) RF_FETCH_NAME="$a" ;; + domain) RF_FETCH_DOMAIN="$a" ;; + app) + RF_FETCH_COUNT=$(( RF_FETCH_COUNT + 1 )) + [[ -z $quiet ]] && printf ' %sโœ”%s %s\n' "$RF_C_GREEN" "$RF_C_RESET" "$c" + ;; + skip) + [[ -z $quiet ]] && printf ' %sโœ˜%s %s %s(%s)%s\n' "$RF_C_RED" "$RF_C_RESET" "$a" "$RF_C_DIM" "$b" "$RF_C_RESET" + ;; + exit) rc="$a" ;; + esac + done < <("$RF_FETCH" "${args[@]}" <<< "$password" 2> "$out/.error"; printf 'exit\t%s\n' "$?") + + (( rc == 0 )) && { rm -f "$out/.error"; return 0; } + detail="$(tail -n1 "$out/.error" 2>/dev/null)" + detail="${detail#rdfeed-fetch: }" + case "$rc" in + 10) RF_FETCH_ERROR="$(rf_msg "The server did not accept the user name or password.")" ;; + 11) RF_FETCH_ERROR="$(rf_msg "Could not reach the server: %s" "$detail")" ;; + 12) RF_FETCH_ERROR="$(rf_msg "There is no RD Web feed at %s." "$url")" ;; + *) RF_FETCH_ERROR="$(rf_msg "Could not load the apps: %s" "${detail:-$rc}")" ;; + esac + return "$rc" +} + +# rf_stage_dir +# A fresh directory next to the workspaces, so moving it in place is a rename. +rf_stage_dir() { + mkdir -p "$RF_DATADIR" && mktemp -d "$RF_DATADIR/.new.XXXXXX" +} + +# rf_ws_install <id> <stage dir> +rf_ws_install() { + local dir="$RF_DATADIR/$1" + rm -f "$2/.error" + rm -rf "$dir.old" + [[ -d $dir ]] && mv "$dir" "$dir.old" + mv "$2" "$dir" || { [[ -d $dir.old ]] && mv "$dir.old" "$dir"; return 1; } + rm -rf "$dir.old" + rf_ws_entries "$1" +} + +# rf_ws_apps_load <id> +# The apps of a workspace in RF_APP_SLUGS, RF_APP_TYPES, RF_APP_TITLES and +# RF_APP_EXTS (the file types it opens, as "docx doc rtf"). +RF_APP_SLUGS=() RF_APP_TYPES=() RF_APP_TITLES=() RF_APP_EXTS=() + +rf_ws_apps_load() { + local slug type title exts file="$RF_DATADIR/$1/apps.tsv" + RF_APP_SLUGS=() RF_APP_TYPES=() RF_APP_TITLES=() RF_APP_EXTS=() + [[ -r $file ]] || return 0 + while IFS=$'\t' read -r slug type title exts; do + [[ -n $slug ]] || continue + RF_APP_SLUGS+=("$slug") RF_APP_TYPES+=("$type") RF_APP_TITLES+=("$title") RF_APP_EXTS+=("$exts") + done < "$file" +} + +# --------------------------------------------------------------------------- +# The app menu +# --------------------------------------------------------------------------- + +_rf_desktop_value() { + local v="${1//\\/\\\\}" + v="${v//$'\n'/ }" + printf '%s' "${v//$'\r'/}" +} + +# The command, as the Exec line of an entry wants it. +_rf_exec_path() { + if [[ $RF_SELF =~ ^[A-Za-z0-9_./+-]+$ ]]; then + printf '%s' "$RF_SELF" + else + local p="${RF_SELF//\\/\\\\\\\\}" + p="${p//\"/\\\\\"}" + p="${p//\$/\\\\\$}" + p="${p//\`/\\\\\`}" + printf '"%s"' "$p" + fi +} + +# _rf_mime_types <ext>... +# The MIME types of the file extensions, as a MimeType= value, from the +# shared-mime-info database every desktop uses. +_rf_mime_types() { + local f + local -a globs=() + for f in "$RF_XDG_DATA/mime/globs2" /usr/local/share/mime/globs2 /usr/share/mime/globs2; do + [[ -r $f ]] && globs+=("$f") + done + (( ${#globs[@]} )) || return 0 + awk -F: -v want=" $* " ' + BEGIN { want = tolower(want) } + /^#/ { next } + { + glob = tolower($3) + if (glob !~ /^\*\.[a-z0-9_+-]+$/) next + if (index(want, " " substr(glob, 3) " ") && !seen[$2]++) out = out $2 ";" + } + END { printf "%s", out } + ' "${globs[@]}" +} + +# rf_menu_refresh +# KDE reads the app menu from a cache, which needs a nudge. GNOME and the rest +# watch the folders themselves. +rf_menu_refresh() { + if rf_have kbuildsycoca6; then + kbuildsycoca6 > /dev/null 2>&1 || true + elif rf_have kbuildsycoca5; then + kbuildsycoca5 > /dev/null 2>&1 || true + fi + rf_have update-desktop-database && update-desktop-database "$RF_APPDIR" > /dev/null 2>&1 + return 0 +} + +# rf_ws_entries <id> +# An entry for each app, a folder for the workspace, and nothing left of apps +# the feed no longer has. +rf_ws_entries() { + local id="$1" i slug title icon comment file keywords mime openwith=no args + local -A keep=() + + rf_ws_load "$id" + rf_ws_apps_load "$id" + mkdir -p "$RF_APPDIR" "$RF_MENUDIR" "$RF_DIRDIR" || return 1 + _rf_kv_lookup "$RF_CONFIG" OpenWith yes + rf_is_true "$RF_KV_VALUE" && openwith=yes + + keywords="$(_rf_desktop_value "$WS_NAME")" + keywords="${keywords//;/\\;}" + for i in "${!RF_APP_SLUGS[@]}"; do + slug="${RF_APP_SLUGS[i]}" + title="${RF_APP_TITLES[i]}" + icon="$RF_DATADIR/$id/icons/$slug.png" + [[ -f $icon ]] || icon=rdfeed + if [[ ${RF_APP_TYPES[i]} == Desktop ]]; then + comment="$(rf_msg "Remote desktop of %s" "$WS_NAME")" + else + comment="$(rf_msg "%s from %s" "$title" "$WS_NAME")" + fi + mime='' args='' + if [[ $openwith == yes && -n ${RF_APP_EXTS[i]} ]]; then + # shellcheck disable=SC2086 # one extension per argument + mime="$(_rf_mime_types ${RF_APP_EXTS[i]})" + [[ -n $mime ]] && args=' %f' + fi + file="$RF_APPDIR/rdfeed-$id--$slug.desktop" + keep[$file]=1 + cat > "$file" <<- EOF + [Desktop Entry] + Type=Application + Name=$(_rf_desktop_value "$title") + GenericName=$(_rf_desktop_value "$WS_NAME") + Comment=$(_rf_desktop_value "$comment") + Exec=$(_rf_exec_path) run $id/$slug$args + Icon=$icon + Terminal=false + StartupNotify=false + Categories=X-rdfeed-$id; + Keywords=RemoteApp;RDP;$keywords; + EOF + [[ -n $mime ]] && printf 'MimeType=%s\n' "$mime" >> "$file" + done + + for file in "$RF_APPDIR"/rdfeed-"$id"--*.desktop; do + [[ -e $file && -z ${keep[$file]:-} ]] && rm -f "$file" + done + + cat > "$RF_MENUDIR/rdfeed-$id.menu" <<- EOF + <!DOCTYPE Menu PUBLIC "-//freedesktop//DTD Menu 1.0//EN" + "http://www.freedesktop.org/standards/menu-spec/menu-1.0.dtd"> + <!-- Written by rdfeed: the folder of the workspace $id. --> + <Menu> + <Name>Applications</Name> + <Menu> + <Name>rdfeed-$id</Name> + <Directory>rdfeed-$id.directory</Directory> + <Include> + <Category>X-rdfeed-$id</Category> + </Include> + </Menu> + </Menu> + EOF + + cat > "$RF_DIRDIR/rdfeed-$id.directory" <<- EOF + [Desktop Entry] + Type=Directory + Name=$(_rf_desktop_value "$WS_NAME") + Icon=rdfeed + EOF + + rf_menu_refresh +} + +# rf_ws_entries_all +# The entries of every workspace again, after a setting that shows in them. +rf_ws_entries_all() { + local id + rf_ws_ids + for id in "${RF_WS_IDS[@]}"; do + rf_ws_entries "$id" + done +} + +# rf_ws_purge <id> +# Everything of a workspace: entries, folder, apps, settings, password, logs. +rf_ws_purge() { + local id="$1" file + for file in "$RF_APPDIR"/rdfeed-"$id"--*.desktop; do + [[ -e $file ]] && rm -f "$file" + done + rm -f "$RF_MENUDIR/rdfeed-$id.menu" "$RF_DIRDIR/rdfeed-$id.directory" + rm -rf "${RF_DATADIR:?}/$id" + rm -f "$(rf_ws_file "$id")" + for file in "$RF_CACHEDIR/$id"--*.log; do + [[ -e $file ]] && rm -f "$file" + done + rf_secret_clear "$id" + rf_menu_refresh +} + +# --------------------------------------------------------------------------- +# Refreshing every day +# --------------------------------------------------------------------------- + +rf_timer_available() { + rf_have systemctl || return 1 + systemctl --user cat "$RF_UNIT_TIMER" > /dev/null 2>&1 +} + +rf_timer_enabled() { + rf_have systemctl || return 1 + [[ $(systemctl --user is-enabled "$RF_UNIT_TIMER" 2>/dev/null) == enabled ]] +} + +# rf_timer_set yes|no +rf_timer_set() { + rf_timer_available || return 1 + if [[ $1 == yes ]]; then + systemctl --user enable --now "$RF_UNIT_TIMER" > /dev/null 2>&1 + else + systemctl --user disable --now "$RF_UNIT_TIMER" > /dev/null 2>&1 + fi +} diff --git a/src/rdfeed b/src/rdfeed new file mode 100755 index 0000000..cdfc474 --- /dev/null +++ b/src/rdfeed @@ -0,0 +1,321 @@ +#!/usr/bin/env bash +# +# rdfeed: the RemoteApps of your company in the Linux app menu +# +# Companies publish their Windows apps (Word, Excel, their own tools) as an RD +# Web Access feed. Windows subscribes to it under "RemoteApp and Desktop +# Connections" and puts the apps in the start menu. rdfeed does the same on +# Linux: each app gets an entry in the app menu and opens as a window of its +# own, through FreeRDP. +# +# This is the front end: the menu and the commands. Signing in to the feed and +# downloading the apps is rdfeed-fetch's job, and rdfeed-hook.so goes into +# FreeRDP to work around two of its problems (see launch.sh). +# +# Copyright (C) 2026 Felitendo +# SPDX-License-Identifier: GPL-3.0-or-later + +set -uo pipefail + +RF_SELF="$(readlink -f "${BASH_SOURCE[0]}")" +RF_LIBDIR="${RF_LIBDIR:-@LIBDIR@}" + +for _mod in common config workspace launch menu; do + # shellcheck source=/dev/null + if ! source "$RF_LIBDIR/$_mod.sh"; then + printf 'rdfeed: cannot load %s/%s.sh\n' "$RF_LIBDIR" "$_mod" >&2 + exit 14 + fi +done +unset _mod + +# --------------------------------------------------------------------------- +# Commands +# --------------------------------------------------------------------------- + +# rf_prompt <question> [default] +# A line typed in, in RF_ANSWER. Without a terminal it is read from stdin, so +# scripts can pipe the answers in. +RF_ANSWER='' + +rf_prompt() { + RF_ANSWER='' + if [[ -t 0 ]]; then + IFS= read -rep " $1 " -i "${2:-}" RF_ANSWER || return 1 + else + IFS= read -r RF_ANSWER || return 1 + fi + return 0 +} + +# rf_prompt_password <question> +rf_prompt_password() { + RF_ANSWER='' + if [[ -t 0 ]]; then + IFS= read -rsp " $1 " RF_ANSWER || return 1 + printf '\n' + else + IFS= read -r RF_ANSWER || return 1 + fi + [[ -n $RF_ANSWER ]] +} + +# rf_signin <id or ""> <url> <user as typed> <password> +# Signs in, downloads the apps and puts them in the app menu. With an empty +# id a new workspace is made, or the one that already has this address. +rf_signin() { + local id="$1" url="$2" typed="$3" password="$4" stage old_account first=0 domain + + [[ -n $id ]] || id="$(rf_ws_id_for "$url")" + rf_ws_ids + (( ${#RF_WS_IDS[@]} == 0 )) && first=1 + old_account='' + if [[ -e $(rf_ws_file "$id") ]]; then + rf_ws_load "$id" + old_account="$WS_ACCOUNT" + fi + + rf_split_user "$typed" + stage="$(rf_stage_dir)" || { rf_bad "$(rf_msg "Could not write to %s." "$RF_DATADIR")"; return 1; } + + local host="${url#*://}" + rf_say " $(rf_msg "Signing in to %sโ€ฆ" "${host%%/*}")" + if ! rf_fetch "$url" "$RF_USER" "$RF_DOMAIN" "$stage" "$password"; then + rm -rf "$stage" + rf_bad "$RF_FETCH_ERROR" + return 1 + fi + + domain="$RF_DOMAIN" + [[ $domain == - ]] && domain="$RF_FETCH_DOMAIN" + rf_ws_set "$id" Url "$url" + rf_ws_set "$id" Name "$RF_FETCH_NAME" + rf_ws_set "$id" User "$RF_USER" + rf_ws_set "$id" Domain "$domain" + rf_ws_set "$id" Refreshed "$(date +%s)" + rf_ws_load "$id" + + [[ -n $old_account && $old_account != "$WS_ACCOUNT" ]] && rf_secret_clear "$id" + if ! rf_secret_set "$id" "$WS_ACCOUNT" "$WS_NAME" "$password"; then + rf_note "$(rf_msg "The password could not be saved in the keyring, so each app asks for it.")" + fi + + rf_ws_install "$id" "$stage" || { rf_bad "$(rf_msg "Could not write to %s." "$RF_DATADIR")"; return 1; } + + # New apps should show up by themselves, as on Windows. + if (( first )) && [[ $(rf_config_get AutoRefresh yes) == yes ]]; then + rf_timer_set yes || true + fi + + if (( RF_FETCH_COUNT == 1 )); then + rf_ok "$(rf_msg "1 app from %s is now in your app menu." "$WS_NAME")" + else + rf_ok "$(rf_msg "%d apps from %s are now in your app menu." "$RF_FETCH_COUNT" "$WS_NAME")" + fi +} + +rf_do_add() { + local input='' user='' url rc + + while (( $# )); do + case "$1" in + --user) user="${2:-}"; shift ;; + *) input="$1" ;; + esac + shift + done + + if [[ -z $input ]]; then + rf_say " $(rf_msg "Your IT department has the address. A work e-mail address often works too.")" + rf_prompt "$(rf_msg "Feed address, server or e-mail:")" || return 1 + input="$RF_ANSWER" + fi + rf_feed_url "$input" + rc=$? + case "$rc" in + 1) return 1 ;; + 2) rf_bad "$(rf_msg "No feed is registered for %s. Ask your IT department for the address." "$input")"; return 1 ;; + esac + url="$RF_URL" + + if [[ -z $user ]]; then + rf_prompt "$(rf_msg "User name (DOMAIN\\name or name@domain):")" || return 1 + user="$RF_ANSWER" + fi + [[ -n $user ]] || return 1 + rf_prompt_password "$(rf_msg "Password:")" || return 1 + + rf_signin '' "$url" "$user" "$RF_ANSWER" +} + +# rf_do_signin_again <id> +# Another password or another user for a workspace. +rf_do_signin_again() { + local id="$1" + rf_ws_load "$id" + rf_prompt "$(rf_msg "User name (DOMAIN\\name or name@domain):")" "$WS_ACCOUNT" || return 1 + [[ -n $RF_ANSWER ]] || return 1 + local user="$RF_ANSWER" + rf_prompt_password "$(rf_msg "Password:")" || return 1 + rf_signin "$id" "$WS_URL" "$user" "$RF_ANSWER" +} + +# rf_do_refresh [id] [--quiet] +# The quiet one is the daily timer's: no questions, and a notification only +# when somebody has to do something. +rf_do_refresh() { + local quiet='' only='' id password stage rc failed=0 + while (( $# )); do + case "$1" in + --quiet) quiet=1 ;; + *) only="$1" ;; + esac + shift + done + + rf_ws_ids + if (( ${#RF_WS_IDS[@]} == 0 )); then + [[ -z $quiet ]] && rf_note "$(rf_msg "No workspace yet. Add one with \`%s add\`." "$RF_NAME")" + return 0 + fi + for id in "${RF_WS_IDS[@]}"; do + [[ -n $only && $id != "$only" ]] && continue + rf_ws_load "$id" + password="$(rf_secret_get "$id" "$WS_ACCOUNT")" || password='' + if [[ -z $password ]]; then + if [[ -n $quiet || ! -t 0 ]]; then + [[ -n $quiet ]] && rf_notify "$WS_NAME" "$(rf_msg "No password saved for %s. Sign in again in rdfeed." "$WS_NAME")" dialog-warning + failed=1 + continue + fi + rf_prompt_password "$(rf_msg "Password for %s:" "$WS_ACCOUNT")" || { failed=1; continue; } + password="$RF_ANSWER" + fi + + [[ -z $quiet ]] && rf_say " $(rf_msg "Refreshing %sโ€ฆ" "$WS_NAME")" + stage="$(rf_stage_dir)" || return 1 + rf_fetch "$WS_URL" "$WS_USER" "${WS_DOMAIN:-}" "$stage" "$password" "$quiet" + rc=$? + if (( rc != 0 )); then + rm -rf "$stage" + failed=1 + if [[ -n $quiet ]]; then + # A server that is not reachable now will be tomorrow. A + # password that no longer works needs the user. + (( rc == 10 )) && rf_notify "$WS_NAME" "$(rf_msg "The password no longer works. Sign in again in rdfeed.")" dialog-warning + else + rf_bad "$RF_FETCH_ERROR" + fi + continue + fi + [[ -n $RF_FETCH_NAME ]] && rf_ws_set "$id" Name "$RF_FETCH_NAME" + rf_ws_set "$id" Refreshed "$(date +%s)" + rf_ws_install "$id" "$stage" || { failed=1; continue; } + [[ -z $quiet ]] && rf_ok "$(rf_msg "%s: %d apps" "$WS_NAME" "$RF_FETCH_COUNT")" + done + return "$failed" +} + +rf_do_remove() { + local id="${1:-}" + if [[ -z $id || ! -e $(rf_ws_file "$id") ]]; then + rf_bad "$(rf_msg "Which workspace? See \`%s list\` for the names." "$RF_NAME")" + return 1 + fi + rf_ws_load "$id" + rf_ws_purge "$id" + rf_ws_ids + (( ${#RF_WS_IDS[@]} == 0 )) && { rf_timer_set no || true; } + rf_ok "$(rf_msg "%s and its apps are removed." "$WS_NAME")" +} + +rf_do_list() { + local id i + rf_ws_ids + if (( ${#RF_WS_IDS[@]} == 0 )); then + rf_note "$(rf_msg "No workspace yet. Add one with \`%s add\`." "$RF_NAME")" + return 0 + fi + for id in "${RF_WS_IDS[@]}"; do + rf_ws_load "$id" + rf_ws_apps_load "$id" + printf '%s%s%s %s(%s, %s)%s\n' "$RF_C_BOLD" "$WS_NAME" "$RF_C_RESET" "$RF_C_DIM" "$id" "$WS_ACCOUNT" "$RF_C_RESET" + for i in "${!RF_APP_SLUGS[@]}"; do + printf ' %-36s %s\n' "$id/${RF_APP_SLUGS[i]}" "${RF_APP_TITLES[i]}" + done + done +} + +# rf_do_run <app or workspace/app> [file] +rf_do_run() { + local spec="${1:-}" file="${2:-}" id slug found=() i + if [[ -z $spec ]]; then + rf_bad "$(rf_msg "Which app? See \`%s list\`." "$RF_NAME")" + return 1 + fi + if [[ $spec == */* ]]; then + rf_launch "${spec%%/*}" "${spec#*/}" "$file" + return + fi + rf_ws_ids + for id in "${RF_WS_IDS[@]}"; do + rf_ws_apps_load "$id" + for i in "${!RF_APP_SLUGS[@]}"; do + [[ ${RF_APP_SLUGS[i]} == "$spec" ]] && found+=("$id") + done + done + case "${#found[@]}" in + 0) rf_bad "$(rf_msg "There is no app %s. See \`%s list\`." "$spec" "$RF_NAME")"; return 1 ;; + 1) slug="$spec"; rf_launch "${found[0]}" "$slug" "$file" ;; + *) rf_bad "$(rf_msg "Several workspaces have %s. Write it as WORKSPACE/APP, see \`%s list\`." "$spec" "$RF_NAME")"; return 1 ;; + esac +} + +rf_do_help() { + cat <<- EOF + $RF_PRETTY $RF_VERSION + + $(rf_msg "Usage: rdfeed [command]") + + $(rf_msg "Commands:") + add [ADDRESS] $(rf_msg "Add the workspace of your company") + refresh [NAME] $(rf_msg "Load the apps again") + list $(rf_msg "List the workspaces and their apps") + run APP [FILE] $(rf_msg "Start an app, or open a file with it") + remove NAME $(rf_msg "Remove a workspace and its apps") + -h, --help $(rf_msg "Show this help") + -V, --version $(rf_msg "Show the version") + + $(rf_msg "Without a command an interactive menu is shown.") + EOF +} + +# --------------------------------------------------------------------------- +# Dispatch +# --------------------------------------------------------------------------- + +main() { + local cmd="${1:-}" + [[ $# -gt 0 ]] && shift + + case "$cmd" in + add|subscribe) rf_do_add "$@" ;; + refresh|sync) rf_do_refresh "$@" ;; + list|ls) rf_do_list ;; + run|start) rf_do_run "$@" ;; + remove|delete) rf_do_remove "$@" ;; + + -h|--help|help) rf_do_help ;; + -V|--version) printf '%s %s\n' "$RF_NAME" "$RF_VERSION" ;; + + '') rf_ui_menu ;; + *) + rf_bad "$(rf_msg "Unknown command: %s" "$cmd")" + printf '\n' + rf_do_help + exit 1 + ;; + esac +} + +main "$@" diff --git a/tests/fake_rdweb.py b/tests/fake_rdweb.py new file mode 100755 index 0000000..560555a --- /dev/null +++ b/tests/fake_rdweb.py @@ -0,0 +1,212 @@ +#!/usr/bin/env python3 +# +# A fake RD Web Access server for the tests: the feed behind the NTLM login, +# the forms ticket it hands out, .rdp files and icons. It checks the NTLMv2 +# answer for real, against one user and password. +# +# python3 tests/fake_rdweb.py [port] serve until Ctrl+C +# +# Copyright (C) 2026 Felitendo +# SPDX-License-Identifier: GPL-3.0-or-later + +import base64 +import hmac +import http.server +import os +import socketserver +import struct +import sys +import threading +import urllib.parse + +import importlib.machinery # noqa: E402 +import importlib.util # noqa: E402 + +# rdfeed-fetch has no .py ending, so it is loaded by path. +_loader = importlib.machinery.SourceFileLoader( + "rdfeed_fetch", os.path.join(os.path.dirname(os.path.abspath(__file__)), "..", "src", "fetch", "rdfeed-fetch")) +_spec = importlib.util.spec_from_loader("rdfeed_fetch", _loader) +fetch = importlib.util.module_from_spec(_spec) +_loader.exec_module(fetch) + +DOMAIN = "CONTOSO" +USER = "jdoe" +PASSWORD = "Correct horse 1" +TOKEN = "5EC2E7F00DBA11ADE0F7EED5ACC0FFEE" + +APPS = [ + ("winword-RemoteApps-CmsRdsh", "Word", "RemoteApp"), + ("excel-RemoteApps-CmsRdsh", "Excel", "RemoteApp"), + ("powerpnt-RemoteApps-CmsRdsh", "PowerPoint", "RemoteApp"), + ("desktop-RemoteApps-CmsRdsh", "Contoso Desktop", "Desktop"), +] + + +def extensions(alias): + """Excel comes with its file types, as an administrator would enter them.""" + if not alias.startswith("excel"): + return "<FileExtensions />" + return ('<FileExtensions><FileExtension Name=".xlsx" PrimaryHandler="True" />' + '<FileExtension Name=".CSV" PrimaryHandler="False" /></FileExtensions>') + + +def feed_xml(): + resources = "" + for alias, title, kind in APPS: + resources += f""" + <Resource ID="{alias}" Alias="{alias}" Title="{title}" LastUpdated="2026-10-01T08:00:00Z" Type="{kind}" ShowByDefault="True"> + <Icons> + <IconRaw FileType="Ico" FileURL="/RDWeb/Pages/rdp/{alias}.ico" /> + <Icon32 Dimensions="32x32" FileType="Png" FileURL="/RDWeb/Pages/rdp/{alias}.png" /> + </Icons> + {extensions(alias)} + <Folders><Folder Name="/" /></Folders> + <HostingTerminalServers> + <HostingTerminalServer> + <ResourceFile FileExtension=".rdp" URL="/RDWeb/Pages/rdp/cpub-{alias}.rdp" /> + <TerminalServerRef Ref="RDSH01.contoso.test" /> + </HostingTerminalServer> + </HostingTerminalServers> + </Resource>""" + return f"""<?xml version="1.0" encoding="utf-8"?> +<ResourceCollection PubDate="2026-10-01T08:00:00Z" SchemaVersion="2.1" xmlns="http://schemas.microsoft.com/ts/2007/05/tswf"> + <Publisher LastUpdated="2026-10-01T08:00:00Z" Name="Contoso Apps" ID="RDSH01.contoso.test" Description=""> + <Resources>{resources} + </Resources> + </Publisher> +</ResourceCollection> +""".encode() + + +def rdp_file(alias, title, kind): + lines = [ + "full address:s:RDBROKER.contoso.test", + "gatewayhostname:s:gateway.contoso.test", + "gatewayusagemethod:i:1", + "promptcredentialonce:i:0", + "prompt for credentials on client:i:1", + "drivestoredirect:s:*", + "devicestoredirect:s:*", + "camerastoredirect:s:*", + "loadbalanceinfo:s:tsv://MS Terminal Services Plugin.1.RemoteApps", + ] + if kind == "RemoteApp": + lines += ["remoteapplicationmode:i:1", f"remoteapplicationprogram:s:||{alias.split('-')[0]}", + f"remoteapplicationname:s:{title}"] + return ("\r\n".join(lines) + "\r\n").encode("utf-16") + + +def ico_32bpp(size=48, rgba=(30, 110, 220, 255)): + """An .ico with one 32-bit bitmap frame, the way Windows writes them.""" + r, g, b, a = rgba + header = struct.pack("<IiiHHIIiiII", 40, size, size * 2, 1, 32, 0, 0, 0, 0, 0, 0) + pixels = bytes((b, g, r, a)) * (size * size) + mask = b"\x00" * (((size + 31) // 32) * 4 * size) + dib = header + pixels + mask + return (struct.pack("<HHH", 0, 1, 1) + + struct.pack("<BBBBHHII", size, size, 0, 0, 1, 32, len(dib), 22) + dib) + + +def ntlm_challenge(): + def av(kind, value): + data = value.encode("utf-16-le") + return struct.pack("<HH", kind, len(data)) + data + info = (av(2, DOMAIN) + av(1, "RDWEB01") + av(4, "contoso.test") + av(3, "rdweb01.contoso.test") + + struct.pack("<HH", 7, 8) + struct.pack("<Q", 133000000000000000) + struct.pack("<HH", 0, 0)) + server_challenge = os.urandom(8) + target = DOMAIN.encode("utf-16-le") + head = 56 + message = (b"NTLMSSP\x00" + struct.pack("<I", 2) + struct.pack("<HHI", len(target), len(target), head) + + struct.pack("<I", 0xE2898215) + server_challenge + b"\x00" * 8 + + struct.pack("<HHI", len(info), len(info), head + len(target)) + fetch.NTLM_VERSION + + target + info) + return message, server_challenge + + +def ntlm_check(message, server_challenge): + """True if the AUTHENTICATE message proves the password.""" + def field(i): + length, _, offset = struct.unpack("<HHI", message[12 + 8 * i:20 + 8 * i]) + return message[offset:offset + length] + nt, domain, user = field(1), field(2).decode("utf-16-le"), field(3).decode("utf-16-le") + if user.lower() != USER or domain.upper() != DOMAIN or len(nt) < 32: + return False + key = fetch.ntowfv2(user, domain, PASSWORD) + proof = hmac.new(key, server_challenge + nt[16:], "md5").digest() + return hmac.compare_digest(proof, nt[:16]) + + +class Handler(http.server.BaseHTTPRequestHandler): + protocol_version = "HTTP/1.1" + + def log_message(self, *args): + pass + + def reply(self, status, body=b"", headers=None): + self.send_response(status) + for k, v in (headers or {}).items(): + if isinstance(v, list): + for one in v: + self.send_header(k, one) + else: + self.send_header(k, v) + self.send_header("Content-Length", str(len(body))) + self.end_headers() + self.wfile.write(body) + + def signed_in(self): + cookies = self.headers.get("Cookie", "") + return f"TSWAFeedAuthCookie={TOKEN}" in cookies + + def do_GET(self): + path = urllib.parse.urlsplit(self.path).path + lower = path.lower() + if lower == "/rdweb/feed/webfeed.aspx": + if self.signed_in(): + return self.reply(200, feed_xml(), {"Content-Type": "application/x-msts-radc+xml; charset=utf-8"}) + return self.reply(302, b"", {"Location": "/RDWeb/FeedLogin/WebFeedLogin.aspx"}) + if lower == "/rdweb/feedlogin/webfeedlogin.aspx": + auth = self.headers.get("Authorization", "") + if auth.startswith("NTLM "): + message = base64.b64decode(auth[5:]) + kind = struct.unpack("<I", message[8:12])[0] + if kind == 1: + challenge, self.server_challenge = ntlm_challenge() + return self.reply(401, b"", {"WWW-Authenticate": "NTLM " + base64.b64encode(challenge).decode()}) + if kind == 3 and ntlm_check(message, getattr(self, "server_challenge", b"")): + return self.reply(200, TOKEN.encode(), {"Content-Type": "application/x-msts-webfeed-login; charset=utf-8"}) + return self.reply(401, b"denied", {"WWW-Authenticate": ["NTLM", "Negotiate"], "Content-Type": "text/html"}) + if lower.startswith("/rdweb/pages/rdp/"): + if not self.signed_in(): + return self.reply(302, b"", {"Location": "/RDWeb/Pages/en-US/login.aspx?ReturnUrl=" + path}) + name = path.rsplit("/", 1)[1] + for alias, title, kind in APPS: + if name == f"cpub-{alias}.rdp": + return self.reply(200, rdp_file(alias, title, kind), {"Content-Type": "application/x-rdp"}) + if name == f"{alias}.ico": + return self.reply(200, ico_32bpp(), {"Content-Type": "image/x-icon"}) + if name == f"{alias}.png": + return self.reply(404) + return self.reply(404, b"<html>not here</html>", {"Content-Type": "text/html"}) + + +class Server(socketserver.ThreadingMixIn, http.server.HTTPServer): + daemon_threads = True + allow_reuse_address = True + + +def serve(port=0): + """Starts the server in a thread. Returns it; its URL base is server.base.""" + server = Server(("127.0.0.1", port), Handler) + server.base = f"http://127.0.0.1:{server.server_address[1]}" + threading.Thread(target=server.serve_forever, daemon=True).start() + return server + + +if __name__ == "__main__": + srv = serve(int(sys.argv[1]) if len(sys.argv) > 1 else 0) + print(srv.base + "/RDWeb/Feed/webfeed.aspx", flush=True) + try: + threading.Event().wait() + except KeyboardInterrupt: + pass diff --git a/tests/test_cli.sh b/tests/test_cli.sh new file mode 100644 index 0000000..bd645eb --- /dev/null +++ b/tests/test_cli.sh @@ -0,0 +1,163 @@ +#!/usr/bin/env bash +# +# The rdfeed command from start to end, against tests/fake_rdweb.py: add a +# workspace, list, refresh, start an app, remove it again. +# +# Everything happens in a temporary home. secret-tool, systemctl, notify-send, +# kbuildsycoca6 and xfreerdp3 are fakes on PATH, so the real keyring, timers, +# notifications and app menu stay untouched. +# +# Copyright (C) 2026 Felitendo +# SPDX-License-Identifier: GPL-3.0-or-later + +set -uo pipefail +root="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")/.." && pwd)" +tmp="$(mktemp -d)" +server='' +trap '[[ -n $server ]] && kill "$server" 2>/dev/null; rm -rf -- "$tmp"' EXIT +failed=0 + +export HOME="$tmp/home" XDG_CONFIG_HOME="$tmp/config" XDG_DATA_HOME="$tmp/data" XDG_CACHE_HOME="$tmp/cache" +export RF_LIBDIR="$root/src/lib" RF_FETCH="$root/src/fetch/rdfeed-fetch" RF_LOCALEDIR="$tmp/locale" +RF_HOOK="$(readlink -f "${RDFEED_HOOK:-$root/build/rdfeed-hook.so}")" +export RF_HOOK +export LANG=C LC_ALL=C NO_COLOR=1 XDG_CURRENT_DESKTOP=test XDG_RUNTIME_DIR="$tmp/run" +mkdir -p "$HOME" "$tmp/bin" + +# The fakes. Each writes what it was asked into $tmp, as a log or a file. +cat > "$tmp/bin/secret-tool" <<- 'EOF' +#!/usr/bin/env bash +store="${0%/bin/*}/secrets"; mkdir -p "$store" +cmd="$1"; shift; ws='' +while (( $# )); do [[ $1 == workspace ]] && ws="$2"; shift; done +case "$cmd" in + store) cat > "$store/$ws" ;; + lookup) [[ -f $store/$ws ]] && cat "$store/$ws" ;; + clear) rm -f "$store/$ws" ;; +esac +EOF +cat > "$tmp/bin/xfreerdp3" <<- 'EOF' +#!/usr/bin/env bash +out="${0%/bin/*}" +cat > "$out/freerdp-args" +cp "$(head -n1 "$out/freerdp-args")" "$out/freerdp-rdp" +env > "$out/freerdp-env" +echo "[INFO] connected" +echo "[rdfeed] gateway-delay 12" >&2 +exit "${FAKE_FREERDP_EXIT:-0}" +EOF +for fake in systemctl notify-send kbuildsycoca6 update-desktop-database; do + # shellcheck disable=SC2016 # the script expands when it runs, not here + printf '#!/usr/bin/env bash\necho "%s $*" >> "${0%%/bin/*}/calls"\n[[ $1 == --user && $2 == cat ]] && exit 1\nexit 0\n' "$fake" > "$tmp/bin/$fake" +done +chmod +x "$tmp/bin"/* +export PATH="$tmp/bin:$PATH" + +rdfeed() { bash "$root/src/rdfeed" "$@"; } + +check() { + if eval "$2"; then + printf 'ok %s\n' "$1" + else + printf 'FAIL %s\n' "$1" + failed=1 + fi +} + +# The URL helpers, on their own. +url_helpers() ( + source "$RF_LIBDIR/common.sh"; source "$RF_LIBDIR/config.sh"; source "$RF_LIBDIR/workspace.sh" + for pair in "rdweb.contoso.com|https://rdweb.contoso.com/RDWeb/Feed/webfeed.aspx" \ + "office.contoso.com/rdweb/feed/webfeed|https://office.contoso.com/rdweb/feed/webfeed.aspx" \ + "https://x.test/RDWeb/Pages/en-US/Default.aspx|https://x.test/RDWeb/Feed/webfeed.aspx" \ + "HTTPS://x.test:8443/RDWeb/|https://x.test:8443/RDWeb/Feed/webfeed.aspx" \ + "https://x.test/custom/feed.aspx|https://x.test/custom/feed.aspx"; do + rf_feed_url "${pair%%|*}" + [[ $RF_URL == "${pair#*|}" ]] || { echo " $RF_URL != ${pair#*|}"; exit 1; } + done + rf_split_user 'CONTOSO\jdoe'; [[ $RF_USER == jdoe && $RF_DOMAIN == CONTOSO ]] || exit 1 + rf_split_user 'jdoe@contoso.com'; [[ $RF_USER == jdoe@contoso.com && -z $RF_DOMAIN ]] || exit 1 + rf_split_user 'jdoe'; [[ $RF_USER == jdoe && $RF_DOMAIN == - ]] || exit 1 + [[ $(rf_ws_id_for "https://Office.Contoso.COM/RDWeb/Feed/webfeed.aspx") == office-contoso-com ]] || exit 1 +) +if url_helpers; then check "addresses, user names and ids" true; else check "addresses, user names and ids" false; fi + +python3 "$root/tests/fake_rdweb.py" > "$tmp/url" & +server=$! +for _ in $(seq 50); do [[ -s $tmp/url ]] && break; sleep 0.1; done +url="$(< "$tmp/url")" +id=127-0-0-1 +password="$(python3 -c 'import sys; sys.path.insert(0, sys.argv[1]); import fake_rdweb; print(fake_rdweb.PASSWORD)' "$root/tests")" + +out="$(printf 'CONTOSO\\jdoe\nwrong\n' | rdfeed add "$url" 2>&1)" +rc=$? +check "a wrong password is refused" "[[ \$rc -ne 0 ]] && grep -q 'did not accept' <<< \"\$out\"" +check "and leaves nothing behind" "[[ ! -e $XDG_CONFIG_HOME/rdfeed/workspaces/$id ]]" + +out="$(printf 'jdoe\n%s\n' "$password" | rdfeed add "${url%/RDWeb/*}" 2>&1)" +check "add signs in and loads the apps" "grep -q '4 apps from Contoso Apps are now in your app menu' <<< \"\$out\"" +ws="$XDG_CONFIG_HOME/rdfeed/workspaces/$id" +check "the workspace remembers the server's domain" "grep -qx 'Domain=CONTOSO' '$ws' && grep -qx 'User=jdoe' '$ws'" +check "the password is in the keyring, as typed" "[[ \$(< '$tmp/secrets/$id') == \"\$password\" ]]" +entry="$XDG_DATA_HOME/applications/rdfeed-$id--powerpnt.desktop" +check "each app has a menu entry" "[[ \$(ls '$XDG_DATA_HOME/applications' | wc -l) -eq 4 ]]" +check "which starts it" "grep -q 'Exec=$root/src/rdfeed run $id/powerpnt' '$entry'" +check "with its own icon" "grep -qx 'Icon=$XDG_DATA_HOME/rdfeed/$id/icons/powerpnt.png' '$entry'" +word="$XDG_DATA_HOME/applications/rdfeed-$id--winword.desktop" +if [[ -r /usr/share/mime/globs2 ]]; then + check "Word is offered for .docx files" "grep -q '^MimeType=.*application/vnd.openxmlformats-officedocument.wordprocessingml.document;' '$word' && grep -q 'run $id/winword %f\$' '$word'" + check "Excel for the types of the feed" "grep -qx 'MimeType=.*text/csv;.*' '$XDG_DATA_HOME/applications/rdfeed-$id--excel.desktop' || grep -q 'MimeType=.*csv' '$XDG_DATA_HOME/applications/rdfeed-$id--excel.desktop'" +fi +check "the desktop for none" "! grep -q MimeType '$XDG_DATA_HOME/applications/rdfeed-$id--desktop.desktop'" +if command -v desktop-file-validate > /dev/null; then + check "the entries are valid" "desktop-file-validate --no-hints '$XDG_DATA_HOME'/applications/*.desktop" +fi +check "the workspace has a folder" "grep -q '<Category>X-rdfeed-$id</Category>' '$XDG_CONFIG_HOME/menus/applications-merged/rdfeed-$id.menu' && grep -qx 'Name=Contoso Apps' '$XDG_DATA_HOME/desktop-directories/rdfeed-$id.directory'" + +out="$(rdfeed list 2>&1)" +check "list shows the apps" "grep -q '$id/powerpnt *PowerPoint' <<< \"\$out\"" + +out="$(rdfeed refresh < /dev/null 2>&1)" +check "refresh takes the password from the keyring" "grep -q 'Contoso Apps: 4 apps' <<< \"\$out\"" + +rdfeed run powerpnt > /dev/null 2>&1 +check "run hands FreeRDP the .rdp file first" "[[ \$(head -n1 '$tmp/freerdp-args') == '$XDG_DATA_HOME/rdfeed/$id/rdp/powerpnt.rdp' ]]" +check "with the user, domain and password" "grep -qx '/u:jdoe' '$tmp/freerdp-args' && grep -qx '/d:CONTOSO' '$tmp/freerdp-args' && grep -qxF \"/p:\$password\" '$tmp/freerdp-args'" +check "and the hook, with GFX left on" "grep -qx 'LD_PRELOAD=$RF_HOOK' '$tmp/freerdp-env' && ! grep -q 'RDFEED_NO_GFX' '$tmp/freerdp-env'" +check "the log has what FreeRDP said" "grep -q 'gateway-delay 12' '$XDG_CACHE_HOME/rdfeed/$id--powerpnt.log'" +check "a notification says a second prompt comes" "grep -q 'notify-send .*A second sign-in is coming' '$tmp/calls'" + +mkdir -p "$HOME/Downloads" "$HOME/Documents/Reports" +: > "$HOME/Downloads/Report, final.docx" +rdfeed run "$id/winword" "$HOME/Downloads/Report, final.docx" > /dev/null 2>&1 +check "a file opens through its shared folder" "grep -qxF 'remoteapplicationcmdline:s:\"\\\\tsclient\\Downloads\\Report, final.docx\"'\$'\\r' '$tmp/freerdp-rdp' && grep -qx '/drive:Downloads,$HOME/Downloads' '$tmp/freerdp-args'" +check "and its copy of the .rdp is gone after" "[[ -z \$(ls -A '$XDG_RUNTIME_DIR/rdfeed' 2>/dev/null) ]]" +check "and a notification names it" "grep -q 'notify-send .*Opening Report, final.docx in Word' '$tmp/calls'" + +rdfeed run "$id/winword" "$HOME/nothing.docx" > /dev/null 2>&1 +rc=$? +check "a missing file is refused" "[[ \$rc -ne 0 ]]" + +# shellcheck disable=SC2016 # user-dirs.dirs wants $HOME as it is +printf 'XDG_DOCUMENTS_DIR="$HOME/Documents"\n' > "$XDG_CONFIG_HOME/user-dirs.dirs" +printf 'Share=documents\nGraphics=classic\nSound=no\nOpenWith=no\n' > "$XDG_CONFIG_HOME/rdfeed/config" +: > "$HOME/Documents/Reports/q3.xlsx" +rdfeed run "$id/excel" "$HOME/Documents/Reports/q3.xlsx" > /dev/null 2>&1 +check "a file in the shared folder needs no drive of its own" "grep -qF 'tsclient\\Documents\\Reports\\q3.xlsx' '$tmp/freerdp-rdp' && [[ \$(grep -c '^/drive:' '$tmp/freerdp-args') -eq 1 ]]" +check "the settings reach FreeRDP" "grep -qx 'RDFEED_NO_GFX=1' '$tmp/freerdp-env' && ! grep -qx '/sound' '$tmp/freerdp-args'" + +rdfeed refresh < /dev/null > /dev/null 2>&1 +check "with Open with off the entries name no file types" "! grep -q MimeType '$word' && grep -q 'run $id/winword\$' '$word'" + +FAKE_FREERDP_EXIT=131 rdfeed run "$id/powerpnt" > /dev/null 2>&1 +rc=$? +check "a failed start says so" "[[ \$rc -ne 0 ]] && grep -q 'notify-send .*PowerPoint could not start' '$tmp/calls'" + +rdfeed run nothing > /dev/null 2>&1 +rc=$? +check "an unknown app is refused" "[[ \$rc -ne 0 ]]" + +rdfeed remove "$id" > /dev/null 2>&1 +check "remove takes everything away" "[[ ! -e '$ws' && ! -e '$tmp/secrets/$id' && ! -d '$XDG_DATA_HOME/rdfeed/$id' ]] && ! ls '$XDG_DATA_HOME'/applications/rdfeed-* > /dev/null 2>&1" + +exit "$failed" diff --git a/tests/test_fetch.py b/tests/test_fetch.py new file mode 100644 index 0000000..e56a248 --- /dev/null +++ b/tests/test_fetch.py @@ -0,0 +1,220 @@ +#!/usr/bin/env python3 +# +# rdfeed-fetch: NTLM against the test vectors of MS-NLMP, the feed, the .rdp +# files, the icons, and whole subscriptions against tests/fake_rdweb.py. +# +# Copyright (C) 2026 Felitendo +# SPDX-License-Identifier: GPL-3.0-or-later + +import hmac +import os +import struct +import subprocess +import sys +import tempfile +import unittest +import zlib + +HERE = os.path.dirname(os.path.abspath(__file__)) +sys.path.insert(0, HERE) +import fake_rdweb # noqa: E402 + +fetch = fake_rdweb.fetch +FETCH = os.path.join(HERE, "..", "src", "fetch", "rdfeed-fetch") + + +def png_info(png): + """Width, height and the RGBA bytes of a PNG written by png_encode.""" + assert png[:8] == fetch.PNG_SIGNATURE + width, height = struct.unpack(">II", png[16:24]) + pos, idat = 8, b"" + while pos < len(png): + length = struct.unpack(">I", png[pos:pos + 4])[0] + kind = png[pos + 4:pos + 8] + if kind == b"IDAT": + idat += png[pos + 8:pos + 8 + length] + pos += 12 + length + raw = zlib.decompress(idat) + return width, height, raw + + +class Ntlm(unittest.TestCase): + def test_md4(self): + self.assertEqual(fetch.md4(b"").hex(), "31d6cfe0d16ae931b73c59d7e0c089c0") + self.assertEqual(fetch.md4(b"abc").hex(), "a448017aaf21d8525fc10ae87aa6729d") + self.assertEqual(fetch.md4(b"1234567890" * 8).hex(), "e33b4ddc9c38f2199c3e7b164fcc0536") + + # MS-NLMP 4.2.4: User, Domain, Password, server challenge 0123456789abcdef, + # client challenge aa..aa, time 0, target info Domain and Server. + def test_ntlmv2_vectors(self): + key = fetch.ntowfv2("User", "Domain", "Password") + self.assertEqual(key.hex(), "0c868a403bfd7a93a3001ef22ef02e3f") + server, client = bytes.fromhex("0123456789abcdef"), b"\xaa" * 8 + lm = hmac.new(key, server + client, "md5").digest() + client + self.assertEqual(lm.hex(), "86c35097ac9cec102554764a57cccc19aaaaaaaaaaaaaaaa") + info = (struct.pack("<HH", 2, 12) + "Domain".encode("utf-16-le") + + struct.pack("<HH", 1, 12) + "Server".encode("utf-16-le") + b"\x00" * 4) + nt = fetch.ntlmv2_response(key, server, client, b"\x00" * 8, info) + self.assertEqual(nt[:16].hex(), "68cd0ab851e51c96aabc927bebef6a1c") + + def test_domain_from_server(self): + challenge, server_challenge = fake_rdweb.ntlm_challenge() + message, domain = fetch.ntlm_authenticate(challenge, "jdoe", None, fake_rdweb.PASSWORD) + self.assertEqual(domain, "CONTOSO") + self.assertTrue(fake_rdweb.ntlm_check(message, server_challenge)) + message, _ = fetch.ntlm_authenticate(challenge, "jdoe", None, "wrong") + self.assertFalse(fake_rdweb.ntlm_check(message, server_challenge)) + + +class Feed(unittest.TestCase): + def test_parse(self): + name, apps = fetch.parse_feed(fake_rdweb.feed_xml()) + self.assertEqual(name, "Contoso Apps") + self.assertEqual([a["slug"] for a in apps], ["winword", "excel", "powerpnt", "desktop"]) + self.assertEqual([a["type"] for a in apps], ["RemoteApp"] * 3 + ["Desktop"]) + self.assertEqual(apps[0]["rdp"], "/RDWeb/Pages/rdp/cpub-winword-RemoteApps-CmsRdsh.rdp") + self.assertEqual(apps[1]["types"], ["xlsx", "csv"]) + self.assertEqual(apps[0]["types"], []) + + def test_known_types(self): + self.assertEqual(fetch.known_types("||winword", "Word", "winword-x")[:2], ["docx", "docm"]) + self.assertEqual(fetch.known_types("||powerpoint", "PowerPoint", ""), "pptx pptm ppt potx potm ppsx pps odp".split()) + self.assertEqual(fetch.known_types("||trueview", "DWG TrueView", "trueview-x"), ["dwg", "dxf"]) + self.assertEqual(fetch.known_types("||pdf24", "PDF24", ""), ["pdf"]) + self.assertEqual(fetch.known_types("||wordpad", "WordPad", "wordpad-x"), []) + self.assertEqual(fetch.known_types("||pwchange", "Passwort รคndern", "pwchange-x"), []) + + def test_not_a_feed(self): + for data in (b"<html><body>login</body></html>", b"", b"<RDWAPage/>"): + with self.assertRaises(fetch.FeedError): + fetch.parse_feed(data) + + def test_slugs(self): + taken = set() + self.assertEqual(fetch.make_slug("Passwort รคndern", taken), "passwort-andern") + self.assertEqual(fetch.make_slug("Passwort รคndern", taken), "passwort-andern-2") + self.assertEqual(fetch.make_slug("่ฎก็ฎ—ๅ™จ", taken), "app") + self.assertEqual(fetch.make_slug("!!!", taken), "app-2") + self.assertEqual(fetch.common_suffix(["a-X-Y", "bb-X-Y"]), "-X-Y") + self.assertEqual(fetch.common_suffix(["word", "sword"]), "") + self.assertEqual(fetch.common_suffix(["only-one"]), "") + + +class Rdp(unittest.TestCase): + def test_sanitize(self): + text = fetch.decode_rdp(fake_rdweb.rdp_file("winword-x", "Word", "RemoteApp")) + out = fetch.sanitize_rdp(text) + self.assertIn("promptcredentialonce:i:1\r\n", out) + self.assertIn("prompt for credentials on client:i:0\r\n", out) + for gone in ("drivestoredirect", "devicestoredirect", "camerastoredirect"): + self.assertNotIn(gone, out) + self.assertIn("remoteapplicationprogram:s:||winword\r\n", out) + + def test_encodings(self): + line = "full address:s:host\r\n" + for data in (line.encode("utf-16"), line.encode("utf-16-le"), line.encode("utf-8-sig"), line.encode()): + self.assertEqual(fetch.decode_rdp(data), line) + + def test_append(self): + self.assertEqual(fetch.rdp_set("a:s:b\r\n", "promptcredentialonce", "i", "1"), + "a:s:b\r\npromptcredentialonce:i:1\r\n") + + +class Icons(unittest.TestCase): + def test_32bpp(self): + width, height, raw = png_info(fetch.ico_to_png(fake_rdweb.ico_32bpp(48, (30, 110, 220, 128)))) + self.assertEqual((width, height), (48, 48)) + self.assertEqual(raw[1:5], bytes((30, 110, 220, 128))) + + def test_24bpp_with_mask(self): + size = 16 + header = struct.pack("<IiiHHIIiiII", 40, size, size * 2, 1, 24, 0, 0, 0, 0, 0, 0) + stride = ((size * 24 + 31) // 32) * 4 + pixels = (bytes((0, 0, 255)) * size + b"\x00" * (stride - size * 3)) * size + # The top row of the image (the last one in the file) is transparent. + mask = b"\x00\x00\x00\x00" * (size - 1) + b"\xff\xff\x00\x00" + dib = header + pixels + mask + ico = (struct.pack("<HHH", 0, 1, 1) + + struct.pack("<BBBBHHII", size, size, 0, 0, 1, 24, len(dib), 22) + dib) + width, height, raw = png_info(fetch.ico_to_png(ico)) + self.assertEqual((width, height), (16, 16)) + self.assertEqual(raw[1:5], bytes((255, 0, 0, 0))) + row = 1 + width * 4 + self.assertEqual(raw[row + 1:row + 5], bytes((255, 0, 0, 255))) + + def test_png_frame_and_biggest(self): + small = fake_rdweb.ico_32bpp(16) + png = fetch.png_encode(2, 1, [bytes((1, 2, 3, 4, 5, 6, 7, 8))]) + # Two entries: a 16 px bitmap and a 256 px PNG (width 0 means 256). + dib = small[22:] + ico = (struct.pack("<HHH", 0, 1, 2) + + struct.pack("<BBBBHHII", 16, 16, 0, 0, 1, 32, len(dib), 38) + + struct.pack("<BBBBHHII", 0, 0, 0, 0, 1, 32, len(png), 38 + len(dib)) + dib + png) + self.assertEqual(fetch.ico_to_png(ico), png) + + def test_garbage(self): + self.assertIsNone(fetch.ico_to_png(b"not an icon")) + self.assertIsNone(fetch.ico_to_png(b"\x00\x00\x01\x00\x01\x00" + b"\x00" * 16)) + + +class Subscribe(unittest.TestCase): + """rdfeed-fetch against the fake server, as rdfeed runs it.""" + + @classmethod + def setUpClass(cls): + cls.server = fake_rdweb.serve() + cls.url = cls.server.base + "/RDWeb/Feed/webfeed.aspx" + + @classmethod + def tearDownClass(cls): + cls.server.shutdown() + + def run_fetch(self, password, url=None, user="jdoe", domain=None): + out = tempfile.mkdtemp() + args = [sys.executable, FETCH, "--url", url or self.url, "--user", user, "--out", out, "--timeout", "5"] + if domain is not None: + args += ["--domain", domain] + proc = subprocess.run(args, input=password + "\n", capture_output=True, text=True, timeout=60) + return proc, out + + def test_ok(self): + proc, out = self.run_fetch(fake_rdweb.PASSWORD) + self.assertEqual(proc.returncode, 0, proc.stderr) + lines = [line.split("\t") for line in proc.stdout.splitlines()] + self.assertIn(["workspace", "Contoso Apps"], lines) + self.assertIn(["domain", "CONTOSO"], lines) + self.assertIn(["app", "powerpnt", "RemoteApp", "PowerPoint"], lines) + self.assertIn(["app", "desktop", "Desktop", "Contoso Desktop"], lines) + with open(os.path.join(out, "apps.tsv")) as f: + rows = [line.split("\t") for line in f.read().splitlines()] + self.assertEqual(len(rows), 4) + types = {row[0]: row[3] for row in rows} + self.assertTrue(types["winword"].startswith("docx docm doc ")) + self.assertEqual(types["excel"], "xlsx csv") + self.assertEqual(types["desktop"], "") + with open(os.path.join(out, "rdp", "winword.rdp")) as f: + rdp = f.read() + self.assertIn("promptcredentialonce:i:1", rdp) + self.assertNotIn("drivestoredirect", rdp) + with open(os.path.join(out, "icons", "excel.png"), "rb") as f: + self.assertEqual(png_info(f.read())[:2], (48, 48)) + + def test_domain_given(self): + proc, _ = self.run_fetch(fake_rdweb.PASSWORD, domain="contoso") + self.assertEqual(proc.returncode, 0, proc.stderr) + + def test_wrong_password(self): + proc, _ = self.run_fetch("nope") + self.assertEqual(proc.returncode, fetch.EXIT_AUTH) + + def test_no_feed(self): + proc, _ = self.run_fetch(fake_rdweb.PASSWORD, url=self.server.base + "/somewhere/else") + self.assertEqual(proc.returncode, fetch.EXIT_NOFEED) + + def test_unreachable(self): + proc, _ = self.run_fetch(fake_rdweb.PASSWORD, url="http://127.0.0.1:9/RDWeb/Feed/webfeed.aspx") + self.assertEqual(proc.returncode, fetch.EXIT_NETWORK) + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/test_hook.sh b/tests/test_hook.sh new file mode 100644 index 0000000..a70ea52 --- /dev/null +++ b/tests/test_hook.sh @@ -0,0 +1,88 @@ +#!/usr/bin/env bash +# +# rdfeed-hook.so: the wait before a new gateway tunnel, and GFX kept off. +# Connections go to port 443 of this computer, nothing has to answer there. +# +# Copyright (C) 2026 Felitendo +# SPDX-License-Identifier: GPL-3.0-or-later + +set -uo pipefail +cd -- "$(dirname -- "${BASH_SOURCE[0]}")/.." || exit 1 + +hook="$(readlink -f "${RDFEED_HOOK:-build/rdfeed-hook.so}")" +[[ -f $hook ]] || { echo "no hook at $hook, run make first" >&2; exit 1; } +failed=0 + +check() { + if [[ $2 == "$3" ]]; then + printf 'ok %s\n' "$1" + else + printf 'FAIL %s: got "%s", want "%s"\n' "$1" "$2" "$3" + failed=1 + fi +} + +# timings <wait mode> <pause> +# One connection, the pause, then two right after each other. Prints how long +# each one waited, in whole seconds, and whether the hook said so. +timings() { + LD_PRELOAD="$hook" RDFEED_GW_DELAY=1 RDFEED_GW_WAIT="$1" python3 - "$2" 2>&1 <<- 'EOF' + import socket, sys, time + def attempt(): + start = time.monotonic() + s = socket.socket() + try: + s.connect(("127.0.0.1", 443)) + except OSError: + pass + s.close() + return round(time.monotonic() - start) + first = attempt() + time.sleep(float(sys.argv[1])) + second = attempt() + third = attempt() + print(first, second, third) + EOF +} + +check "auto waits after a long first sign-in" "$(timings auto 3 | tr '\n' ' ')" "[rdfeed] gateway-delay 1 0 1 0 " +check "auto does not wait after a quick one" "$(timings auto 2.1)" "0 0 0" +check "always waits" "$(timings always 2.1 | tail -n1)" "0 1 0" +check "never waits not" "$(timings never 3)" "0 0 0" +check "the same tunnel does not wait" "$(timings always 0.5)" "0 0 0" + +# GFX: what FreeRDP's own command line parser makes of it, with and without. +gfx() { + env "$@" python3 - <<- 'EOF' + import ctypes + try: + core = ctypes.CDLL("libfreerdp3.so.3", mode=ctypes.RTLD_GLOBAL) + client = ctypes.CDLL("libfreerdp-client3.so.3", mode=ctypes.RTLD_GLOBAL) + except OSError: + print("skip") + raise SystemExit + core.freerdp_settings_new.restype = ctypes.c_void_p + core.freerdp_settings_new.argtypes = [ctypes.c_uint32] + core.freerdp_settings_get_key_for_name.restype = ctypes.c_ssize_t + core.freerdp_settings_get_key_for_name.argtypes = [ctypes.c_char_p] + core.freerdp_settings_get_bool.argtypes = [ctypes.c_void_p, ctypes.c_size_t] + client.freerdp_client_settings_parse_command_line.argtypes = [ + ctypes.c_void_p, ctypes.c_int, ctypes.POINTER(ctypes.c_char_p), ctypes.c_int] + settings = core.freerdp_settings_new(0) + argv = [b"xfreerdp3", b"/v:host.invalid", b"/gfx"] + client.freerdp_client_settings_parse_command_line( + settings, len(argv), (ctypes.c_char_p * len(argv))(*argv), 0) + key = core.freerdp_settings_get_key_for_name(b"FreeRDP_SupportGraphicsPipeline") + print("on" if core.freerdp_settings_get_bool(settings, key) else "off") + EOF +} + +with="$(gfx LD_PRELOAD="$hook" RDFEED_NO_GFX=1 WLOG_LEVEL=OFF)" +if [[ $with == skip ]]; then + echo "skip GFX: FreeRDP 3 is not installed" +else + check "GFX stays off with RDFEED_NO_GFX" "$with" "off" + check "GFX is left alone without it" "$(gfx LD_PRELOAD="$hook" WLOG_LEVEL=OFF)" "on" +fi + +exit "$failed"