mirror of
https://github.com/arsenetar/dupeguru.git
synced 2026-01-28 17:31:38 +00:00
Compare commits
10 Commits
334119ae6c
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
| 16aa6c21ff | |||
|
|
c32a1246b5 | ||
| 7f61330dda | |||
|
eb4d791434
|
|||
|
e9948d9b3f
|
|||
|
|
c9aa6f1b7a | ||
|
|
8f197ea7e1 | ||
|
3a97ba941a
|
|||
|
e3bcf9d686
|
|||
|
a81069be61
|
12
.github/FUNDING.yml
vendored
12
.github/FUNDING.yml
vendored
@@ -1,13 +1 @@
|
|||||||
# These are supported funding model platforms
|
|
||||||
|
|
||||||
github: arsenetar
|
github: arsenetar
|
||||||
patreon: # Replace with a single Patreon username
|
|
||||||
open_collective: # Replace with a single Open Collective username
|
|
||||||
ko_fi: # Replace with a single Ko-fi username
|
|
||||||
tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel
|
|
||||||
community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry
|
|
||||||
liberapay: # Replace with a single Liberapay username
|
|
||||||
issuehunt: # Replace with a single IssueHunt username
|
|
||||||
otechie: # Replace with a single Otechie username
|
|
||||||
lfx_crowdfunding: # Replace with a single LFX Crowdfunding project-name e.g., cloud-foundry
|
|
||||||
custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2']
|
|
||||||
|
|||||||
9
.github/workflows/codeql-analysis.yml
vendored
9
.github/workflows/codeql-analysis.yml
vendored
@@ -25,11 +25,10 @@ jobs:
|
|||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout repository
|
- name: Checkout repository
|
||||||
uses: actions/checkout@v2
|
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||||
|
|
||||||
# Initializes the CodeQL tools for scanning.
|
# Initializes the CodeQL tools for scanning.
|
||||||
- name: Initialize CodeQL
|
- name: Initialize CodeQL
|
||||||
uses: github/codeql-action/init@v1
|
uses: github/codeql-action/init@5d4e8d1aca955e8d8589aabd499c5cae939e33c7 # v4.31.9
|
||||||
with:
|
with:
|
||||||
languages: ${{ matrix.language }}
|
languages: ${{ matrix.language }}
|
||||||
# If you wish to specify custom queries, you can do so here or in a config file.
|
# If you wish to specify custom queries, you can do so here or in a config file.
|
||||||
@@ -44,7 +43,7 @@ jobs:
|
|||||||
make modules
|
make modules
|
||||||
- if: matrix.language == 'python'
|
- if: matrix.language == 'python'
|
||||||
name: Autobuild
|
name: Autobuild
|
||||||
uses: github/codeql-action/autobuild@v1
|
uses: github/codeql-action/autobuild@5d4e8d1aca955e8d8589aabd499c5cae939e33c7 # v4.31.9
|
||||||
# Analysis
|
# Analysis
|
||||||
- name: Perform CodeQL Analysis
|
- name: Perform CodeQL Analysis
|
||||||
uses: github/codeql-action/analyze@v1
|
uses: github/codeql-action/analyze@5d4e8d1aca955e8d8589aabd499c5cae939e33c7 # v4.31.9
|
||||||
|
|||||||
24
.github/workflows/default.yml
vendored
24
.github/workflows/default.yml
vendored
@@ -11,9 +11,9 @@ jobs:
|
|||||||
pre-commit:
|
pre-commit:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||||
- name: Set up Python 3.12
|
- name: Set up Python 3.12
|
||||||
uses: actions/setup-python@v5
|
uses: actions/setup-python@83679a892e2d95755f2dac6acb0bfd1e9ac5d548 # v6.1.0
|
||||||
with:
|
with:
|
||||||
python-version: "3.12"
|
python-version: "3.12"
|
||||||
- uses: pre-commit/action@v3.0.1
|
- uses: pre-commit/action@v3.0.1
|
||||||
@@ -23,7 +23,7 @@ jobs:
|
|||||||
strategy:
|
strategy:
|
||||||
matrix:
|
matrix:
|
||||||
os: [ubuntu-latest]
|
os: [ubuntu-latest]
|
||||||
python-version: [3.7, 3.8, 3.9, "3.10", "3.11", "3.12"]
|
python-version: [3.8, 3.9, "3.10", "3.11", "3.12", "3.13", "3.14"]
|
||||||
include:
|
include:
|
||||||
- os: windows-latest
|
- os: windows-latest
|
||||||
python-version: "3.12"
|
python-version: "3.12"
|
||||||
@@ -31,9 +31,9 @@ jobs:
|
|||||||
python-version: "3.12"
|
python-version: "3.12"
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||||
- name: Set up Python ${{ matrix.python-version }}
|
- name: Set up Python ${{ matrix.python-version }}
|
||||||
uses: actions/setup-python@v5
|
uses: actions/setup-python@83679a892e2d95755f2dac6acb0bfd1e9ac5d548 # v6.1.0
|
||||||
with:
|
with:
|
||||||
python-version: ${{ matrix.python-version }}
|
python-version: ${{ matrix.python-version }}
|
||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
@@ -49,7 +49,17 @@ jobs:
|
|||||||
pytest core hscommon
|
pytest core hscommon
|
||||||
- name: Upload Artifacts
|
- name: Upload Artifacts
|
||||||
if: matrix.os == 'ubuntu-latest'
|
if: matrix.os == 'ubuntu-latest'
|
||||||
uses: actions/upload-artifact@v4
|
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0
|
||||||
with:
|
with:
|
||||||
name: modules ${{ matrix.python-version }}
|
name: modules ${{ matrix.python-version }}
|
||||||
path: ${{ github.workspace }}/**/*.so
|
path: build/**/*.so
|
||||||
|
merge-artifacts:
|
||||||
|
needs: [test]
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Merge Artifacts
|
||||||
|
uses: actions/upload-artifact/merge@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0
|
||||||
|
with:
|
||||||
|
name: modules
|
||||||
|
pattern: modules*
|
||||||
|
delete-merged: true
|
||||||
|
|||||||
2
.github/workflows/tx-push.yml
vendored
2
.github/workflows/tx-push.yml
vendored
@@ -15,7 +15,7 @@ jobs:
|
|||||||
push-source:
|
push-source:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||||
- name: Get Transifex Client
|
- name: Get Transifex Client
|
||||||
run: |
|
run: |
|
||||||
curl -o- https://raw.githubusercontent.com/transifex/cli/master/install.sh | bash -s -- $TX_VERSION
|
curl -o- https://raw.githubusercontent.com/transifex/cli/master/install.sh | bash -s -- $TX_VERSION
|
||||||
|
|||||||
2
.vscode/launch.json
vendored
2
.vscode/launch.json
vendored
@@ -6,7 +6,7 @@
|
|||||||
"configurations": [
|
"configurations": [
|
||||||
{
|
{
|
||||||
"name": "DupuGuru",
|
"name": "DupuGuru",
|
||||||
"type": "python",
|
"type": "debugpy",
|
||||||
"request": "launch",
|
"request": "launch",
|
||||||
"program": "run.py",
|
"program": "run.py",
|
||||||
"console": "integratedTerminal",
|
"console": "integratedTerminal",
|
||||||
|
|||||||
3
.vscode/settings.json
vendored
3
.vscode/settings.json
vendored
@@ -12,5 +12,6 @@
|
|||||||
"[python]": {
|
"[python]": {
|
||||||
"editor.formatOnSave": true,
|
"editor.formatOnSave": true,
|
||||||
"editor.defaultFormatter": "ms-python.black-formatter"
|
"editor.defaultFormatter": "ms-python.black-formatter"
|
||||||
}
|
},
|
||||||
|
"python.testing.pytestEnabled": true
|
||||||
}
|
}
|
||||||
4
build.py
4
build.py
@@ -10,7 +10,6 @@ from optparse import OptionParser
|
|||||||
import shutil
|
import shutil
|
||||||
from multiprocessing import Pool
|
from multiprocessing import Pool
|
||||||
|
|
||||||
from setuptools import sandbox
|
|
||||||
from hscommon import sphinxgen
|
from hscommon import sphinxgen
|
||||||
from hscommon.build import (
|
from hscommon.build import (
|
||||||
add_to_pythonpath,
|
add_to_pythonpath,
|
||||||
@@ -18,6 +17,7 @@ from hscommon.build import (
|
|||||||
fix_qt_resource_file,
|
fix_qt_resource_file,
|
||||||
)
|
)
|
||||||
from hscommon import loc
|
from hscommon import loc
|
||||||
|
import subprocess
|
||||||
|
|
||||||
|
|
||||||
def parse_args():
|
def parse_args():
|
||||||
@@ -118,7 +118,7 @@ def build_normpo():
|
|||||||
def build_pe_modules():
|
def build_pe_modules():
|
||||||
print("Building PE Modules")
|
print("Building PE Modules")
|
||||||
# Leverage setup.py to build modules
|
# Leverage setup.py to build modules
|
||||||
sandbox.run_setup("setup.py", ["build_ext", "--inplace"])
|
subprocess.check_call([sys.executable, "setup.py", "build_ext", "--inplace"])
|
||||||
|
|
||||||
|
|
||||||
def build_normal():
|
def build_normal():
|
||||||
|
|||||||
@@ -158,7 +158,7 @@ class SqliteCache:
|
|||||||
ids = ",".join(map(str, rowids))
|
ids = ",".join(map(str, rowids))
|
||||||
sql = (
|
sql = (
|
||||||
"select rowid, blocks, blocks2, blocks3, blocks4, blocks5, blocks6, blocks7, blocks8 "
|
"select rowid, blocks, blocks2, blocks3, blocks4, blocks5, blocks6, blocks7, blocks8 "
|
||||||
f"from pictures where rowid in {ids}"
|
f"from pictures where rowid in ({ids})"
|
||||||
)
|
)
|
||||||
cur = self.con.execute(sql)
|
cur = self.con.execute(sql)
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -54,7 +54,7 @@ def get_cache(cache_path, readonly=False):
|
|||||||
return SqliteCache(cache_path, readonly=readonly)
|
return SqliteCache(cache_path, readonly=readonly)
|
||||||
|
|
||||||
|
|
||||||
def prepare_pictures(pictures, cache_path, with_dimensions, j=job.nulljob):
|
def prepare_pictures(pictures, cache_path, with_dimensions, match_rotated, j=job.nulljob):
|
||||||
# The MemoryError handlers in there use logging without first caring about whether or not
|
# The MemoryError handlers in there use logging without first caring about whether or not
|
||||||
# there is enough memory left to carry on the operation because it is assumed that the
|
# there is enough memory left to carry on the operation because it is assumed that the
|
||||||
# MemoryError happens when trying to read an image file, which is freed from memory by the
|
# MemoryError happens when trying to read an image file, which is freed from memory by the
|
||||||
@@ -76,8 +76,14 @@ def prepare_pictures(pictures, cache_path, with_dimensions, j=job.nulljob):
|
|||||||
if with_dimensions:
|
if with_dimensions:
|
||||||
picture.dimensions # pre-read dimensions
|
picture.dimensions # pre-read dimensions
|
||||||
try:
|
try:
|
||||||
if picture.unicode_path not in cache:
|
if picture.unicode_path not in cache or (
|
||||||
blocks = [picture.get_blocks(BLOCK_COUNT_PER_SIDE, orientation) for orientation in range(1, 9)]
|
match_rotated and any(block == [] for block in cache[picture.unicode_path])
|
||||||
|
):
|
||||||
|
if match_rotated:
|
||||||
|
blocks = [picture.get_blocks(BLOCK_COUNT_PER_SIDE, orientation) for orientation in range(1, 9)]
|
||||||
|
else:
|
||||||
|
blocks = [[]] * 8
|
||||||
|
blocks[max(picture.get_orientation() - 1, 0)] = picture.get_blocks(BLOCK_COUNT_PER_SIDE)
|
||||||
cache[picture.unicode_path] = blocks
|
cache[picture.unicode_path] = blocks
|
||||||
prepared.append(picture)
|
prepared.append(picture)
|
||||||
except (OSError, ValueError) as e:
|
except (OSError, ValueError) as e:
|
||||||
@@ -187,7 +193,7 @@ def getmatches(pictures, cache_path, threshold, match_scaled=False, match_rotate
|
|||||||
j.set_progress(comparison_count, progress_msg)
|
j.set_progress(comparison_count, progress_msg)
|
||||||
|
|
||||||
j = j.start_subjob([3, 7])
|
j = j.start_subjob([3, 7])
|
||||||
pictures = prepare_pictures(pictures, cache_path, with_dimensions=not match_scaled, j=j)
|
pictures = prepare_pictures(pictures, cache_path, not match_scaled, match_rotated, j=j)
|
||||||
j = j.start_subjob([9, 1], tr("Preparing for matching"))
|
j = j.start_subjob([9, 1], tr("Preparing for matching"))
|
||||||
cache = get_cache(cache_path)
|
cache = get_cache(cache_path)
|
||||||
id2picture = {}
|
id2picture = {}
|
||||||
|
|||||||
@@ -37,7 +37,7 @@ class Photo(fs.File):
|
|||||||
def _plat_get_blocks(self, block_count_per_side, orientation):
|
def _plat_get_blocks(self, block_count_per_side, orientation):
|
||||||
raise NotImplementedError()
|
raise NotImplementedError()
|
||||||
|
|
||||||
def _get_orientation(self):
|
def get_orientation(self):
|
||||||
if not hasattr(self, "_cached_orientation"):
|
if not hasattr(self, "_cached_orientation"):
|
||||||
try:
|
try:
|
||||||
with self.path.open("rb") as fp:
|
with self.path.open("rb") as fp:
|
||||||
@@ -95,13 +95,13 @@ class Photo(fs.File):
|
|||||||
fs.File._read_info(self, field)
|
fs.File._read_info(self, field)
|
||||||
if field == "dimensions":
|
if field == "dimensions":
|
||||||
self.dimensions = self._plat_get_dimensions()
|
self.dimensions = self._plat_get_dimensions()
|
||||||
if self._get_orientation() in {5, 6, 7, 8}:
|
if self.get_orientation() in {5, 6, 7, 8}:
|
||||||
self.dimensions = (self.dimensions[1], self.dimensions[0])
|
self.dimensions = (self.dimensions[1], self.dimensions[0])
|
||||||
elif field == "exif_timestamp":
|
elif field == "exif_timestamp":
|
||||||
self.exif_timestamp = self._get_exif_timestamp()
|
self.exif_timestamp = self._get_exif_timestamp()
|
||||||
|
|
||||||
def get_blocks(self, block_count_per_side, orientation: int = None):
|
def get_blocks(self, block_count_per_side, orientation: int = None):
|
||||||
if orientation is None:
|
if orientation is None:
|
||||||
return self._plat_get_blocks(block_count_per_side, self._get_orientation())
|
return self._plat_get_blocks(block_count_per_side, self.get_orientation())
|
||||||
else:
|
else:
|
||||||
return self._plat_get_blocks(block_count_per_side, orientation)
|
return self._plat_get_blocks(block_count_per_side, orientation)
|
||||||
|
|||||||
@@ -96,6 +96,8 @@ class FilenameCategory(CriterionCategory):
|
|||||||
DOESNT_END_WITH_NUMBER = 1
|
DOESNT_END_WITH_NUMBER = 1
|
||||||
LONGEST = 2
|
LONGEST = 2
|
||||||
SHORTEST = 3
|
SHORTEST = 3
|
||||||
|
LONGEST_PATH = 4
|
||||||
|
SHORTEST_PATH = 5
|
||||||
|
|
||||||
def format_criterion_value(self, value):
|
def format_criterion_value(self, value):
|
||||||
return {
|
return {
|
||||||
@@ -103,6 +105,8 @@ class FilenameCategory(CriterionCategory):
|
|||||||
self.DOESNT_END_WITH_NUMBER: tr("Doesn't end with number"),
|
self.DOESNT_END_WITH_NUMBER: tr("Doesn't end with number"),
|
||||||
self.LONGEST: tr("Longest"),
|
self.LONGEST: tr("Longest"),
|
||||||
self.SHORTEST: tr("Shortest"),
|
self.SHORTEST: tr("Shortest"),
|
||||||
|
self.LONGEST_PATH: tr("Longest Path"),
|
||||||
|
self.SHORTEST_PATH: tr("Shortest Path"),
|
||||||
}[value]
|
}[value]
|
||||||
|
|
||||||
def extract_value(self, dupe):
|
def extract_value(self, dupe):
|
||||||
@@ -116,6 +120,10 @@ class FilenameCategory(CriterionCategory):
|
|||||||
return 0 if ends_with_digit else 1
|
return 0 if ends_with_digit else 1
|
||||||
else:
|
else:
|
||||||
return 1 if ends_with_digit else 0
|
return 1 if ends_with_digit else 0
|
||||||
|
elif crit_value == self.LONGEST_PATH:
|
||||||
|
return len(str(dupe.folder_path)) * -1
|
||||||
|
elif crit_value == self.SHORTEST_PATH:
|
||||||
|
return len(str(dupe.folder_path))
|
||||||
else:
|
else:
|
||||||
value = len(value)
|
value = len(value)
|
||||||
if crit_value == self.LONGEST:
|
if crit_value == self.LONGEST:
|
||||||
@@ -130,6 +138,8 @@ class FilenameCategory(CriterionCategory):
|
|||||||
self.DOESNT_END_WITH_NUMBER,
|
self.DOESNT_END_WITH_NUMBER,
|
||||||
self.LONGEST,
|
self.LONGEST,
|
||||||
self.SHORTEST,
|
self.SHORTEST,
|
||||||
|
self.LONGEST_PATH,
|
||||||
|
self.SHORTEST_PATH,
|
||||||
]
|
]
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|||||||
@@ -325,7 +325,7 @@ class Results(Markable):
|
|||||||
del self.__group_of_duplicate[dupe]
|
del self.__group_of_duplicate[dupe]
|
||||||
self._remove_mark_flag(dupe)
|
self._remove_mark_flag(dupe)
|
||||||
self.__total_count -= 1
|
self.__total_count -= 1
|
||||||
self.__total_size -= dupe.size
|
self.__total_size = max(0, self.__total_size - dupe.size)
|
||||||
if not group:
|
if not group:
|
||||||
del self.__group_of_duplicate[ref]
|
del self.__group_of_duplicate[ref]
|
||||||
self.__groups.remove(group)
|
self.__groups.remove(group)
|
||||||
|
|||||||
@@ -59,13 +59,13 @@ class BaseTestCaseCache:
|
|||||||
|
|
||||||
def test_set_then_retrieve_blocks(self):
|
def test_set_then_retrieve_blocks(self):
|
||||||
c = self.get_cache()
|
c = self.get_cache()
|
||||||
b = [(0, 0, 0), (1, 2, 3)]
|
b = [[(0, 0, 0), (1, 2, 3)]] * 8
|
||||||
c["foo"] = b
|
c["foo"] = b
|
||||||
eq_(b, c["foo"])
|
eq_(b, c["foo"])
|
||||||
|
|
||||||
def test_delitem(self):
|
def test_delitem(self):
|
||||||
c = self.get_cache()
|
c = self.get_cache()
|
||||||
c["foo"] = ""
|
c["foo"] = [[]] * 8
|
||||||
del c["foo"]
|
del c["foo"]
|
||||||
assert "foo" not in c
|
assert "foo" not in c
|
||||||
with raises(KeyError):
|
with raises(KeyError):
|
||||||
@@ -74,16 +74,16 @@ class BaseTestCaseCache:
|
|||||||
def test_persistance(self, tmpdir):
|
def test_persistance(self, tmpdir):
|
||||||
DBNAME = tmpdir.join("hstest.db")
|
DBNAME = tmpdir.join("hstest.db")
|
||||||
c = self.get_cache(str(DBNAME))
|
c = self.get_cache(str(DBNAME))
|
||||||
c["foo"] = [(1, 2, 3)]
|
c["foo"] = [[(1, 2, 3)]] * 8
|
||||||
del c
|
del c
|
||||||
c = self.get_cache(str(DBNAME))
|
c = self.get_cache(str(DBNAME))
|
||||||
eq_([(1, 2, 3)], c["foo"])
|
eq_([[(1, 2, 3)]] * 8, c["foo"])
|
||||||
|
|
||||||
def test_filter(self):
|
def test_filter(self):
|
||||||
c = self.get_cache()
|
c = self.get_cache()
|
||||||
c["foo"] = ""
|
c["foo"] = [[]] * 8
|
||||||
c["bar"] = ""
|
c["bar"] = [[]] * 8
|
||||||
c["baz"] = ""
|
c["baz"] = [[]] * 8
|
||||||
c.filter(lambda p: p != "bar") # only 'bar' is removed
|
c.filter(lambda p: p != "bar") # only 'bar' is removed
|
||||||
eq_(2, len(c))
|
eq_(2, len(c))
|
||||||
assert "foo" in c
|
assert "foo" in c
|
||||||
@@ -92,9 +92,9 @@ class BaseTestCaseCache:
|
|||||||
|
|
||||||
def test_clear(self):
|
def test_clear(self):
|
||||||
c = self.get_cache()
|
c = self.get_cache()
|
||||||
c["foo"] = ""
|
c["foo"] = [[]] * 8
|
||||||
c["bar"] = ""
|
c["bar"] = [[]] * 8
|
||||||
c["baz"] = ""
|
c["baz"] = [[]] * 8
|
||||||
c.clear()
|
c.clear()
|
||||||
eq_(0, len(c))
|
eq_(0, len(c))
|
||||||
assert "foo" not in c
|
assert "foo" not in c
|
||||||
@@ -104,7 +104,7 @@ class BaseTestCaseCache:
|
|||||||
def test_by_id(self):
|
def test_by_id(self):
|
||||||
# it's possible to use the cache by referring to the files by their row_id
|
# it's possible to use the cache by referring to the files by their row_id
|
||||||
c = self.get_cache()
|
c = self.get_cache()
|
||||||
b = [(0, 0, 0), (1, 2, 3)]
|
b = [[(0, 0, 0), (1, 2, 3)]] * 8
|
||||||
c["foo"] = b
|
c["foo"] = b
|
||||||
foo_id = c.get_id("foo")
|
foo_id = c.get_id("foo")
|
||||||
eq_(c[foo_id], b)
|
eq_(c[foo_id], b)
|
||||||
@@ -127,10 +127,10 @@ class TestCaseSqliteCache(BaseTestCaseCache):
|
|||||||
fp.write("invalid sqlite content")
|
fp.write("invalid sqlite content")
|
||||||
fp.close()
|
fp.close()
|
||||||
c = self.get_cache(dbname) # should not raise a DatabaseError
|
c = self.get_cache(dbname) # should not raise a DatabaseError
|
||||||
c["foo"] = [(1, 2, 3)]
|
c["foo"] = [[(1, 2, 3)]] * 8
|
||||||
del c
|
del c
|
||||||
c = self.get_cache(dbname)
|
c = self.get_cache(dbname)
|
||||||
eq_(c["foo"], [(1, 2, 3)])
|
eq_(c["foo"], [[(1, 2, 3)]] * 8)
|
||||||
|
|
||||||
|
|
||||||
class TestCaseCacheSQLEscape:
|
class TestCaseCacheSQLEscape:
|
||||||
@@ -152,7 +152,7 @@ class TestCaseCacheSQLEscape:
|
|||||||
|
|
||||||
def test_delitem(self):
|
def test_delitem(self):
|
||||||
c = self.get_cache()
|
c = self.get_cache()
|
||||||
c["foo'bar"] = []
|
c["foo'bar"] = [[]] * 8
|
||||||
try:
|
try:
|
||||||
del c["foo'bar"]
|
del c["foo'bar"]
|
||||||
except KeyError:
|
except KeyError:
|
||||||
|
|||||||
@@ -112,7 +112,7 @@ msgstr "Размеры"
|
|||||||
|
|
||||||
#: core\pe\result_table.py:21 core\se\result_table.py:21
|
#: core\pe\result_table.py:21 core\se\result_table.py:21
|
||||||
msgid "Size (KB)"
|
msgid "Size (KB)"
|
||||||
msgstr "Размер (кБ)"
|
msgstr "Размер (КБ)"
|
||||||
|
|
||||||
#: core\pe\result_table.py:24
|
#: core\pe\result_table.py:24
|
||||||
msgid "EXIF Timestamp"
|
msgid "EXIF Timestamp"
|
||||||
|
|||||||
@@ -100,15 +100,15 @@ msgstr ""
|
|||||||
|
|
||||||
#: core\app.py:469
|
#: core\app.py:469
|
||||||
msgid "Select a directory to copy marked files to"
|
msgid "Select a directory to copy marked files to"
|
||||||
msgstr "Выберите каталог, в который Вы хотите скопировать отмеченные файлы"
|
msgstr "Выберите каталог, в который вы хотите скопировать отмеченные файлы"
|
||||||
|
|
||||||
#: core\app.py:471
|
#: core\app.py:471
|
||||||
msgid "Select a directory to move marked files to"
|
msgid "Select a directory to move marked files to"
|
||||||
msgstr "Выберите каталог для перемещения отмеченных файлов"
|
msgstr "Выберите каталог, в который вы хотите переместить отмеченные файлы"
|
||||||
|
|
||||||
#: core\app.py:510
|
#: core\app.py:510
|
||||||
msgid "Select a destination for your exported CSV"
|
msgid "Select a destination for your exported CSV"
|
||||||
msgstr "Выберите назначение для Вашего экспортируемого CSV"
|
msgstr "Выберите назначение для экспортируемого "
|
||||||
|
|
||||||
#: core\app.py:516 core\app.py:777 core\app.py:787
|
#: core\app.py:516 core\app.py:777 core\app.py:787
|
||||||
msgid "Couldn't write to file: {}"
|
msgid "Couldn't write to file: {}"
|
||||||
@@ -124,7 +124,7 @@ msgstr "Вы собираетесь удалить %d файлов из резу
|
|||||||
|
|
||||||
#: core\app.py:749
|
#: core\app.py:749
|
||||||
msgid "{} duplicate groups were changed by the re-prioritization."
|
msgid "{} duplicate groups were changed by the re-prioritization."
|
||||||
msgstr "{} групп дубликатов было изменено при обновлении приоритета."
|
msgstr "{} групп дубликатов было изменено при реприоритезации."
|
||||||
|
|
||||||
#: core\app.py:797
|
#: core\app.py:797
|
||||||
msgid "The selected directories contain no scannable file."
|
msgid "The selected directories contain no scannable file."
|
||||||
@@ -136,7 +136,7 @@ msgstr "Сбор файлов для сканирования"
|
|||||||
|
|
||||||
#: core\app.py:863
|
#: core\app.py:863
|
||||||
msgid "%s (%d discarded)"
|
msgid "%s (%d discarded)"
|
||||||
msgstr "%s (%d отменено)"
|
msgstr "%s. (%d отменено)"
|
||||||
|
|
||||||
#: core\directories.py:191
|
#: core\directories.py:191
|
||||||
msgid "Collected {} files to scan"
|
msgid "Collected {} files to scan"
|
||||||
@@ -148,7 +148,7 @@ msgstr "Собрано {} каталогов для сканирования"
|
|||||||
|
|
||||||
#: core\engine.py:27
|
#: core\engine.py:27
|
||||||
msgid "%d matches found from %d groups"
|
msgid "%d matches found from %d groups"
|
||||||
msgstr "%d совпадений найдено из %d групп"
|
msgstr "Найдено %d совпадений из %d групп"
|
||||||
|
|
||||||
#: core\gui\deletion_options.py:71
|
#: core\gui\deletion_options.py:71
|
||||||
msgid "You are sending {} file(s) to the Trash."
|
msgid "You are sending {} file(s) to the Trash."
|
||||||
@@ -249,7 +249,7 @@ msgstr "%d / %d (%s / %s) дубликатов отмечено."
|
|||||||
|
|
||||||
#: core\results.py:142
|
#: core\results.py:142
|
||||||
msgid " filter: %s"
|
msgid " filter: %s"
|
||||||
msgstr " фильтр: %s"
|
msgstr "фильтр: %s"
|
||||||
|
|
||||||
#: core\scanner.py:114
|
#: core\scanner.py:114
|
||||||
msgid "Read metadata of %d/%d files"
|
msgid "Read metadata of %d/%d files"
|
||||||
@@ -262,4 +262,3 @@ msgstr "Почти готово! Вожусь с результатами..."
|
|||||||
#: core\se\scanner.py:18
|
#: core\se\scanner.py:18
|
||||||
msgid "Folders"
|
msgid "Folders"
|
||||||
msgstr "Папки"
|
msgstr "Папки"
|
||||||
|
|
||||||
|
|||||||
@@ -93,8 +93,8 @@ msgid ""
|
|||||||
"Instead of sending files to trash, delete them directly. This option is "
|
"Instead of sending files to trash, delete them directly. This option is "
|
||||||
"usually used as a workaround when the normal deletion method doesn't work."
|
"usually used as a workaround when the normal deletion method doesn't work."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
"Удалить файлы с диска вместо отправки в Корзину. Используйте, если нормальный "
|
"Удалить файлы с диска вместо отправки в Корзину. Используйте если нормальный"
|
||||||
"метод удаления не работает."
|
" метод удаления не работает."
|
||||||
|
|
||||||
#: qt/deletion_options.py:59 cocoa/en.lproj/Localizable.strings:0
|
#: qt/deletion_options.py:59 cocoa/en.lproj/Localizable.strings:0
|
||||||
msgid "Proceed"
|
msgid "Proceed"
|
||||||
@@ -189,7 +189,7 @@ msgstr "Несохранённые результаты"
|
|||||||
|
|
||||||
#: qt/directories_dialog.py:231 cocoa/en.lproj/Localizable.strings:0
|
#: qt/directories_dialog.py:231 cocoa/en.lproj/Localizable.strings:0
|
||||||
msgid "You have unsaved results, do you really want to quit?"
|
msgid "You have unsaved results, do you really want to quit?"
|
||||||
msgstr "Имеются несохранённые результаты, Вы действительно хотите выйти?"
|
msgstr "Имеются несохранённые результаты, вы действительно хотите выйти?"
|
||||||
|
|
||||||
#: qt/directories_dialog.py:239 cocoa/en.lproj/Localizable.strings:0
|
#: qt/directories_dialog.py:239 cocoa/en.lproj/Localizable.strings:0
|
||||||
msgid "Select a folder to add to the scanning list"
|
msgid "Select a folder to add to the scanning list"
|
||||||
@@ -205,7 +205,7 @@ msgstr "Все файлы (*.*)"
|
|||||||
|
|
||||||
#: qt/directories_dialog.py:267 qt/result_window.py:311
|
#: qt/directories_dialog.py:267 qt/result_window.py:311
|
||||||
msgid "dupeGuru Results (*.dupeguru)"
|
msgid "dupeGuru Results (*.dupeguru)"
|
||||||
msgstr "Результаты dupeGuru (*.dupeguru)"
|
msgstr "Результаты dupeGuru (*. dupeguru)"
|
||||||
|
|
||||||
#: qt/directories_dialog.py:278
|
#: qt/directories_dialog.py:278
|
||||||
msgid "Start a new scan"
|
msgid "Start a new scan"
|
||||||
@@ -213,7 +213,7 @@ msgstr "Начать новую проверку"
|
|||||||
|
|
||||||
#: qt/directories_dialog.py:279 cocoa/en.lproj/Localizable.strings:0
|
#: qt/directories_dialog.py:279 cocoa/en.lproj/Localizable.strings:0
|
||||||
msgid "You have unsaved results, do you really want to continue?"
|
msgid "You have unsaved results, do you really want to continue?"
|
||||||
msgstr "Имеются несохранённые результаты, Вы действительно хотите продолжить?"
|
msgstr "Имеются несохранённые результаты, вы действительно хотите продолжить?"
|
||||||
|
|
||||||
#: qt/directories_model.py:23 cocoa/en.lproj/Localizable.strings:0
|
#: qt/directories_model.py:23 cocoa/en.lproj/Localizable.strings:0
|
||||||
msgid "Name"
|
msgid "Name"
|
||||||
@@ -256,7 +256,7 @@ msgstr "Теги для проверки:"
|
|||||||
|
|
||||||
#: qt/me/preferences_dialog.py:36 cocoa/en.lproj/Localizable.strings:0
|
#: qt/me/preferences_dialog.py:36 cocoa/en.lproj/Localizable.strings:0
|
||||||
msgid "Track"
|
msgid "Track"
|
||||||
msgstr "Дорожка"
|
msgstr "Трек"
|
||||||
|
|
||||||
#: qt/me/preferences_dialog.py:38 cocoa/en.lproj/Localizable.strings:0
|
#: qt/me/preferences_dialog.py:38 cocoa/en.lproj/Localizable.strings:0
|
||||||
msgid "Artist"
|
msgid "Artist"
|
||||||
@@ -323,7 +323,7 @@ msgstr "Уровень фильтрации:"
|
|||||||
|
|
||||||
#: qt/preferences_dialog.py:69
|
#: qt/preferences_dialog.py:69
|
||||||
msgid "More Results"
|
msgid "More Results"
|
||||||
msgstr "Больше результатов"
|
msgstr "Дополнительные результаты"
|
||||||
|
|
||||||
#: qt/preferences_dialog.py:74
|
#: qt/preferences_dialog.py:74
|
||||||
msgid "Fewer Results"
|
msgid "Fewer Results"
|
||||||
@@ -379,7 +379,7 @@ msgstr ""
|
|||||||
|
|
||||||
#: qt/problem_dialog.py:33 cocoa/en.lproj/Localizable.strings:0
|
#: qt/problem_dialog.py:33 cocoa/en.lproj/Localizable.strings:0
|
||||||
msgid "Problems!"
|
msgid "Problems!"
|
||||||
msgstr "Проблема!"
|
msgstr "Проблемка!"
|
||||||
|
|
||||||
#: qt/problem_dialog.py:37 cocoa/en.lproj/Localizable.strings:0
|
#: qt/problem_dialog.py:37 cocoa/en.lproj/Localizable.strings:0
|
||||||
msgid ""
|
msgid ""
|
||||||
@@ -409,15 +409,15 @@ msgstr "Показать значения разницы"
|
|||||||
|
|
||||||
#: qt/result_window.py:60
|
#: qt/result_window.py:60
|
||||||
msgid "Send Marked to Recycle Bin..."
|
msgid "Send Marked to Recycle Bin..."
|
||||||
msgstr "Переместить отмеченные в Корзину..."
|
msgstr "Переместить отмеченные в Корзину…"
|
||||||
|
|
||||||
#: qt/result_window.py:61 cocoa/en.lproj/Localizable.strings:0
|
#: qt/result_window.py:61 cocoa/en.lproj/Localizable.strings:0
|
||||||
msgid "Move Marked to..."
|
msgid "Move Marked to..."
|
||||||
msgstr "Переместить отмеченные в..."
|
msgstr "Переместить отмеченные в…"
|
||||||
|
|
||||||
#: qt/result_window.py:62 cocoa/en.lproj/Localizable.strings:0
|
#: qt/result_window.py:62 cocoa/en.lproj/Localizable.strings:0
|
||||||
msgid "Copy Marked to..."
|
msgid "Copy Marked to..."
|
||||||
msgstr "Скопировать отмеченные в..."
|
msgstr "Скопировать отмеченные в…"
|
||||||
|
|
||||||
#: qt/result_window.py:63 cocoa/en.lproj/Localizable.strings:0
|
#: qt/result_window.py:63 cocoa/en.lproj/Localizable.strings:0
|
||||||
msgid "Remove Marked from Results"
|
msgid "Remove Marked from Results"
|
||||||
@@ -477,7 +477,7 @@ msgstr "Экспорт в CSV"
|
|||||||
|
|
||||||
#: qt/result_window.py:89 cocoa/en.lproj/Localizable.strings:0
|
#: qt/result_window.py:89 cocoa/en.lproj/Localizable.strings:0
|
||||||
msgid "Save Results..."
|
msgid "Save Results..."
|
||||||
msgstr "Сохранить результаты..."
|
msgstr "Сохранить результаты…"
|
||||||
|
|
||||||
#: qt/result_window.py:90 cocoa/en.lproj/Localizable.strings:0
|
#: qt/result_window.py:90 cocoa/en.lproj/Localizable.strings:0
|
||||||
msgid "Invoke Custom Command"
|
msgid "Invoke Custom Command"
|
||||||
@@ -509,15 +509,15 @@ msgstr "Значения разницы"
|
|||||||
|
|
||||||
#: qt/result_window.py:310 cocoa/en.lproj/Localizable.strings:0
|
#: qt/result_window.py:310 cocoa/en.lproj/Localizable.strings:0
|
||||||
msgid "Select a file to save your results to"
|
msgid "Select a file to save your results to"
|
||||||
msgstr "Выберите файл для сохранения Ваших результатов"
|
msgstr "Выберите файл, чтобы сохранить ваши результаты"
|
||||||
|
|
||||||
#: qt/se/preferences_dialog.py:41
|
#: qt/se/preferences_dialog.py:41
|
||||||
msgid "Ignore files smaller than"
|
msgid "Ignore files smaller than"
|
||||||
msgstr "Игнорировать файлы меньше, чем"
|
msgstr "Игнорировать файлы меньше чем"
|
||||||
|
|
||||||
#: qt/se/preferences_dialog.py:52 cocoa/en.lproj/Localizable.strings:0
|
#: qt/se/preferences_dialog.py:52 cocoa/en.lproj/Localizable.strings:0
|
||||||
msgid "KB"
|
msgid "KB"
|
||||||
msgstr "кБ"
|
msgstr "КБ"
|
||||||
|
|
||||||
#: cocoa/en.lproj/Localizable.strings:0
|
#: cocoa/en.lproj/Localizable.strings:0
|
||||||
msgid "%@ Results"
|
msgid "%@ Results"
|
||||||
@@ -549,7 +549,7 @@ msgstr "Все на передний план"
|
|||||||
|
|
||||||
#: cocoa/en.lproj/Localizable.strings:0
|
#: cocoa/en.lproj/Localizable.strings:0
|
||||||
msgid "Check for update..."
|
msgid "Check for update..."
|
||||||
msgstr "Проверка обновлений..."
|
msgstr "Проверка обновлений…"
|
||||||
|
|
||||||
#: cocoa/en.lproj/Localizable.strings:0
|
#: cocoa/en.lproj/Localizable.strings:0
|
||||||
msgid "Close Window"
|
msgid "Close Window"
|
||||||
@@ -594,7 +594,7 @@ msgstr "Настройки dupeGuru"
|
|||||||
|
|
||||||
#: cocoa/en.lproj/Localizable.strings:0
|
#: cocoa/en.lproj/Localizable.strings:0
|
||||||
msgid "dupeGuru Results"
|
msgid "dupeGuru Results"
|
||||||
msgstr "Результаты dupeGuru"
|
msgstr "Результаты dupeGuru "
|
||||||
|
|
||||||
#: cocoa/en.lproj/Localizable.strings:0
|
#: cocoa/en.lproj/Localizable.strings:0
|
||||||
msgid "dupeGuru Website"
|
msgid "dupeGuru Website"
|
||||||
@@ -626,7 +626,7 @@ msgstr "Уровень фильтрации:"
|
|||||||
|
|
||||||
#: cocoa/en.lproj/Localizable.strings:0
|
#: cocoa/en.lproj/Localizable.strings:0
|
||||||
msgid "Filter Results..."
|
msgid "Filter Results..."
|
||||||
msgstr "Отфильтровать результаты..."
|
msgstr "Отфильтровать результаты…"
|
||||||
|
|
||||||
#: cocoa/en.lproj/Localizable.strings:0
|
#: cocoa/en.lproj/Localizable.strings:0
|
||||||
msgid "Folder Selection Window"
|
msgid "Folder Selection Window"
|
||||||
@@ -646,11 +646,11 @@ msgstr "Скрыть остальные"
|
|||||||
|
|
||||||
#: cocoa/en.lproj/Localizable.strings:0
|
#: cocoa/en.lproj/Localizable.strings:0
|
||||||
msgid "Ignore files smaller than:"
|
msgid "Ignore files smaller than:"
|
||||||
msgstr "Пропускать файлы меньше, чем:"
|
msgstr "Пропускать файлы меньше чем:"
|
||||||
|
|
||||||
#: cocoa/en.lproj/Localizable.strings:0
|
#: cocoa/en.lproj/Localizable.strings:0
|
||||||
msgid "Load from file..."
|
msgid "Load from file..."
|
||||||
msgstr "Загрузить из файла..."
|
msgstr "Загрузить из файла…"
|
||||||
|
|
||||||
#: cocoa/en.lproj/Localizable.strings:0
|
#: cocoa/en.lproj/Localizable.strings:0
|
||||||
msgid "Minimize"
|
msgid "Minimize"
|
||||||
@@ -674,7 +674,7 @@ msgstr "Вставить"
|
|||||||
|
|
||||||
#: cocoa/en.lproj/Localizable.strings:0
|
#: cocoa/en.lproj/Localizable.strings:0
|
||||||
msgid "Preferences..."
|
msgid "Preferences..."
|
||||||
msgstr "Настройки..."
|
msgstr "Настройки…"
|
||||||
|
|
||||||
#: cocoa/en.lproj/Localizable.strings:0
|
#: cocoa/en.lproj/Localizable.strings:0
|
||||||
msgid "Quick Look"
|
msgid "Quick Look"
|
||||||
@@ -686,7 +686,7 @@ msgstr "Выйти из dupeGuru"
|
|||||||
|
|
||||||
#: cocoa/en.lproj/Localizable.strings:0
|
#: cocoa/en.lproj/Localizable.strings:0
|
||||||
msgid "Reset to Default"
|
msgid "Reset to Default"
|
||||||
msgstr "Восстановить значение по умолчанию"
|
msgstr "Восстановить значения по умолчанию"
|
||||||
|
|
||||||
#: cocoa/en.lproj/Localizable.strings:0
|
#: cocoa/en.lproj/Localizable.strings:0
|
||||||
msgid "Reset To Defaults"
|
msgid "Reset To Defaults"
|
||||||
@@ -706,7 +706,7 @@ msgstr "Выбрать все"
|
|||||||
|
|
||||||
#: cocoa/en.lproj/Localizable.strings:0
|
#: cocoa/en.lproj/Localizable.strings:0
|
||||||
msgid "Send Marked to Trash..."
|
msgid "Send Marked to Trash..."
|
||||||
msgstr "Переместить отмеченные в Корзину..."
|
msgstr "Переместить отмеченные в Корзину…"
|
||||||
|
|
||||||
#: cocoa/en.lproj/Localizable.strings:0
|
#: cocoa/en.lproj/Localizable.strings:0
|
||||||
msgid "Services"
|
msgid "Services"
|
||||||
@@ -754,11 +754,11 @@ msgstr "Выберите файл каталогов для загрузки"
|
|||||||
|
|
||||||
#: qt\directories_dialog.py:338
|
#: qt\directories_dialog.py:338
|
||||||
msgid "dupeGuru Results (*.dupegurudirs)"
|
msgid "dupeGuru Results (*.dupegurudirs)"
|
||||||
msgstr "Результаты dupeGuru (*.dupegurudirs)"
|
msgstr "Каталоги dupeGuru (*.dupegurudirs)"
|
||||||
|
|
||||||
#: qt\directories_dialog.py:347
|
#: qt\directories_dialog.py:347
|
||||||
msgid "Select a file to save your directories to"
|
msgid "Select a file to save your directories to"
|
||||||
msgstr "Выберите файл для сохранения Ваших каталогов"
|
msgstr "Выберите файл для сохранения каталогов"
|
||||||
|
|
||||||
#: qt\directories_dialog.py:348
|
#: qt\directories_dialog.py:348
|
||||||
msgid "dupeGuru Directories (*.dupegurudirs)"
|
msgid "dupeGuru Directories (*.dupegurudirs)"
|
||||||
@@ -798,7 +798,7 @@ msgstr ""
|
|||||||
|
|
||||||
#: qt\exclude_list_table.py:36
|
#: qt\exclude_list_table.py:36
|
||||||
msgid "Compilation error: "
|
msgid "Compilation error: "
|
||||||
msgstr "Ошибка компиляции: "
|
msgstr "Ошибка компиляции:"
|
||||||
|
|
||||||
#: qt\pe\image_viewer.py:56
|
#: qt\pe\image_viewer.py:56
|
||||||
msgid "Increase zoom"
|
msgid "Increase zoom"
|
||||||
@@ -835,7 +835,7 @@ msgstr "Переопределить значки темы на панели и
|
|||||||
#: qt\pe\preferences_dialog.py:58
|
#: qt\pe\preferences_dialog.py:58
|
||||||
msgid ""
|
msgid ""
|
||||||
"Use our own internal icons instead of those provided by the theme engine"
|
"Use our own internal icons instead of those provided by the theme engine"
|
||||||
msgstr "Использовать наши внутренние значки вместо значков, встроенных в тему"
|
msgstr "Используйте внутренние значки вместо значков, встроенных в тему"
|
||||||
|
|
||||||
#: qt\pe\preferences_dialog.py:66
|
#: qt\pe\preferences_dialog.py:66
|
||||||
msgid "Show scrollbars in image viewers"
|
msgid "Show scrollbars in image viewers"
|
||||||
@@ -965,7 +965,7 @@ msgstr ""
|
|||||||
|
|
||||||
#: qt\se\preferences_dialog.py:68
|
#: qt\se\preferences_dialog.py:68
|
||||||
msgid "Ignore files larger than"
|
msgid "Ignore files larger than"
|
||||||
msgstr "Игнорировать файлы больше, чем"
|
msgstr "Игнорировать файлы больше чем"
|
||||||
|
|
||||||
#: qt\app.py:135 qt\app.py:293
|
#: qt\app.py:135 qt\app.py:293
|
||||||
msgid "Clear Cache"
|
msgid "Clear Cache"
|
||||||
@@ -981,7 +981,7 @@ msgstr ""
|
|||||||
|
|
||||||
#: qt\app.py:299
|
#: qt\app.py:299
|
||||||
msgid "Cache cleared."
|
msgid "Cache cleared."
|
||||||
msgstr "Кэш очищен."
|
msgstr "Кэш очищен "
|
||||||
|
|
||||||
#: qt\preferences_dialog.py:173
|
#: qt\preferences_dialog.py:173
|
||||||
msgid "Use dark style"
|
msgid "Use dark style"
|
||||||
@@ -994,7 +994,8 @@ msgstr "Сохранить профиль сканирования"
|
|||||||
#: qt\preferences_dialog.py:242
|
#: qt\preferences_dialog.py:242
|
||||||
msgid "Profile the scan operation and save logs for optimization."
|
msgid "Profile the scan operation and save logs for optimization."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
"Настройте операцию сканирования и сохраните журналы для оптимизации."
|
"В папке установленной или портативной программы, есть папка Data в которую "
|
||||||
|
"сохраняется логи и файл с раширением *.profile для оптимизации."
|
||||||
|
|
||||||
#: qt\preferences_dialog.py:246
|
#: qt\preferences_dialog.py:246
|
||||||
msgid "Logs located in: <a href=\"{}\">{}</a>"
|
msgid "Logs located in: <a href=\"{}\">{}</a>"
|
||||||
@@ -1022,11 +1023,11 @@ msgstr "Под лицензией GPLv3"
|
|||||||
|
|
||||||
#: qt\about_box.py:68
|
#: qt\about_box.py:68
|
||||||
msgid "No update available."
|
msgid "No update available."
|
||||||
msgstr "Обновления недоступны."
|
msgstr "У вас самая свежая версия"
|
||||||
|
|
||||||
#: qt\about_box.py:71
|
#: qt\about_box.py:71
|
||||||
msgid "New version {} available, download <a href=\"{}\">here</a>."
|
msgid "New version {} available, download <a href=\"{}\">here</a>."
|
||||||
msgstr "Обнаружена новая {} версия, загрузить <a href=\"{}\">тут</a>."
|
msgstr "Обнаружена новая {} версия, загружать <a href=\"{}\">тут</a>."
|
||||||
|
|
||||||
#: qt\error_report_dialog.py:50
|
#: qt\error_report_dialog.py:50
|
||||||
msgid "Error Report"
|
msgid "Error Report"
|
||||||
|
|||||||
Reference in New Issue
Block a user