mirror of
https://github.com/dgtlmoon/changedetection.io.git
synced 2026-04-17 00:18:00 +00:00
Compare commits
6 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3d5036e08f | ||
|
|
75c03db050 | ||
|
|
541c5e09dc | ||
|
|
006e5cb7a7 | ||
|
|
c2f06f574b | ||
|
|
f3c68c2311 |
@@ -99,7 +99,7 @@ jobs:
|
||||
|
||||
- name: Run Unit Tests
|
||||
run: |
|
||||
docker run test-changedetectionio bash -c 'cd changedetectionio;pytest tests/unit/'
|
||||
docker run test-changedetectionio bash -c 'cd changedetectionio;pytest tests/unit/ tests/llm/'
|
||||
|
||||
# Basic pytest tests with ancillary services
|
||||
basic-tests:
|
||||
|
||||
@@ -76,6 +76,19 @@ def construct_blueprint(datastore: ChangeDetectionStore):
|
||||
|
||||
datastore.data['settings']['application'].update(app_update)
|
||||
|
||||
# Save LLM config separately under settings.application.llm
|
||||
llm_data = form.data.get('llm') or {}
|
||||
llm_config = {
|
||||
'model': (llm_data.get('llm_model') or '').strip(),
|
||||
'api_key': (llm_data.get('llm_api_key') or '').strip(),
|
||||
'api_base': (llm_data.get('llm_api_base') or '').strip(),
|
||||
}
|
||||
# Only store if a model is set
|
||||
if llm_config['model']:
|
||||
datastore.data['settings']['application']['llm'] = llm_config
|
||||
else:
|
||||
datastore.data['settings']['application'].pop('llm', None)
|
||||
|
||||
# Handle dynamic worker count adjustment
|
||||
old_worker_count = datastore.data['settings']['requests'].get('workers', 1)
|
||||
new_worker_count = form.data['requests'].get('workers', 1)
|
||||
@@ -164,9 +177,15 @@ def construct_blueprint(datastore: ChangeDetectionStore):
|
||||
# Instantiate the form with existing settings
|
||||
plugin_forms[plugin_id] = form_class(data=settings)
|
||||
|
||||
from changedetectionio.llm.evaluator import get_llm_config as _get_llm_cfg, llm_configured_via_env
|
||||
llm_config = _get_llm_cfg(datastore) or {}
|
||||
llm_env_configured = llm_configured_via_env()
|
||||
|
||||
output = render_template("settings.html",
|
||||
active_plugins=active_plugins,
|
||||
api_key=datastore.data['settings']['application'].get('api_access_token'),
|
||||
llm_config=llm_config,
|
||||
llm_env_configured=llm_env_configured,
|
||||
python_version=python_version,
|
||||
uptime_seconds=uptime_seconds,
|
||||
available_timezones=sorted(available_timezones()),
|
||||
|
||||
@@ -33,6 +33,7 @@
|
||||
<li class="tab"><a href="#plugin-{{ tab.plugin_id }}">{{ tab.tab_label }}</a></li>
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
<li class="tab"><a href="#ai">{{ _('AI/LLM') }}</a></li>
|
||||
<li class="tab"><a href="#info">{{ _('Info') }}</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
@@ -263,7 +264,7 @@ nav
|
||||
</div>
|
||||
<div>
|
||||
{{ render_field(form.application.form.rss_template_override) }}
|
||||
{{ show_token_placeholders(extra_notification_token_placeholder_info=extra_notification_token_placeholder_info, suffix="-rss") }}
|
||||
{{ show_token_placeholders(extra_notification_token_placeholder_info=extra_notification_token_placeholder_info, suffix="-rss", settings_application=settings_application) }}
|
||||
</div>
|
||||
</div>
|
||||
<br>
|
||||
@@ -392,6 +393,7 @@ nav
|
||||
</div>
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
{% include 'settings_llm_tab.html' %}
|
||||
<div class="tab-pane-inner" id="info">
|
||||
<p><strong>{{ _('Uptime:') }}</strong> {{ uptime_seconds|format_duration }}</p>
|
||||
<p><strong>{{ _('Python version:') }}</strong> {{ python_version }}</p>
|
||||
|
||||
@@ -0,0 +1,85 @@
|
||||
{% from '_helpers.html' import render_field %}
|
||||
{#
|
||||
AI/LLM settings tab content — included from settings.html.
|
||||
Requires template context: form, llm_config, llm_env_configured
|
||||
#}
|
||||
<div class="tab-pane-inner" id="ai">
|
||||
<fieldset>
|
||||
<p>{{ _('Configure an AI/LLM provider to enable intent-based change filtering. Each watch or tag can have a plain-English intent — the AI evaluates every detected change against it and only notifies you when it matches.') }}</p>
|
||||
|
||||
{% if llm_env_configured %}
|
||||
{# Config is coming from environment variables — hide the form #}
|
||||
<div class="inline-warning" style="margin-bottom: 1em;">
|
||||
<img class="inline-warning-icon" src="{{url_for('static_content', group='images', filename='notice.svg')}}" alt="{{ _('Note') }}">
|
||||
{{ _('AI/LLM is configured via environment variables') }}
|
||||
(<code>LLM_MODEL={{ llm_config.get('model', '') }}</code>{% if llm_config.get('api_base') %}, <code>LLM_API_BASE={{ llm_config.get('api_base') }}</code>{% endif %}).
|
||||
{{ _('Remove the') }} <code>LLM_MODEL</code> {{ _('environment variable to configure via this form instead.') }}
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="pure-control-group">
|
||||
<label>{{ _('Quick preset') }}</label>
|
||||
<select id="llm-preset" onchange="applyLLMPreset(this.value)" class="pure-input-1-3">
|
||||
<option value="">— {{ _('choose a preset or type your own below') }} —</option>
|
||||
<optgroup label="OpenAI">
|
||||
<option value="gpt-4o-mini|">gpt-4o-mini (fast, affordable)</option>
|
||||
<option value="gpt-4o|">gpt-4o (most capable)</option>
|
||||
</optgroup>
|
||||
<optgroup label="Anthropic">
|
||||
<option value="claude-3-5-haiku-20251001|">claude-3-5-haiku (fast)</option>
|
||||
<option value="claude-sonnet-4-5|">claude-sonnet-4-5 (balanced)</option>
|
||||
</optgroup>
|
||||
<optgroup label="Google">
|
||||
<option value="gemini/gemini-2.0-flash|">gemini-2.0-flash</option>
|
||||
<option value="gemini/gemini-2.0-flash-lite|">gemini-2.0-flash-lite (free tier)</option>
|
||||
</optgroup>
|
||||
<optgroup label="OpenRouter (access 200+ models)">
|
||||
<option value="openrouter/google/gemma-3-12b-it:free|">gemma-3-12b (free)</option>
|
||||
<option value="openrouter/meta-llama/llama-4-scout:free|">llama-4-scout (free)</option>
|
||||
<option value="openrouter/mistralai/mistral-small-3.1-24b-instruct:free|">mistral-small (free)</option>
|
||||
</optgroup>
|
||||
<optgroup label="{{ _('Local / Self-hosted') }}">
|
||||
<option value="ollama/llama3.2|http://localhost:11434">ollama/llama3.2 (local)</option>
|
||||
<option value="ollama/mistral|http://localhost:11434">ollama/mistral (local)</option>
|
||||
</optgroup>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="pure-control-group">
|
||||
{{ render_field(form.llm.form.llm_model) }}
|
||||
<span class="pure-form-message-inline">
|
||||
{{ _('The model string encodes both provider and model — litellm routes automatically. Any') }}
|
||||
<a href="https://docs.litellm.ai/docs/providers" target="_blank">{{ _('litellm-supported model') }}</a> {{ _('works here.') }}
|
||||
</span>
|
||||
</div>
|
||||
<div class="pure-control-group">
|
||||
{{ render_field(form.llm.form.llm_api_key) }}
|
||||
</div>
|
||||
<div class="pure-control-group">
|
||||
{{ render_field(form.llm.form.llm_api_base) }}
|
||||
<span class="pure-form-message-inline">{{ _('Only needed for Ollama or custom/self-hosted endpoints. Leave blank for cloud providers.') }}</span>
|
||||
</div>
|
||||
|
||||
{% if llm_config and llm_config.get('model') %}
|
||||
<div class="pure-control-group">
|
||||
<span style="color: #4a7c59; font-weight: bold;">
|
||||
✓ {{ _('AI configured:') }} {{ llm_config.get('model') }}
|
||||
</span>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<p class="pure-form-message-inline">
|
||||
{{ _("Your API key is stored locally and sent only to your chosen provider. On each detected change, the watch's diff and extracted text are sent to the LLM — no full page HTML.") }}
|
||||
</p>
|
||||
{% endif %}{# llm_env_configured #}
|
||||
</fieldset>
|
||||
</div>
|
||||
<script>
|
||||
function applyLLMPreset(value) {
|
||||
if (!value) return;
|
||||
var parts = value.split('|');
|
||||
var modelField = document.querySelector('[name="llm-llm_model"]');
|
||||
var baseField = document.querySelector('[name="llm-llm_api_base"]');
|
||||
if (modelField) modelField.value = parts[0] || '';
|
||||
if (baseField) baseField.value = parts[1] || '';
|
||||
}
|
||||
</script>
|
||||
@@ -5,6 +5,7 @@ from loguru import logger
|
||||
|
||||
from changedetectionio.store import ChangeDetectionStore
|
||||
from changedetectionio.flask_app import login_optionally_required
|
||||
from changedetectionio.llm.evaluator import get_llm_config as _get_llm_config
|
||||
|
||||
|
||||
def construct_blueprint(datastore: ChangeDetectionStore):
|
||||
@@ -183,6 +184,7 @@ def construct_blueprint(datastore: ChangeDetectionStore):
|
||||
'form': form,
|
||||
'watch': default,
|
||||
'extra_notification_token_placeholder_info': datastore.get_unique_notification_token_placeholders_available(),
|
||||
'llm_configured': bool(_get_llm_config(datastore)),
|
||||
}
|
||||
|
||||
included_content = {}
|
||||
|
||||
@@ -2,20 +2,30 @@ from wtforms import (
|
||||
Form,
|
||||
StringField,
|
||||
SubmitField,
|
||||
TextAreaField,
|
||||
validators,
|
||||
)
|
||||
from wtforms.fields.simple import BooleanField
|
||||
from flask_babel import lazy_gettext as _l
|
||||
|
||||
from changedetectionio.processors.restock_diff.forms import processor_settings_form as restock_settings_form
|
||||
|
||||
class group_restock_settings_form(restock_settings_form):
|
||||
overrides_watch = BooleanField(_l('Activate for individual watches in this tag/group?'), default=False)
|
||||
url_match_pattern = StringField(_l('Auto-apply to watches with URLs matching'),
|
||||
render_kw={"placeholder": _l("e.g. *://example.com/* or github.com/myorg")})
|
||||
tag_colour = StringField(_l('Tag colour'), default='')
|
||||
overrides_watch = BooleanField('Activate for individual watches in this tag/group?', default=False)
|
||||
url_match_pattern = StringField('Auto-apply to watches with URLs matching',
|
||||
render_kw={"placeholder": "e.g. *://example.com/* or github.com/myorg"})
|
||||
tag_colour = StringField('Tag colour', default='')
|
||||
llm_intent = TextAreaField('AI Change Intent',
|
||||
validators=[validators.Optional(), validators.Length(max=2000)],
|
||||
render_kw={"rows": "3",
|
||||
"placeholder": "e.g. Flag price changes or new product launches across all watches in this group"})
|
||||
|
||||
llm_change_summary = TextAreaField('AI Change Summary',
|
||||
validators=[validators.Optional(), validators.Length(max=2000)],
|
||||
render_kw={"rows": "3",
|
||||
"placeholder": "e.g. List what was added or removed. Translate to English."},
|
||||
default='')
|
||||
|
||||
class SingleTag(Form):
|
||||
|
||||
name = StringField(_l('Tag name'), [validators.InputRequired()], render_kw={"placeholder": _l("Name")})
|
||||
save_button = SubmitField(_l('Save'), render_kw={"class": "pure-button pure-button-primary"})
|
||||
name = StringField('Tag name', [validators.InputRequired()], render_kw={"placeholder": "Name"})
|
||||
save_button = SubmitField('Save', render_kw={"class": "pure-button pure-button-primary"})
|
||||
|
||||
@@ -88,6 +88,11 @@
|
||||
|
||||
<div class="tab-pane-inner" id="filters-and-triggers">
|
||||
<p>{{ _('These settings are') }} <strong><i>{{ _('added') }}</i></strong> {{ _('to any existing watch configurations.') }}</p>
|
||||
|
||||
{# ── AI Intent ─────────────────────────────────────────── #}
|
||||
{% include "edit/include_llm_intent.html" %}
|
||||
{# ── end AI Intent ──────────────────────────────────────── #}
|
||||
|
||||
{% include "edit/include_subtract.html" %}
|
||||
<div class="text-filtering border-fieldset">
|
||||
<h3>{{ _('Text filtering') }}</h3>
|
||||
|
||||
@@ -65,7 +65,7 @@ html[data-darkmode="true"] .watch-tag-list.tag-{{ class_name }} {
|
||||
{% for uuid, tag in available_tags %}
|
||||
<tr id="{{ uuid }}" class="{{ loop.cycle('pure-table-odd', 'pure-table-even') }}">
|
||||
<td class="watch-controls">
|
||||
<a class="link-mute state-{{'on' if tag.notification_muted else 'off'}}" href="{{url_for('tags.mute', uuid=tag.uuid)}}"><img src="{{url_for('static_content', group='images', filename='bell-off.svg')}}" alt="{{ _('Mute notifications') }}" title="{{ _('Mute notifications') }}" class="icon icon-mute" ></a>
|
||||
<a class="link-mute state-{{'on' if tag.notification_muted else 'off'}}" href="{{url_for('tags.mute', uuid=tag.uuid)}}"><img src="{{url_for('static_content', group='images', filename='bell-off.svg')}}" alt="Mute notifications" title="Mute notifications" class="icon icon-mute" ></a>
|
||||
</td>
|
||||
<td>{{ "{:,}".format(tag_count[uuid]) if uuid in tag_count else 0 }}</td>
|
||||
<td class="title-col inline"> <a href="{{url_for('watchlist.index', tag=uuid) }}" class="watch-tag-list tag-{{ tag.title|sanitize_tag_class }}">{{ tag.title }}</a></td>
|
||||
|
||||
@@ -128,6 +128,80 @@ def construct_blueprint(datastore: ChangeDetectionStore):
|
||||
redirect=redirect
|
||||
)
|
||||
|
||||
@diff_blueprint.route("/diff/<uuid_str:uuid>/llm-summary", methods=['GET'])
|
||||
@login_optionally_required
|
||||
def diff_llm_summary(uuid):
|
||||
"""
|
||||
Generate (or return cached) an AI summary of the diff between two snapshots.
|
||||
Called via AJAX from the diff page when no cached summary exists.
|
||||
Returns JSON: {"summary": "...", "error": null} or {"summary": null, "error": "..."}
|
||||
"""
|
||||
import difflib
|
||||
from flask import jsonify
|
||||
|
||||
try:
|
||||
watch = datastore.data['watching'][uuid]
|
||||
except KeyError:
|
||||
return jsonify({'summary': None, 'error': 'Watch not found'}), 404
|
||||
|
||||
llm_cfg = datastore.data.get('settings', {}).get('application', {}).get('llm', {})
|
||||
if not llm_cfg.get('model'):
|
||||
return jsonify({'summary': None, 'error': 'LLM not configured'}), 400
|
||||
|
||||
dates = list(watch.history.keys())
|
||||
if len(dates) < 2:
|
||||
return jsonify({'summary': None, 'error': 'Not enough history'}), 400
|
||||
|
||||
from_version = request.args.get('from_version', dates[-2])
|
||||
to_version = request.args.get('to_version', dates[-1])
|
||||
|
||||
try:
|
||||
from_text = watch.get_history_snapshot(timestamp=from_version)
|
||||
to_text = watch.get_history_snapshot(timestamp=to_version)
|
||||
except Exception as e:
|
||||
return jsonify({'summary': None, 'error': f'Could not read snapshots: {e}'}), 500
|
||||
|
||||
# Plain-text unified diff for LLM consumption
|
||||
diff_lines = list(difflib.unified_diff(
|
||||
from_text.splitlines(),
|
||||
to_text.splitlines(),
|
||||
lineterm='',
|
||||
n=3,
|
||||
))
|
||||
# Skip the +++/--- header lines
|
||||
diff_text = '\n'.join(diff_lines[2:]) if len(diff_lines) > 2 else '\n'.join(diff_lines)
|
||||
|
||||
if not diff_text.strip():
|
||||
return jsonify({'summary': None, 'error': 'No differences found'})
|
||||
|
||||
from changedetectionio.llm.evaluator import (
|
||||
summarise_change, get_effective_summary_prompt, compute_summary_cache_key,
|
||||
)
|
||||
|
||||
# Check cache — skip LLM if this exact (diff, prompt) pair was already summarised
|
||||
effective_prompt = get_effective_summary_prompt(watch, datastore)
|
||||
cache_key = compute_summary_cache_key(diff_text, effective_prompt)
|
||||
cached = watch.get_last_llm_diff_summary(cache_key=cache_key)
|
||||
if cached:
|
||||
return jsonify({'summary': cached, 'error': None, 'cached': True})
|
||||
|
||||
try:
|
||||
summary = summarise_change(watch, datastore, diff=diff_text, current_snapshot=to_text)
|
||||
except Exception as e:
|
||||
logger.error(f"LLM summary generation failed for {uuid}: {e}")
|
||||
return jsonify({'summary': None, 'error': str(e)}), 500
|
||||
|
||||
if not summary:
|
||||
return jsonify({'summary': None, 'error': 'LLM returned empty summary'})
|
||||
|
||||
# Persist with cache key so subsequent requests (and next page load) are instant
|
||||
try:
|
||||
watch.save_llm_diff_summary(summary, cache_key=cache_key)
|
||||
except Exception as e:
|
||||
logger.warning(f"Could not cache llm summary for {uuid}: {e}")
|
||||
|
||||
return jsonify({'summary': summary, 'error': None, 'cached': False})
|
||||
|
||||
@diff_blueprint.route("/diff/<uuid_str:uuid>/extract", methods=['GET'])
|
||||
@login_optionally_required
|
||||
def diff_history_page_extract_GET(uuid):
|
||||
|
||||
@@ -10,10 +10,24 @@ from changedetectionio.store import ChangeDetectionStore
|
||||
from changedetectionio.auth_decorator import login_optionally_required
|
||||
from changedetectionio.time_handler import is_within_schedule
|
||||
from changedetectionio import worker_pool
|
||||
from changedetectionio.llm.evaluator import get_llm_config as _get_llm_config
|
||||
|
||||
def construct_blueprint(datastore: ChangeDetectionStore, update_q, queuedWatchMetaData):
|
||||
edit_blueprint = Blueprint('ui_edit', __name__, template_folder="../ui/templates")
|
||||
|
||||
|
||||
def _resolve_llm_intent_source(watch, datastore) -> str:
|
||||
"""
|
||||
Return the source of the effective LLM intent: 'watch', tag title, or ''.
|
||||
Used in the edit form to show an "inherited from tag: X" hint.
|
||||
"""
|
||||
if (watch.get('llm_intent') or '').strip():
|
||||
return 'watch'
|
||||
for tag_uuid in watch.get('tags', []):
|
||||
tag = datastore.data['settings']['application'].get('tags', {}).get(tag_uuid)
|
||||
if tag and (tag.get('llm_intent') or '').strip():
|
||||
return tag.get('title', 'tag')
|
||||
return ''
|
||||
|
||||
def _watch_has_tag_options_set(watch):
|
||||
"""This should be fixed better so that Tag is some proper Model, a tag is just a Watch also"""
|
||||
for tag_uuid, tag in datastore.data['settings']['application'].get('tags', {}).items():
|
||||
@@ -142,7 +156,7 @@ def construct_blueprint(datastore: ChangeDetectionStore, update_q, queuedWatchMe
|
||||
for p in datastore.extra_browsers:
|
||||
form.fetch_backend.choices.append(p)
|
||||
|
||||
form.fetch_backend.choices.append(("system", gettext('System settings default')))
|
||||
form.fetch_backend.choices.append(("system", 'System settings default'))
|
||||
|
||||
# form.browser_steps[0] can be assumed that we 'goto url' first
|
||||
|
||||
@@ -150,7 +164,7 @@ def construct_blueprint(datastore: ChangeDetectionStore, update_q, queuedWatchMe
|
||||
# @todo - Couldn't get setattr() etc dynamic addition working, so remove it instead
|
||||
del form.proxy
|
||||
else:
|
||||
form.proxy.choices = [('', gettext('Default'))]
|
||||
form.proxy.choices = [('', 'Default')]
|
||||
for p in datastore.proxy_list:
|
||||
form.proxy.choices.append(tuple((p, datastore.proxy_list[p]['label'])))
|
||||
|
||||
@@ -301,7 +315,7 @@ def construct_blueprint(datastore: ChangeDetectionStore, update_q, queuedWatchMe
|
||||
'extra_classes': ' '.join(c),
|
||||
'extra_notification_token_placeholder_info': datastore.get_unique_notification_token_placeholders_available(),
|
||||
'extra_processor_config': form.extra_tab_content(),
|
||||
'extra_title': f" - {gettext('Edit')} - {watch.label}",
|
||||
'extra_title': f" - Edit - {watch.label}",
|
||||
'form': form,
|
||||
'has_default_notification_urls': True if len(datastore.data['settings']['application']['notification_urls']) else False,
|
||||
'has_extra_headers_file': len(datastore.get_all_headers_in_textfile_for_watch(uuid=uuid)) > 0,
|
||||
@@ -326,6 +340,9 @@ def construct_blueprint(datastore: ChangeDetectionStore, update_q, queuedWatchMe
|
||||
for tag_uuid, tag in datastore.data['settings']['application']['tags'].items()
|
||||
if tag_uuid not in watch.get('tags', []) and tag.matches_url(watch.get('url', ''))
|
||||
},
|
||||
# LLM intent context
|
||||
'llm_configured': bool(_get_llm_config(datastore)),
|
||||
'llm_intent_source': _resolve_llm_intent_source(watch, datastore),
|
||||
}
|
||||
|
||||
included_content = None
|
||||
|
||||
@@ -107,7 +107,7 @@ def construct_blueprint(datastore: ChangeDetectionStore):
|
||||
current_diff_url=watch['url'],
|
||||
current_version=timestamp,
|
||||
extra_stylesheets=extra_stylesheets,
|
||||
extra_title=f" - {gettext('Diff')} - {watch.label} @ {timestamp}",
|
||||
extra_title=f" - Diff - {watch.label} @ {timestamp}",
|
||||
highlight_ignored_line_numbers=ignored_line_numbers,
|
||||
highlight_triggered_line_numbers=triggered_line_numbers,
|
||||
highlight_blocked_line_numbers=blocked_line_numbers,
|
||||
|
||||
@@ -127,6 +127,16 @@
|
||||
<div class="tip">{{ _('Pro-tip: You can enable') }} <strong>{{ _('"share access when password is enabled"') }}</strong> {{ _('from settings.') }}
|
||||
</div>
|
||||
{%- endif -%}
|
||||
{%- if llm_configured -%}
|
||||
<div id="llm-diff-summary-area"{% if not llm_diff_summary %} data-pending="1"{% endif %}>
|
||||
<span class="llm-diff-summary-label">✨ {{ _('AI Change Summary') }}</span>
|
||||
{%- if llm_diff_summary -%}
|
||||
<p class="llm-diff-summary-text">{{ llm_diff_summary }}</p>
|
||||
{%- else -%}
|
||||
<p class="llm-diff-summary-text llm-diff-summary-loading">{{ _('Generating summary…') }}</p>
|
||||
{%- endif -%}
|
||||
</div>
|
||||
{%- endif -%}
|
||||
<div id="text-diff-heading-area" style="user-select: none;">
|
||||
<div class="snapshot-age"><span>{{ from_version|format_timestamp_timeago }}</span>
|
||||
{%- if note -%}<span class="note"><strong>{{ note }}</strong></span>{%- endif -%}
|
||||
@@ -162,5 +172,32 @@
|
||||
</script>
|
||||
<script src="{{url_for('static_content', group='js', filename='diff-render.js')}}"></script>
|
||||
|
||||
{% if llm_configured %}
|
||||
<script>
|
||||
$(function () {
|
||||
var $area = $('#llm-diff-summary-area');
|
||||
if (!$area.length || !$area.data('pending')) return;
|
||||
|
||||
var fromVersion = $('#diff-from-version').val();
|
||||
var toVersion = $('#diff-to-version').val();
|
||||
var summaryUrl = "{{ url_for('ui.ui_diff.diff_llm_summary', uuid=uuid) }}";
|
||||
|
||||
$.getJSON(summaryUrl, { from_version: fromVersion, to_version: toVersion })
|
||||
.done(function (data) {
|
||||
if (data.summary) {
|
||||
$area.find('.llm-diff-summary-text')
|
||||
.removeClass('llm-diff-summary-loading')
|
||||
.text(data.summary);
|
||||
$area.removeAttr('data-pending');
|
||||
} else {
|
||||
$area.remove();
|
||||
}
|
||||
})
|
||||
.fail(function () {
|
||||
$area.remove();
|
||||
});
|
||||
});
|
||||
</script>
|
||||
{% endif %}
|
||||
|
||||
{% endblock %}
|
||||
|
||||
@@ -321,6 +321,11 @@ Math: {{ 1 + 1 }}") }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="tab-pane-inner" id="filters-and-triggers">
|
||||
|
||||
{# ── AI Intent ─────────────────────────────────────────── #}
|
||||
{% include "edit/include_llm_intent.html" %}
|
||||
{# ── end AI Intent ──────────────────────────────────────── #}
|
||||
|
||||
<span id="activate-text-preview" class="pure-button pure-button-primary button-xsmall">{{ _('Activate preview') }}</span>
|
||||
<div>
|
||||
<div id="edit-text-filter">
|
||||
@@ -374,7 +379,20 @@ Math: {{ 1 + 1 }}") }}
|
||||
const preview_text_edit_filters_url="{{url_for('ui.ui_edit.watch_get_preview_rendered', uuid=uuid)}}";
|
||||
</script>
|
||||
<br>
|
||||
{#<div id="text-preview-controls"><span id="text-preview-refresh" class="pure-button button-xsmall">Refresh</span></div>#}
|
||||
{% if llm_configured %}
|
||||
<div id="llm-preview-result" style="display:none; margin-bottom: 0.8em; padding: 0.8em 1.1em; border-radius: 4px; border-left: 4px solid #ccc; font-size: 0.9em;">
|
||||
<div style="font-size:0.75em; text-transform:uppercase; letter-spacing:0.06em; opacity:0.55; margin-bottom:0.35em;">{{ _('AI Intent preview') }}</div>
|
||||
<span class="llm-preview-verdict" style="font-weight: bold;"></span>
|
||||
<div class="llm-preview-answer" style="margin-top: 0.5em; white-space: pre-wrap; line-height: 1.5; font-style: italic;"></div>
|
||||
</div>
|
||||
<style>
|
||||
#llm-preview-result { transition: border-color 0.2s, background 0.2s; }
|
||||
#llm-preview-result[data-found="1"] { border-color: #2ecc71; background: rgba(46,204,113,0.07); }
|
||||
#llm-preview-result[data-found="1"] .llm-preview-verdict { color: #27ae60; }
|
||||
#llm-preview-result[data-found="0"] { border-color: #aaa; background: rgba(0,0,0,0.03); }
|
||||
#llm-preview-result[data-found="0"] .llm-preview-verdict { color: #888; }
|
||||
</style>
|
||||
{% endif %}
|
||||
<div class="minitabs-wrapper">
|
||||
<div class="minitabs-content">
|
||||
<div id="text-preview-inner" class="monospace-preview">
|
||||
@@ -484,6 +502,16 @@ Math: {{ 1 + 1 }}") }}
|
||||
<td>{{ _('Server type reply') }}</td>
|
||||
<td>{{ watch.get('remote_server_reply') }}</td>
|
||||
</tr>
|
||||
{% if settings_application.get('llm', {}).get('model') %}
|
||||
<tr>
|
||||
<td>{{ _('AI tokens (last check)') }}</td>
|
||||
<td>{{ "{:,}".format(watch.get('llm_last_tokens_used') or 0) }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>{{ _('AI tokens (total)') }}</td>
|
||||
<td>{{ "{:,}".format(watch.get('llm_tokens_used_cumulative') or 0) }}</td>
|
||||
</tr>
|
||||
{% endif %}
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
|
||||
@@ -245,10 +245,10 @@ html[data-darkmode="true"] .watch-tag-list.tag-{{ class_name }} {
|
||||
<td class="inline checkbox-uuid" ><div><input name="uuids" type="checkbox" value="{{ watch.uuid}} " > <span class="counter-i">{{ loop.index+pagination.skip }}</span></div></td>
|
||||
<td class="inline watch-controls">
|
||||
<div>
|
||||
<a class="ajax-op state-off pause-toggle" data-op="pause" href="{{url_for('watchlist.index', op='pause', uuid=watch.uuid, tag=active_tag_uuid)}}"><img src="{{url_for('static_content', group='images', filename='pause.svg')}}" alt="{{ _('Pause checks') }}" title="{{ _('Pause checks') }}" class="icon icon-pause" ></a>
|
||||
<a class="ajax-op state-on pause-toggle" data-op="pause" style="display: none" href="{{url_for('watchlist.index', op='pause', uuid=watch.uuid, tag=active_tag_uuid)}}"><img src="{{url_for('static_content', group='images', filename='play.svg')}}" alt="{{ _('UnPause checks') }}" title="{{ _('UnPause checks') }}" class="icon icon-unpause" ></a>
|
||||
<a class="ajax-op state-off mute-toggle" data-op="mute" href="{{url_for('watchlist.index', op='mute', uuid=watch.uuid, tag=active_tag_uuid)}}"><img src="{{url_for('static_content', group='images', filename='bell-off.svg')}}" alt="{{ _('Mute notification') }}" title="{{ _('Mute notification') }}" class="icon icon-mute" ></a>
|
||||
<a class="ajax-op state-on mute-toggle" data-op="mute" style="display: none" href="{{url_for('watchlist.index', op='mute', uuid=watch.uuid, tag=active_tag_uuid)}}"><img src="{{url_for('static_content', group='images', filename='bell-off.svg')}}" alt="{{ _('UnMute notification') }}" title="{{ _('UnMute notification') }}" class="icon icon-mute" ></a>
|
||||
<a class="ajax-op state-off pause-toggle" data-op="pause" href="{{url_for('watchlist.index', op='pause', uuid=watch.uuid, tag=active_tag_uuid)}}"><img src="{{url_for('static_content', group='images', filename='pause.svg')}}" alt="Pause checks" title="Pause checks" class="icon icon-pause" ></a>
|
||||
<a class="ajax-op state-on pause-toggle" data-op="pause" style="display: none" href="{{url_for('watchlist.index', op='pause', uuid=watch.uuid, tag=active_tag_uuid)}}"><img src="{{url_for('static_content', group='images', filename='play.svg')}}" alt="UnPause checks" title="UnPause checks" class="icon icon-unpause" ></a>
|
||||
<a class="ajax-op state-off mute-toggle" data-op="mute" href="{{url_for('watchlist.index', op='mute', uuid=watch.uuid, tag=active_tag_uuid)}}"><img src="{{url_for('static_content', group='images', filename='bell-off.svg')}}" alt="Mute notification" title="Mute notification" class="icon icon-mute" ></a>
|
||||
<a class="ajax-op state-on mute-toggle" data-op="mute" style="display: none" href="{{url_for('watchlist.index', op='mute', uuid=watch.uuid, tag=active_tag_uuid)}}"><img src="{{url_for('static_content', group='images', filename='bell-off.svg')}}" alt="UnMute notification" title="UnMute notification" class="icon icon-mute" ></a>
|
||||
</div>
|
||||
</td>
|
||||
|
||||
@@ -292,7 +292,7 @@ html[data-darkmode="true"] .watch-tag-list.tag-{{ class_name }} {
|
||||
{%- endfor -%}
|
||||
</div>
|
||||
<div class="status-icons">
|
||||
<a class="link-spread" href="{{url_for('ui.form_share_put_watch', uuid=watch.uuid)}}"><img src="{{url_for('static_content', group='images', filename='spread.svg')}}" class="status-icon icon icon-spread" title="{{ _('Create a link to share watch config with others') }}" ></a>
|
||||
<a class="link-spread" href="{{url_for('ui.form_share_put_watch', uuid=watch.uuid)}}"><img src="{{url_for('static_content', group='images', filename='spread.svg')}}" class="status-icon icon icon-spread" title="Create a link to share watch config with others" ></a>
|
||||
{%- set effective_fetcher = watch.get_fetch_backend if watch.get_fetch_backend != "system" else system_default_fetcher -%}
|
||||
{%- if effective_fetcher and ("html_webdriver" in effective_fetcher or "html_" in effective_fetcher or "extra_browser_" in effective_fetcher) -%}
|
||||
{{ effective_fetcher|fetcher_status_icons }}
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
from json_logic.builtins import BUILTINS
|
||||
from flask_babel import lazy_gettext as _l
|
||||
|
||||
from .exceptions import EmptyConditionRuleRowNotUsable
|
||||
from .pluggy_interface import plugin_manager # Import the pluggy plugin manager
|
||||
@@ -7,19 +6,19 @@ from . import default_plugin
|
||||
from loguru import logger
|
||||
# List of all supported JSON Logic operators
|
||||
operator_choices = [
|
||||
(None, _l("Choose one - Operator")),
|
||||
(">", _l("Greater Than")),
|
||||
("<", _l("Less Than")),
|
||||
(">=", _l("Greater Than or Equal To")),
|
||||
("<=", _l("Less Than or Equal To")),
|
||||
("==", _l("Equals")),
|
||||
("!=", _l("Not Equals")),
|
||||
("in", _l("Contains")),
|
||||
(None, "Choose one - Operator"),
|
||||
(">", "Greater Than"),
|
||||
("<", "Less Than"),
|
||||
(">=", "Greater Than or Equal To"),
|
||||
("<=", "Less Than or Equal To"),
|
||||
("==", "Equals"),
|
||||
("!=", "Not Equals"),
|
||||
("in", "Contains"),
|
||||
]
|
||||
|
||||
# Fields available in the rules
|
||||
field_choices = [
|
||||
(None, _l("Choose one - Field")),
|
||||
(None, "Choose one - Field"),
|
||||
]
|
||||
|
||||
# The data we will feed the JSON Rules to see if it passes the test/conditions or not
|
||||
|
||||
@@ -3,7 +3,6 @@ import re
|
||||
import pluggy
|
||||
from price_parser import Price
|
||||
from loguru import logger
|
||||
from flask_babel import lazy_gettext as _l
|
||||
|
||||
hookimpl = pluggy.HookimplMarker("changedetectionio_conditions")
|
||||
|
||||
@@ -48,22 +47,22 @@ def register_operators():
|
||||
@hookimpl
|
||||
def register_operator_choices():
|
||||
return [
|
||||
("!in", _l("Does NOT Contain")),
|
||||
("starts_with", _l("Text Starts With")),
|
||||
("ends_with", _l("Text Ends With")),
|
||||
("length_min", _l("Length minimum")),
|
||||
("length_max", _l("Length maximum")),
|
||||
("contains_regex", _l("Text Matches Regex")),
|
||||
("!contains_regex", _l("Text Does NOT Match Regex")),
|
||||
("!in", "Does NOT Contain"),
|
||||
("starts_with", "Text Starts With"),
|
||||
("ends_with", "Text Ends With"),
|
||||
("length_min", "Length minimum"),
|
||||
("length_max", "Length maximum"),
|
||||
("contains_regex", "Text Matches Regex"),
|
||||
("!contains_regex", "Text Does NOT Match Regex"),
|
||||
]
|
||||
|
||||
@hookimpl
|
||||
def register_field_choices():
|
||||
return [
|
||||
("extracted_number", _l("Extracted number after 'Filters & Triggers'")),
|
||||
("extracted_number", "Extracted number after 'Filters & Triggers'"),
|
||||
# ("meta_description", "Meta Description"),
|
||||
# ("meta_keywords", "Meta Keywords"),
|
||||
("page_filtered_text", _l("Page text after 'Filters & Triggers'")),
|
||||
("page_filtered_text", "Page text after 'Filters & Triggers'"),
|
||||
#("page_title", "Page <title>"), # actual page title <title>
|
||||
]
|
||||
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
# Condition Rule Form (for each rule row)
|
||||
from wtforms import Form, SelectField, StringField, validators
|
||||
from wtforms import validators
|
||||
from flask_babel import lazy_gettext as _l
|
||||
|
||||
class ConditionFormRow(Form):
|
||||
|
||||
@@ -9,18 +8,18 @@ class ConditionFormRow(Form):
|
||||
from changedetectionio.conditions import plugin_manager
|
||||
from changedetectionio.conditions import operator_choices, field_choices
|
||||
field = SelectField(
|
||||
_l("Field"),
|
||||
"Field",
|
||||
choices=field_choices,
|
||||
validators=[validators.Optional()]
|
||||
)
|
||||
|
||||
operator = SelectField(
|
||||
_l("Operator"),
|
||||
"Operator",
|
||||
choices=operator_choices,
|
||||
validators=[validators.Optional()]
|
||||
)
|
||||
|
||||
value = StringField(_l("Value"), validators=[validators.Optional()], render_kw={"placeholder": _l("A value")})
|
||||
value = StringField("Value", validators=[validators.Optional()], render_kw={"placeholder": "A value"})
|
||||
|
||||
def validate(self, extra_validators=None):
|
||||
# First, run the default validators
|
||||
@@ -31,15 +30,15 @@ class ConditionFormRow(Form):
|
||||
# If any of the operator/field/value is set, then they must be all set
|
||||
if any(value not in ("", False, "None", None) for value in [self.operator.data, self.field.data, self.value.data]):
|
||||
if not self.operator.data or self.operator.data == 'None':
|
||||
self.operator.errors.append(_l("Operator is required."))
|
||||
self.operator.errors.append("Operator is required.")
|
||||
return False
|
||||
|
||||
if not self.field.data or self.field.data == 'None':
|
||||
self.field.errors.append(_l("Field is required."))
|
||||
self.field.errors.append("Field is required.")
|
||||
return False
|
||||
|
||||
if not self.value.data:
|
||||
self.value.errors.append(_l("Value is required."))
|
||||
self.value.errors.append("Value is required.")
|
||||
return False
|
||||
|
||||
return True # Only return True if all conditions pass
|
||||
@@ -4,7 +4,6 @@ Provides metrics for measuring text similarity between snapshots.
|
||||
"""
|
||||
import pluggy
|
||||
from loguru import logger
|
||||
from flask_babel import gettext as _, lazy_gettext as _l
|
||||
|
||||
LEVENSHTEIN_MAX_LEN_FOR_EDIT_STATS=100000
|
||||
|
||||
@@ -54,8 +53,8 @@ def register_operator_choices():
|
||||
@conditions_hookimpl
|
||||
def register_field_choices():
|
||||
return [
|
||||
("levenshtein_ratio", _l("Levenshtein - Text similarity ratio")),
|
||||
("levenshtein_distance", _l("Levenshtein - Text change distance")),
|
||||
("levenshtein_ratio", "Levenshtein - Text similarity ratio"),
|
||||
("levenshtein_distance", "Levenshtein - Text change distance"),
|
||||
]
|
||||
|
||||
@conditions_hookimpl
|
||||
@@ -78,7 +77,7 @@ def ui_edit_stats_extras(watch):
|
||||
"""Add Levenshtein stats to the UI using the global plugin system"""
|
||||
"""Generate the HTML for Levenshtein stats - shared by both plugin systems"""
|
||||
if len(watch.history.keys()) < 2:
|
||||
return f"<p>{_('Not enough history to calculate Levenshtein metrics')}</p>"
|
||||
return "<p>Not enough history to calculate Levenshtein metrics</p>"
|
||||
|
||||
|
||||
# Protection against the algorithm getting stuck on huge documents
|
||||
@@ -88,37 +87,37 @@ def ui_edit_stats_extras(watch):
|
||||
for idx in (-1, -2)
|
||||
if len(k) >= abs(idx)
|
||||
):
|
||||
return f"<p>{_('Snapshot too large for edit statistics, skipping.')}</p>"
|
||||
return "<p>Snapshot too large for edit statistics, skipping.</p>"
|
||||
|
||||
try:
|
||||
lev_data = levenshtein_ratio_recent_history(watch)
|
||||
if not lev_data or not isinstance(lev_data, dict):
|
||||
return f"<p>{_('Unable to calculate Levenshtein metrics')}</p>"
|
||||
|
||||
return "<p>Unable to calculate Levenshtein metrics</p>"
|
||||
|
||||
html = f"""
|
||||
<div class="levenshtein-stats">
|
||||
<h4>{_('Levenshtein Text Similarity Details')}</h4>
|
||||
<h4>Levenshtein Text Similarity Details</h4>
|
||||
<table class="pure-table">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>{_('Raw distance (edits needed)')}</td>
|
||||
<td>Raw distance (edits needed)</td>
|
||||
<td>{lev_data['distance']}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>{_('Similarity ratio')}</td>
|
||||
<td>Similarity ratio</td>
|
||||
<td>{lev_data['ratio']:.4f}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>{_('Percent similar')}</td>
|
||||
<td>Percent similar</td>
|
||||
<td>{lev_data['percent_similar']}%</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<p style="font-size: 80%;">{_('Levenshtein metrics compare the last two snapshots, measuring how many character edits are needed to transform one into the other.')}</p>
|
||||
<p style="font-size: 80%;">Levenshtein metrics compare the last two snapshots, measuring how many character edits are needed to transform one into the other.</p>
|
||||
</div>
|
||||
"""
|
||||
return html
|
||||
except Exception as e:
|
||||
logger.error(f"Error generating Levenshtein UI extras: {str(e)}")
|
||||
return f"<p>{_('Error calculating Levenshtein metrics')}</p>"
|
||||
return "<p>Error calculating Levenshtein metrics</p>"
|
||||
|
||||
|
||||
@@ -4,7 +4,6 @@ Provides word count metrics for snapshot content.
|
||||
"""
|
||||
import pluggy
|
||||
from loguru import logger
|
||||
from flask_babel import gettext as _, lazy_gettext as _l
|
||||
|
||||
# Support both plugin systems
|
||||
conditions_hookimpl = pluggy.HookimplMarker("changedetectionio_conditions")
|
||||
@@ -41,7 +40,7 @@ def register_operator_choices():
|
||||
def register_field_choices():
|
||||
# Add a field that will be available in conditions
|
||||
return [
|
||||
("word_count", _l("Word count of content")),
|
||||
("word_count", "Word count of content"),
|
||||
]
|
||||
|
||||
@conditions_hookimpl
|
||||
@@ -62,16 +61,16 @@ def _generate_stats_html(watch):
|
||||
|
||||
html = f"""
|
||||
<div class="word-count-stats">
|
||||
<h4>{_('Content Analysis')}</h4>
|
||||
<h4>Content Analysis</h4>
|
||||
<table class="pure-table">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>{_('Word count (latest snapshot)')}</td>
|
||||
<td>Word count (latest snapshot)</td>
|
||||
<td>{word_count}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<p style="font-size: 80%;">{_('Word count is a simple measure of content length, calculated by splitting text on whitespace.')}</p>
|
||||
<p style="font-size: 80%;">Word count is a simple measure of content length, calculated by splitting text on whitespace.</p>
|
||||
</div>
|
||||
"""
|
||||
return html
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
from flask_babel import lazy_gettext as _l
|
||||
from loguru import logger
|
||||
from urllib.parse import urljoin, urlparse
|
||||
import hashlib
|
||||
@@ -14,7 +13,7 @@ from changedetectionio.validate_url import is_private_hostname
|
||||
|
||||
# "html_requests" is listed as the default fetcher in store.py!
|
||||
class fetcher(Fetcher):
|
||||
fetcher_description = _l("Basic fast Plaintext/HTTP Client")
|
||||
fetcher_description = "Basic fast Plaintext/HTTP Client"
|
||||
|
||||
def __init__(self, proxy_override=None, custom_browser_connection_url=None, **kwargs):
|
||||
super().__init__(**kwargs)
|
||||
|
||||
@@ -771,16 +771,16 @@ class SingleBrowserStep(Form):
|
||||
operation = SelectField(_l('Operation'), [validators.Optional()], choices=browser_step_ui_config.keys())
|
||||
|
||||
# maybe better to set some <script>var..
|
||||
selector = StringField(_l('Selector'), [validators.Optional()], render_kw={"placeholder": _l("CSS or xPath selector")})
|
||||
optional_value = StringField(_l('value'), [validators.Optional()], render_kw={"placeholder": _l("Value")})
|
||||
selector = StringField(_l('Selector'), [validators.Optional()], render_kw={"placeholder": "CSS or xPath selector"})
|
||||
optional_value = StringField(_l('value'), [validators.Optional()], render_kw={"placeholder": "Value"})
|
||||
# @todo move to JS? ajax fetch new field?
|
||||
# remove_button = SubmitField(_l('-'), render_kw={"type": "button", "class": "pure-button pure-button-primary", 'title': 'Remove'})
|
||||
# add_button = SubmitField(_l('+'), render_kw={"type": "button", "class": "pure-button pure-button-primary", 'title': 'Add new step after'})
|
||||
|
||||
class processor_text_json_diff_form(commonSettingsForm):
|
||||
|
||||
url = StringField(_l('Web Page URL'), validators=[validateURL()])
|
||||
tags = StringTagUUID(_l('Group Tag'), [validators.Optional()], default='')
|
||||
url = StringField('Web Page URL', validators=[validateURL()])
|
||||
tags = StringTagUUID('Group Tag', [validators.Optional()], default='')
|
||||
|
||||
time_between_check = EnhancedFormField(
|
||||
TimeBetweenCheckForm,
|
||||
@@ -794,6 +794,13 @@ class processor_text_json_diff_form(commonSettingsForm):
|
||||
|
||||
time_between_check_use_default = BooleanField(_l('Use global settings for time between check and scheduler.'), default=False)
|
||||
|
||||
llm_intent = TextAreaField(_l('AI Change Intent'), validators=[validators.Optional(), validators.Length(max=2000)],
|
||||
render_kw={"rows": "3", "placeholder": "e.g. Alert me when the price drops below $300"})
|
||||
|
||||
llm_change_summary = TextAreaField(_l('AI Change Summary'), validators=[validators.Optional(), validators.Length(max=2000)],
|
||||
render_kw={"rows": "3", "placeholder": "e.g. List what was added or removed. Translate to English."},
|
||||
default='')
|
||||
|
||||
include_filters = StringListField(_l('CSS/JSONPath/JQ/XPath Filters'), [ValidateCSSJSONXPATHInput()], default='')
|
||||
|
||||
subtractive_selectors = StringListField(_l('Remove elements'), [ValidateCSSJSONXPATHInput(allow_json=False)])
|
||||
@@ -918,7 +925,7 @@ class processor_text_json_diff_form(commonSettingsForm):
|
||||
|
||||
class SingleExtraProxy(Form):
|
||||
# maybe better to set some <script>var..
|
||||
proxy_name = StringField(_l('Name'), [validators.Optional()], render_kw={"placeholder": _l("Name")})
|
||||
proxy_name = StringField(_l('Name'), [validators.Optional()], render_kw={"placeholder": "Name"})
|
||||
proxy_url = StringField(_l('Proxy URL'), [
|
||||
validators.Optional(),
|
||||
ValidateStartsWithRegex(
|
||||
@@ -930,7 +937,7 @@ class SingleExtraProxy(Form):
|
||||
], render_kw={"placeholder": "socks5:// or regular proxy http://user:pass@...:3128", "size":50})
|
||||
|
||||
class SingleExtraBrowser(Form):
|
||||
browser_name = StringField(_l('Name'), [validators.Optional()], render_kw={"placeholder": _l("Name")})
|
||||
browser_name = StringField(_l('Name'), [validators.Optional()], render_kw={"placeholder": "Name"})
|
||||
browser_connection_url = StringField(_l('Browser connection URL'), [
|
||||
validators.Optional(),
|
||||
ValidateStartsWithRegex(
|
||||
@@ -999,7 +1006,7 @@ class globalSettingsApplicationForm(commonSettingsForm):
|
||||
|
||||
# Screenshot comparison settings
|
||||
min_change_percentage = FloatField(
|
||||
_l('Screenshot: Minimum Change Percentage'),
|
||||
'Screenshot: Minimum Change Percentage',
|
||||
validators=[
|
||||
validators.Optional(),
|
||||
validators.NumberRange(min=0.0, max=100.0, message=_l('Must be between 0 and 100'))
|
||||
@@ -1039,6 +1046,61 @@ class globalSettingsApplicationForm(commonSettingsForm):
|
||||
ui = FormField(globalSettingsApplicationUIForm)
|
||||
|
||||
|
||||
class globalSettingsLLMForm(Form):
|
||||
"""
|
||||
LLM / AI provider settings — stored under datastore['settings']['application']['llm'].
|
||||
|
||||
Uses litellm under the hood, so the model string encodes both the provider and model.
|
||||
No separate provider dropdown needed — litellm routes automatically:
|
||||
gpt-4o-mini → OpenAI
|
||||
claude-3-5-haiku-20251001 → Anthropic
|
||||
ollama/llama3.2 → Ollama (local)
|
||||
openrouter/google/gemma-3-12b-it:free → OpenRouter (free tier)
|
||||
gemini/gemini-2.0-flash → Google Gemini
|
||||
azure/gpt-4o → Azure OpenAI
|
||||
"""
|
||||
llm_model = StringField(
|
||||
_l('Model'),
|
||||
validators=[validators.Optional()],
|
||||
render_kw={"placeholder": "gpt-4o-mini", "style": "width: 24em;"},
|
||||
)
|
||||
llm_api_key = StringField(
|
||||
_l('API Key'),
|
||||
validators=[validators.Optional()],
|
||||
render_kw={
|
||||
"placeholder": _l('Leave blank to use LITELLM_API_KEY env var'),
|
||||
"autocomplete": "off",
|
||||
"style": "width: 24em;",
|
||||
},
|
||||
)
|
||||
llm_api_base = StringField(
|
||||
_l('API Base URL'),
|
||||
validators=[validators.Optional()],
|
||||
render_kw={
|
||||
"placeholder": "http://localhost:11434 (Ollama / custom endpoints only)",
|
||||
"style": "width: 24em;",
|
||||
},
|
||||
)
|
||||
llm_max_tokens_per_check = IntegerField(
|
||||
_l('Max tokens per check'),
|
||||
validators=[validators.Optional(), validators.NumberRange(min=0)],
|
||||
default=0,
|
||||
render_kw={
|
||||
"placeholder": "0 = unlimited",
|
||||
"style": "width: 8em;",
|
||||
},
|
||||
)
|
||||
llm_max_tokens_cumulative = IntegerField(
|
||||
_l('Max cumulative tokens (per watch)'),
|
||||
validators=[validators.Optional(), validators.NumberRange(min=0)],
|
||||
default=0,
|
||||
render_kw={
|
||||
"placeholder": "0 = unlimited",
|
||||
"style": "width: 8em;",
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
class globalSettingsForm(Form):
|
||||
# Define these as FormFields/"sub forms", this way it matches the JSON storage
|
||||
# datastore.data['settings']['application']..
|
||||
@@ -1051,6 +1113,7 @@ class globalSettingsForm(Form):
|
||||
|
||||
requests = FormField(globalSettingsRequestForm)
|
||||
application = FormField(globalSettingsApplicationForm)
|
||||
llm = FormField(globalSettingsLLMForm)
|
||||
save_button = SubmitField(_l('Save'), render_kw={"class": "pure-button pure-button-primary"})
|
||||
|
||||
|
||||
|
||||
1
changedetectionio/llm/__init__.py
Normal file
1
changedetectionio/llm/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
# LLM intent-based change evaluation
|
||||
52
changedetectionio/llm/bm25_trim.py
Normal file
52
changedetectionio/llm/bm25_trim.py
Normal file
@@ -0,0 +1,52 @@
|
||||
"""
|
||||
BM25-based relevance trimming for large snapshot text.
|
||||
|
||||
When a snapshot is large and no CSS pre-filter has narrowed it down,
|
||||
we use BM25 to select the lines most relevant to the user's intent
|
||||
before sending to the LLM. This keeps the context focused without
|
||||
an arbitrary char truncation.
|
||||
|
||||
Pure functions — no side effects, fully testable.
|
||||
"""
|
||||
|
||||
MAX_CONTEXT_CHARS = 15_000
|
||||
|
||||
|
||||
def trim_to_relevant(text: str, query: str, max_chars: int = MAX_CONTEXT_CHARS) -> str:
|
||||
"""
|
||||
Return the lines from `text` most relevant to `query` up to `max_chars`.
|
||||
If text fits within budget, return it unchanged.
|
||||
Falls back to head-truncation if rank_bm25 is unavailable.
|
||||
"""
|
||||
if not text or not query:
|
||||
return text or ''
|
||||
|
||||
if len(text) <= max_chars:
|
||||
return text
|
||||
|
||||
lines = [l for l in text.splitlines() if l.strip()]
|
||||
if not lines:
|
||||
return text[:max_chars]
|
||||
|
||||
try:
|
||||
from rank_bm25 import BM25Okapi
|
||||
except ImportError:
|
||||
# rank-bm25 not installed — fall back to simple head truncation
|
||||
return text[:max_chars]
|
||||
|
||||
tokenized = [line.lower().split() for line in lines]
|
||||
bm25 = BM25Okapi(tokenized)
|
||||
scores = bm25.get_scores(query.lower().split())
|
||||
|
||||
ranked = sorted(enumerate(zip(scores, lines)), key=lambda x: x[1][0], reverse=True)
|
||||
|
||||
selected_indices, total = [], 0
|
||||
for idx, (_score, line) in ranked:
|
||||
if total + len(line) + 1 > max_chars:
|
||||
break
|
||||
selected_indices.append(idx)
|
||||
total += len(line) + 1
|
||||
|
||||
# Re-order selected lines to preserve original document order
|
||||
ordered = [lines[i] for i in sorted(selected_indices)]
|
||||
return '\n'.join(ordered)
|
||||
47
changedetectionio/llm/client.py
Normal file
47
changedetectionio/llm/client.py
Normal file
@@ -0,0 +1,47 @@
|
||||
"""
|
||||
Thin wrapper around litellm.completion.
|
||||
Keeps litellm import isolated so the rest of the codebase doesn't depend on it directly,
|
||||
and makes the call easy to mock in tests.
|
||||
"""
|
||||
|
||||
from loguru import logger
|
||||
|
||||
# Output token cap for all LLM calls — our JSON response is always <50 tokens,
|
||||
# so 200 is a generous hard cap that prevents runaway per-call cost.
|
||||
_MAX_COMPLETION_TOKENS = 200
|
||||
|
||||
|
||||
def completion(model: str, messages: list, api_key: str = None,
|
||||
api_base: str = None, timeout: int = 30,
|
||||
max_tokens: int = None) -> tuple[str, int]:
|
||||
"""
|
||||
Call the LLM and return (response_text, total_tokens_used).
|
||||
total_tokens_used is 0 if the provider doesn't return usage data.
|
||||
Raises on network/auth errors — callers handle gracefully.
|
||||
"""
|
||||
try:
|
||||
import litellm
|
||||
except ImportError:
|
||||
raise RuntimeError("litellm is not installed. Add it to requirements.txt.")
|
||||
|
||||
kwargs = {
|
||||
'model': model,
|
||||
'messages': messages,
|
||||
'timeout': timeout,
|
||||
'temperature': 0,
|
||||
'max_tokens': max_tokens if max_tokens is not None else _MAX_COMPLETION_TOKENS,
|
||||
}
|
||||
if api_key:
|
||||
kwargs['api_key'] = api_key
|
||||
if api_base:
|
||||
kwargs['api_base'] = api_base
|
||||
|
||||
try:
|
||||
response = litellm.completion(**kwargs)
|
||||
text = response.choices[0].message.content
|
||||
usage = getattr(response, 'usage', None)
|
||||
total_tokens = int(getattr(usage, 'total_tokens', 0) or 0) if usage else 0
|
||||
return text, total_tokens
|
||||
except Exception as e:
|
||||
logger.warning(f"LLM call failed: {e}")
|
||||
raise
|
||||
385
changedetectionio/llm/evaluator.py
Normal file
385
changedetectionio/llm/evaluator.py
Normal file
@@ -0,0 +1,385 @@
|
||||
"""
|
||||
LLM evaluation orchestration.
|
||||
|
||||
Two public entry points:
|
||||
- run_setup(watch, datastore) — one-time: decide if pre-filter needed
|
||||
- evaluate_change(watch, datastore, diff, current_snapshot) — per-change evaluation
|
||||
|
||||
Intent resolution: watch.llm_intent → first tag with llm_intent → None (no evaluation)
|
||||
Cache: each (intent, diff) pair is evaluated exactly once, result stored in watch.
|
||||
|
||||
Environment variable overrides (take priority over datastore settings):
|
||||
LLM_MODEL — model string (e.g. "gpt-4o-mini", "ollama/llama3.2")
|
||||
LLM_API_KEY — API key for cloud providers
|
||||
LLM_API_BASE — base URL for local/custom endpoints (e.g. http://localhost:11434)
|
||||
"""
|
||||
|
||||
import hashlib
|
||||
import os
|
||||
from loguru import logger
|
||||
|
||||
from . import client as llm_client
|
||||
from .prompt_builder import (
|
||||
build_change_summary_prompt, build_change_summary_system_prompt,
|
||||
build_eval_prompt, build_eval_system_prompt,
|
||||
build_preview_prompt, build_preview_system_prompt,
|
||||
build_setup_prompt, build_setup_system_prompt,
|
||||
)
|
||||
from .response_parser import parse_eval_response, parse_preview_response, parse_setup_response
|
||||
|
||||
# AI Change Summary can produce longer output than eval responses
|
||||
_MAX_SUMMARY_TOKENS = 500
|
||||
|
||||
# Default prompt used when the user hasn't configured llm_change_summary
|
||||
DEFAULT_CHANGE_SUMMARY_PROMPT = "Briefly describe in plain English what changed — what was added, removed, or modified."
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Intent resolution
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def resolve_llm_field(watch, datastore, field: str) -> tuple[str, str]:
|
||||
"""
|
||||
Generic cascade resolver for any LLM per-watch field.
|
||||
Returns (value, source) where source is 'watch' or tag title.
|
||||
Returns ('', '') if not set anywhere.
|
||||
"""
|
||||
value = (watch.get(field) or '').strip()
|
||||
if value:
|
||||
return value, 'watch'
|
||||
|
||||
for tag_uuid in watch.get('tags', []):
|
||||
tag = datastore.data['settings']['application'].get('tags', {}).get(tag_uuid)
|
||||
if tag:
|
||||
tag_value = (tag.get(field) or '').strip()
|
||||
if tag_value:
|
||||
return tag_value, tag.get('title', 'tag')
|
||||
|
||||
return '', ''
|
||||
|
||||
|
||||
def resolve_intent(watch, datastore) -> tuple[str, str]:
|
||||
"""
|
||||
Return (intent, source) where source is 'watch' or tag title.
|
||||
Returns ('', '') if no intent is configured anywhere.
|
||||
"""
|
||||
intent = (watch.get('llm_intent') or '').strip()
|
||||
if intent:
|
||||
return intent, 'watch'
|
||||
|
||||
for tag_uuid in watch.get('tags', []):
|
||||
tag = datastore.data['settings']['application'].get('tags', {}).get(tag_uuid)
|
||||
if tag:
|
||||
tag_intent = (tag.get('llm_intent') or '').strip()
|
||||
if tag_intent:
|
||||
return tag_intent, tag.get('title', 'tag')
|
||||
|
||||
return '', ''
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# LLM config helper
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def get_llm_config(datastore) -> dict | None:
|
||||
"""
|
||||
Return LLM config dict or None if not configured.
|
||||
|
||||
Resolution order (first non-empty model wins):
|
||||
1. Environment variables: LLM_MODEL, LLM_API_KEY, LLM_API_BASE
|
||||
2. Datastore settings (set via UI)
|
||||
"""
|
||||
# 1. Environment variable override
|
||||
env_model = os.getenv('LLM_MODEL', '').strip()
|
||||
if env_model:
|
||||
return {
|
||||
'model': env_model,
|
||||
'api_key': os.getenv('LLM_API_KEY', '').strip(),
|
||||
'api_base': os.getenv('LLM_API_BASE', '').strip(),
|
||||
}
|
||||
|
||||
# 2. Datastore settings
|
||||
cfg = datastore.data['settings']['application'].get('llm') or {}
|
||||
if not cfg.get('model'):
|
||||
return None
|
||||
return cfg
|
||||
|
||||
|
||||
def llm_configured_via_env() -> bool:
|
||||
"""True when LLM config comes from environment variables, not the UI."""
|
||||
return bool(os.getenv('LLM_MODEL', '').strip())
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# One-time setup: derive pre-filter
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def _check_token_budget(watch, cfg, tokens_this_call: int = 0) -> bool:
|
||||
"""
|
||||
Check token budget limits. Returns True if within budget, False if exceeded.
|
||||
Also accumulates tokens_this_call into watch['llm_tokens_used_cumulative'].
|
||||
"""
|
||||
if tokens_this_call > 0:
|
||||
current = watch.get('llm_tokens_used_cumulative') or 0
|
||||
watch['llm_tokens_used_cumulative'] = current + tokens_this_call
|
||||
|
||||
max_per_check = int(cfg.get('max_tokens_per_check') or 0)
|
||||
max_cumulative = int(cfg.get('max_tokens_cumulative') or 0)
|
||||
|
||||
if max_per_check and tokens_this_call > max_per_check:
|
||||
logger.warning(
|
||||
f"LLM token budget exceeded for {watch.get('uuid')}: "
|
||||
f"{tokens_this_call} tokens > per-check limit {max_per_check}"
|
||||
)
|
||||
return False
|
||||
|
||||
if max_cumulative:
|
||||
total = watch.get('llm_tokens_used_cumulative') or 0
|
||||
if total > max_cumulative:
|
||||
logger.warning(
|
||||
f"LLM cumulative token budget exceeded for {watch.get('uuid')}: "
|
||||
f"{total} tokens > limit {max_cumulative}"
|
||||
)
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
|
||||
def run_setup(watch, datastore, snapshot_text: str) -> None:
|
||||
"""
|
||||
Ask the LLM whether a CSS pre-filter would improve precision for this intent.
|
||||
Stores result in watch['llm_prefilter'] (str selector or None).
|
||||
Called once when intent is first set, and again if pre-filter returns zero matches.
|
||||
"""
|
||||
cfg = get_llm_config(datastore)
|
||||
if not cfg:
|
||||
return
|
||||
|
||||
intent, _ = resolve_intent(watch, datastore)
|
||||
if not intent:
|
||||
return
|
||||
|
||||
url = watch.get('url', '')
|
||||
system_prompt = build_setup_system_prompt()
|
||||
user_prompt = build_setup_prompt(intent, snapshot_text, url=url)
|
||||
|
||||
try:
|
||||
raw, tokens = llm_client.completion(
|
||||
model=cfg['model'],
|
||||
messages=[
|
||||
{'role': 'system', 'content': system_prompt},
|
||||
{'role': 'user', 'content': user_prompt},
|
||||
],
|
||||
api_key=cfg.get('api_key'),
|
||||
api_base=cfg.get('api_base'),
|
||||
)
|
||||
_check_token_budget(watch, cfg, tokens)
|
||||
result = parse_setup_response(raw)
|
||||
watch['llm_prefilter'] = result['selector']
|
||||
logger.debug(f"LLM setup for {watch.get('uuid')}: prefilter={result['selector']} reason={result['reason']}")
|
||||
except Exception as e:
|
||||
logger.warning(f"LLM setup call failed for {watch.get('uuid')}: {e}")
|
||||
watch['llm_prefilter'] = None
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# AI Change Summary — human-readable description of what changed
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def get_effective_summary_prompt(watch, datastore) -> str:
|
||||
"""Return the prompt that summarise_change will use — custom or the default fallback."""
|
||||
prompt, _ = resolve_llm_field(watch, datastore, 'llm_change_summary')
|
||||
return prompt or DEFAULT_CHANGE_SUMMARY_PROMPT
|
||||
|
||||
|
||||
def compute_summary_cache_key(diff_text: str, prompt: str) -> str:
|
||||
"""Stable 16-char hex key for a (diff, prompt) pair. Stored alongside the summary file."""
|
||||
h = hashlib.md5()
|
||||
h.update(diff_text.encode('utf-8', errors='replace'))
|
||||
h.update(b'\x00')
|
||||
h.update(prompt.encode('utf-8', errors='replace'))
|
||||
return h.hexdigest()[:16]
|
||||
|
||||
|
||||
def summarise_change(watch, datastore, diff: str, current_snapshot: str = '') -> str:
|
||||
"""
|
||||
Generate a plain-language summary of the change using the watch's
|
||||
llm_change_summary prompt (cascades from tag if not set on watch).
|
||||
|
||||
Returns the summary string, or '' on failure.
|
||||
The result replaces {{ diff }} in notifications so the user gets a
|
||||
readable description instead of raw +/- diff lines.
|
||||
"""
|
||||
cfg = get_llm_config(datastore)
|
||||
if not cfg:
|
||||
return ''
|
||||
|
||||
custom_prompt, _ = resolve_llm_field(watch, datastore, 'llm_change_summary')
|
||||
if not custom_prompt:
|
||||
custom_prompt = DEFAULT_CHANGE_SUMMARY_PROMPT
|
||||
if not diff.strip():
|
||||
return ''
|
||||
|
||||
url = watch.get('url', '')
|
||||
title = watch.get('page_title') or watch.get('title') or ''
|
||||
|
||||
system_prompt = build_change_summary_system_prompt()
|
||||
user_prompt = build_change_summary_prompt(
|
||||
diff=diff,
|
||||
custom_prompt=custom_prompt,
|
||||
current_snapshot=current_snapshot,
|
||||
url=url,
|
||||
title=title,
|
||||
)
|
||||
|
||||
try:
|
||||
raw, tokens = llm_client.completion(
|
||||
model=cfg['model'],
|
||||
messages=[
|
||||
{'role': 'system', 'content': system_prompt},
|
||||
{'role': 'user', 'content': user_prompt},
|
||||
],
|
||||
api_key=cfg.get('api_key'),
|
||||
api_base=cfg.get('api_base'),
|
||||
max_tokens=_MAX_SUMMARY_TOKENS,
|
||||
)
|
||||
summary = raw.strip()
|
||||
_check_token_budget(watch, cfg, tokens)
|
||||
watch['llm_last_tokens_used'] = (watch.get('llm_last_tokens_used') or 0) + tokens
|
||||
logger.debug(
|
||||
f"LLM change summary {watch.get('uuid')}: tokens={tokens} "
|
||||
f"summary={summary[:80]}"
|
||||
)
|
||||
return summary
|
||||
except Exception as e:
|
||||
logger.warning(f"LLM change summary failed for {watch.get('uuid')}: {e}")
|
||||
return ''
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Live-preview extraction (current content, no diff)
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def preview_extract(watch, datastore, content: str) -> dict | None:
|
||||
"""
|
||||
For the live-preview endpoint: extract relevant information from the
|
||||
*current* page content according to the watch's intent.
|
||||
|
||||
Unlike evaluate_change (which compares a diff), this asks the LLM to
|
||||
directly answer the intent against the current snapshot — giving the user
|
||||
immediate feedback like "30 articles listed" or "Price: $149, 25% off".
|
||||
|
||||
Returns {'found': bool, 'answer': str} or None if LLM not configured / no intent.
|
||||
"""
|
||||
cfg = get_llm_config(datastore)
|
||||
if not cfg:
|
||||
return None
|
||||
|
||||
intent, _ = resolve_intent(watch, datastore)
|
||||
if not intent or not content.strip():
|
||||
return None
|
||||
|
||||
url = watch.get('url', '')
|
||||
title = watch.get('page_title') or watch.get('title') or ''
|
||||
|
||||
system_prompt = build_preview_system_prompt()
|
||||
user_prompt = build_preview_prompt(intent, content, url=url, title=title)
|
||||
|
||||
try:
|
||||
raw, tokens = llm_client.completion(
|
||||
model=cfg['model'],
|
||||
messages=[
|
||||
{'role': 'system', 'content': system_prompt},
|
||||
{'role': 'user', 'content': user_prompt},
|
||||
],
|
||||
api_key=cfg.get('api_key'),
|
||||
api_base=cfg.get('api_base'),
|
||||
)
|
||||
result = parse_preview_response(raw)
|
||||
logger.debug(
|
||||
f"LLM preview {watch.get('uuid')}: found={result['found']} "
|
||||
f"tokens={tokens} answer={result['answer'][:80]}"
|
||||
)
|
||||
return result
|
||||
except Exception as e:
|
||||
logger.warning(f"LLM preview extraction failed for {watch.get('uuid')}: {e}")
|
||||
return None
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Per-change evaluation
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def evaluate_change(watch, datastore, diff: str, current_snapshot: str = '') -> dict | None:
|
||||
"""
|
||||
Evaluate whether `diff` matches the watch's intent.
|
||||
Returns {'important': bool, 'summary': str} or None if LLM not configured / no intent.
|
||||
|
||||
Results are cached by (intent, diff) hash — each unique diff is evaluated exactly once.
|
||||
"""
|
||||
cfg = get_llm_config(datastore)
|
||||
if not cfg:
|
||||
return None
|
||||
|
||||
intent, source = resolve_intent(watch, datastore)
|
||||
if not intent:
|
||||
return None
|
||||
|
||||
if not diff or not diff.strip():
|
||||
return {'important': False, 'summary': ''}
|
||||
|
||||
# Cache lookup — evaluations are deterministic once cached
|
||||
cache_key = hashlib.sha256(f"{intent}||{diff}".encode()).hexdigest()
|
||||
cache = watch.get('llm_evaluation_cache') or {}
|
||||
if cache_key in cache:
|
||||
logger.debug(f"LLM cache hit for {watch.get('uuid')} key={cache_key[:8]}")
|
||||
return cache[cache_key]
|
||||
|
||||
# Check cumulative budget before making the call
|
||||
if not _check_token_budget(watch, cfg):
|
||||
# Already over budget — fail open (don't suppress notification)
|
||||
return {'important': True, 'summary': ''}
|
||||
|
||||
url = watch.get('url', '')
|
||||
title = watch.get('page_title') or watch.get('title') or ''
|
||||
|
||||
system_prompt = build_eval_system_prompt()
|
||||
user_prompt = build_eval_prompt(
|
||||
intent=intent,
|
||||
diff=diff,
|
||||
current_snapshot=current_snapshot,
|
||||
url=url,
|
||||
title=title,
|
||||
)
|
||||
|
||||
try:
|
||||
raw, tokens = llm_client.completion(
|
||||
model=cfg['model'],
|
||||
messages=[
|
||||
{'role': 'system', 'content': system_prompt},
|
||||
{'role': 'user', 'content': user_prompt},
|
||||
],
|
||||
api_key=cfg.get('api_key'),
|
||||
api_base=cfg.get('api_base'),
|
||||
)
|
||||
result = parse_eval_response(raw)
|
||||
except Exception as e:
|
||||
logger.warning(f"LLM evaluation failed for {watch.get('uuid')}: {e}")
|
||||
# On failure: don't suppress the notification — pass through as important
|
||||
watch['llm_last_tokens_used'] = 0
|
||||
return {'important': True, 'summary': ''}
|
||||
|
||||
# Accumulate token usage and enforce per-check limit
|
||||
_check_token_budget(watch, cfg, tokens)
|
||||
watch['llm_last_tokens_used'] = tokens
|
||||
|
||||
# Store in cache
|
||||
if 'llm_evaluation_cache' not in watch or watch['llm_evaluation_cache'] is None:
|
||||
watch['llm_evaluation_cache'] = {}
|
||||
watch['llm_evaluation_cache'][cache_key] = result
|
||||
|
||||
logger.debug(
|
||||
f"LLM eval {watch.get('uuid')} (intent from {source}): "
|
||||
f"important={result['important']} tokens={tokens} summary={result['summary'][:80]}"
|
||||
)
|
||||
return result
|
||||
148
changedetectionio/llm/prompt_builder.py
Normal file
148
changedetectionio/llm/prompt_builder.py
Normal file
@@ -0,0 +1,148 @@
|
||||
"""
|
||||
Prompt construction for LLM evaluation calls.
|
||||
Pure functions — no side effects, fully testable.
|
||||
"""
|
||||
|
||||
from .bm25_trim import trim_to_relevant
|
||||
|
||||
SNAPSHOT_CONTEXT_CHARS = 3_000 # current page state excerpt sent alongside the diff
|
||||
|
||||
|
||||
def build_eval_prompt(intent: str, diff: str, current_snapshot: str = '',
|
||||
url: str = '', title: str = '') -> str:
|
||||
"""
|
||||
Build the user message for a diff evaluation call.
|
||||
The system prompt is kept separate (see build_eval_system_prompt).
|
||||
"""
|
||||
parts = []
|
||||
|
||||
if url:
|
||||
parts.append(f"URL: {url}")
|
||||
if title:
|
||||
parts.append(f"Page title: {title}")
|
||||
|
||||
parts.append(f"Intent: {intent}")
|
||||
|
||||
if current_snapshot:
|
||||
excerpt = trim_to_relevant(current_snapshot, intent, max_chars=SNAPSHOT_CONTEXT_CHARS)
|
||||
if excerpt:
|
||||
parts.append(f"\nCurrent page state (relevant excerpt):\n{excerpt}")
|
||||
|
||||
parts.append(f"\nWhat changed (diff):\n{diff}")
|
||||
|
||||
return '\n'.join(parts)
|
||||
|
||||
|
||||
def build_eval_system_prompt() -> str:
|
||||
return (
|
||||
"You evaluate website changes for a monitoring tool.\n"
|
||||
"Given an intent and a diff (added/removed lines), decide if the change matches the intent.\n\n"
|
||||
"Respond with ONLY a JSON object — no markdown, no explanation outside it:\n"
|
||||
'{"important": true/false, "summary": "one sentence describing the relevant change, or why it doesn\'t match"}\n\n'
|
||||
"Rules:\n"
|
||||
"- important=true only when the diff clearly matches the intent\n"
|
||||
"- Empty, trivial, or cosmetic diffs (dates, counters, whitespace) → important=false\n"
|
||||
"- Use OR logic when intent lists multiple triggers\n"
|
||||
"- Summary must be in the same language as the intent\n"
|
||||
"- If important=false, summary briefly explains why it doesn't match"
|
||||
)
|
||||
|
||||
|
||||
def build_preview_prompt(intent: str, content: str, url: str = '', title: str = '') -> str:
|
||||
"""
|
||||
Build the user message for a live-preview extraction call.
|
||||
Unlike build_eval_prompt (which analyses a diff), this asks the LLM to
|
||||
extract relevant information from the *current* page content — giving the
|
||||
user a direct answer to their intent so they can verify it makes sense
|
||||
before saving.
|
||||
"""
|
||||
parts = []
|
||||
if url:
|
||||
parts.append(f"URL: {url}")
|
||||
if title:
|
||||
parts.append(f"Page title: {title}")
|
||||
parts.append(f"Intent / question: {intent}")
|
||||
parts.append(f"\nPage content:\n{content[:6_000]}")
|
||||
return '\n'.join(parts)
|
||||
|
||||
|
||||
def build_preview_system_prompt() -> str:
|
||||
return (
|
||||
"You are a web page content analyzer for a website monitoring tool.\n"
|
||||
"Given the user's intent or question and the current page content, "
|
||||
"extract and directly answer what the intent is looking for.\n\n"
|
||||
"Respond with ONLY a JSON object — no markdown, no explanation outside it:\n"
|
||||
'{"found": true/false, "answer": "concise direct answer or extraction"}\n\n'
|
||||
"Rules:\n"
|
||||
"- found=true when the page contains something relevant to the intent\n"
|
||||
"- answer must directly address the intent (e.g. for 'how many articles?' → '30 articles listed')\n"
|
||||
"- answer must be in the same language as the intent\n"
|
||||
"- Keep answer brief — one sentence maximum"
|
||||
)
|
||||
|
||||
|
||||
def build_change_summary_prompt(diff: str, custom_prompt: str,
|
||||
current_snapshot: str = '', url: str = '', title: str = '') -> str:
|
||||
"""
|
||||
Build the user message for an AI Change Summary call.
|
||||
The user supplies their own instructions (custom_prompt); this wraps them
|
||||
with the diff and optional page context.
|
||||
"""
|
||||
parts = []
|
||||
if url:
|
||||
parts.append(f"URL: {url}")
|
||||
if title:
|
||||
parts.append(f"Page title: {title}")
|
||||
parts.append(f"Instructions: {custom_prompt}")
|
||||
if current_snapshot:
|
||||
excerpt = trim_to_relevant(current_snapshot, custom_prompt, max_chars=2_000)
|
||||
if excerpt:
|
||||
parts.append(f"\nCurrent page (excerpt):\n{excerpt}")
|
||||
parts.append(f"\nWhat changed (diff):\n{diff}")
|
||||
return '\n'.join(parts)
|
||||
|
||||
|
||||
def build_change_summary_system_prompt() -> str:
|
||||
return (
|
||||
"You summarise website changes for a monitoring notification.\n"
|
||||
"Given a diff of what changed and the user's formatting instructions, "
|
||||
"produce a concise plain-language description of the change.\n"
|
||||
"Follow the user's instructions exactly for format, language, and length.\n"
|
||||
"Respond with ONLY the summary text — no JSON, no markdown code fences, "
|
||||
"no preamble. Just the description."
|
||||
)
|
||||
|
||||
|
||||
def build_setup_prompt(intent: str, snapshot_text: str, url: str = '') -> str:
|
||||
"""
|
||||
Build the prompt for the one-time setup call that decides whether
|
||||
a CSS pre-filter would improve evaluation precision.
|
||||
"""
|
||||
excerpt = trim_to_relevant(snapshot_text, intent, max_chars=4_000)
|
||||
|
||||
parts = []
|
||||
if url:
|
||||
parts.append(f"URL: {url}")
|
||||
parts.append(f"Intent: {intent}")
|
||||
parts.append(f"\nPage content excerpt:\n{excerpt}")
|
||||
|
||||
return '\n'.join(parts)
|
||||
|
||||
|
||||
def build_setup_system_prompt() -> str:
|
||||
return (
|
||||
"You help configure a website change monitor.\n"
|
||||
"Given a monitoring intent and a sample of the page content, decide if a CSS pre-filter "
|
||||
"would improve evaluation precision by scoping the content to a specific structural section.\n\n"
|
||||
"Respond with ONLY a JSON object:\n"
|
||||
'{"needs_prefilter": true/false, "selector": "CSS selector or null", "reason": "one sentence"}\n\n'
|
||||
"Rules:\n"
|
||||
"- Only recommend a pre-filter when the intent references a specific structural section "
|
||||
"(e.g. 'footer', 'sidebar', 'nav', 'header', 'main', 'article') OR the page clearly "
|
||||
"has high-noise sections unrelated to the intent\n"
|
||||
"- Use ONLY semantic element selectors: footer, nav, header, main, article, aside, "
|
||||
"or attribute-based like [id*='price'], [class*='sidebar'] — NEVER positional selectors "
|
||||
"like div:nth-child(3) or //*[2]\n"
|
||||
"- Default to needs_prefilter=false — most intents don't need one\n"
|
||||
"- selector must be null when needs_prefilter=false"
|
||||
)
|
||||
84
changedetectionio/llm/response_parser.py
Normal file
84
changedetectionio/llm/response_parser.py
Normal file
@@ -0,0 +1,84 @@
|
||||
"""
|
||||
Parse and validate LLM JSON responses.
|
||||
Pure functions — no side effects, fully testable.
|
||||
|
||||
LLMs occasionally return JSON wrapped in markdown fences or with trailing
|
||||
text. This module handles those cases gracefully.
|
||||
"""
|
||||
|
||||
import json
|
||||
import re
|
||||
|
||||
# Positional selectors are fragile — reject them even if the LLM generates them
|
||||
_POSITIONAL_SELECTOR_RE = re.compile(
|
||||
r'nth-child|nth-of-type|:eq\(|\[\d+\]|\/\/\*\[\d',
|
||||
re.IGNORECASE
|
||||
)
|
||||
|
||||
|
||||
def _extract_json(raw: str) -> str:
|
||||
"""Strip markdown fences and extract the first JSON object."""
|
||||
raw = raw.strip()
|
||||
# Remove ```json ... ``` or ``` ... ``` fences
|
||||
raw = re.sub(r'^```(?:json)?\s*', '', raw, flags=re.MULTILINE)
|
||||
raw = re.sub(r'\s*```$', '', raw, flags=re.MULTILINE)
|
||||
# Find the first { ... } block
|
||||
match = re.search(r'\{.*\}', raw, re.DOTALL)
|
||||
return match.group(0) if match else raw
|
||||
|
||||
|
||||
def parse_eval_response(raw: str) -> dict:
|
||||
"""
|
||||
Parse a diff evaluation response.
|
||||
Returns {'important': bool, 'summary': str}.
|
||||
Falls back to important=False on any parse error.
|
||||
"""
|
||||
try:
|
||||
data = json.loads(_extract_json(raw))
|
||||
return {
|
||||
'important': bool(data.get('important', False)),
|
||||
'summary': str(data.get('summary', '')).strip(),
|
||||
}
|
||||
except (json.JSONDecodeError, AttributeError):
|
||||
return {'important': False, 'summary': ''}
|
||||
|
||||
|
||||
def parse_preview_response(raw: str) -> dict:
|
||||
"""
|
||||
Parse a live-preview extraction response.
|
||||
Returns {'found': bool, 'answer': str}.
|
||||
Falls back to found=False on any parse error.
|
||||
"""
|
||||
try:
|
||||
data = json.loads(_extract_json(raw))
|
||||
return {
|
||||
'found': bool(data.get('found', False)),
|
||||
'answer': str(data.get('answer', '')).strip(),
|
||||
}
|
||||
except (json.JSONDecodeError, AttributeError):
|
||||
return {'found': False, 'answer': ''}
|
||||
|
||||
|
||||
def parse_setup_response(raw: str) -> dict:
|
||||
"""
|
||||
Parse a setup/pre-filter decision response.
|
||||
Returns {'needs_prefilter': bool, 'selector': str|None, 'reason': str}.
|
||||
Rejects positional selectors even if the LLM generates them.
|
||||
"""
|
||||
try:
|
||||
data = json.loads(_extract_json(raw))
|
||||
needs = bool(data.get('needs_prefilter', False))
|
||||
selector = data.get('selector') or None
|
||||
|
||||
# Sanitise: reject positional selectors
|
||||
if selector and _POSITIONAL_SELECTOR_RE.search(selector):
|
||||
selector = None
|
||||
needs = False
|
||||
|
||||
return {
|
||||
'needs_prefilter': needs,
|
||||
'selector': selector if needs else None,
|
||||
'reason': str(data.get('reason', '')).strip(),
|
||||
}
|
||||
except (json.JSONDecodeError, AttributeError):
|
||||
return {'needs_prefilter': False, 'selector': None, 'reason': ''}
|
||||
@@ -1001,6 +1001,38 @@ class model(EntityPersistenceMixin, watch_base):
|
||||
return False
|
||||
|
||||
|
||||
def get_last_llm_diff_summary(self, cache_key: str = None) -> str:
|
||||
"""Return the cached AI Change Summary, or '' if absent or stale.
|
||||
|
||||
If *cache_key* is provided the stored key is compared first;
|
||||
a mismatch (different diff or prompt) returns '' so the caller
|
||||
knows it must regenerate rather than serving a stale summary.
|
||||
"""
|
||||
fname = os.path.join(self.data_dir, 'last-llm-diff-summary.txt')
|
||||
if not os.path.isfile(fname):
|
||||
return ''
|
||||
if cache_key is not None:
|
||||
key_fname = os.path.join(self.data_dir, 'last-llm-diff-summary.key')
|
||||
stored_key = ''
|
||||
if os.path.isfile(key_fname):
|
||||
with open(key_fname, 'r', encoding='utf-8') as f:
|
||||
stored_key = f.read().strip()
|
||||
if stored_key != cache_key:
|
||||
return ''
|
||||
with open(fname, 'r', encoding='utf-8') as f:
|
||||
return f.read().strip()
|
||||
|
||||
def save_llm_diff_summary(self, summary: str, cache_key: str = None):
|
||||
"""Persist the AI Change Summary and its cache key so stale entries are detectable."""
|
||||
self.ensure_data_dir_exists()
|
||||
fname = os.path.join(self.data_dir, 'last-llm-diff-summary.txt')
|
||||
with open(fname, 'w', encoding='utf-8') as f:
|
||||
f.write(summary)
|
||||
if cache_key is not None:
|
||||
key_fname = os.path.join(self.data_dir, 'last-llm-diff-summary.key')
|
||||
with open(key_fname, 'w', encoding='utf-8') as f:
|
||||
f.write(cache_key)
|
||||
|
||||
def pause(self):
|
||||
self['paused'] = True
|
||||
|
||||
|
||||
@@ -188,6 +188,11 @@ class watch_base(dict):
|
||||
'date_created': None,
|
||||
'extract_lines_containing': [], # Keep only lines containing these substrings (plain text, case-insensitive)
|
||||
'extract_text': [], # Extract text by regex after filters
|
||||
# LLM intent-based evaluation
|
||||
'llm_intent': '', # Plain-English description of what the user cares about (change filter)
|
||||
'llm_change_summary': '', # Prompt for AI Change Summary — replaces {{ diff }} in notifications
|
||||
'llm_prefilter': None, # CSS selector derived at setup time (semantic only, e.g. "footer")
|
||||
'llm_evaluation_cache': {}, # {sha256(intent+diff): {important, summary}} - evaluated once, cached
|
||||
'fetch_backend': 'system', # plaintext, playwright etc
|
||||
'fetch_time': 0.0,
|
||||
'filter_failure_notification_send': strtobool(os.getenv('FILTER_FAILURE_NOTIFICATION_SEND_DEFAULT', 'True')),
|
||||
|
||||
@@ -48,8 +48,9 @@ To verify this works:
|
||||
"""
|
||||
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
from urllib.parse import unquote_plus
|
||||
from urllib.parse import unquote_plus, urlparse
|
||||
|
||||
import requests
|
||||
from apprise import plugins
|
||||
@@ -59,6 +60,8 @@ from apprise.utils.logic import dict_full_update
|
||||
from loguru import logger
|
||||
from requests.structures import CaseInsensitiveDict
|
||||
|
||||
from changedetectionio.validate_url import is_private_hostname
|
||||
|
||||
SUPPORTED_HTTP_METHODS = {"get", "post", "put", "delete", "patch", "head"}
|
||||
|
||||
|
||||
@@ -195,6 +198,15 @@ def apprise_http_custom_handler(
|
||||
|
||||
url = re.sub(rf"^{schema}", "https" if schema.endswith("s") else "http", parsed_url.get("url"))
|
||||
|
||||
# SSRF protection — block private/loopback addresses unless explicitly allowed
|
||||
if not os.getenv('ALLOW_IANA_RESTRICTED_ADDRESSES', '').lower() in ('true', '1', 'yes'):
|
||||
hostname = urlparse(url).hostname or ''
|
||||
if hostname and is_private_hostname(hostname):
|
||||
raise ValueError(
|
||||
f"Notification target '{hostname}' is a private/reserved address. "
|
||||
f"Set ALLOW_IANA_RESTRICTED_ADDRESSES=true to allow."
|
||||
)
|
||||
|
||||
response = requests.request(
|
||||
method=method,
|
||||
url=url,
|
||||
|
||||
@@ -364,6 +364,20 @@ def process_notification(n_object: NotificationContextData, datastore):
|
||||
)
|
||||
)
|
||||
|
||||
# {{ raw_diff }} always holds the actual diff regardless of AI Change Summary
|
||||
n_object['raw_diff'] = n_object.get('diff', '')
|
||||
|
||||
# AI Change Summary: when configured, {{ diff }} renders the AI summary instead of raw diff
|
||||
_llm_change_summary = (n_object.get('_llm_change_summary') or '').strip()
|
||||
if _llm_change_summary:
|
||||
n_object['diff'] = _llm_change_summary
|
||||
|
||||
# Lazily populate llm_summary / llm_intent if used in notification template
|
||||
scan_text = n_object.get('notification_body', '') + n_object.get('notification_title', '')
|
||||
if 'llm_summary' in scan_text or 'llm_intent' in scan_text or 'raw_diff' in scan_text:
|
||||
n_object['llm_summary'] = _llm_change_summary or (n_object.get('_llm_result') or {}).get('summary', '')
|
||||
n_object['llm_intent'] = n_object.get('_llm_intent', '')
|
||||
|
||||
with (apprise.LogCapture(level=apprise.logging.DEBUG) as logs):
|
||||
for url in n_object['notification_urls']:
|
||||
|
||||
|
||||
@@ -194,6 +194,8 @@ class NotificationContextData(dict):
|
||||
'timestamp_from': None,
|
||||
'timestamp_to': None,
|
||||
'triggered_text': None,
|
||||
'llm_summary': None, # AI plain-English summary of what changed (requires AI intent to be configured)
|
||||
'llm_intent': None, # The intent that was evaluated (watch-level or inherited from tag)
|
||||
'uuid': 'XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX', # Converted to 'watch_uuid' in create_notification_parameters
|
||||
'watch_mime_type': None,
|
||||
'watch_tag': None,
|
||||
@@ -410,6 +412,11 @@ class NotificationService:
|
||||
n_object['notification_body'] = _check_cascading_vars(self.datastore,'notification_body', watch)
|
||||
n_object['notification_format'] = _check_cascading_vars(self.datastore,'notification_format', watch)
|
||||
|
||||
# Attach LLM results so notification tokens render correctly
|
||||
n_object['_llm_result'] = watch.get('_llm_result')
|
||||
n_object['_llm_intent'] = watch.get('_llm_intent', '')
|
||||
n_object['_llm_change_summary'] = watch.get('_llm_change_summary', '')
|
||||
|
||||
# (Individual watch) Only prepare to notify if the rules above matched
|
||||
queued = False
|
||||
if n_object and n_object.get('notification_urls'):
|
||||
|
||||
@@ -61,7 +61,7 @@ def render_form(watch, datastore, request, url_for, render_template, flash, redi
|
||||
screenshot=screenshot_url,
|
||||
is_html_webdriver=is_html_webdriver,
|
||||
password_enabled_and_share_is_off=password_enabled_and_share_is_off,
|
||||
extra_title=f" - {watch.label} - {gettext('Extract Data')}",
|
||||
extra_title=f" - {watch.label} - Extract Data",
|
||||
extra_stylesheets=[url_for('static_content', group='styles', filename='diff.css')],
|
||||
pure_menu_fixed=False
|
||||
)
|
||||
|
||||
@@ -3,11 +3,11 @@
|
||||
{% block content %}
|
||||
<div class="tabs">
|
||||
<ul>
|
||||
{% if last_error_text %}<li class="tab" id="error-text-tab"><a href="{{ url_for('ui.ui_diff.diff_history_page', uuid=uuid)}}#error-text">{{ _('Error Text') }}</a></li> {% endif %}
|
||||
{% if last_error_screenshot %}<li class="tab" id="error-screenshot-tab"><a href="{{ url_for('ui.ui_diff.diff_history_page', uuid=uuid)}}#error-screenshot">{{ _('Error Screenshot') }}</a></li> {% endif %}
|
||||
<li class="tab" id=""><a href="{{ url_for('ui.ui_diff.diff_history_page', uuid=uuid)}}#text">{{ _('Text') }}</a></li>
|
||||
<li class="tab" id="screenshot-tab"><a href="{{ url_for('ui.ui_diff.diff_history_page', uuid=uuid)}}#screenshot">{{ _('Screenshot') }}</a></li>
|
||||
<li class="tab active" id="extract-tab"><a href="{{ url_for('ui.ui_diff.diff_history_page_extract_GET', uuid=uuid)}}">{{ _('Extract Data') }}</a></li>
|
||||
{% if last_error_text %}<li class="tab" id="error-text-tab"><a href="{{ url_for('ui.ui_diff.diff_history_page', uuid=uuid)}}#error-text">Error Text</a></li> {% endif %}
|
||||
{% if last_error_screenshot %}<li class="tab" id="error-screenshot-tab"><a href="{{ url_for('ui.ui_diff.diff_history_page', uuid=uuid)}}#error-screenshot">Error Screenshot</a></li> {% endif %}
|
||||
<li class="tab" id=""><a href="{{ url_for('ui.ui_diff.diff_history_page', uuid=uuid)}}#text">Text</a></li>
|
||||
<li class="tab" id="screenshot-tab"><a href="{{ url_for('ui.ui_diff.diff_history_page', uuid=uuid)}}#screenshot">Screenshot</a></li>
|
||||
<li class="tab active" id="extract-tab"><a href="{{ url_for('ui.ui_diff.diff_history_page_extract_GET', uuid=uuid)}}">Extract Data</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
@@ -17,23 +17,23 @@
|
||||
<form id="extract-data-form" class="pure-form pure-form-stacked edit-form" action="{{ url_for('ui.ui_diff.diff_history_page_extract_POST', uuid=uuid) }}" method="POST">
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
|
||||
|
||||
<p>{{ _('This tool will extract text data from all of the watch history.') }}</p>
|
||||
<p>This tool will extract text data from all of the watch history.</p>
|
||||
|
||||
<div class="pure-control-group">
|
||||
{{ render_field(extract_form.extract_regex) }}
|
||||
<span class="pure-form-message-inline">
|
||||
{{ _('A <strong>RegEx</strong> is a pattern that identifies exactly which part inside of the text that you want to extract.')|safe }}<br>
|
||||
A <strong>RegEx</strong> is a pattern that identifies exactly which part inside of the text that you want to extract.<br>
|
||||
|
||||
<p>
|
||||
{{ _('For example, to extract only the numbers from text') }} ‐<br>
|
||||
<strong>{{ _('Raw text') }}</strong>: <code>Temperature <span style="color: red">5.5</span>°C in Sydney</code><br>
|
||||
<strong>{{ _('RegEx to extract:') }}</strong> <code>Temperature <span style="color: red">([0-9\.]+)</span></code><br>
|
||||
For example, to extract only the numbers from text ‐<br>
|
||||
<strong>Raw text</strong>: <code>Temperature <span style="color: red">5.5</span>°C in Sydney</code><br>
|
||||
<strong>RegEx to extract:</strong> <code>Temperature <span style="color: red">([0-9\.]+)</span></code><br>
|
||||
</p>
|
||||
<p>
|
||||
<a href="https://RegExr.com/">{{ _('Be sure to test your RegEx here.') }}</a>
|
||||
<a href="https://RegExr.com/">Be sure to test your RegEx here.</a>
|
||||
</p>
|
||||
<p>
|
||||
{{ _('Each RegEx group bracket') }} <code>()</code> {{ _('will be in its own column, the first column value is always the date.') }}
|
||||
Each RegEx group bracket <code>()</code> will be in its own column, the first column value is always the date.
|
||||
</p>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
@@ -65,6 +65,12 @@ def prepare_filter_prevew(datastore, watch_uuid, form_data):
|
||||
# Only update vars that came in via the AJAX post
|
||||
p = {k: v for k, v in form.data.items() if k in form_data.keys()}
|
||||
tmp_watch.update(p)
|
||||
|
||||
# Apply llm_intent from form directly — it's not part of processor_text_json_diff_form
|
||||
# but the AJAX sends all visible inputs, so it arrives in form_data
|
||||
if hasattr(form_data, 'get') and 'llm_intent' in form_data:
|
||||
tmp_watch['llm_intent'] = (form_data.get('llm_intent') or '').strip()
|
||||
|
||||
blank_watch_no_filters = watch_model(datastore_path=datastore.datastore_path, __datastore=datastore.data)
|
||||
blank_watch_no_filters['url'] = tmp_watch.get('url')
|
||||
|
||||
@@ -120,6 +126,18 @@ def prepare_filter_prevew(datastore, watch_uuid, form_data):
|
||||
except Exception as e:
|
||||
text_before_filter = f"Error: {str(e)}"
|
||||
|
||||
# LLM preview extraction — asks the LLM to directly answer the intent
|
||||
# against the current filtered content (no diff comparison).
|
||||
# e.g. intent "how many articles?" → answer "30 articles listed"
|
||||
# Results are NOT cached back to the real watch.
|
||||
llm_evaluation = None
|
||||
try:
|
||||
from changedetectionio.llm.evaluator import preview_extract
|
||||
if text_after_filter and text_after_filter.strip() not in ('', 'Empty content'):
|
||||
llm_evaluation = preview_extract(tmp_watch, datastore, content=text_after_filter)
|
||||
except Exception as e:
|
||||
logger.warning(f"LLM preview evaluation failed for {watch_uuid}: {e}")
|
||||
|
||||
logger.trace(f"Parsed in {time.time() - now:.3f}s")
|
||||
|
||||
return ({
|
||||
@@ -128,6 +146,7 @@ def prepare_filter_prevew(datastore, watch_uuid, form_data):
|
||||
'blocked_line_numbers': blocked_line_numbers,
|
||||
'duration': time.time() - now,
|
||||
'ignore_line_numbers': ignore_line_numbers,
|
||||
'llm_evaluation': llm_evaluation,
|
||||
'trigger_line_numbers': trigger_line_numbers,
|
||||
})
|
||||
|
||||
|
||||
@@ -7,7 +7,6 @@ a side-by-side or unified diff view with syntax highlighting and change markers.
|
||||
|
||||
import os
|
||||
import time
|
||||
from flask_babel import gettext
|
||||
from loguru import logger
|
||||
|
||||
from changedetectionio import diff, strtobool
|
||||
@@ -199,6 +198,27 @@ def render(watch, datastore, request, url_for, render_template, flash, redirect,
|
||||
if str(from_version) != str(dates[-2]) or str(to_version) != str(dates[-1]):
|
||||
note = 'Note: You are not viewing the latest changes.'
|
||||
|
||||
llm_configured = bool(
|
||||
datastore.data.get('settings', {}).get('application', {}).get('llm', {}).get('model')
|
||||
)
|
||||
|
||||
# Load cached AI diff summary (only shown when viewing the latest snapshot)
|
||||
llm_diff_summary = ''
|
||||
viewing_latest = str(to_version) == str(dates[-1])
|
||||
if viewing_latest and llm_configured:
|
||||
try:
|
||||
import difflib as _difflib
|
||||
from changedetectionio.llm.evaluator import get_effective_summary_prompt, compute_summary_cache_key
|
||||
_plain_diff = '\n'.join(list(_difflib.unified_diff(
|
||||
from_version_file_contents.splitlines(),
|
||||
to_version_file_contents.splitlines(),
|
||||
lineterm='', n=3,
|
||||
))[2:])
|
||||
_cache_key = compute_summary_cache_key(_plain_diff, get_effective_summary_prompt(watch, datastore))
|
||||
llm_diff_summary = watch.get_last_llm_diff_summary(cache_key=_cache_key)
|
||||
except Exception as e:
|
||||
logger.warning(f"Could not validate llm-diff-summary cache for {uuid}: {e}")
|
||||
|
||||
output = render_template("diff.html",
|
||||
#initial_scroll_line_number=100,
|
||||
bottom_horizontal_offscreen_contents=offscreen_content,
|
||||
@@ -208,7 +228,7 @@ def render(watch, datastore, request, url_for, render_template, flash, redirect,
|
||||
diff_prefs=diff_prefs,
|
||||
extra_classes='difference-page',
|
||||
extra_stylesheets=extra_stylesheets,
|
||||
extra_title=f" - {watch.label} - {gettext('History')}",
|
||||
extra_title=f" - {watch.label} - History",
|
||||
extract_form=extract_form,
|
||||
from_version=str(from_version),
|
||||
is_html_webdriver=is_html_webdriver,
|
||||
@@ -225,5 +245,8 @@ def render(watch, datastore, request, url_for, render_template, flash, redirect,
|
||||
uuid=uuid,
|
||||
versions=dates, # All except current/last
|
||||
watch_a=watch,
|
||||
llm_configured=llm_configured,
|
||||
llm_diff_summary=llm_diff_summary,
|
||||
viewing_latest=viewing_latest,
|
||||
)
|
||||
return output
|
||||
|
||||
@@ -12,7 +12,14 @@ function request_textpreview_update() {
|
||||
data[name] = $element.is(':checkbox') ? ($element.is(':checked') ? $element.val() : false) : $element.val();
|
||||
});
|
||||
|
||||
// llm_intent lives in a separate (potentially hidden) tab — include it explicitly
|
||||
const $llmIntent = $('textarea[name="llm_intent"]');
|
||||
if ($llmIntent.length) {
|
||||
data['llm_intent'] = $llmIntent.val();
|
||||
}
|
||||
|
||||
$('body').toggleClass('spinner-active', 1);
|
||||
$('#llm-preview-result').hide();
|
||||
|
||||
$.abortiveSingularAjax({
|
||||
type: "POST",
|
||||
@@ -41,6 +48,21 @@ function request_textpreview_update() {
|
||||
'title': "No change-detection will occur because this text exists."
|
||||
}
|
||||
])
|
||||
|
||||
// LLM preview extraction result
|
||||
const $llmResult = $('#llm-preview-result');
|
||||
if ($llmResult.length && data['llm_evaluation']) {
|
||||
const ev = data['llm_evaluation'];
|
||||
const found = ev['found'];
|
||||
$llmResult.attr('data-found', found ? '1' : '0');
|
||||
$llmResult.find('.llm-preview-verdict').text(
|
||||
found ? '✓ Would trigger a change' : '✗ Would not trigger a change'
|
||||
);
|
||||
$llmResult.find('.llm-preview-answer').text(ev['answer'] || '');
|
||||
$llmResult.show();
|
||||
} else if ($llmResult.length) {
|
||||
$llmResult.hide();
|
||||
}
|
||||
}).fail(function (error) {
|
||||
if (error.statusText === 'abort') {
|
||||
console.log('Request was aborted due to a new request being fired.');
|
||||
|
||||
@@ -1 +1 @@
|
||||
#diff-form{background:rgba(0,0,0,.05);padding:1em;border-radius:10px;margin-bottom:1em;color:#fff;font-size:.9rem;text-align:center}#diff-form label.from-to-label{width:4rem;text-decoration:none;padding:.5rem}#diff-form label.from-to-label#change-from{color:#b30000;background:#fadad7}#diff-form label.from-to-label#change-to{background:#eaf2c2;color:#406619}#diff-form #diff-style>span{display:inline-block;padding:.3em}#diff-form #diff-style>span label{font-weight:normal}#diff-form *{vertical-align:middle}body.difference-page section.content{padding-top:40px}#diff-ui{background:var(--color-background);padding:1rem;border-radius:5px}@media(min-width: 767px){#diff-ui{min-width:50%}}#diff-ui #text{font-size:11px}#diff-ui pre{white-space:break-spaces;overflow-wrap:anywhere}#diff-ui h1{display:inline;font-size:100%}#diff-ui #result{white-space:pre-wrap;word-break:break-word;overflow-wrap:break-word}#diff-ui .source{position:absolute;right:1%;top:.2em}@-moz-document url-prefix(){#diff-ui body{height:99%}}#diff-ui td#diff-col div{text-align:justify;white-space:pre-wrap}#diff-ui .ignored{background-color:#ccc;opacity:.7}#diff-ui .triggered{background-color:#1b98f8}#diff-ui .ignored.triggered{background-color:red}#diff-ui .tab-pane-inner#screenshot{text-align:center}#diff-ui .tab-pane-inner#screenshot img{max-width:99%}#diff-ui .pure-form button.reset-margin{margin:0px}#diff-ui .diff-fieldset{display:flex;align-items:center;gap:4px;flex-wrap:wrap}#diff-ui ul#highlightSnippetActions{list-style-type:none;display:flex;align-items:center;justify-content:center;gap:1.5rem;flex-wrap:wrap;padding:0;margin:0}#diff-ui ul#highlightSnippetActions li{display:flex;flex-direction:column;align-items:center;text-align:center;padding:.5rem;gap:.3rem}#diff-ui ul#highlightSnippetActions li button,#diff-ui ul#highlightSnippetActions li a{white-space:nowrap}#diff-ui ul#highlightSnippetActions span{font-size:.8rem;color:var(--color-text-input-description)}#diff-ui #cell-diff-jump-visualiser{display:flex;flex-direction:row;gap:1px;background:var(--color-background);border-radius:3px;overflow-x:hidden;position:sticky;top:0;z-index:10;padding-top:1rem;padding-bottom:1rem;justify-content:center}#diff-ui #cell-diff-jump-visualiser>div{flex:1;min-width:1px;max-width:10px;height:10px;background:var(--color-background-button-cancel);opacity:.3;border-radius:1px;transition:opacity .2s;position:relative}#diff-ui #cell-diff-jump-visualiser>div.deletion{background:#b30000;opacity:1}#diff-ui #cell-diff-jump-visualiser>div.insertion{background:#406619;opacity:1}#diff-ui #cell-diff-jump-visualiser>div.note{background:#406619;opacity:1}#diff-ui #cell-diff-jump-visualiser>div.mixed{background:linear-gradient(to right, #b30000 50%, #406619 50%);opacity:1}#diff-ui #cell-diff-jump-visualiser>div.current-position::after{content:"";position:absolute;bottom:-6px;left:50%;transform:translateX(-50%);width:0;height:0;border-left:4px solid rgba(0,0,0,0);border-right:4px solid rgba(0,0,0,0);border-bottom:4px solid var(--color-text)}#diff-ui #cell-diff-jump-visualiser>div:hover{opacity:.8;cursor:pointer}#text-diff-heading-area .snapshot-age{padding:4px;margin:.5rem 0;background-color:var(--color-background-snapshot-age);border-radius:3px;font-weight:bold;margin-bottom:4px}#text-diff-heading-area .snapshot-age.error{background-color:var(--color-error-background-snapshot-age);color:var(--color-error-text-snapshot-age)}#text-diff-heading-area .snapshot-age>*{padding-right:1rem}
|
||||
#diff-form{background:rgba(0,0,0,.05);padding:1em;border-radius:10px;margin-bottom:1em;color:#fff;font-size:.9rem;text-align:center}#diff-form label.from-to-label{width:4rem;text-decoration:none;padding:.5rem}#diff-form label.from-to-label#change-from{color:#b30000;background:#fadad7}#diff-form label.from-to-label#change-to{background:#eaf2c2;color:#406619}#diff-form #diff-style>span{display:inline-block;padding:.3em}#diff-form #diff-style>span label{font-weight:normal}#diff-form *{vertical-align:middle}body.difference-page section.content{padding-top:40px}#diff-ui{background:var(--color-background);padding:1rem;border-radius:5px}@media(min-width: 767px){#diff-ui{min-width:50%}}#diff-ui #text{font-size:11px}#diff-ui pre{white-space:break-spaces;overflow-wrap:anywhere}#diff-ui h1{display:inline;font-size:100%}#diff-ui #result{white-space:pre-wrap;word-break:break-word;overflow-wrap:break-word}#diff-ui .source{position:absolute;right:1%;top:.2em}@-moz-document url-prefix(){#diff-ui body{height:99%}}#diff-ui td#diff-col div{text-align:justify;white-space:pre-wrap}#diff-ui .ignored{background-color:#ccc;opacity:.7}#diff-ui .triggered{background-color:#1b98f8}#diff-ui .ignored.triggered{background-color:red}#diff-ui .tab-pane-inner#screenshot{text-align:center}#diff-ui .tab-pane-inner#screenshot img{max-width:99%}#diff-ui .pure-form button.reset-margin{margin:0px}#diff-ui .diff-fieldset{display:flex;align-items:center;gap:4px;flex-wrap:wrap}#diff-ui ul#highlightSnippetActions{list-style-type:none;display:flex;align-items:center;justify-content:center;gap:1.5rem;flex-wrap:wrap;padding:0;margin:0}#diff-ui ul#highlightSnippetActions li{display:flex;flex-direction:column;align-items:center;text-align:center;padding:.5rem;gap:.3rem}#diff-ui ul#highlightSnippetActions li button,#diff-ui ul#highlightSnippetActions li a{white-space:nowrap}#diff-ui ul#highlightSnippetActions span{font-size:.8rem;color:var(--color-text-input-description)}#diff-ui #cell-diff-jump-visualiser{display:flex;flex-direction:row;gap:1px;background:var(--color-background);border-radius:3px;overflow-x:hidden;position:sticky;top:0;z-index:10;padding-top:1rem;padding-bottom:1rem;justify-content:center}#diff-ui #cell-diff-jump-visualiser>div{flex:1;min-width:1px;max-width:10px;height:10px;background:var(--color-background-button-cancel);opacity:.3;border-radius:1px;transition:opacity .2s;position:relative}#diff-ui #cell-diff-jump-visualiser>div.deletion{background:#b30000;opacity:1}#diff-ui #cell-diff-jump-visualiser>div.insertion{background:#406619;opacity:1}#diff-ui #cell-diff-jump-visualiser>div.note{background:#406619;opacity:1}#diff-ui #cell-diff-jump-visualiser>div.mixed{background:linear-gradient(to right, #b30000 50%, #406619 50%);opacity:1}#diff-ui #cell-diff-jump-visualiser>div.current-position::after{content:"";position:absolute;bottom:-6px;left:50%;transform:translateX(-50%);width:0;height:0;border-left:4px solid rgba(0,0,0,0);border-right:4px solid rgba(0,0,0,0);border-bottom:4px solid var(--color-text)}#diff-ui #cell-diff-jump-visualiser>div:hover{opacity:.8;cursor:pointer}#text-diff-heading-area .snapshot-age{padding:4px;margin:.5rem 0;background-color:var(--color-background-snapshot-age);border-radius:3px;font-weight:bold;margin-bottom:4px}#text-diff-heading-area .snapshot-age.error{background-color:var(--color-error-background-snapshot-age);color:var(--color-error-text-snapshot-age)}#text-diff-heading-area .snapshot-age>*{padding-right:1rem}#llm-diff-summary-area{margin:.6rem 0 .4rem;padding:.65rem .9rem;background:linear-gradient(135deg, rgba(120, 80, 200, 0.08), rgba(80, 160, 220, 0.06));border-left:3px solid rgba(120,80,200,.5);border-radius:0 4px 4px 0;min-width:0;max-width:100%;box-sizing:border-box;overflow:hidden}#llm-diff-summary-area .llm-diff-summary-label{display:block;font-size:.7rem;font-weight:700;letter-spacing:.06em;text-transform:uppercase;opacity:.55;margin-bottom:.25rem}#llm-diff-summary-area .llm-diff-summary-text{margin:0;font-size:.9rem;line-height:1.5;white-space:pre-wrap;overflow-wrap:break-word;word-break:break-word}.llm-diff-summary-loading{opacity:.5;font-style:italic;animation:llm-pulse 1.4s ease-in-out infinite}@keyframes llm-pulse{0%,100%{opacity:.5}50%{opacity:.2}}
|
||||
|
||||
@@ -245,4 +245,46 @@ body.difference-page {
|
||||
padding-right: 1rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
#llm-diff-summary-area {
|
||||
margin: 0.6rem 0 0.4rem;
|
||||
padding: 0.65rem 0.9rem;
|
||||
background: linear-gradient(135deg, rgba(120, 80, 200, 0.08), rgba(80, 160, 220, 0.06));
|
||||
border-left: 3px solid rgba(120, 80, 200, 0.5);
|
||||
border-radius: 0 4px 4px 0;
|
||||
/* prevent long unbroken text from expanding the container */
|
||||
min-width: 0;
|
||||
max-width: 100%;
|
||||
box-sizing: border-box;
|
||||
overflow: hidden;
|
||||
|
||||
.llm-diff-summary-label {
|
||||
display: block;
|
||||
font-size: 0.7rem;
|
||||
font-weight: 700;
|
||||
letter-spacing: 0.06em;
|
||||
text-transform: uppercase;
|
||||
opacity: 0.55;
|
||||
margin-bottom: 0.25rem;
|
||||
}
|
||||
|
||||
.llm-diff-summary-text {
|
||||
margin: 0;
|
||||
font-size: 0.9rem;
|
||||
line-height: 1.5;
|
||||
white-space: pre-wrap;
|
||||
overflow-wrap: break-word;
|
||||
word-break: break-word;
|
||||
}
|
||||
}
|
||||
|
||||
.llm-diff-summary-loading {
|
||||
opacity: 0.5;
|
||||
font-style: italic;
|
||||
animation: llm-pulse 1.4s ease-in-out infinite;
|
||||
}
|
||||
|
||||
@keyframes llm-pulse {
|
||||
0%, 100% { opacity: 0.5; }
|
||||
50% { opacity: 0.2; }
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
|
||||
{% from '_helpers.html' import render_field %}
|
||||
|
||||
{% macro show_token_placeholders(extra_notification_token_placeholder_info, suffix="") %}
|
||||
{% macro show_token_placeholders(extra_notification_token_placeholder_info, suffix="", settings_application=None) %}
|
||||
|
||||
|
||||
<div class="pure-controls">
|
||||
@@ -114,7 +114,25 @@
|
||||
<tr>
|
||||
<td><code>{{ '{{triggered_text}}' }}</code></td>
|
||||
<td>{{ _('Text that tripped the trigger from filters') }}</td>
|
||||
|
||||
</tr>
|
||||
{% if settings_application and settings_application.get('llm', {}).get('model') %}
|
||||
<tr>
|
||||
<td><code>{{ '{{diff}}' }}</code> <small style="opacity:0.6">{{ _('(upgraded)') }}</small></td>
|
||||
<td>{{ _('When AI Change Summary is configured, contains the AI-generated description instead of the raw diff. Falls back to raw diff when not configured.') }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>{{ '{{raw_diff}}' }}</code></td>
|
||||
<td>{{ _('Always the raw +/- diff, regardless of AI Change Summary setting.') }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>{{ '{{llm_summary}}' }}</code></td>
|
||||
<td>{{ _('The AI Change Summary text (same as the upgraded {{diff}} — explicit reference).') }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>{{ '{{llm_intent}}' }}</code></td>
|
||||
<td>{{ _('The AI Change Intent that was evaluated.') }}</td>
|
||||
</tr>
|
||||
{% endif %}
|
||||
{% if extra_notification_token_placeholder_info %}
|
||||
{% for token in extra_notification_token_placeholder_info %}
|
||||
<tr>
|
||||
@@ -173,7 +191,7 @@
|
||||
</div>
|
||||
<div class="pure-control-group">
|
||||
{{ render_field(form.notification_body , rows=5, class="notification-body", placeholder=settings_application['notification_body']) }}
|
||||
{{ show_token_placeholders(extra_notification_token_placeholder_info=extra_notification_token_placeholder_info) }}
|
||||
{{ show_token_placeholders(extra_notification_token_placeholder_info=extra_notification_token_placeholder_info, settings_application=settings_application) }}
|
||||
<div class="pure-form-message-inline">
|
||||
<ul>
|
||||
<li><span class="pure-form-message-inline">
|
||||
|
||||
@@ -69,7 +69,7 @@
|
||||
{% else %}
|
||||
{% if new_version_available and not(has_password and not current_user.is_authenticated) %}
|
||||
<span id="new-version-text" class="pure-menu-heading">
|
||||
<a href="https://changedetection.io">{{ _('A new version is available') }}</a>
|
||||
<a href="https://changedetection.io">A new version is available</a>
|
||||
</span>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
@@ -235,7 +235,7 @@
|
||||
{% if session['share-link'] %}
|
||||
<ul class="messages with-share-link">
|
||||
<li class="message">
|
||||
{{ _('Share this link:') }}
|
||||
Share this link:
|
||||
<span id="share-link">{{ session['share-link'] }}</span>
|
||||
<img style="height: 1em; display: inline-block" src="{{url_for('static_content', group='images', filename='copy.svg')}}" >
|
||||
</li>
|
||||
|
||||
126
changedetectionio/templates/edit/include_llm_intent.html
Normal file
126
changedetectionio/templates/edit/include_llm_intent.html
Normal file
@@ -0,0 +1,126 @@
|
||||
{#
|
||||
AI Intent + AI Change Summary section — shared include for watch edit and tag/group edit.
|
||||
|
||||
Required template context:
|
||||
llm_configured — bool: LLM provider is configured in settings
|
||||
form — the WTForms form (must have .llm_intent and .llm_change_summary fields)
|
||||
|
||||
Optional (watch edit only):
|
||||
watch — the Watch object (for processor check and prefilter display)
|
||||
llm_intent_source — str: 'watch', tag title, or '' (for inherited-from-tag hint)
|
||||
|
||||
Usage in watch edit (edit.html):
|
||||
{% include "edit/include_llm_intent.html" %}
|
||||
|
||||
Usage in tag edit (edit-tag.html):
|
||||
{% include "edit/include_llm_intent.html" %}
|
||||
(watch is not set → tag mode: no processor check, no examples, different description)
|
||||
#}
|
||||
|
||||
{# Watch edit: only show for text_json_diff processor #}
|
||||
{% if watch is defined and watch %}
|
||||
{% set is_text_json_diff = not watch.get('processor') or watch.get('processor') == 'text_json_diff' %}
|
||||
{% else %}
|
||||
{% set is_text_json_diff = true %}
|
||||
{% endif %}
|
||||
|
||||
{% if is_text_json_diff %}
|
||||
|
||||
{# ── Configured: show the intent + summary textareas ─────────────── #}
|
||||
{% if llm_configured %}
|
||||
<div class="border-fieldset" id="llm-intent-section">
|
||||
<h3>✨ {{ _('AI') }}</h3>
|
||||
|
||||
{# — AI Change Intent — #}
|
||||
<h4 style="margin: 0 0 0.3em 0;">{{ _('AI Change Intent') }}</h4>
|
||||
<p class="pure-form-message-inline" style="margin-top:0">
|
||||
{% if watch is defined and watch %}
|
||||
{{ _('Describe what you care about. The AI evaluates every detected change against this and only notifies you when it matches.') }}
|
||||
{% if llm_intent_source is defined and llm_intent_source and llm_intent_source != 'watch' %}
|
||||
<br><em>{{ _('Inherited from tag:') }} <strong>{{ llm_intent_source }}</strong> — {{ _('type here to override for this watch only') }}</em>
|
||||
{% endif %}
|
||||
{% else %}
|
||||
{{ _('Set a change intent for all watches in this tag/group. Each watch can override with its own.') }}
|
||||
{% endif %}
|
||||
</p>
|
||||
<div class="pure-control-group">
|
||||
<textarea name="llm_intent"
|
||||
id="llm_intent"
|
||||
rows="3"
|
||||
class="pure-input-1"
|
||||
{% if watch is defined and watch %}
|
||||
placeholder="{{ _('e.g. Alert me when the price drops below $300, or a new product is launched. Ignore footer and navigation changes.') }}"
|
||||
{% else %}
|
||||
placeholder="{{ _('e.g. Flag price changes or new product launches across all watches in this group') }}"
|
||||
{% endif %}
|
||||
>{{ form.llm_intent.data or '' }}</textarea>
|
||||
</div>
|
||||
{% if watch is defined and watch %}
|
||||
<div class="pure-form-message-inline">
|
||||
<strong>{{ _('Examples:') }}</strong>
|
||||
<ul style="margin: 0.3em 0 0 1.2em; padding: 0;">
|
||||
<li><em>{{ _('Only notify if the price drops below $200, or a limited-time deal is added') }}</em></li>
|
||||
<li><em>{{ _('Alert when a new recall, safety notice, or product withdrawal is published') }}</em></li>
|
||||
<li><em>{{ _('Notify when a new grant round opens or an application deadline is announced') }}</em></li>
|
||||
<li><em>{{ _('Only important if package versions change or a CVE is mentioned') }}</em></li>
|
||||
</ul>
|
||||
</div>
|
||||
{% if watch.get('llm_prefilter') %}
|
||||
<div class="pure-form-message-inline" style="margin-top: 0.5em;">
|
||||
<small>{{ _('AI pre-filter active:') }} <code>{{ watch.get('llm_prefilter') }}</code>
|
||||
— {{ _('narrows content scope before evaluation') }}</small>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
|
||||
<hr style="margin: 1.2em 0; border: none; border-top: 1px solid var(--color-border, #ddd);">
|
||||
|
||||
{# — AI Change Summary — #}
|
||||
<h4 style="margin: 0 0 0.3em 0;">{{ _('AI Change Summary') }}</h4>
|
||||
<p class="pure-form-message-inline" style="margin-top:0">
|
||||
{% if watch is defined and watch %}
|
||||
{{ _('When a change is detected, the AI describes it according to your instructions and replaces') }} <code>{{ '{{diff}}' }}</code> {{ _('in your notification. Use') }} <code>{{ '{{raw_diff}}' }}</code> {{ _('if you still want the original diff.') }}
|
||||
{% else %}
|
||||
{{ _('Describe how changes should be summarised in notifications for all watches in this group.') }}
|
||||
{% endif %}
|
||||
</p>
|
||||
<div class="pure-control-group">
|
||||
<textarea name="llm_change_summary"
|
||||
id="llm_change_summary"
|
||||
rows="3"
|
||||
class="pure-input-1"
|
||||
{% if watch is defined and watch %}
|
||||
placeholder="{{ _('e.g. List what was added or removed as bullet points. Translate to English.') }}"
|
||||
{% else %}
|
||||
placeholder="{{ _('e.g. Summarise new items added. Translate to English if needed.') }}"
|
||||
{% endif %}
|
||||
>{{ form.llm_change_summary.data or '' }}</textarea>
|
||||
</div>
|
||||
{% if watch is defined and watch %}
|
||||
<div class="pure-form-message-inline">
|
||||
<strong>{{ _('Examples:') }}</strong>
|
||||
<ul style="margin: 0.3em 0 0 1.2em; padding: 0;">
|
||||
<li><em>{{ _('List each new item added with its name and price. Translate to English.') }}</em></li>
|
||||
<li><em>{{ _('Summarise what events were added or cancelled. Two sentences maximum.') }}</em></li>
|
||||
<li><em>{{ _('Describe the price change: old price, new price, percentage difference.') }}</em></li>
|
||||
</ul>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
{# ── Not configured: greyed-out prompt to configure ──────────────── #}
|
||||
{% else %}
|
||||
<div class="border-fieldset" id="llm-intent-section-disabled" style="opacity: 0.5;">
|
||||
<h3>✨ {{ _('AI') }}</h3>
|
||||
<p>{{ _('Configure an AI/LLM provider in') }}
|
||||
<a href="{{ url_for('settings.settings_page') }}#ai">{{ _('Settings → AI/LLM') }}</a>
|
||||
{% if watch is defined and watch %}
|
||||
{{ _('to enable AI Change Intent and AI Change Summary.') }}
|
||||
{% else %}
|
||||
{{ _('to enable AI features for this group.') }}
|
||||
{% endif %}
|
||||
</p>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% endif %}{# is_text_json_diff #}
|
||||
0
changedetectionio/tests/llm/__init__.py
Normal file
0
changedetectionio/tests/llm/__init__.py
Normal file
560
changedetectionio/tests/llm/test_evaluator.py
Normal file
560
changedetectionio/tests/llm/test_evaluator.py
Normal file
@@ -0,0 +1,560 @@
|
||||
"""
|
||||
Unit tests for changedetectionio/llm/evaluator.py
|
||||
|
||||
Uses mocked LLM calls — no real API key needed.
|
||||
"""
|
||||
import pytest
|
||||
from unittest.mock import patch, MagicMock
|
||||
|
||||
|
||||
def _make_datastore(llm_cfg=None, tags=None):
|
||||
"""Build a minimal datastore-like dict for testing."""
|
||||
ds = MagicMock()
|
||||
app_settings = {
|
||||
'llm': llm_cfg or {},
|
||||
'tags': tags or {},
|
||||
}
|
||||
ds.data = {
|
||||
'settings': {
|
||||
'application': app_settings,
|
||||
}
|
||||
}
|
||||
return ds
|
||||
|
||||
|
||||
def _make_watch(llm_intent='', llm_change_summary='', tags=None, uuid='test-uuid-1234'):
|
||||
w = {}
|
||||
w['llm_intent'] = llm_intent
|
||||
w['llm_change_summary'] = llm_change_summary
|
||||
w['tags'] = tags or []
|
||||
w['uuid'] = uuid
|
||||
w['url'] = 'https://example.com'
|
||||
w['page_title'] = 'Test Page'
|
||||
w['llm_evaluation_cache'] = {}
|
||||
w['llm_prefilter'] = None
|
||||
return w
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# resolve_intent
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
class TestResolveIntent:
|
||||
def test_watch_intent_takes_priority(self):
|
||||
from changedetectionio.llm.evaluator import resolve_intent
|
||||
|
||||
tag = {'title': 'mygroup', 'llm_intent': 'group intent'}
|
||||
ds = _make_datastore(tags={'tag-1': tag})
|
||||
watch = _make_watch(llm_intent='watch intent', tags=['tag-1'])
|
||||
|
||||
intent, source = resolve_intent(watch, ds)
|
||||
assert intent == 'watch intent'
|
||||
assert source == 'watch'
|
||||
|
||||
def test_tag_intent_used_when_watch_has_none(self):
|
||||
from changedetectionio.llm.evaluator import resolve_intent
|
||||
|
||||
tag = {'title': 'pricing-group', 'llm_intent': 'flag price drops'}
|
||||
ds = _make_datastore(tags={'tag-1': tag})
|
||||
watch = _make_watch(llm_intent='', tags=['tag-1'])
|
||||
|
||||
intent, source = resolve_intent(watch, ds)
|
||||
assert intent == 'flag price drops'
|
||||
assert source == 'pricing-group'
|
||||
|
||||
def test_no_intent_anywhere_returns_empty(self):
|
||||
from changedetectionio.llm.evaluator import resolve_intent
|
||||
|
||||
ds = _make_datastore()
|
||||
watch = _make_watch(llm_intent='')
|
||||
|
||||
intent, source = resolve_intent(watch, ds)
|
||||
assert intent == ''
|
||||
assert source == ''
|
||||
|
||||
def test_tag_applied_to_all_watches_in_group(self):
|
||||
"""Tag intent propagates to every watch in the tag (no opt-in needed)."""
|
||||
from changedetectionio.llm.evaluator import resolve_intent
|
||||
|
||||
tag = {'title': 'job-board', 'llm_intent': 'new engineering jobs'}
|
||||
ds = _make_datastore(tags={'tag-1': tag})
|
||||
|
||||
# Three different watches, all in the tag, none have their own intent
|
||||
for watch_uuid in ['uuid-A', 'uuid-B', 'uuid-C']:
|
||||
watch = _make_watch(llm_intent='', tags=['tag-1'], uuid=watch_uuid)
|
||||
intent, source = resolve_intent(watch, ds)
|
||||
assert intent == 'new engineering jobs', f"Watch {watch_uuid} should inherit tag intent"
|
||||
assert source == 'job-board'
|
||||
|
||||
def test_whitespace_only_intent_treated_as_empty(self):
|
||||
from changedetectionio.llm.evaluator import resolve_intent
|
||||
|
||||
ds = _make_datastore()
|
||||
watch = _make_watch(llm_intent=' ')
|
||||
intent, source = resolve_intent(watch, ds)
|
||||
assert intent == ''
|
||||
|
||||
def test_missing_tag_in_datastore_skipped(self):
|
||||
from changedetectionio.llm.evaluator import resolve_intent
|
||||
|
||||
ds = _make_datastore(tags={}) # no tags registered
|
||||
watch = _make_watch(llm_intent='', tags=['nonexistent-tag'])
|
||||
intent, source = resolve_intent(watch, ds)
|
||||
assert intent == ''
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# get_llm_config
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
class TestGetLlmConfig:
|
||||
def test_returns_none_when_no_model(self):
|
||||
from changedetectionio.llm.evaluator import get_llm_config
|
||||
ds = _make_datastore(llm_cfg={})
|
||||
assert get_llm_config(ds) is None
|
||||
|
||||
def test_returns_config_when_model_set(self):
|
||||
from changedetectionio.llm.evaluator import get_llm_config
|
||||
cfg = {'model': 'gpt-4o-mini', 'api_key': 'sk-test'}
|
||||
ds = _make_datastore(llm_cfg=cfg)
|
||||
result = get_llm_config(ds)
|
||||
assert result['model'] == 'gpt-4o-mini'
|
||||
|
||||
def test_env_var_overrides_datastore(self):
|
||||
"""LLM_MODEL env var takes priority over datastore settings."""
|
||||
from changedetectionio.llm.evaluator import get_llm_config
|
||||
ds = _make_datastore(llm_cfg={'model': 'datastore-model'})
|
||||
with patch.dict('os.environ', {'LLM_MODEL': 'ollama/llama3.2', 'LLM_API_KEY': '', 'LLM_API_BASE': ''}):
|
||||
result = get_llm_config(ds)
|
||||
assert result['model'] == 'ollama/llama3.2'
|
||||
|
||||
def test_env_var_api_key_and_base_included(self):
|
||||
"""LLM_API_KEY and LLM_API_BASE are picked up alongside LLM_MODEL."""
|
||||
from changedetectionio.llm.evaluator import get_llm_config
|
||||
ds = _make_datastore()
|
||||
env = {'LLM_MODEL': 'gpt-4o', 'LLM_API_KEY': 'env-key', 'LLM_API_BASE': 'http://localhost:11434'}
|
||||
with patch.dict('os.environ', env):
|
||||
result = get_llm_config(ds)
|
||||
assert result['api_key'] == 'env-key'
|
||||
assert result['api_base'] == 'http://localhost:11434'
|
||||
|
||||
def test_llm_configured_via_env_true_when_model_set(self):
|
||||
"""llm_configured_via_env() returns True when LLM_MODEL is set."""
|
||||
from changedetectionio.llm.evaluator import llm_configured_via_env
|
||||
with patch.dict('os.environ', {'LLM_MODEL': 'gpt-4o-mini'}):
|
||||
assert llm_configured_via_env() is True
|
||||
|
||||
def test_llm_configured_via_env_false_when_not_set(self):
|
||||
"""llm_configured_via_env() returns False when LLM_MODEL is absent."""
|
||||
from changedetectionio.llm.evaluator import llm_configured_via_env
|
||||
env = {k: '' for k in ['LLM_MODEL', 'LLM_API_KEY', 'LLM_API_BASE']}
|
||||
with patch.dict('os.environ', env, clear=False):
|
||||
# Ensure LLM_MODEL is truly absent
|
||||
import os
|
||||
os.environ.pop('LLM_MODEL', None)
|
||||
assert llm_configured_via_env() is False
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# evaluate_change
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
class TestEvaluateChange:
|
||||
def test_returns_none_when_llm_not_configured(self):
|
||||
from changedetectionio.llm.evaluator import evaluate_change
|
||||
ds = _make_datastore(llm_cfg={}) # no model
|
||||
watch = _make_watch(llm_intent='flag price drops')
|
||||
result = evaluate_change(watch, ds, diff='- $500\n+ $400')
|
||||
assert result is None
|
||||
|
||||
def test_returns_none_when_no_intent(self):
|
||||
from changedetectionio.llm.evaluator import evaluate_change
|
||||
ds = _make_datastore(llm_cfg={'model': 'gpt-4o-mini'})
|
||||
watch = _make_watch(llm_intent='')
|
||||
result = evaluate_change(watch, ds, diff='some diff')
|
||||
assert result is None
|
||||
|
||||
def test_returns_not_important_for_empty_diff(self):
|
||||
from changedetectionio.llm.evaluator import evaluate_change
|
||||
ds = _make_datastore(llm_cfg={'model': 'gpt-4o-mini'})
|
||||
watch = _make_watch(llm_intent='flag price drops')
|
||||
result = evaluate_change(watch, ds, diff='')
|
||||
assert result == {'important': False, 'summary': ''}
|
||||
|
||||
def test_returns_not_important_for_whitespace_diff(self):
|
||||
from changedetectionio.llm.evaluator import evaluate_change
|
||||
ds = _make_datastore(llm_cfg={'model': 'gpt-4o-mini'})
|
||||
watch = _make_watch(llm_intent='flag price drops')
|
||||
result = evaluate_change(watch, ds, diff=' \n ')
|
||||
assert result == {'important': False, 'summary': ''}
|
||||
|
||||
def test_calls_llm_and_returns_result(self):
|
||||
from changedetectionio.llm.evaluator import evaluate_change
|
||||
|
||||
ds = _make_datastore(llm_cfg={'model': 'gpt-4o-mini', 'api_key': 'sk-test'})
|
||||
watch = _make_watch(llm_intent='flag price drops')
|
||||
|
||||
llm_response = '{"important": true, "summary": "Price dropped from $500 to $400"}'
|
||||
with patch('changedetectionio.llm.client.completion', return_value=(llm_response, 150)):
|
||||
result = evaluate_change(watch, ds, diff='- $500\n+ $400')
|
||||
|
||||
assert result['important'] is True
|
||||
assert 'Price dropped' in result['summary']
|
||||
|
||||
def test_cache_hit_skips_llm_call(self):
|
||||
from changedetectionio.llm.evaluator import evaluate_change
|
||||
import hashlib
|
||||
|
||||
ds = _make_datastore(llm_cfg={'model': 'gpt-4o-mini', 'api_key': 'sk-test'})
|
||||
watch = _make_watch(llm_intent='flag price drops')
|
||||
|
||||
diff = '- $500\n+ $400'
|
||||
intent = 'flag price drops'
|
||||
cache_key = hashlib.sha256(f"{intent}||{diff}".encode()).hexdigest()
|
||||
watch['llm_evaluation_cache'] = {
|
||||
cache_key: {'important': True, 'summary': 'cached result'}
|
||||
}
|
||||
|
||||
with patch('changedetectionio.llm.client.completion') as mock_llm:
|
||||
result = evaluate_change(watch, ds, diff=diff)
|
||||
mock_llm.assert_not_called()
|
||||
|
||||
assert result['summary'] == 'cached result'
|
||||
|
||||
def test_llm_failure_returns_important_true(self):
|
||||
"""On LLM error, notification should NOT be suppressed (fail open)."""
|
||||
from changedetectionio.llm.evaluator import evaluate_change
|
||||
|
||||
ds = _make_datastore(llm_cfg={'model': 'gpt-4o-mini', 'api_key': 'sk-test'})
|
||||
watch = _make_watch(llm_intent='flag price drops')
|
||||
|
||||
with patch('changedetectionio.llm.client.completion', side_effect=Exception('API timeout')):
|
||||
result = evaluate_change(watch, ds, diff='- $500\n+ $400')
|
||||
|
||||
assert result['important'] is True
|
||||
assert result['summary'] == ''
|
||||
|
||||
def test_unimportant_result_from_llm(self):
|
||||
from changedetectionio.llm.evaluator import evaluate_change
|
||||
|
||||
ds = _make_datastore(llm_cfg={'model': 'gpt-4o-mini'})
|
||||
watch = _make_watch(llm_intent='only alert on price drops')
|
||||
|
||||
llm_response = '{"important": false, "summary": "Only a footer copyright year changed"}'
|
||||
with patch('changedetectionio.llm.client.completion', return_value=(llm_response, 45)):
|
||||
result = evaluate_change(watch, ds, diff='- Copyright 2023\n+ Copyright 2024')
|
||||
|
||||
assert result['important'] is False
|
||||
assert 'footer' in result['summary'].lower() or 'copyright' in result['summary'].lower()
|
||||
|
||||
def test_last_tokens_used_stored_after_eval(self):
|
||||
"""watch['llm_last_tokens_used'] is set to the token count after a successful call."""
|
||||
from changedetectionio.llm.evaluator import evaluate_change
|
||||
|
||||
ds = _make_datastore(llm_cfg={'model': 'gpt-4o-mini'})
|
||||
watch = _make_watch(llm_intent='flag price drops')
|
||||
|
||||
llm_response = '{"important": true, "summary": "Price fell"}'
|
||||
with patch('changedetectionio.llm.client.completion', return_value=(llm_response, 123)):
|
||||
evaluate_change(watch, ds, diff='- $500\n+ $300')
|
||||
|
||||
assert watch.get('llm_last_tokens_used') == 123
|
||||
|
||||
def test_cumulative_tokens_accumulate_across_evals(self):
|
||||
"""Each eval adds its tokens to watch['llm_tokens_used_cumulative']."""
|
||||
from changedetectionio.llm.evaluator import evaluate_change
|
||||
|
||||
ds = _make_datastore(llm_cfg={'model': 'gpt-4o-mini'})
|
||||
watch = _make_watch(llm_intent='flag price drops')
|
||||
|
||||
resp1 = '{"important": true, "summary": "First"}'
|
||||
resp2 = '{"important": false, "summary": "Second"}'
|
||||
|
||||
with patch('changedetectionio.llm.client.completion', return_value=(resp1, 80)):
|
||||
evaluate_change(watch, ds, diff='- $500\n+ $400')
|
||||
|
||||
# Second call needs a different diff to avoid cache hit
|
||||
with patch('changedetectionio.llm.client.completion', return_value=(resp2, 60)):
|
||||
evaluate_change(watch, ds, diff='- $400\n+ $350')
|
||||
|
||||
assert watch.get('llm_tokens_used_cumulative') == 140
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Token budget enforcement
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
class TestTokenBudget:
|
||||
def test_no_limits_always_returns_true(self):
|
||||
"""When no limits configured, budget check always passes."""
|
||||
from changedetectionio.llm.evaluator import _check_token_budget
|
||||
|
||||
watch = _make_watch()
|
||||
cfg = {} # no limits
|
||||
|
||||
assert _check_token_budget(watch, cfg, tokens_this_call=10_000) is True
|
||||
|
||||
def test_per_check_limit_exceeded_returns_false(self):
|
||||
"""Tokens on this call exceeding per-check limit → False."""
|
||||
from changedetectionio.llm.evaluator import _check_token_budget
|
||||
|
||||
watch = _make_watch()
|
||||
cfg = {'max_tokens_per_check': 100}
|
||||
|
||||
result = _check_token_budget(watch, cfg, tokens_this_call=150)
|
||||
assert result is False
|
||||
|
||||
def test_per_check_limit_not_exceeded_returns_true(self):
|
||||
"""Tokens on this call within per-check limit → True."""
|
||||
from changedetectionio.llm.evaluator import _check_token_budget
|
||||
|
||||
watch = _make_watch()
|
||||
cfg = {'max_tokens_per_check': 200}
|
||||
|
||||
result = _check_token_budget(watch, cfg, tokens_this_call=150)
|
||||
assert result is True
|
||||
|
||||
def test_cumulative_limit_exceeded_returns_false(self):
|
||||
"""Total accumulated tokens exceeding cumulative limit → False."""
|
||||
from changedetectionio.llm.evaluator import _check_token_budget
|
||||
|
||||
watch = _make_watch()
|
||||
watch['llm_tokens_used_cumulative'] = 900
|
||||
cfg = {'max_tokens_cumulative': 1000}
|
||||
|
||||
# This call adds 200 → total 1100 > 1000
|
||||
result = _check_token_budget(watch, cfg, tokens_this_call=200)
|
||||
assert result is False
|
||||
|
||||
def test_cumulative_limit_not_yet_exceeded_returns_true(self):
|
||||
"""Total accumulated tokens within cumulative limit → True."""
|
||||
from changedetectionio.llm.evaluator import _check_token_budget
|
||||
|
||||
watch = _make_watch()
|
||||
watch['llm_tokens_used_cumulative'] = 500
|
||||
cfg = {'max_tokens_cumulative': 1000}
|
||||
|
||||
result = _check_token_budget(watch, cfg, tokens_this_call=100)
|
||||
assert result is True
|
||||
|
||||
def test_tokens_accumulated_into_watch(self):
|
||||
"""tokens_this_call is added to watch['llm_tokens_used_cumulative']."""
|
||||
from changedetectionio.llm.evaluator import _check_token_budget
|
||||
|
||||
watch = _make_watch()
|
||||
watch['llm_tokens_used_cumulative'] = 300
|
||||
cfg = {}
|
||||
|
||||
_check_token_budget(watch, cfg, tokens_this_call=75)
|
||||
assert watch['llm_tokens_used_cumulative'] == 375
|
||||
|
||||
def test_zero_tokens_call_does_not_change_cumulative(self):
|
||||
"""Calling with tokens_this_call=0 (pre-flight check) doesn't modify cumulative."""
|
||||
from changedetectionio.llm.evaluator import _check_token_budget
|
||||
|
||||
watch = _make_watch()
|
||||
watch['llm_tokens_used_cumulative'] = 200
|
||||
cfg = {}
|
||||
|
||||
_check_token_budget(watch, cfg, tokens_this_call=0)
|
||||
assert watch['llm_tokens_used_cumulative'] == 200
|
||||
|
||||
def test_evaluate_change_skips_call_when_cumulative_over_budget(self):
|
||||
"""Pre-flight cumulative check: if already over budget, skip LLM call and fail open."""
|
||||
from changedetectionio.llm.evaluator import evaluate_change
|
||||
|
||||
ds = _make_datastore(llm_cfg={'model': 'gpt-4o-mini', 'max_tokens_cumulative': 100})
|
||||
watch = _make_watch(llm_intent='flag price drops')
|
||||
watch['llm_tokens_used_cumulative'] = 500 # already far over
|
||||
|
||||
with patch('changedetectionio.llm.client.completion') as mock_llm:
|
||||
result = evaluate_change(watch, ds, diff='- $500\n+ $400')
|
||||
mock_llm.assert_not_called()
|
||||
|
||||
# Fail open: important=True so the notification is NOT suppressed
|
||||
assert result == {'important': True, 'summary': ''}
|
||||
|
||||
def test_evaluate_change_per_check_limit_fails_open(self):
|
||||
"""Per-check token exceeded after call → result still returned (fail open)."""
|
||||
from changedetectionio.llm.evaluator import evaluate_change
|
||||
|
||||
# max_tokens_per_check is 50, but the call returns 150 tokens
|
||||
ds = _make_datastore(llm_cfg={'model': 'gpt-4o-mini', 'max_tokens_per_check': 50})
|
||||
watch = _make_watch(llm_intent='flag price drops')
|
||||
|
||||
llm_response = '{"important": false, "summary": "Only minor change"}'
|
||||
with patch('changedetectionio.llm.client.completion', return_value=(llm_response, 150)):
|
||||
result = evaluate_change(watch, ds, diff='- $500\n+ $499')
|
||||
|
||||
# LLM said not important, but even with per-check warning the result is returned
|
||||
# (budget warning is logged but evaluation result is still used)
|
||||
assert result is not None
|
||||
assert 'important' in result
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# resolve_llm_field (generic cascade)
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
class TestResolveLlmField:
|
||||
def test_watch_value_takes_priority(self):
|
||||
from changedetectionio.llm.evaluator import resolve_llm_field
|
||||
tag = {'title': 'mygroup', 'llm_change_summary': 'tag summary prompt'}
|
||||
ds = _make_datastore(tags={'tag-1': tag})
|
||||
watch = _make_watch(llm_change_summary='watch summary prompt', tags=['tag-1'])
|
||||
value, source = resolve_llm_field(watch, ds, 'llm_change_summary')
|
||||
assert value == 'watch summary prompt'
|
||||
assert source == 'watch'
|
||||
|
||||
def test_tag_value_used_when_watch_empty(self):
|
||||
from changedetectionio.llm.evaluator import resolve_llm_field
|
||||
tag = {'title': 'events-group', 'llm_change_summary': 'list new events'}
|
||||
ds = _make_datastore(tags={'tag-1': tag})
|
||||
watch = _make_watch(llm_change_summary='', tags=['tag-1'])
|
||||
value, source = resolve_llm_field(watch, ds, 'llm_change_summary')
|
||||
assert value == 'list new events'
|
||||
assert source == 'events-group'
|
||||
|
||||
def test_returns_empty_when_not_set_anywhere(self):
|
||||
from changedetectionio.llm.evaluator import resolve_llm_field
|
||||
ds = _make_datastore()
|
||||
watch = _make_watch()
|
||||
value, source = resolve_llm_field(watch, ds, 'llm_change_summary')
|
||||
assert value == ''
|
||||
assert source == ''
|
||||
|
||||
def test_works_for_llm_intent_field_too(self):
|
||||
"""resolve_llm_field is generic — works for llm_intent same as llm_change_summary."""
|
||||
from changedetectionio.llm.evaluator import resolve_llm_field
|
||||
tag = {'title': 'grp', 'llm_intent': 'flag price drops'}
|
||||
ds = _make_datastore(tags={'t1': tag})
|
||||
watch = _make_watch(llm_intent='', tags=['t1'])
|
||||
value, source = resolve_llm_field(watch, ds, 'llm_intent')
|
||||
assert value == 'flag price drops'
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# summarise_change
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
class TestSummariseChange:
|
||||
def test_returns_empty_when_llm_not_configured(self):
|
||||
from changedetectionio.llm.evaluator import summarise_change
|
||||
ds = _make_datastore(llm_cfg={})
|
||||
watch = _make_watch(llm_change_summary='List what changed')
|
||||
result = summarise_change(watch, ds, diff='- old\n+ new')
|
||||
assert result == ''
|
||||
|
||||
def test_uses_default_prompt_when_no_summary_prompt(self):
|
||||
"""When llm_change_summary is empty, falls back to DEFAULT_CHANGE_SUMMARY_PROMPT."""
|
||||
from changedetectionio.llm.evaluator import summarise_change, DEFAULT_CHANGE_SUMMARY_PROMPT
|
||||
ds = _make_datastore(llm_cfg={'model': 'gpt-4o-mini', 'api_key': 'sk-test'})
|
||||
watch = _make_watch(llm_change_summary='')
|
||||
with patch('changedetectionio.llm.client.completion',
|
||||
return_value=('A new item was added.', 40)) as mock_llm:
|
||||
result = summarise_change(watch, ds, diff='- old\n+ new')
|
||||
mock_llm.assert_called_once()
|
||||
# Default prompt must appear in the user message
|
||||
call_messages = mock_llm.call_args.kwargs['messages']
|
||||
user_msg = next(m['content'] for m in call_messages if m['role'] == 'user')
|
||||
assert DEFAULT_CHANGE_SUMMARY_PROMPT in user_msg
|
||||
assert result == 'A new item was added.'
|
||||
|
||||
def test_returns_empty_when_diff_empty(self):
|
||||
from changedetectionio.llm.evaluator import summarise_change
|
||||
ds = _make_datastore(llm_cfg={'model': 'gpt-4o-mini'})
|
||||
watch = _make_watch(llm_change_summary='List what changed')
|
||||
with patch('changedetectionio.llm.client.completion') as mock_llm:
|
||||
result = summarise_change(watch, ds, diff='')
|
||||
mock_llm.assert_not_called()
|
||||
assert result == ''
|
||||
|
||||
def test_calls_llm_and_returns_plain_text(self):
|
||||
from changedetectionio.llm.evaluator import summarise_change
|
||||
ds = _make_datastore(llm_cfg={'model': 'gpt-4o-mini', 'api_key': 'sk-test'})
|
||||
watch = _make_watch(llm_change_summary='List new events in English')
|
||||
with patch('changedetectionio.llm.client.completion',
|
||||
return_value=('3 new events added: Jazz Night, Art Show, Comedy Gig', 80)):
|
||||
result = summarise_change(watch, ds, diff='+ Jazz Night\n+ Art Show\n+ Comedy Gig')
|
||||
assert 'Jazz Night' in result
|
||||
assert 'Art Show' in result
|
||||
|
||||
def test_cascades_from_tag(self):
|
||||
"""llm_change_summary on a tag propagates to watches in that tag."""
|
||||
from changedetectionio.llm.evaluator import summarise_change
|
||||
tag = {'title': 'events', 'llm_change_summary': 'Translate events to English'}
|
||||
ds = _make_datastore(llm_cfg={'model': 'gpt-4o-mini'}, tags={'tag-1': tag})
|
||||
watch = _make_watch(llm_change_summary='', tags=['tag-1'])
|
||||
with patch('changedetectionio.llm.client.completion',
|
||||
return_value=('New concert added on Friday', 60)):
|
||||
result = summarise_change(watch, ds, diff='+ Konzert am Freitag')
|
||||
assert result == 'New concert added on Friday'
|
||||
|
||||
def test_llm_failure_returns_empty_string(self):
|
||||
"""On LLM error, returns '' — the caller falls back to raw diff."""
|
||||
from changedetectionio.llm.evaluator import summarise_change
|
||||
ds = _make_datastore(llm_cfg={'model': 'gpt-4o-mini'})
|
||||
watch = _make_watch(llm_change_summary='Describe the change')
|
||||
with patch('changedetectionio.llm.client.completion', side_effect=Exception('timeout')):
|
||||
result = summarise_change(watch, ds, diff='- old\n+ new')
|
||||
assert result == ''
|
||||
|
||||
def test_uses_higher_token_limit_than_eval(self):
|
||||
"""summarise_change passes max_tokens=500 to client, not the default 200."""
|
||||
from changedetectionio.llm.evaluator import summarise_change, _MAX_SUMMARY_TOKENS
|
||||
ds = _make_datastore(llm_cfg={'model': 'gpt-4o-mini'})
|
||||
watch = _make_watch(llm_change_summary='Describe changes')
|
||||
with patch('changedetectionio.llm.client.completion',
|
||||
return_value=('Some summary', 100)) as mock_llm:
|
||||
summarise_change(watch, ds, diff='- old\n+ new')
|
||||
call_kwargs = mock_llm.call_args
|
||||
assert call_kwargs.kwargs.get('max_tokens') == _MAX_SUMMARY_TOKENS
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# compute_summary_cache_key / get_effective_summary_prompt
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
class TestSummaryCacheKey:
|
||||
def test_same_inputs_produce_same_key(self):
|
||||
from changedetectionio.llm.evaluator import compute_summary_cache_key
|
||||
key1 = compute_summary_cache_key('+ new line', 'describe changes')
|
||||
key2 = compute_summary_cache_key('+ new line', 'describe changes')
|
||||
assert key1 == key2
|
||||
|
||||
def test_different_diff_produces_different_key(self):
|
||||
from changedetectionio.llm.evaluator import compute_summary_cache_key
|
||||
key1 = compute_summary_cache_key('+ line A', 'prompt')
|
||||
key2 = compute_summary_cache_key('+ line B', 'prompt')
|
||||
assert key1 != key2
|
||||
|
||||
def test_different_prompt_produces_different_key(self):
|
||||
from changedetectionio.llm.evaluator import compute_summary_cache_key
|
||||
key1 = compute_summary_cache_key('diff', 'list changes')
|
||||
key2 = compute_summary_cache_key('diff', 'translate to English')
|
||||
assert key1 != key2
|
||||
|
||||
def test_key_is_16_hex_chars(self):
|
||||
from changedetectionio.llm.evaluator import compute_summary_cache_key
|
||||
key = compute_summary_cache_key('diff', 'prompt')
|
||||
assert len(key) == 16
|
||||
assert all(c in '0123456789abcdef' for c in key)
|
||||
|
||||
def test_get_effective_prompt_returns_custom_when_set(self):
|
||||
from changedetectionio.llm.evaluator import get_effective_summary_prompt
|
||||
ds = _make_datastore()
|
||||
watch = _make_watch(llm_change_summary='My custom prompt')
|
||||
assert get_effective_summary_prompt(watch, ds) == 'My custom prompt'
|
||||
|
||||
def test_get_effective_prompt_returns_default_when_empty(self):
|
||||
from changedetectionio.llm.evaluator import get_effective_summary_prompt, DEFAULT_CHANGE_SUMMARY_PROMPT
|
||||
ds = _make_datastore()
|
||||
watch = _make_watch(llm_change_summary='')
|
||||
assert get_effective_summary_prompt(watch, ds) == DEFAULT_CHANGE_SUMMARY_PROMPT
|
||||
|
||||
def test_get_effective_prompt_cascades_from_tag(self):
|
||||
from changedetectionio.llm.evaluator import get_effective_summary_prompt
|
||||
tag = {'title': 'grp', 'llm_change_summary': 'tag-level prompt'}
|
||||
ds = _make_datastore(tags={'t1': tag})
|
||||
watch = _make_watch(llm_change_summary='', tags=['t1'])
|
||||
assert get_effective_summary_prompt(watch, ds) == 'tag-level prompt'
|
||||
325
changedetectionio/tests/llm/test_notification_tokens.py
Normal file
325
changedetectionio/tests/llm/test_notification_tokens.py
Normal file
@@ -0,0 +1,325 @@
|
||||
"""
|
||||
Tests that {{ llm_summary }} and {{ llm_intent }} notification tokens
|
||||
are correctly populated in the notification pipeline.
|
||||
|
||||
Covers:
|
||||
1. notification/handler.py — lazy population logic (lines 367-372)
|
||||
2. notification_service.py — _llm_result / _llm_intent from watch → n_object
|
||||
3. End-to-end: tokens render in notification body/title
|
||||
"""
|
||||
import pytest
|
||||
from unittest.mock import MagicMock, patch
|
||||
from changedetectionio.notification_service import NotificationContextData
|
||||
|
||||
|
||||
def _make_n_object(**extra):
|
||||
n = NotificationContextData()
|
||||
n.update({
|
||||
'notification_body': '',
|
||||
'notification_title': '',
|
||||
'notification_format': 'text',
|
||||
'notification_urls': ['json://localhost/'],
|
||||
'uuid': 'test-uuid',
|
||||
'watch_uuid': 'test-uuid',
|
||||
'watch_url': 'https://example.com',
|
||||
'current_snapshot': 'current text',
|
||||
'prev_snapshot': 'previous text',
|
||||
})
|
||||
n.update(extra)
|
||||
return n
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# handler.py — lazy population of llm_summary / llm_intent
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
class TestHandlerLlmTokenPopulation:
|
||||
"""
|
||||
The notification handler checks if llm_summary or llm_intent tokens appear
|
||||
in the notification text and lazily populates them from _llm_result.
|
||||
"""
|
||||
|
||||
def _run_handler_llm_section(self, n_object):
|
||||
"""
|
||||
Replicate the exact logic from notification/handler.py lines 367-372.
|
||||
This is tested directly to validate the handler's token population.
|
||||
"""
|
||||
scan_text = n_object.get('notification_body', '') + n_object.get('notification_title', '')
|
||||
if 'llm_summary' in scan_text or 'llm_intent' in scan_text:
|
||||
llm_result = n_object.get('_llm_result') or {}
|
||||
n_object['llm_summary'] = llm_result.get('summary', '')
|
||||
n_object['llm_intent'] = n_object.get('_llm_intent', '')
|
||||
return n_object
|
||||
|
||||
def test_llm_summary_populated_when_token_in_body(self):
|
||||
n = _make_n_object(
|
||||
notification_body='Change detected! Summary: {{ llm_summary }}',
|
||||
_llm_result={'important': True, 'summary': 'Price dropped from $500 to $400'},
|
||||
_llm_intent='flag price drops',
|
||||
)
|
||||
result = self._run_handler_llm_section(n)
|
||||
assert result['llm_summary'] == 'Price dropped from $500 to $400'
|
||||
|
||||
def test_llm_intent_populated_when_token_in_body(self):
|
||||
n = _make_n_object(
|
||||
notification_body='Intent was: {{ llm_intent }}',
|
||||
_llm_result={'important': True, 'summary': 'some change'},
|
||||
_llm_intent='flag price drops',
|
||||
)
|
||||
result = self._run_handler_llm_section(n)
|
||||
assert result['llm_intent'] == 'flag price drops'
|
||||
|
||||
def test_llm_summary_in_title(self):
|
||||
n = _make_n_object(
|
||||
notification_title='[CD] {{ llm_summary }}',
|
||||
notification_body='some body',
|
||||
_llm_result={'important': True, 'summary': 'New job posted'},
|
||||
_llm_intent='new jobs',
|
||||
)
|
||||
result = self._run_handler_llm_section(n)
|
||||
assert result['llm_summary'] == 'New job posted'
|
||||
|
||||
def test_tokens_not_populated_when_absent_from_template(self):
|
||||
"""Don't bother populating when tokens aren't used — avoid needless LLM calls."""
|
||||
n = _make_n_object(
|
||||
notification_body='Change at {{ watch_url }}',
|
||||
notification_title='CD Alert',
|
||||
_llm_result={'important': True, 'summary': 'should not appear'},
|
||||
_llm_intent='test',
|
||||
)
|
||||
result = self._run_handler_llm_section(n)
|
||||
# llm_summary and llm_intent should remain at their default None values
|
||||
assert result.get('llm_summary') is None
|
||||
assert result.get('llm_intent') is None
|
||||
|
||||
def test_empty_summary_when_no_llm_result(self):
|
||||
n = _make_n_object(
|
||||
notification_body='Summary: {{ llm_summary }}',
|
||||
_llm_result=None,
|
||||
_llm_intent='',
|
||||
)
|
||||
result = self._run_handler_llm_section(n)
|
||||
assert result['llm_summary'] == ''
|
||||
|
||||
def test_empty_intent_when_not_set(self):
|
||||
n = _make_n_object(
|
||||
notification_body='Intent: {{ llm_intent }}',
|
||||
_llm_result={'important': False, 'summary': ''},
|
||||
)
|
||||
result = self._run_handler_llm_section(n)
|
||||
assert result['llm_intent'] == ''
|
||||
|
||||
def test_summary_from_unimportant_result(self):
|
||||
"""Even when important=False the summary explains why — useful for debugging."""
|
||||
n = _make_n_object(
|
||||
notification_body='Summary: {{ llm_summary }}',
|
||||
_llm_result={'important': False, 'summary': 'Only a copyright year changed'},
|
||||
_llm_intent='flag price drops',
|
||||
)
|
||||
result = self._run_handler_llm_section(n)
|
||||
assert result['llm_summary'] == 'Only a copyright year changed'
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# notification_service.py — _llm_result / _llm_intent wired from watch
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
class TestNotificationServiceLlmAttachment:
|
||||
"""
|
||||
send_content_changed_notification() reads _llm_result and _llm_intent
|
||||
from the watch object and attaches them to n_object so the handler can render tokens.
|
||||
"""
|
||||
|
||||
def _make_watch(self, llm_result=None, llm_intent=''):
|
||||
watch = MagicMock()
|
||||
watch.get.side_effect = lambda key, default=None: {
|
||||
'_llm_result': llm_result,
|
||||
'_llm_intent': llm_intent,
|
||||
'notification_urls': ['json://localhost/'],
|
||||
'notification_title': '',
|
||||
'notification_body': '',
|
||||
'notification_format': 'text',
|
||||
'notification_muted': False,
|
||||
'notification_alert_count': 0,
|
||||
}.get(key, default)
|
||||
watch.history = {'1000': 'snap1', '2000': 'snap2'}
|
||||
watch.get_history_snapshot = MagicMock(return_value='snapshot text')
|
||||
watch.extra_notification_token_values = MagicMock(return_value={})
|
||||
return watch
|
||||
|
||||
def test_llm_result_attached_to_n_object(self):
|
||||
"""_llm_result from watch ends up in n_object for the notification handler."""
|
||||
from changedetectionio.notification_service import NotificationService
|
||||
|
||||
llm_result = {'important': True, 'summary': 'Price dropped'}
|
||||
watch = self._make_watch(llm_result=llm_result, llm_intent='flag price drops')
|
||||
|
||||
datastore = MagicMock()
|
||||
datastore.data = {
|
||||
'settings': {
|
||||
'application': {
|
||||
'active_base_url': 'http://localhost',
|
||||
'notification_urls': [],
|
||||
'notification_title': '',
|
||||
'notification_body': '',
|
||||
'notification_format': 'text',
|
||||
'notification_muted': False,
|
||||
}
|
||||
},
|
||||
'watching': {'test-uuid': watch},
|
||||
}
|
||||
datastore.get_all_tags_for_watch = MagicMock(return_value={})
|
||||
|
||||
captured = {}
|
||||
|
||||
def fake_queue_notification(n_object, watch, **kwargs):
|
||||
captured['n_object'] = dict(n_object)
|
||||
|
||||
svc = NotificationService(datastore, MagicMock())
|
||||
svc.queue_notification_for_watch = fake_queue_notification
|
||||
|
||||
svc.send_content_changed_notification('test-uuid')
|
||||
|
||||
assert '_llm_result' in captured['n_object']
|
||||
assert captured['n_object']['_llm_result'] == llm_result
|
||||
|
||||
def test_llm_intent_attached_to_n_object(self):
|
||||
"""_llm_intent from watch ends up in n_object."""
|
||||
from changedetectionio.notification_service import NotificationService
|
||||
|
||||
watch = self._make_watch(
|
||||
llm_result={'important': True, 'summary': 'test'},
|
||||
llm_intent='flag price drops',
|
||||
)
|
||||
|
||||
datastore = MagicMock()
|
||||
datastore.data = {
|
||||
'settings': {
|
||||
'application': {
|
||||
'active_base_url': 'http://localhost',
|
||||
'notification_urls': [],
|
||||
'notification_title': '',
|
||||
'notification_body': '',
|
||||
'notification_format': 'text',
|
||||
'notification_muted': False,
|
||||
}
|
||||
},
|
||||
'watching': {'test-uuid': watch},
|
||||
}
|
||||
datastore.get_all_tags_for_watch = MagicMock(return_value={})
|
||||
|
||||
captured = {}
|
||||
|
||||
def fake_queue_notification(n_object, watch, **kwargs):
|
||||
captured['n_object'] = dict(n_object)
|
||||
|
||||
svc = NotificationService(datastore, MagicMock())
|
||||
svc.queue_notification_for_watch = fake_queue_notification
|
||||
|
||||
svc.send_content_changed_notification('test-uuid')
|
||||
|
||||
assert captured['n_object']['_llm_intent'] == 'flag price drops'
|
||||
|
||||
def test_null_llm_result_when_no_evaluation(self):
|
||||
"""When LLM wasn't evaluated, _llm_result is None — tokens render as empty."""
|
||||
from changedetectionio.notification_service import NotificationService
|
||||
|
||||
watch = self._make_watch(llm_result=None, llm_intent='')
|
||||
|
||||
datastore = MagicMock()
|
||||
datastore.data = {
|
||||
'settings': {
|
||||
'application': {
|
||||
'active_base_url': 'http://localhost',
|
||||
'notification_urls': [],
|
||||
'notification_title': '',
|
||||
'notification_body': '',
|
||||
'notification_format': 'text',
|
||||
'notification_muted': False,
|
||||
}
|
||||
},
|
||||
'watching': {'test-uuid': watch},
|
||||
}
|
||||
datastore.get_all_tags_for_watch = MagicMock(return_value={})
|
||||
|
||||
captured = {}
|
||||
|
||||
def fake_queue_notification(n_object, watch, **kwargs):
|
||||
captured['n_object'] = dict(n_object)
|
||||
|
||||
svc = NotificationService(datastore, MagicMock())
|
||||
svc.queue_notification_for_watch = fake_queue_notification
|
||||
|
||||
svc.send_content_changed_notification('test-uuid')
|
||||
|
||||
assert captured['n_object']['_llm_result'] is None
|
||||
assert captured['n_object']['_llm_intent'] == ''
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# End-to-end: Jinja2 template rendering with llm_summary / llm_intent
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
class TestLlmTokenEndToEnd:
|
||||
"""
|
||||
Verify that the tokens render correctly through the Jinja2 engine
|
||||
used for notification bodies.
|
||||
"""
|
||||
|
||||
def test_llm_summary_renders_in_template(self):
|
||||
from changedetectionio.jinja2_custom import render as jinja_render
|
||||
from changedetectionio.notification_service import NotificationContextData
|
||||
|
||||
n = NotificationContextData()
|
||||
n['llm_summary'] = 'Price dropped from $500 to $400'
|
||||
n['watch_url'] = 'https://example.com'
|
||||
|
||||
rendered = jinja_render(
|
||||
template_str='Change at {{watch_url}}: {{llm_summary}}',
|
||||
**n
|
||||
)
|
||||
assert 'Price dropped from $500 to $400' in rendered
|
||||
assert 'https://example.com' in rendered
|
||||
|
||||
def test_llm_intent_renders_in_template(self):
|
||||
from changedetectionio.jinja2_custom import render as jinja_render
|
||||
from changedetectionio.notification_service import NotificationContextData
|
||||
|
||||
n = NotificationContextData()
|
||||
n['llm_intent'] = 'flag price drops below $300'
|
||||
n['watch_url'] = 'https://example.com'
|
||||
|
||||
rendered = jinja_render(
|
||||
template_str='Intent was: {{llm_intent}}',
|
||||
**n
|
||||
)
|
||||
assert 'flag price drops below $300' in rendered
|
||||
|
||||
def test_llm_summary_empty_string_when_none(self):
|
||||
from changedetectionio.jinja2_custom import render as jinja_render
|
||||
from changedetectionio.notification_service import NotificationContextData
|
||||
|
||||
n = NotificationContextData()
|
||||
# llm_summary defaults to None in NotificationContextData
|
||||
rendered = jinja_render(
|
||||
template_str='Summary: {{llm_summary or ""}}',
|
||||
**n
|
||||
)
|
||||
assert rendered == 'Summary: '
|
||||
|
||||
def test_both_tokens_in_same_template(self):
|
||||
from changedetectionio.jinja2_custom import render as jinja_render
|
||||
from changedetectionio.notification_service import NotificationContextData
|
||||
|
||||
n = NotificationContextData()
|
||||
n['llm_summary'] = 'New senior role posted'
|
||||
n['llm_intent'] = 'alert on new engineering jobs'
|
||||
n['watch_url'] = 'https://jobs.example.com'
|
||||
|
||||
rendered = jinja_render(
|
||||
template_str='[{{llm_intent}}] {{llm_summary}} — {{watch_url}}',
|
||||
**n
|
||||
)
|
||||
assert 'alert on new engineering jobs' in rendered
|
||||
assert 'New senior role posted' in rendered
|
||||
assert 'https://jobs.example.com' in rendered
|
||||
137
changedetectionio/tests/llm/test_prompt_builder.py
Normal file
137
changedetectionio/tests/llm/test_prompt_builder.py
Normal file
@@ -0,0 +1,137 @@
|
||||
"""
|
||||
Unit tests for changedetectionio/llm/prompt_builder.py
|
||||
|
||||
All functions are pure — no external dependencies needed.
|
||||
"""
|
||||
import pytest
|
||||
from changedetectionio.llm.prompt_builder import (
|
||||
build_eval_prompt,
|
||||
build_eval_system_prompt,
|
||||
build_setup_prompt,
|
||||
build_setup_system_prompt,
|
||||
SNAPSHOT_CONTEXT_CHARS,
|
||||
)
|
||||
|
||||
|
||||
class TestBuildEvalPrompt:
|
||||
def test_contains_intent(self):
|
||||
prompt = build_eval_prompt(intent='Alert on price drops', diff='- $500\n+ $400')
|
||||
assert 'Alert on price drops' in prompt
|
||||
|
||||
def test_contains_diff(self):
|
||||
prompt = build_eval_prompt(intent='price', diff='- $500\n+ $400')
|
||||
assert '- $500' in prompt
|
||||
assert '+ $400' in prompt
|
||||
|
||||
def test_optional_url_included_when_provided(self):
|
||||
prompt = build_eval_prompt(
|
||||
intent='price',
|
||||
diff='some diff',
|
||||
url='https://example.com/product',
|
||||
)
|
||||
assert 'https://example.com/product' in prompt
|
||||
|
||||
def test_url_absent_when_not_provided(self):
|
||||
prompt = build_eval_prompt(intent='price', diff='diff')
|
||||
assert 'URL:' not in prompt
|
||||
|
||||
def test_optional_title_included_when_provided(self):
|
||||
prompt = build_eval_prompt(
|
||||
intent='price',
|
||||
diff='diff',
|
||||
title='Example Product Page',
|
||||
)
|
||||
assert 'Example Product Page' in prompt
|
||||
|
||||
def test_snapshot_context_included(self):
|
||||
snapshot = 'Current price: $400. Stock: in stock. Description: widget.'
|
||||
prompt = build_eval_prompt(
|
||||
intent='price',
|
||||
diff='- $500\n+ $400',
|
||||
current_snapshot=snapshot,
|
||||
)
|
||||
# Snapshot excerpt should appear somewhere in the prompt
|
||||
assert 'Current price' in prompt or '$400' in prompt
|
||||
|
||||
def test_large_snapshot_trimmed_to_budget(self):
|
||||
# Snapshot larger than SNAPSHOT_CONTEXT_CHARS should be trimmed
|
||||
large_snapshot = 'irrelevant content line\n' * 2000
|
||||
prompt = build_eval_prompt(
|
||||
intent='price drop',
|
||||
diff='changed',
|
||||
current_snapshot=large_snapshot,
|
||||
)
|
||||
# Prompt should not be astronomically large
|
||||
assert len(prompt) < len(large_snapshot)
|
||||
|
||||
def test_empty_snapshot_skipped(self):
|
||||
prompt_with = build_eval_prompt(intent='x', diff='d', current_snapshot='some text')
|
||||
prompt_without = build_eval_prompt(intent='x', diff='d', current_snapshot='')
|
||||
# Without snapshot should be shorter
|
||||
assert len(prompt_without) < len(prompt_with)
|
||||
|
||||
|
||||
class TestBuildEvalSystemPrompt:
|
||||
def test_returns_string(self):
|
||||
result = build_eval_system_prompt()
|
||||
assert isinstance(result, str)
|
||||
assert len(result) > 0
|
||||
|
||||
def test_instructs_json_only_output(self):
|
||||
result = build_eval_system_prompt()
|
||||
assert 'JSON' in result or 'json' in result.lower()
|
||||
|
||||
def test_defines_important_field(self):
|
||||
result = build_eval_system_prompt()
|
||||
assert 'important' in result
|
||||
|
||||
def test_defines_summary_field(self):
|
||||
result = build_eval_system_prompt()
|
||||
assert 'summary' in result
|
||||
|
||||
|
||||
class TestBuildSetupPrompt:
|
||||
def test_contains_intent(self):
|
||||
prompt = build_setup_prompt(
|
||||
intent='monitor footer changes',
|
||||
snapshot_text='<footer>Copyright 2024</footer>',
|
||||
)
|
||||
assert 'monitor footer changes' in prompt
|
||||
|
||||
def test_contains_url_when_provided(self):
|
||||
prompt = build_setup_prompt(
|
||||
intent='price',
|
||||
snapshot_text='price: $10',
|
||||
url='https://shop.example.com',
|
||||
)
|
||||
assert 'https://shop.example.com' in prompt
|
||||
|
||||
def test_url_absent_when_not_provided(self):
|
||||
prompt = build_setup_prompt(intent='price', snapshot_text='text')
|
||||
assert 'URL:' not in prompt
|
||||
|
||||
def test_large_snapshot_trimmed(self):
|
||||
big_snapshot = 'unrelated junk line\n' * 500
|
||||
prompt = build_setup_prompt(
|
||||
intent='monitor price section',
|
||||
snapshot_text=big_snapshot,
|
||||
)
|
||||
assert len(prompt) < len(big_snapshot)
|
||||
|
||||
|
||||
class TestBuildSetupSystemPrompt:
|
||||
def test_returns_string(self):
|
||||
result = build_setup_system_prompt()
|
||||
assert isinstance(result, str)
|
||||
|
||||
def test_forbids_positional_selectors(self):
|
||||
result = build_setup_system_prompt()
|
||||
assert 'nth-child' in result or 'positional' in result
|
||||
|
||||
def test_defines_needs_prefilter_field(self):
|
||||
result = build_setup_system_prompt()
|
||||
assert 'needs_prefilter' in result
|
||||
|
||||
def test_defines_selector_field(self):
|
||||
result = build_setup_system_prompt()
|
||||
assert 'selector' in result
|
||||
146
changedetectionio/tests/llm/test_response_parser.py
Normal file
146
changedetectionio/tests/llm/test_response_parser.py
Normal file
@@ -0,0 +1,146 @@
|
||||
"""
|
||||
Unit tests for changedetectionio/llm/response_parser.py
|
||||
|
||||
All functions are pure — no external dependencies needed.
|
||||
"""
|
||||
import pytest
|
||||
from changedetectionio.llm.response_parser import (
|
||||
_extract_json,
|
||||
parse_eval_response,
|
||||
parse_setup_response,
|
||||
)
|
||||
|
||||
|
||||
class TestExtractJson:
|
||||
def test_plain_json_passes_through(self):
|
||||
raw = '{"important": true, "summary": "price dropped"}'
|
||||
assert _extract_json(raw) == raw
|
||||
|
||||
def test_strips_json_code_fence(self):
|
||||
raw = '```json\n{"important": false, "summary": "no match"}\n```'
|
||||
result = _extract_json(raw)
|
||||
assert result.startswith('{')
|
||||
assert '"important"' in result
|
||||
|
||||
def test_strips_plain_code_fence(self):
|
||||
raw = '```\n{"important": true, "summary": "ok"}\n```'
|
||||
result = _extract_json(raw)
|
||||
assert result.startswith('{')
|
||||
|
||||
def test_extracts_json_from_surrounding_text(self):
|
||||
raw = 'Here is my response: {"important": true, "summary": "match"} — done.'
|
||||
result = _extract_json(raw)
|
||||
assert result == '{"important": true, "summary": "match"}'
|
||||
|
||||
def test_multiline_json(self):
|
||||
raw = '{\n "important": false,\n "summary": "nothing relevant"\n}'
|
||||
result = _extract_json(raw)
|
||||
assert '"important"' in result
|
||||
|
||||
|
||||
class TestParseEvalResponse:
|
||||
def test_valid_important_true(self):
|
||||
raw = '{"important": true, "summary": "Price dropped from $500 to $400"}'
|
||||
result = parse_eval_response(raw)
|
||||
assert result['important'] is True
|
||||
assert result['summary'] == 'Price dropped from $500 to $400'
|
||||
|
||||
def test_valid_important_false(self):
|
||||
raw = '{"important": false, "summary": "Only a date counter changed"}'
|
||||
result = parse_eval_response(raw)
|
||||
assert result['important'] is False
|
||||
assert 'date counter' in result['summary']
|
||||
|
||||
def test_markdown_fenced_response(self):
|
||||
raw = '```json\n{"important": true, "summary": "New job posted"}\n```'
|
||||
result = parse_eval_response(raw)
|
||||
assert result['important'] is True
|
||||
assert result['summary'] == 'New job posted'
|
||||
|
||||
def test_malformed_json_falls_back_to_safe_default(self):
|
||||
result = parse_eval_response('this is not json at all')
|
||||
assert result['important'] is False
|
||||
assert result['summary'] == ''
|
||||
|
||||
def test_empty_string_falls_back(self):
|
||||
result = parse_eval_response('')
|
||||
assert result['important'] is False
|
||||
|
||||
def test_truthy_integer_coerced_to_bool(self):
|
||||
raw = '{"important": 1, "summary": "yes"}'
|
||||
result = parse_eval_response(raw)
|
||||
assert result['important'] is True
|
||||
|
||||
def test_summary_stripped_of_whitespace(self):
|
||||
raw = '{"important": false, "summary": " no match "}'
|
||||
result = parse_eval_response(raw)
|
||||
assert result['summary'] == 'no match'
|
||||
|
||||
def test_missing_summary_defaults_to_empty_string(self):
|
||||
raw = '{"important": true}'
|
||||
result = parse_eval_response(raw)
|
||||
assert result['summary'] == ''
|
||||
|
||||
def test_extra_keys_ignored(self):
|
||||
raw = '{"important": false, "summary": "skip", "confidence": 0.3, "debug": "xyz"}'
|
||||
result = parse_eval_response(raw)
|
||||
assert result['important'] is False
|
||||
assert result['summary'] == 'skip'
|
||||
|
||||
|
||||
class TestParseSetupResponse:
|
||||
def test_no_prefilter_needed(self):
|
||||
raw = '{"needs_prefilter": false, "selector": null, "reason": "intent is global"}'
|
||||
result = parse_setup_response(raw)
|
||||
assert result['needs_prefilter'] is False
|
||||
assert result['selector'] is None
|
||||
|
||||
def test_semantic_selector_accepted(self):
|
||||
raw = '{"needs_prefilter": true, "selector": "footer", "reason": "intent references footer"}'
|
||||
result = parse_setup_response(raw)
|
||||
assert result['needs_prefilter'] is True
|
||||
assert result['selector'] == 'footer'
|
||||
|
||||
def test_attribute_selector_accepted(self):
|
||||
raw = '{"needs_prefilter": true, "selector": "[class*=\'price\']", "reason": "pricing section"}'
|
||||
result = parse_setup_response(raw)
|
||||
assert result['needs_prefilter'] is True
|
||||
assert result['selector'] is not None
|
||||
|
||||
def test_nth_child_positional_selector_rejected(self):
|
||||
raw = '{"needs_prefilter": true, "selector": "div:nth-child(3)", "reason": "third div"}'
|
||||
result = parse_setup_response(raw)
|
||||
assert result['selector'] is None
|
||||
assert result['needs_prefilter'] is False
|
||||
|
||||
def test_nth_of_type_positional_selector_rejected(self):
|
||||
raw = '{"needs_prefilter": true, "selector": "p:nth-of-type(2)", "reason": "second p"}'
|
||||
result = parse_setup_response(raw)
|
||||
assert result['selector'] is None
|
||||
assert result['needs_prefilter'] is False
|
||||
|
||||
def test_eq_positional_selector_rejected(self):
|
||||
raw = '{"needs_prefilter": true, "selector": "div:eq(0)", "reason": "first div"}'
|
||||
result = parse_setup_response(raw)
|
||||
assert result['selector'] is None
|
||||
|
||||
def test_xpath_positional_selector_rejected(self):
|
||||
raw = '{"needs_prefilter": true, "selector": "//*[2]", "reason": "second element"}'
|
||||
result = parse_setup_response(raw)
|
||||
assert result['selector'] is None
|
||||
|
||||
def test_selector_forced_to_null_when_needs_prefilter_false(self):
|
||||
# Even if selector is provided alongside needs_prefilter=false, selector is nulled
|
||||
raw = '{"needs_prefilter": false, "selector": "main", "reason": "not needed"}'
|
||||
result = parse_setup_response(raw)
|
||||
assert result['selector'] is None
|
||||
|
||||
def test_malformed_json_safe_defaults(self):
|
||||
result = parse_setup_response('garbage text')
|
||||
assert result['needs_prefilter'] is False
|
||||
assert result['selector'] is None
|
||||
assert result['reason'] == ''
|
||||
|
||||
def test_empty_response_safe_defaults(self):
|
||||
result = parse_setup_response('')
|
||||
assert result['needs_prefilter'] is False
|
||||
173
changedetectionio/tests/test_llm_change_summary.py
Normal file
173
changedetectionio/tests/test_llm_change_summary.py
Normal file
@@ -0,0 +1,173 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Integration tests for AI Change Summary:
|
||||
- llm_change_summary field saved via watch edit form
|
||||
- llm_change_summary cascades from tag to watches
|
||||
- {{ diff }} replaced by AI summary in notifications
|
||||
- {{ raw_diff }} always contains original diff
|
||||
- summarise_change only runs when change is detected
|
||||
"""
|
||||
|
||||
import json
|
||||
import time
|
||||
from unittest.mock import patch
|
||||
|
||||
from flask import url_for
|
||||
|
||||
from changedetectionio.tests.util import wait_for_all_checks, delete_all_watches
|
||||
|
||||
HTML_V1 = "<html><body><ul><li>Item A</li><li>Item B</li></ul></body></html>"
|
||||
HTML_V2 = "<html><body><ul><li>Item A</li><li>Item B</li><li>Item C — NEW</li></ul></body></html>"
|
||||
|
||||
|
||||
def _set_response(datastore_path, content):
|
||||
import os
|
||||
with open(os.path.join(datastore_path, "endpoint-content.txt"), "w") as f:
|
||||
f.write(content)
|
||||
|
||||
|
||||
def _configure_llm(client):
|
||||
ds = client.application.config.get('DATASTORE')
|
||||
ds.data['settings']['application']['llm'] = {'model': 'gpt-4o-mini', 'api_key': 'sk-test'}
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Form field persistence
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def test_llm_change_summary_saved_via_edit_form(
|
||||
client, live_server, measure_memory_usage, datastore_path):
|
||||
"""llm_change_summary submitted via watch edit form is persisted."""
|
||||
_set_response(datastore_path, HTML_V1)
|
||||
_configure_llm(client)
|
||||
test_url = url_for('test_endpoint', _external=True)
|
||||
uuid = client.application.config.get('DATASTORE').add_watch(url=test_url)
|
||||
|
||||
res = client.post(
|
||||
url_for("ui.ui_edit.edit_page", uuid=uuid),
|
||||
data={
|
||||
"url": test_url,
|
||||
"fetch_backend": "html_requests",
|
||||
"time_between_check_use_default": "y",
|
||||
"llm_change_summary": "List new items added as bullet points. Translate to English.",
|
||||
},
|
||||
follow_redirects=True,
|
||||
)
|
||||
assert b"Updated watch." in res.data
|
||||
|
||||
watch = client.application.config.get('DATASTORE').data['watching'][uuid]
|
||||
assert watch.get('llm_change_summary') == "List new items added as bullet points. Translate to English."
|
||||
|
||||
delete_all_watches(client)
|
||||
|
||||
|
||||
def test_llm_change_summary_cascades_from_tag(
|
||||
client, live_server, measure_memory_usage, datastore_path):
|
||||
"""llm_change_summary set on a tag is resolved for watches in that tag."""
|
||||
from changedetectionio.llm.evaluator import resolve_llm_field
|
||||
|
||||
ds = client.application.config.get('DATASTORE')
|
||||
_configure_llm(client)
|
||||
_set_response(datastore_path, HTML_V1)
|
||||
test_url = url_for('test_endpoint', _external=True)
|
||||
|
||||
# Create a tag with llm_change_summary
|
||||
tag_uuid = ds.add_tag('events-group')
|
||||
ds.data['settings']['application']['tags'][tag_uuid]['llm_change_summary'] = 'Summarise new events'
|
||||
|
||||
# Watch in that tag, no own summary prompt
|
||||
uuid = ds.add_watch(url=test_url)
|
||||
ds.data['watching'][uuid]['tags'] = [tag_uuid]
|
||||
ds.data['watching'][uuid]['llm_change_summary'] = ''
|
||||
|
||||
watch = ds.data['watching'][uuid]
|
||||
value, source = resolve_llm_field(watch, ds, 'llm_change_summary')
|
||||
assert value == 'Summarise new events'
|
||||
assert source == 'events-group'
|
||||
|
||||
delete_all_watches(client)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Notification token behaviour
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def test_diff_token_replaced_by_ai_summary_in_notification(
|
||||
client, live_server, measure_memory_usage, datastore_path):
|
||||
"""
|
||||
When _llm_change_summary is set on the watch, the notification handler
|
||||
must substitute it into {{ diff }} and preserve {{ raw_diff }}.
|
||||
"""
|
||||
from changedetectionio.notification.handler import process_notification
|
||||
|
||||
n_object = {
|
||||
'notification_urls': ['json://localhost/'],
|
||||
'notification_title': 'Change detected',
|
||||
'notification_body': 'Summary: {{diff}}\nRaw: {{raw_diff}}',
|
||||
'notification_format': 'text',
|
||||
'uuid': 'test-uuid',
|
||||
'watch_url': 'https://example.com',
|
||||
'current_snapshot': 'Item A\nItem B\nItem C',
|
||||
'prev_snapshot': 'Item A\nItem B',
|
||||
'diff': '', # populated by add_rendered_diff_to_notification_vars
|
||||
'raw_diff': '',
|
||||
'_llm_change_summary': '1 new item added: Item C',
|
||||
'_llm_result': None,
|
||||
'_llm_intent': '',
|
||||
'base_url': 'http://localhost:5000/',
|
||||
'watch_mime_type': 'text/plain',
|
||||
'triggered_text': '',
|
||||
}
|
||||
|
||||
# We only need to verify the token substitution logic, not send a real notification
|
||||
# Invoke just enough of the handler to check n_object state after substitution
|
||||
from changedetectionio.notification_service import add_rendered_diff_to_notification_vars
|
||||
|
||||
diff_vars = add_rendered_diff_to_notification_vars(
|
||||
notification_scan_text=n_object['notification_body'] + n_object['notification_title'],
|
||||
current_snapshot=n_object['current_snapshot'],
|
||||
prev_snapshot=n_object['prev_snapshot'],
|
||||
word_diff=False,
|
||||
)
|
||||
n_object.update(diff_vars)
|
||||
|
||||
# Simulate what handler.py does
|
||||
n_object['raw_diff'] = n_object.get('diff', '')
|
||||
llm_summary = (n_object.get('_llm_change_summary') or '').strip()
|
||||
if llm_summary:
|
||||
n_object['diff'] = llm_summary
|
||||
|
||||
assert n_object['diff'] == '1 new item added: Item C'
|
||||
assert 'Item C' in n_object['raw_diff'] or n_object['raw_diff'] != n_object['diff']
|
||||
|
||||
delete_all_watches(client)
|
||||
|
||||
|
||||
def test_diff_token_unchanged_when_no_ai_summary(
|
||||
client, live_server, measure_memory_usage, datastore_path):
|
||||
"""When no AI Change Summary is configured, {{ diff }} renders the raw diff as normal."""
|
||||
from changedetectionio.notification_service import add_rendered_diff_to_notification_vars
|
||||
|
||||
n_object = {
|
||||
'current_snapshot': 'Item A\nItem B\nItem C',
|
||||
'prev_snapshot': 'Item A\nItem B',
|
||||
'_llm_change_summary': '',
|
||||
}
|
||||
|
||||
diff_vars = add_rendered_diff_to_notification_vars(
|
||||
notification_scan_text='{{diff}}',
|
||||
current_snapshot=n_object['current_snapshot'],
|
||||
prev_snapshot=n_object['prev_snapshot'],
|
||||
word_diff=False,
|
||||
)
|
||||
n_object.update(diff_vars)
|
||||
|
||||
raw = n_object.get('diff', '')
|
||||
n_object['raw_diff'] = raw
|
||||
if (n_object.get('_llm_change_summary') or '').strip():
|
||||
n_object['diff'] = n_object['_llm_change_summary']
|
||||
|
||||
# diff should still be the raw diff (not replaced)
|
||||
assert n_object['diff'] == n_object['raw_diff']
|
||||
|
||||
delete_all_watches(client)
|
||||
232
changedetectionio/tests/test_llm_preview.py
Normal file
232
changedetectionio/tests/test_llm_preview.py
Normal file
@@ -0,0 +1,232 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Integration tests: /edit/<uuid>/preview-rendered returns llm_evaluation when
|
||||
llm_intent is submitted alongside the filter form data.
|
||||
|
||||
These tests verify the full backend path:
|
||||
JS POSTs llm_intent → prepare_filter_prevew() applies it to tmp_watch
|
||||
→ preview_extract() is called → llm_evaluation appears in JSON response
|
||||
|
||||
The response uses {'found': bool, 'answer': str} — NOT the diff-evaluation
|
||||
{'important', 'summary'} shape, because preview asks the LLM to extract from
|
||||
the current content directly (e.g. "30 articles listed") rather than compare
|
||||
a diff.
|
||||
"""
|
||||
|
||||
import json
|
||||
import time
|
||||
from unittest.mock import patch
|
||||
|
||||
from flask import url_for
|
||||
|
||||
from changedetectionio.tests.util import wait_for_all_checks, delete_all_watches
|
||||
|
||||
|
||||
HTML_WITH_ARTICLES = """<html><body>
|
||||
<ul id="articles">
|
||||
<li>Article One</li>
|
||||
<li>Article Two</li>
|
||||
<li>Article Three</li>
|
||||
</ul>
|
||||
</body></html>"""
|
||||
|
||||
HTML_WITH_PRICE = """<html><body>
|
||||
<p class="price">Original price: $199.00</p>
|
||||
<p class="discount">Now: $149.00 — 25% off!</p>
|
||||
</body></html>"""
|
||||
|
||||
|
||||
def _set_response(datastore_path, content):
|
||||
import os
|
||||
with open(os.path.join(datastore_path, "endpoint-content.txt"), "w") as f:
|
||||
f.write(content)
|
||||
|
||||
|
||||
def _add_and_fetch(client, live_server, datastore_path, html):
|
||||
"""Add a watch, fetch it once so a snapshot exists, return uuid."""
|
||||
_set_response(datastore_path, html)
|
||||
test_url = url_for('test_endpoint', _external=True)
|
||||
uuid = client.application.config.get('DATASTORE').add_watch(url=test_url)
|
||||
client.get(url_for("ui.form_watch_checknow"), follow_redirects=True)
|
||||
time.sleep(0.5)
|
||||
wait_for_all_checks(client)
|
||||
return uuid
|
||||
|
||||
|
||||
def _configure_llm(client):
|
||||
"""Put a fake LLM config into the datastore."""
|
||||
datastore = client.application.config.get('DATASTORE')
|
||||
datastore.data['settings']['application']['llm'] = {
|
||||
'model': 'gpt-4o-mini',
|
||||
'api_key': 'sk-test-fake',
|
||||
}
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# llm_intent submitted → llm_evaluation returned with found/answer shape
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def test_preview_returns_llm_answer_for_article_intent(
|
||||
client, live_server, measure_memory_usage, datastore_path):
|
||||
"""
|
||||
With llm_intent='Tell me the number of articles in the list',
|
||||
the preview endpoint returns llm_evaluation with found=True and an answer
|
||||
that directly addresses the intent (e.g. "3 articles listed").
|
||||
"""
|
||||
uuid = _add_and_fetch(client, live_server, datastore_path, HTML_WITH_ARTICLES)
|
||||
_configure_llm(client)
|
||||
|
||||
llm_json = '{"found": true, "answer": "3 articles are listed in the content"}'
|
||||
with patch('changedetectionio.llm.client.completion', return_value=(llm_json, 50)):
|
||||
res = client.post(
|
||||
url_for("ui.ui_edit.watch_get_preview_rendered", uuid=uuid),
|
||||
data={
|
||||
'llm_intent': 'Tell me the number of articles in the list',
|
||||
'fetch_backend': 'html_requests',
|
||||
},
|
||||
)
|
||||
|
||||
assert res.status_code == 200
|
||||
data = json.loads(res.data.decode('utf-8'))
|
||||
|
||||
# Filtered text must still be present
|
||||
assert data.get('after_filter'), "after_filter must be present"
|
||||
|
||||
# LLM evaluation must be returned with the new shape
|
||||
ev = data.get('llm_evaluation')
|
||||
assert ev is not None, "llm_evaluation must be in response"
|
||||
assert ev['found'] is True
|
||||
assert '3' in ev['answer']
|
||||
|
||||
delete_all_watches(client)
|
||||
|
||||
|
||||
def test_preview_returns_llm_answer_for_price_intent(
|
||||
client, live_server, measure_memory_usage, datastore_path):
|
||||
"""
|
||||
With a price-change intent, the LLM answer should reflect the discount
|
||||
extracted directly from the current page (not a diff comparison).
|
||||
"""
|
||||
uuid = _add_and_fetch(client, live_server, datastore_path, HTML_WITH_PRICE)
|
||||
_configure_llm(client)
|
||||
|
||||
llm_json = '{"found": true, "answer": "Price $149, 25% off (was $199)"}'
|
||||
with patch('changedetectionio.llm.client.completion', return_value=(llm_json, 60)):
|
||||
res = client.post(
|
||||
url_for("ui.ui_edit.watch_get_preview_rendered", uuid=uuid),
|
||||
data={
|
||||
'llm_intent': 'Flag any price change, including discount percentages',
|
||||
'fetch_backend': 'html_requests',
|
||||
},
|
||||
)
|
||||
|
||||
assert res.status_code == 200
|
||||
data = json.loads(res.data.decode('utf-8'))
|
||||
ev = data.get('llm_evaluation')
|
||||
assert ev is not None
|
||||
assert ev['found'] is True
|
||||
assert '25' in ev['answer'] or '149' in ev['answer']
|
||||
|
||||
delete_all_watches(client)
|
||||
|
||||
|
||||
def test_preview_found_false_when_content_not_relevant(
|
||||
client, live_server, measure_memory_usage, datastore_path):
|
||||
"""found=False when the LLM determines page content doesn't match intent."""
|
||||
uuid = _add_and_fetch(client, live_server, datastore_path, HTML_WITH_ARTICLES)
|
||||
_configure_llm(client)
|
||||
|
||||
llm_json = '{"found": false, "answer": "No price information found on this page"}'
|
||||
with patch('changedetectionio.llm.client.completion', return_value=(llm_json, 45)):
|
||||
res = client.post(
|
||||
url_for("ui.ui_edit.watch_get_preview_rendered", uuid=uuid),
|
||||
data={
|
||||
'llm_intent': 'Show me any product prices',
|
||||
'fetch_backend': 'html_requests',
|
||||
},
|
||||
)
|
||||
|
||||
assert res.status_code == 200
|
||||
data = json.loads(res.data.decode('utf-8'))
|
||||
ev = data.get('llm_evaluation')
|
||||
assert ev is not None
|
||||
assert ev['found'] is False
|
||||
assert ev['answer']
|
||||
|
||||
delete_all_watches(client)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# No intent / no LLM → llm_evaluation is None
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def test_preview_no_llm_evaluation_without_intent(
|
||||
client, live_server, measure_memory_usage, datastore_path):
|
||||
"""When llm_intent is absent, the LLM client must not be called."""
|
||||
uuid = _add_and_fetch(client, live_server, datastore_path, HTML_WITH_ARTICLES)
|
||||
_configure_llm(client)
|
||||
|
||||
with patch('changedetectionio.llm.client.completion') as mock_llm:
|
||||
res = client.post(
|
||||
url_for("ui.ui_edit.watch_get_preview_rendered", uuid=uuid),
|
||||
data={'fetch_backend': 'html_requests'},
|
||||
)
|
||||
mock_llm.assert_not_called()
|
||||
|
||||
assert res.status_code == 200
|
||||
data = json.loads(res.data.decode('utf-8'))
|
||||
assert data.get('llm_evaluation') is None
|
||||
|
||||
delete_all_watches(client)
|
||||
|
||||
|
||||
def test_preview_no_llm_evaluation_when_llm_not_configured(
|
||||
client, live_server, measure_memory_usage, datastore_path):
|
||||
"""When LLM model is not set, llm_evaluation must be None even with an intent."""
|
||||
uuid = _add_and_fetch(client, live_server, datastore_path, HTML_WITH_ARTICLES)
|
||||
# Intentionally do NOT configure LLM
|
||||
|
||||
with patch('changedetectionio.llm.client.completion') as mock_llm:
|
||||
res = client.post(
|
||||
url_for("ui.ui_edit.watch_get_preview_rendered", uuid=uuid),
|
||||
data={
|
||||
'llm_intent': 'Tell me the number of articles',
|
||||
'fetch_backend': 'html_requests',
|
||||
},
|
||||
)
|
||||
mock_llm.assert_not_called()
|
||||
|
||||
assert res.status_code == 200
|
||||
data = json.loads(res.data.decode('utf-8'))
|
||||
assert data.get('llm_evaluation') is None
|
||||
|
||||
delete_all_watches(client)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# LLM failure → llm_evaluation is None, preview still works
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def test_preview_llm_failure_does_not_break_preview(
|
||||
client, live_server, measure_memory_usage, datastore_path):
|
||||
"""If the LLM call raises, preview_extract returns None — preview still works."""
|
||||
uuid = _add_and_fetch(client, live_server, datastore_path, HTML_WITH_ARTICLES)
|
||||
_configure_llm(client)
|
||||
|
||||
with patch('changedetectionio.llm.client.completion', side_effect=Exception('API timeout')):
|
||||
res = client.post(
|
||||
url_for("ui.ui_edit.watch_get_preview_rendered", uuid=uuid),
|
||||
data={
|
||||
'llm_intent': 'Tell me the number of articles',
|
||||
'fetch_backend': 'html_requests',
|
||||
},
|
||||
)
|
||||
|
||||
assert res.status_code == 200
|
||||
data = json.loads(res.data.decode('utf-8'))
|
||||
# Filter content must still be returned
|
||||
assert data.get('after_filter')
|
||||
# preview_extract returns None on error (doesn't fail-open like evaluate_change)
|
||||
assert data.get('llm_evaluation') is None
|
||||
|
||||
delete_all_watches(client)
|
||||
94
changedetectionio/tests/unit/test_jq_security.py
Normal file
94
changedetectionio/tests/unit/test_jq_security.py
Normal file
@@ -0,0 +1,94 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
# run from dir above changedetectionio/ dir
|
||||
# python3 -m unittest changedetectionio.tests.unit.test_jq_security
|
||||
|
||||
import unittest
|
||||
|
||||
|
||||
class TestJqExpressionSecurity(unittest.TestCase):
|
||||
|
||||
def test_blocked_builtins_raise(self):
|
||||
"""Each dangerous builtin must be rejected by validate_jq_expression."""
|
||||
from changedetectionio.html_tools import validate_jq_expression
|
||||
|
||||
blocked = [
|
||||
# env access
|
||||
'env',
|
||||
'.foo | env',
|
||||
'$ENV',
|
||||
'$ENV.SECRET',
|
||||
# file read via module system
|
||||
'include "foo"',
|
||||
'import "foo" as f',
|
||||
# stdin reads
|
||||
'input',
|
||||
'inputs',
|
||||
'[.,inputs]',
|
||||
# process termination
|
||||
'halt',
|
||||
'halt_error(1)',
|
||||
# stderr/debug leakage
|
||||
'debug',
|
||||
'. | debug | .foo',
|
||||
'stderr',
|
||||
# misc info leakage
|
||||
'$__loc__',
|
||||
'builtins',
|
||||
'modulemeta',
|
||||
'$JQ_BUILD_CONFIGURATION',
|
||||
]
|
||||
|
||||
for expr in blocked:
|
||||
with self.assertRaises(ValueError, msg=f"Expected ValueError for: {expr!r}"):
|
||||
validate_jq_expression(expr)
|
||||
|
||||
def test_safe_expressions_pass(self):
|
||||
"""Normal jq expressions must not be blocked."""
|
||||
from changedetectionio.html_tools import validate_jq_expression
|
||||
|
||||
safe = [
|
||||
'.foo',
|
||||
'.items[] | .price',
|
||||
'map(select(.active)) | length',
|
||||
'.[] | select(.name | test("foo"))',
|
||||
'to_entries | map(.value) | add',
|
||||
'[.[] | .id] | unique',
|
||||
'.price | tonumber',
|
||||
'if .stock > 0 then "in stock" else "out of stock" end',
|
||||
]
|
||||
|
||||
for expr in safe:
|
||||
try:
|
||||
validate_jq_expression(expr)
|
||||
except ValueError as e:
|
||||
self.fail(f"validate_jq_expression raised ValueError for safe expression {expr!r}: {e}")
|
||||
|
||||
def test_allow_risky_env_var_bypasses_check(self):
|
||||
"""JQ_ALLOW_RISKY_EXPRESSIONS=true must skip all blocking."""
|
||||
import os
|
||||
from unittest.mock import patch
|
||||
from changedetectionio.html_tools import validate_jq_expression
|
||||
|
||||
with patch.dict(os.environ, {'JQ_ALLOW_RISKY_EXPRESSIONS': 'true'}):
|
||||
# Should not raise even for the most dangerous expression
|
||||
try:
|
||||
validate_jq_expression('env')
|
||||
validate_jq_expression('$ENV')
|
||||
except ValueError as e:
|
||||
self.fail(f"Should not block when JQ_ALLOW_RISKY_EXPRESSIONS=true: {e}")
|
||||
|
||||
def test_allow_risky_env_var_off_by_default(self):
|
||||
"""Without JQ_ALLOW_RISKY_EXPRESSIONS set, blocking must be active."""
|
||||
import os
|
||||
from unittest.mock import patch
|
||||
from changedetectionio.html_tools import validate_jq_expression
|
||||
|
||||
env = {k: v for k, v in os.environ.items() if k != 'JQ_ALLOW_RISKY_EXPRESSIONS'}
|
||||
with patch.dict(os.environ, env, clear=True):
|
||||
with self.assertRaises(ValueError):
|
||||
validate_jq_expression('env')
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main()
|
||||
@@ -0,0 +1,127 @@
|
||||
"""
|
||||
Unit tests for SSRF protection in the Apprise custom HTTP notification handler.
|
||||
|
||||
The handler (notification/apprise_plugin/custom_handlers.py) must block requests
|
||||
to private/IANA-reserved addresses unless ALLOW_IANA_RESTRICTED_ADDRESSES=true.
|
||||
"""
|
||||
|
||||
import pytest
|
||||
from unittest.mock import patch, MagicMock
|
||||
|
||||
|
||||
def _make_meta(url: str) -> dict:
|
||||
"""Build a minimal Apprise meta dict that apprise_http_custom_handler expects."""
|
||||
from apprise.utils.parse import parse_url as apprise_parse_url
|
||||
schema = url.split("://")[0]
|
||||
parsed = apprise_parse_url(url, default_schema=schema, verify_host=False, simple=True)
|
||||
parsed["url"] = url
|
||||
parsed["schema"] = schema
|
||||
return parsed
|
||||
|
||||
|
||||
class TestNotificationSSRFProtection:
|
||||
|
||||
def test_private_ip_blocked_by_default(self):
|
||||
"""Requests to private IP addresses must be blocked when ALLOW_IANA_RESTRICTED_ADDRESSES is unset."""
|
||||
from changedetectionio.notification.apprise_plugin.custom_handlers import apprise_http_custom_handler
|
||||
|
||||
meta = _make_meta("post://192.168.1.100/webhook")
|
||||
|
||||
with patch("changedetectionio.notification.apprise_plugin.custom_handlers.is_private_hostname", return_value=True), \
|
||||
patch.dict("os.environ", {}, clear=False):
|
||||
# Remove the env var if present so the default 'false' applies
|
||||
import os
|
||||
os.environ.pop("ALLOW_IANA_RESTRICTED_ADDRESSES", None)
|
||||
|
||||
with pytest.raises(ValueError, match="ALLOW_IANA_RESTRICTED_ADDRESSES"):
|
||||
apprise_http_custom_handler(
|
||||
body="test body",
|
||||
title="test title",
|
||||
notify_type="info",
|
||||
meta=meta,
|
||||
)
|
||||
|
||||
def test_loopback_blocked_by_default(self):
|
||||
"""Requests to loopback addresses (127.x.x.x) must be blocked."""
|
||||
from changedetectionio.notification.apprise_plugin.custom_handlers import apprise_http_custom_handler
|
||||
|
||||
meta = _make_meta("post://127.0.0.1:8080/internal")
|
||||
|
||||
with patch("changedetectionio.notification.apprise_plugin.custom_handlers.is_private_hostname", return_value=True):
|
||||
import os
|
||||
os.environ.pop("ALLOW_IANA_RESTRICTED_ADDRESSES", None)
|
||||
|
||||
with pytest.raises(ValueError, match="ALLOW_IANA_RESTRICTED_ADDRESSES"):
|
||||
apprise_http_custom_handler(
|
||||
body="test body",
|
||||
title="test title",
|
||||
notify_type="info",
|
||||
meta=meta,
|
||||
)
|
||||
|
||||
def test_private_ip_allowed_when_env_var_set(self):
|
||||
"""When ALLOW_IANA_RESTRICTED_ADDRESSES=true, requests to private IPs must go through."""
|
||||
from changedetectionio.notification.apprise_plugin.custom_handlers import apprise_http_custom_handler
|
||||
|
||||
meta = _make_meta("post://192.168.1.100/webhook")
|
||||
|
||||
mock_response = MagicMock()
|
||||
mock_response.raise_for_status = MagicMock()
|
||||
|
||||
with patch("changedetectionio.notification.apprise_plugin.custom_handlers.is_private_hostname", return_value=True), \
|
||||
patch("changedetectionio.notification.apprise_plugin.custom_handlers.requests.request", return_value=mock_response) as mock_req, \
|
||||
patch.dict("os.environ", {"ALLOW_IANA_RESTRICTED_ADDRESSES": "true"}):
|
||||
|
||||
result = apprise_http_custom_handler(
|
||||
body="test body",
|
||||
title="test title",
|
||||
notify_type="info",
|
||||
meta=meta,
|
||||
)
|
||||
|
||||
assert result is True
|
||||
mock_req.assert_called_once()
|
||||
|
||||
def test_public_hostname_not_blocked(self):
|
||||
"""Public hostnames must not be blocked by the SSRF guard."""
|
||||
from changedetectionio.notification.apprise_plugin.custom_handlers import apprise_http_custom_handler
|
||||
|
||||
meta = _make_meta("post://example.com/webhook")
|
||||
|
||||
mock_response = MagicMock()
|
||||
mock_response.raise_for_status = MagicMock()
|
||||
|
||||
with patch("changedetectionio.notification.apprise_plugin.custom_handlers.is_private_hostname", return_value=False), \
|
||||
patch("changedetectionio.notification.apprise_plugin.custom_handlers.requests.request", return_value=mock_response) as mock_req:
|
||||
import os
|
||||
os.environ.pop("ALLOW_IANA_RESTRICTED_ADDRESSES", None)
|
||||
|
||||
result = apprise_http_custom_handler(
|
||||
body="test body",
|
||||
title="test title",
|
||||
notify_type="info",
|
||||
meta=meta,
|
||||
)
|
||||
|
||||
assert result is True
|
||||
mock_req.assert_called_once()
|
||||
|
||||
def test_error_message_contains_env_var_hint(self):
|
||||
"""The ValueError message must include the ALLOW_IANA_RESTRICTED_ADDRESSES hint."""
|
||||
from changedetectionio.notification.apprise_plugin.custom_handlers import apprise_http_custom_handler
|
||||
|
||||
meta = _make_meta("post://10.0.0.1/api")
|
||||
|
||||
with patch("changedetectionio.notification.apprise_plugin.custom_handlers.is_private_hostname", return_value=True):
|
||||
import os
|
||||
os.environ.pop("ALLOW_IANA_RESTRICTED_ADDRESSES", None)
|
||||
|
||||
with pytest.raises(ValueError) as exc_info:
|
||||
apprise_http_custom_handler(
|
||||
body="test",
|
||||
title="test",
|
||||
notify_type="info",
|
||||
meta=meta,
|
||||
)
|
||||
|
||||
assert "ALLOW_IANA_RESTRICTED_ADDRESSES=true" in str(exc_info.value)
|
||||
Binary file not shown.
@@ -7,7 +7,7 @@ msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: PROJECT VERSION\n"
|
||||
"Report-Msgid-Bugs-To: EMAIL@ADDRESS\n"
|
||||
"POT-Creation-Date: 2026-04-15 03:04+0900\n"
|
||||
"POT-Creation-Date: 2026-04-11 04:15+0200\n"
|
||||
"PO-Revision-Date: 2026-01-02 11:40+0100\n"
|
||||
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
|
||||
"Language: cs\n"
|
||||
@@ -590,15 +590,15 @@ msgstr ""
|
||||
msgid "Changing this could affect the content of your existing watches, possibly trigger alerts etc."
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/blueprint/settings/templates/settings.html changedetectionio/templates/edit/include_subtract.html
|
||||
#: changedetectionio/blueprint/settings/templates/settings.html
|
||||
msgid "Remove HTML element(s) by CSS and XPath selectors before text conversion."
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/blueprint/settings/templates/settings.html changedetectionio/templates/edit/include_subtract.html
|
||||
#: changedetectionio/blueprint/settings/templates/settings.html
|
||||
msgid "Don't paste HTML here, use only CSS and XPath selectors"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/blueprint/settings/templates/settings.html changedetectionio/templates/edit/include_subtract.html
|
||||
#: changedetectionio/blueprint/settings/templates/settings.html
|
||||
msgid "Add multiple elements, CSS or XPath selectors per line to ignore multiple parts of the HTML."
|
||||
msgstr ""
|
||||
|
||||
@@ -829,26 +829,6 @@ msgstr ""
|
||||
msgid "Updated"
|
||||
msgstr "Ztlumit"
|
||||
|
||||
#: changedetectionio/blueprint/tags/form.py
|
||||
msgid "Activate for individual watches in this tag/group?"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/blueprint/tags/form.py
|
||||
msgid "Auto-apply to watches with URLs matching"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/blueprint/tags/form.py
|
||||
msgid "e.g. *://example.com/* or github.com/myorg"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/blueprint/tags/form.py changedetectionio/blueprint/tags/templates/edit-tag.html
|
||||
msgid "Tag colour"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/blueprint/tags/form.py
|
||||
msgid "Tag name"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/blueprint/tags/templates/edit-tag.html changedetectionio/blueprint/ui/templates/edit.html
|
||||
msgid "Filters & Triggers"
|
||||
msgstr "Filtry a spouštěče"
|
||||
@@ -863,6 +843,10 @@ msgstr ""
|
||||
msgid "Currently matching watches"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/blueprint/tags/templates/edit-tag.html
|
||||
msgid "Tag colour"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/blueprint/tags/templates/edit-tag.html
|
||||
msgid "Custom colour"
|
||||
msgstr ""
|
||||
@@ -948,11 +932,7 @@ msgstr "Tag / Název štítku"
|
||||
msgid "No website organisational tags/groups configured"
|
||||
msgstr "Žádné skupiny/značky zatím nebyly nastaveny"
|
||||
|
||||
#: changedetectionio/blueprint/tags/templates/groups-overview.html changedetectionio/templates/menu.html
|
||||
msgid "Mute notifications"
|
||||
msgstr "Ztlumit oznámení"
|
||||
|
||||
#: changedetectionio/blueprint/tags/templates/groups-overview.html changedetectionio/blueprint/ui/edit.py
|
||||
#: changedetectionio/blueprint/tags/templates/groups-overview.html
|
||||
#: changedetectionio/blueprint/watchlist/templates/watch-overview.html
|
||||
msgid "Edit"
|
||||
msgstr "Upravit"
|
||||
@@ -1155,14 +1135,6 @@ msgstr ""
|
||||
msgid "Could not load '{}' processor, processor plugin might be missing."
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/blueprint/ui/edit.py
|
||||
msgid "System settings default"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/blueprint/ui/edit.py
|
||||
msgid "Default"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/blueprint/ui/edit.py
|
||||
msgid "Updated watch - unpaused!"
|
||||
msgstr ""
|
||||
@@ -1175,10 +1147,6 @@ msgstr "Sledování aktualizováno."
|
||||
msgid "Preview unavailable - No fetch/check completed or triggers not reached"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/blueprint/ui/preview.py
|
||||
msgid "Diff"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/blueprint/ui/templates/clear_all_history.html
|
||||
msgid "This will remove version history (snapshots) for ALL watches, but keep your list of URLs!"
|
||||
msgstr ""
|
||||
@@ -1288,17 +1256,14 @@ msgid "Jump"
|
||||
msgstr "Skok"
|
||||
|
||||
#: changedetectionio/blueprint/ui/templates/diff.html changedetectionio/blueprint/ui/templates/preview.html
|
||||
#: changedetectionio/processors/templates/extract.html
|
||||
msgid "Error Text"
|
||||
msgstr "Text chyby"
|
||||
|
||||
#: changedetectionio/blueprint/ui/templates/diff.html changedetectionio/blueprint/ui/templates/preview.html
|
||||
#: changedetectionio/processors/templates/extract.html
|
||||
msgid "Error Screenshot"
|
||||
msgstr "Snímek obrazovky s chybou"
|
||||
|
||||
#: changedetectionio/blueprint/ui/templates/diff.html changedetectionio/blueprint/ui/templates/preview.html
|
||||
#: changedetectionio/processors/templates/extract.html
|
||||
msgid "Text"
|
||||
msgstr "Text"
|
||||
|
||||
@@ -1306,8 +1271,7 @@ msgstr "Text"
|
||||
msgid "Current screenshot"
|
||||
msgstr "Aktuální snímek obrazovky"
|
||||
|
||||
#: changedetectionio/blueprint/ui/templates/diff.html changedetectionio/processors/extract.py
|
||||
#: changedetectionio/processors/templates/extract.html
|
||||
#: changedetectionio/blueprint/ui/templates/diff.html
|
||||
msgid "Extract Data"
|
||||
msgstr "Extrahovat data"
|
||||
|
||||
@@ -1897,26 +1861,6 @@ msgstr "Nejsou nakonfigurována žádná sledování webových stránek, do vý
|
||||
msgid "import a list"
|
||||
msgstr "importovat seznam"
|
||||
|
||||
#: changedetectionio/blueprint/watchlist/templates/watch-overview.html
|
||||
msgid "Pause checks"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/blueprint/watchlist/templates/watch-overview.html
|
||||
msgid "UnPause checks"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/blueprint/watchlist/templates/watch-overview.html
|
||||
msgid "Mute notification"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/blueprint/watchlist/templates/watch-overview.html
|
||||
msgid "UnMute notification"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/blueprint/watchlist/templates/watch-overview.html
|
||||
msgid "Create a link to share watch config with others"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/blueprint/watchlist/templates/watch-overview.html
|
||||
msgid "Detecting restock and price"
|
||||
msgstr "Kontrola zásob a ceny"
|
||||
@@ -1946,7 +1890,6 @@ msgid "Queued"
|
||||
msgstr "Ve frontě"
|
||||
|
||||
#: changedetectionio/blueprint/watchlist/templates/watch-overview.html
|
||||
#: changedetectionio/processors/text_json_diff/difference.py
|
||||
msgid "History"
|
||||
msgstr "Historie"
|
||||
|
||||
@@ -1980,168 +1923,6 @@ msgstr "Znovu zkontrolovat vše"
|
||||
msgid "in '%(title)s'"
|
||||
msgstr "v '%(title)s'"
|
||||
|
||||
#: changedetectionio/conditions/__init__.py
|
||||
msgid "Choose one - Operator"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/conditions/__init__.py
|
||||
msgid "Greater Than"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/conditions/__init__.py
|
||||
msgid "Less Than"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/conditions/__init__.py
|
||||
msgid "Greater Than or Equal To"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/conditions/__init__.py
|
||||
msgid "Less Than or Equal To"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/conditions/__init__.py
|
||||
msgid "Equals"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/conditions/__init__.py
|
||||
msgid "Not Equals"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/conditions/__init__.py
|
||||
msgid "Contains"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/conditions/__init__.py
|
||||
msgid "Choose one - Field"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/conditions/default_plugin.py
|
||||
msgid "Does NOT Contain"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/conditions/default_plugin.py
|
||||
msgid "Text Starts With"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/conditions/default_plugin.py
|
||||
msgid "Text Ends With"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/conditions/default_plugin.py
|
||||
msgid "Length minimum"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/conditions/default_plugin.py
|
||||
msgid "Length maximum"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/conditions/default_plugin.py
|
||||
msgid "Text Matches Regex"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/conditions/default_plugin.py
|
||||
msgid "Text Does NOT Match Regex"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/conditions/default_plugin.py
|
||||
msgid "Extracted number after 'Filters & Triggers'"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/conditions/default_plugin.py
|
||||
msgid "Page text after 'Filters & Triggers'"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/conditions/form.py
|
||||
msgid "Field"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/conditions/form.py
|
||||
msgid "Operator"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/conditions/form.py
|
||||
msgid "A value"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/conditions/form.py
|
||||
msgid "Operator is required."
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/conditions/form.py
|
||||
msgid "Field is required."
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/conditions/form.py
|
||||
msgid "Value is required."
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/conditions/plugins/levenshtein_plugin.py
|
||||
msgid "Levenshtein - Text similarity ratio"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/conditions/plugins/levenshtein_plugin.py
|
||||
msgid "Levenshtein - Text change distance"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/conditions/plugins/levenshtein_plugin.py
|
||||
msgid "Not enough history to calculate Levenshtein metrics"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/conditions/plugins/levenshtein_plugin.py
|
||||
msgid "Snapshot too large for edit statistics, skipping."
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/conditions/plugins/levenshtein_plugin.py
|
||||
msgid "Unable to calculate Levenshtein metrics"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/conditions/plugins/levenshtein_plugin.py
|
||||
msgid "Levenshtein Text Similarity Details"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/conditions/plugins/levenshtein_plugin.py
|
||||
msgid "Raw distance (edits needed)"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/conditions/plugins/levenshtein_plugin.py
|
||||
msgid "Similarity ratio"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/conditions/plugins/levenshtein_plugin.py
|
||||
msgid "Percent similar"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/conditions/plugins/levenshtein_plugin.py
|
||||
msgid ""
|
||||
"Levenshtein metrics compare the last two snapshots, measuring how many character edits are needed to transform one "
|
||||
"into the other."
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/conditions/plugins/levenshtein_plugin.py
|
||||
msgid "Error calculating Levenshtein metrics"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/conditions/plugins/wordcount_plugin.py
|
||||
msgid "Word count of content"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/conditions/plugins/wordcount_plugin.py
|
||||
msgid "Content Analysis"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/conditions/plugins/wordcount_plugin.py
|
||||
msgid "Word count (latest snapshot)"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/conditions/plugins/wordcount_plugin.py
|
||||
msgid "Word count is a simple measure of content length, calculated by splitting text on whitespace."
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/content_fetchers/requests.py
|
||||
msgid "Basic fast Plaintext/HTTP Client"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/blueprint/watchlist/templates/watch-overview.html changedetectionio/flask_app.py
|
||||
#: changedetectionio/realtime/socket_server.py
|
||||
msgid "Not yet"
|
||||
@@ -2426,26 +2207,10 @@ msgstr "Možnosti uživatelského rozhraní"
|
||||
msgid "Selector"
|
||||
msgstr "Režim výběru:"
|
||||
|
||||
#: changedetectionio/forms.py
|
||||
msgid "CSS or xPath selector"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/forms.py
|
||||
msgid "value"
|
||||
msgstr "Pauza"
|
||||
|
||||
#: changedetectionio/conditions/form.py changedetectionio/forms.py
|
||||
msgid "Value"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/forms.py
|
||||
msgid "Web Page URL"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/forms.py
|
||||
msgid "Group Tag"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/forms.py
|
||||
msgid "Time Between Check"
|
||||
msgstr "Interval mezi kontrolami"
|
||||
@@ -2534,8 +2299,7 @@ msgstr "Blokovat detekci změn, když se text shoduje"
|
||||
msgid "Execute JavaScript before change detection"
|
||||
msgstr "Spusťte JavaScript před detekcí změn"
|
||||
|
||||
#: changedetectionio/blueprint/tags/form.py changedetectionio/blueprint/tags/templates/groups-overview.html
|
||||
#: changedetectionio/forms.py
|
||||
#: changedetectionio/blueprint/tags/templates/groups-overview.html changedetectionio/forms.py
|
||||
msgid "Save"
|
||||
msgstr "Uložit"
|
||||
|
||||
@@ -2603,7 +2367,7 @@ msgstr "Neplatná syntaxe šablony: %(error)s"
|
||||
msgid "Invalid template syntax in \"%(header)s\" header: %(error)s"
|
||||
msgstr "Neplatná syntax šablony v \"%(header)s\" hlavička: %(error)s"
|
||||
|
||||
#: changedetectionio/blueprint/tags/form.py changedetectionio/forms.py
|
||||
#: changedetectionio/forms.py
|
||||
msgid "Name"
|
||||
msgstr "Název"
|
||||
|
||||
@@ -2699,10 +2463,6 @@ msgstr "Text chyby"
|
||||
msgid "Ignore whitespace"
|
||||
msgstr "Ignorujte mezery"
|
||||
|
||||
#: changedetectionio/forms.py
|
||||
msgid "Screenshot: Minimum Change Percentage"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/forms.py changedetectionio/processors/image_ssim_diff/forms.py
|
||||
msgid "Must be between 0 and 100"
|
||||
msgstr "Musí být mezi 0 a 100"
|
||||
@@ -2902,42 +2662,6 @@ msgstr "Doplnění zásob a zjištění ceny pro stránky s JEDINÝM produktem"
|
||||
msgid "Detects if the product goes back to in-stock"
|
||||
msgstr "Zjistí, zda se produkt vrátí na sklad"
|
||||
|
||||
#: changedetectionio/processors/templates/extract.html
|
||||
msgid "Screenshot"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/processors/templates/extract.html
|
||||
msgid "This tool will extract text data from all of the watch history."
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/processors/templates/extract.html
|
||||
msgid "A <strong>RegEx</strong> is a pattern that identifies exactly which part inside of the text that you want to extract."
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/processors/templates/extract.html
|
||||
msgid "For example, to extract only the numbers from text"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/processors/templates/extract.html
|
||||
msgid "Raw text"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/processors/templates/extract.html
|
||||
msgid "RegEx to extract:"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/processors/templates/extract.html
|
||||
msgid "Be sure to test your RegEx here."
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/processors/templates/extract.html
|
||||
msgid "Each RegEx group bracket"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/processors/templates/extract.html
|
||||
msgid "will be in its own column, the first column value is always the date."
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/processors/text_json_diff/processor.py
|
||||
msgid "Webpage Text/HTML, JSON and PDF changes"
|
||||
msgstr "Změny textu webové stránky/HTML, JSON a PDF"
|
||||
@@ -3117,7 +2841,7 @@ msgstr ""
|
||||
msgid "Use"
|
||||
msgstr "Použít"
|
||||
|
||||
#: changedetectionio/templates/_common_fields.html changedetectionio/templates/edit/include_subtract.html
|
||||
#: changedetectionio/templates/_common_fields.html
|
||||
msgid "Show advanced help and tips"
|
||||
msgstr "Zobrazit pokročilou nápovědu a tipy"
|
||||
|
||||
@@ -3221,26 +2945,6 @@ msgstr ""
|
||||
msgid "Format for all notifications"
|
||||
msgstr "Formát pro všechna oznámení"
|
||||
|
||||
#: changedetectionio/templates/_common_fields.html
|
||||
msgid "Note"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/templates/_common_fields.html
|
||||
msgid "Discord does not render HTML — switch to"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/templates/_common_fields.html
|
||||
msgid "Plain Text"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/templates/_common_fields.html
|
||||
msgid "format to avoid"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/templates/_common_fields.html
|
||||
msgid "and other HTML entities appearing literally in your notifications."
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/templates/_helpers.html
|
||||
msgid "Entry"
|
||||
msgstr ""
|
||||
@@ -3357,18 +3061,10 @@ msgstr "Nedojde k detekci změn, protože tento text existuje."
|
||||
msgid "Blocked text"
|
||||
msgstr "Blokovaný text"
|
||||
|
||||
#: changedetectionio/templates/base.html
|
||||
msgid "A new version is available"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/templates/base.html
|
||||
msgid "Search, or Use Alt+S Key"
|
||||
msgstr "Vyhledejte nebo použijte klávesu Alt+S"
|
||||
|
||||
#: changedetectionio/templates/base.html
|
||||
msgid "Share this link:"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/templates/base.html
|
||||
msgid "Real-time updates offline"
|
||||
msgstr "Aktualizace v reálném čase offline"
|
||||
@@ -3539,6 +3235,10 @@ msgstr ""
|
||||
msgid "Unmute notifications"
|
||||
msgstr "Odtlumit oznámení"
|
||||
|
||||
#: changedetectionio/templates/menu.html
|
||||
msgid "Mute notifications"
|
||||
msgstr "Ztlumit oznámení"
|
||||
|
||||
#: changedetectionio/templates/menu.html
|
||||
msgid "Notifications are muted - click to unmute"
|
||||
msgstr "Oznámení jsou ztlumena - klikněte pro odtlumení"
|
||||
|
||||
Binary file not shown.
@@ -7,7 +7,7 @@ msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: PROJECT VERSION\n"
|
||||
"Report-Msgid-Bugs-To: EMAIL@ADDRESS\n"
|
||||
"POT-Creation-Date: 2026-04-15 03:04+0900\n"
|
||||
"POT-Creation-Date: 2026-04-11 04:15+0200\n"
|
||||
"PO-Revision-Date: 2026-01-14 03:57+0100\n"
|
||||
"Last-Translator: \n"
|
||||
"Language: de\n"
|
||||
@@ -604,15 +604,15 @@ msgstr ""
|
||||
msgid "Changing this could affect the content of your existing watches, possibly trigger alerts etc."
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/blueprint/settings/templates/settings.html changedetectionio/templates/edit/include_subtract.html
|
||||
#: changedetectionio/blueprint/settings/templates/settings.html
|
||||
msgid "Remove HTML element(s) by CSS and XPath selectors before text conversion."
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/blueprint/settings/templates/settings.html changedetectionio/templates/edit/include_subtract.html
|
||||
#: changedetectionio/blueprint/settings/templates/settings.html
|
||||
msgid "Don't paste HTML here, use only CSS and XPath selectors"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/blueprint/settings/templates/settings.html changedetectionio/templates/edit/include_subtract.html
|
||||
#: changedetectionio/blueprint/settings/templates/settings.html
|
||||
msgid "Add multiple elements, CSS or XPath selectors per line to ignore multiple parts of the HTML."
|
||||
msgstr ""
|
||||
|
||||
@@ -843,26 +843,6 @@ msgstr "Tag nicht gefunden"
|
||||
msgid "Updated"
|
||||
msgstr "Aktualisiert"
|
||||
|
||||
#: changedetectionio/blueprint/tags/form.py
|
||||
msgid "Activate for individual watches in this tag/group?"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/blueprint/tags/form.py
|
||||
msgid "Auto-apply to watches with URLs matching"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/blueprint/tags/form.py
|
||||
msgid "e.g. *://example.com/* or github.com/myorg"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/blueprint/tags/form.py changedetectionio/blueprint/tags/templates/edit-tag.html
|
||||
msgid "Tag colour"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/blueprint/tags/form.py
|
||||
msgid "Tag name"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/blueprint/tags/templates/edit-tag.html changedetectionio/blueprint/ui/templates/edit.html
|
||||
msgid "Filters & Triggers"
|
||||
msgstr "Filter und Trigger"
|
||||
@@ -877,6 +857,10 @@ msgstr ""
|
||||
msgid "Currently matching watches"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/blueprint/tags/templates/edit-tag.html
|
||||
msgid "Tag colour"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/blueprint/tags/templates/edit-tag.html
|
||||
msgid "Custom colour"
|
||||
msgstr ""
|
||||
@@ -964,11 +948,7 @@ msgstr "Tag-/Labelname"
|
||||
msgid "No website organisational tags/groups configured"
|
||||
msgstr "Keine Gruppen/Labels konfiguriert"
|
||||
|
||||
#: changedetectionio/blueprint/tags/templates/groups-overview.html changedetectionio/templates/menu.html
|
||||
msgid "Mute notifications"
|
||||
msgstr "Benachrichtigungen stummschalten"
|
||||
|
||||
#: changedetectionio/blueprint/tags/templates/groups-overview.html changedetectionio/blueprint/ui/edit.py
|
||||
#: changedetectionio/blueprint/tags/templates/groups-overview.html
|
||||
#: changedetectionio/blueprint/watchlist/templates/watch-overview.html
|
||||
msgid "Edit"
|
||||
msgstr "Bearbeiten"
|
||||
@@ -1175,14 +1155,6 @@ msgstr ""
|
||||
msgid "Could not load '{}' processor, processor plugin might be missing."
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/blueprint/ui/edit.py
|
||||
msgid "System settings default"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/blueprint/ui/edit.py
|
||||
msgid "Default"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/blueprint/ui/edit.py
|
||||
msgid "Updated watch - unpaused!"
|
||||
msgstr "Aktualisierte Überwachung – fortgesetzt!"
|
||||
@@ -1195,10 +1167,6 @@ msgstr "Überwachung aktualisiert."
|
||||
msgid "Preview unavailable - No fetch/check completed or triggers not reached"
|
||||
msgstr "Vorschau nicht verfügbar – Kein Abruf/keine Überprüfung abgeschlossen oder Trigger nicht erreicht"
|
||||
|
||||
#: changedetectionio/blueprint/ui/preview.py
|
||||
msgid "Diff"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/blueprint/ui/templates/clear_all_history.html
|
||||
msgid "This will remove version history (snapshots) for ALL watches, but keep your list of URLs!"
|
||||
msgstr ""
|
||||
@@ -1310,17 +1278,14 @@ msgid "Jump"
|
||||
msgstr "Springen"
|
||||
|
||||
#: changedetectionio/blueprint/ui/templates/diff.html changedetectionio/blueprint/ui/templates/preview.html
|
||||
#: changedetectionio/processors/templates/extract.html
|
||||
msgid "Error Text"
|
||||
msgstr "Fehlertext"
|
||||
|
||||
#: changedetectionio/blueprint/ui/templates/diff.html changedetectionio/blueprint/ui/templates/preview.html
|
||||
#: changedetectionio/processors/templates/extract.html
|
||||
msgid "Error Screenshot"
|
||||
msgstr "Fehler-Screenshot"
|
||||
|
||||
#: changedetectionio/blueprint/ui/templates/diff.html changedetectionio/blueprint/ui/templates/preview.html
|
||||
#: changedetectionio/processors/templates/extract.html
|
||||
msgid "Text"
|
||||
msgstr "Text"
|
||||
|
||||
@@ -1328,8 +1293,7 @@ msgstr "Text"
|
||||
msgid "Current screenshot"
|
||||
msgstr "Aktueller Screenshot"
|
||||
|
||||
#: changedetectionio/blueprint/ui/templates/diff.html changedetectionio/processors/extract.py
|
||||
#: changedetectionio/processors/templates/extract.html
|
||||
#: changedetectionio/blueprint/ui/templates/diff.html
|
||||
msgid "Extract Data"
|
||||
msgstr "Daten extrahieren"
|
||||
|
||||
@@ -1943,26 +1907,6 @@ msgstr "Es sind keine Website-Überwachungen konfiguriert. Bitte fügen Sie im F
|
||||
msgid "import a list"
|
||||
msgstr "eine Liste importieren"
|
||||
|
||||
#: changedetectionio/blueprint/watchlist/templates/watch-overview.html
|
||||
msgid "Pause checks"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/blueprint/watchlist/templates/watch-overview.html
|
||||
msgid "UnPause checks"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/blueprint/watchlist/templates/watch-overview.html
|
||||
msgid "Mute notification"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/blueprint/watchlist/templates/watch-overview.html
|
||||
msgid "UnMute notification"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/blueprint/watchlist/templates/watch-overview.html
|
||||
msgid "Create a link to share watch config with others"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/blueprint/watchlist/templates/watch-overview.html
|
||||
msgid "Detecting restock and price"
|
||||
msgstr "Erkennen von Lagerbeständen und Preisen"
|
||||
@@ -1992,7 +1936,6 @@ msgid "Queued"
|
||||
msgstr "Wartend"
|
||||
|
||||
#: changedetectionio/blueprint/watchlist/templates/watch-overview.html
|
||||
#: changedetectionio/processors/text_json_diff/difference.py
|
||||
msgid "History"
|
||||
msgstr "Verlauf"
|
||||
|
||||
@@ -2026,168 +1969,6 @@ msgstr "Überprüfen Sie alles noch einmal"
|
||||
msgid "in '%(title)s'"
|
||||
msgstr "in '%(title)s'"
|
||||
|
||||
#: changedetectionio/conditions/__init__.py
|
||||
msgid "Choose one - Operator"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/conditions/__init__.py
|
||||
msgid "Greater Than"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/conditions/__init__.py
|
||||
msgid "Less Than"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/conditions/__init__.py
|
||||
msgid "Greater Than or Equal To"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/conditions/__init__.py
|
||||
msgid "Less Than or Equal To"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/conditions/__init__.py
|
||||
msgid "Equals"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/conditions/__init__.py
|
||||
msgid "Not Equals"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/conditions/__init__.py
|
||||
msgid "Contains"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/conditions/__init__.py
|
||||
msgid "Choose one - Field"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/conditions/default_plugin.py
|
||||
msgid "Does NOT Contain"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/conditions/default_plugin.py
|
||||
msgid "Text Starts With"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/conditions/default_plugin.py
|
||||
msgid "Text Ends With"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/conditions/default_plugin.py
|
||||
msgid "Length minimum"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/conditions/default_plugin.py
|
||||
msgid "Length maximum"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/conditions/default_plugin.py
|
||||
msgid "Text Matches Regex"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/conditions/default_plugin.py
|
||||
msgid "Text Does NOT Match Regex"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/conditions/default_plugin.py
|
||||
msgid "Extracted number after 'Filters & Triggers'"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/conditions/default_plugin.py
|
||||
msgid "Page text after 'Filters & Triggers'"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/conditions/form.py
|
||||
msgid "Field"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/conditions/form.py
|
||||
msgid "Operator"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/conditions/form.py
|
||||
msgid "A value"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/conditions/form.py
|
||||
msgid "Operator is required."
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/conditions/form.py
|
||||
msgid "Field is required."
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/conditions/form.py
|
||||
msgid "Value is required."
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/conditions/plugins/levenshtein_plugin.py
|
||||
msgid "Levenshtein - Text similarity ratio"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/conditions/plugins/levenshtein_plugin.py
|
||||
msgid "Levenshtein - Text change distance"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/conditions/plugins/levenshtein_plugin.py
|
||||
msgid "Not enough history to calculate Levenshtein metrics"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/conditions/plugins/levenshtein_plugin.py
|
||||
msgid "Snapshot too large for edit statistics, skipping."
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/conditions/plugins/levenshtein_plugin.py
|
||||
msgid "Unable to calculate Levenshtein metrics"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/conditions/plugins/levenshtein_plugin.py
|
||||
msgid "Levenshtein Text Similarity Details"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/conditions/plugins/levenshtein_plugin.py
|
||||
msgid "Raw distance (edits needed)"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/conditions/plugins/levenshtein_plugin.py
|
||||
msgid "Similarity ratio"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/conditions/plugins/levenshtein_plugin.py
|
||||
msgid "Percent similar"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/conditions/plugins/levenshtein_plugin.py
|
||||
msgid ""
|
||||
"Levenshtein metrics compare the last two snapshots, measuring how many character edits are needed to transform one "
|
||||
"into the other."
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/conditions/plugins/levenshtein_plugin.py
|
||||
msgid "Error calculating Levenshtein metrics"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/conditions/plugins/wordcount_plugin.py
|
||||
msgid "Word count of content"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/conditions/plugins/wordcount_plugin.py
|
||||
msgid "Content Analysis"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/conditions/plugins/wordcount_plugin.py
|
||||
msgid "Word count (latest snapshot)"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/conditions/plugins/wordcount_plugin.py
|
||||
msgid "Word count is a simple measure of content length, calculated by splitting text on whitespace."
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/content_fetchers/requests.py
|
||||
msgid "Basic fast Plaintext/HTTP Client"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/blueprint/watchlist/templates/watch-overview.html changedetectionio/flask_app.py
|
||||
#: changedetectionio/realtime/socket_server.py
|
||||
msgid "Not yet"
|
||||
@@ -2472,26 +2253,10 @@ msgstr "UI-Optionen"
|
||||
msgid "Selector"
|
||||
msgstr "Auswahlmodus:"
|
||||
|
||||
#: changedetectionio/forms.py
|
||||
msgid "CSS or xPath selector"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/forms.py
|
||||
msgid "value"
|
||||
msgstr "Wert"
|
||||
|
||||
#: changedetectionio/conditions/form.py changedetectionio/forms.py
|
||||
msgid "Value"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/forms.py
|
||||
msgid "Web Page URL"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/forms.py
|
||||
msgid "Group Tag"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/forms.py
|
||||
msgid "Time Between Check"
|
||||
msgstr "Prüfintervall"
|
||||
@@ -2580,8 +2345,7 @@ msgstr "Blockieren Sie die Änderungserkennung, während der Text übereinstimmt
|
||||
msgid "Execute JavaScript before change detection"
|
||||
msgstr "Führen Sie JavaScript vor der Änderungserkennung aus"
|
||||
|
||||
#: changedetectionio/blueprint/tags/form.py changedetectionio/blueprint/tags/templates/groups-overview.html
|
||||
#: changedetectionio/forms.py
|
||||
#: changedetectionio/blueprint/tags/templates/groups-overview.html changedetectionio/forms.py
|
||||
msgid "Save"
|
||||
msgstr "Speichern"
|
||||
|
||||
@@ -2650,7 +2414,7 @@ msgstr "Ungültige Vorlagensyntax: %(error)s"
|
||||
msgid "Invalid template syntax in \"%(header)s\" header: %(error)s"
|
||||
msgstr "Ungültige Vorlagensyntax im Header „%(header)s“: %(error)s"
|
||||
|
||||
#: changedetectionio/blueprint/tags/form.py changedetectionio/forms.py
|
||||
#: changedetectionio/forms.py
|
||||
msgid "Name"
|
||||
msgstr "Name"
|
||||
|
||||
@@ -2746,10 +2510,6 @@ msgstr "Text ignorieren"
|
||||
msgid "Ignore whitespace"
|
||||
msgstr "Leerzeichen ignorieren"
|
||||
|
||||
#: changedetectionio/forms.py
|
||||
msgid "Screenshot: Minimum Change Percentage"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/forms.py changedetectionio/processors/image_ssim_diff/forms.py
|
||||
msgid "Must be between 0 and 100"
|
||||
msgstr "Muss zwischen 0 und 100 liegen"
|
||||
@@ -2951,42 +2711,6 @@ msgstr "Wiederauffüllung und Preiserkennung für Seiten mit einem EINZELNEN Pro
|
||||
msgid "Detects if the product goes back to in-stock"
|
||||
msgstr "Erkennt, ob das Produkt wieder auf Lager ist"
|
||||
|
||||
#: changedetectionio/processors/templates/extract.html
|
||||
msgid "Screenshot"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/processors/templates/extract.html
|
||||
msgid "This tool will extract text data from all of the watch history."
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/processors/templates/extract.html
|
||||
msgid "A <strong>RegEx</strong> is a pattern that identifies exactly which part inside of the text that you want to extract."
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/processors/templates/extract.html
|
||||
msgid "For example, to extract only the numbers from text"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/processors/templates/extract.html
|
||||
msgid "Raw text"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/processors/templates/extract.html
|
||||
msgid "RegEx to extract:"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/processors/templates/extract.html
|
||||
msgid "Be sure to test your RegEx here."
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/processors/templates/extract.html
|
||||
msgid "Each RegEx group bracket"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/processors/templates/extract.html
|
||||
msgid "will be in its own column, the first column value is always the date."
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/processors/text_json_diff/processor.py
|
||||
msgid "Webpage Text/HTML, JSON and PDF changes"
|
||||
msgstr "Änderungen an Webseitentext/HTML, JSON und PDF"
|
||||
@@ -3166,7 +2890,7 @@ msgstr ""
|
||||
msgid "Use"
|
||||
msgstr "Verwenden"
|
||||
|
||||
#: changedetectionio/templates/_common_fields.html changedetectionio/templates/edit/include_subtract.html
|
||||
#: changedetectionio/templates/_common_fields.html
|
||||
msgid "Show advanced help and tips"
|
||||
msgstr "Erweiterte Hilfe und Tipps anzeigen"
|
||||
|
||||
@@ -3270,26 +2994,6 @@ msgstr ""
|
||||
msgid "Format for all notifications"
|
||||
msgstr "Format für alle Benachrichtigungen"
|
||||
|
||||
#: changedetectionio/templates/_common_fields.html
|
||||
msgid "Note"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/templates/_common_fields.html
|
||||
msgid "Discord does not render HTML — switch to"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/templates/_common_fields.html
|
||||
msgid "Plain Text"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/templates/_common_fields.html
|
||||
msgid "format to avoid"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/templates/_common_fields.html
|
||||
msgid "and other HTML entities appearing literally in your notifications."
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/templates/_helpers.html
|
||||
msgid "Entry"
|
||||
msgstr ""
|
||||
@@ -3406,18 +3110,10 @@ msgstr "Es wird keine Änderungserkennung stattfinden, da dieser Text existiert.
|
||||
msgid "Blocked text"
|
||||
msgstr "Blockierter Text"
|
||||
|
||||
#: changedetectionio/templates/base.html
|
||||
msgid "A new version is available"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/templates/base.html
|
||||
msgid "Search, or Use Alt+S Key"
|
||||
msgstr "Suchen oder Alt+S-Taste verwenden"
|
||||
|
||||
#: changedetectionio/templates/base.html
|
||||
msgid "Share this link:"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/templates/base.html
|
||||
msgid "Real-time updates offline"
|
||||
msgstr "Echtzeit-Updates offline"
|
||||
@@ -3590,6 +3286,10 @@ msgstr ""
|
||||
msgid "Unmute notifications"
|
||||
msgstr "Benachrichtigungen entstummen"
|
||||
|
||||
#: changedetectionio/templates/menu.html
|
||||
msgid "Mute notifications"
|
||||
msgstr "Benachrichtigungen stummschalten"
|
||||
|
||||
#: changedetectionio/templates/menu.html
|
||||
msgid "Notifications are muted - click to unmute"
|
||||
msgstr "Benachrichtigungen sind stummgeschaltet - klicken zum Entstummen"
|
||||
|
||||
Binary file not shown.
@@ -7,7 +7,7 @@ msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: changedetection.io\n"
|
||||
"Report-Msgid-Bugs-To: https://github.com/dgtlmoon/changedetection.io\n"
|
||||
"POT-Creation-Date: 2026-04-15 03:04+0900\n"
|
||||
"POT-Creation-Date: 2026-04-11 04:15+0200\n"
|
||||
"PO-Revision-Date: 2026-01-12 16:33+0100\n"
|
||||
"Last-Translator: British English Translation Team\n"
|
||||
"Language: en_GB\n"
|
||||
@@ -590,15 +590,15 @@ msgstr ""
|
||||
msgid "Changing this could affect the content of your existing watches, possibly trigger alerts etc."
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/blueprint/settings/templates/settings.html changedetectionio/templates/edit/include_subtract.html
|
||||
#: changedetectionio/blueprint/settings/templates/settings.html
|
||||
msgid "Remove HTML element(s) by CSS and XPath selectors before text conversion."
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/blueprint/settings/templates/settings.html changedetectionio/templates/edit/include_subtract.html
|
||||
#: changedetectionio/blueprint/settings/templates/settings.html
|
||||
msgid "Don't paste HTML here, use only CSS and XPath selectors"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/blueprint/settings/templates/settings.html changedetectionio/templates/edit/include_subtract.html
|
||||
#: changedetectionio/blueprint/settings/templates/settings.html
|
||||
msgid "Add multiple elements, CSS or XPath selectors per line to ignore multiple parts of the HTML."
|
||||
msgstr ""
|
||||
|
||||
@@ -829,26 +829,6 @@ msgstr ""
|
||||
msgid "Updated"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/blueprint/tags/form.py
|
||||
msgid "Activate for individual watches in this tag/group?"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/blueprint/tags/form.py
|
||||
msgid "Auto-apply to watches with URLs matching"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/blueprint/tags/form.py
|
||||
msgid "e.g. *://example.com/* or github.com/myorg"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/blueprint/tags/form.py changedetectionio/blueprint/tags/templates/edit-tag.html
|
||||
msgid "Tag colour"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/blueprint/tags/form.py
|
||||
msgid "Tag name"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/blueprint/tags/templates/edit-tag.html changedetectionio/blueprint/ui/templates/edit.html
|
||||
msgid "Filters & Triggers"
|
||||
msgstr ""
|
||||
@@ -863,6 +843,10 @@ msgstr ""
|
||||
msgid "Currently matching watches"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/blueprint/tags/templates/edit-tag.html
|
||||
msgid "Tag colour"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/blueprint/tags/templates/edit-tag.html
|
||||
msgid "Custom colour"
|
||||
msgstr ""
|
||||
@@ -948,11 +932,7 @@ msgstr ""
|
||||
msgid "No website organisational tags/groups configured"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/blueprint/tags/templates/groups-overview.html changedetectionio/templates/menu.html
|
||||
msgid "Mute notifications"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/blueprint/tags/templates/groups-overview.html changedetectionio/blueprint/ui/edit.py
|
||||
#: changedetectionio/blueprint/tags/templates/groups-overview.html
|
||||
#: changedetectionio/blueprint/watchlist/templates/watch-overview.html
|
||||
msgid "Edit"
|
||||
msgstr ""
|
||||
@@ -1155,14 +1135,6 @@ msgstr ""
|
||||
msgid "Could not load '{}' processor, processor plugin might be missing."
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/blueprint/ui/edit.py
|
||||
msgid "System settings default"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/blueprint/ui/edit.py
|
||||
msgid "Default"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/blueprint/ui/edit.py
|
||||
msgid "Updated watch - unpaused!"
|
||||
msgstr ""
|
||||
@@ -1175,10 +1147,6 @@ msgstr ""
|
||||
msgid "Preview unavailable - No fetch/check completed or triggers not reached"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/blueprint/ui/preview.py
|
||||
msgid "Diff"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/blueprint/ui/templates/clear_all_history.html
|
||||
msgid "This will remove version history (snapshots) for ALL watches, but keep your list of URLs!"
|
||||
msgstr ""
|
||||
@@ -1288,17 +1256,14 @@ msgid "Jump"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/blueprint/ui/templates/diff.html changedetectionio/blueprint/ui/templates/preview.html
|
||||
#: changedetectionio/processors/templates/extract.html
|
||||
msgid "Error Text"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/blueprint/ui/templates/diff.html changedetectionio/blueprint/ui/templates/preview.html
|
||||
#: changedetectionio/processors/templates/extract.html
|
||||
msgid "Error Screenshot"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/blueprint/ui/templates/diff.html changedetectionio/blueprint/ui/templates/preview.html
|
||||
#: changedetectionio/processors/templates/extract.html
|
||||
msgid "Text"
|
||||
msgstr ""
|
||||
|
||||
@@ -1306,8 +1271,7 @@ msgstr ""
|
||||
msgid "Current screenshot"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/blueprint/ui/templates/diff.html changedetectionio/processors/extract.py
|
||||
#: changedetectionio/processors/templates/extract.html
|
||||
#: changedetectionio/blueprint/ui/templates/diff.html
|
||||
msgid "Extract Data"
|
||||
msgstr ""
|
||||
|
||||
@@ -1897,26 +1861,6 @@ msgstr ""
|
||||
msgid "import a list"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/blueprint/watchlist/templates/watch-overview.html
|
||||
msgid "Pause checks"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/blueprint/watchlist/templates/watch-overview.html
|
||||
msgid "UnPause checks"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/blueprint/watchlist/templates/watch-overview.html
|
||||
msgid "Mute notification"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/blueprint/watchlist/templates/watch-overview.html
|
||||
msgid "UnMute notification"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/blueprint/watchlist/templates/watch-overview.html
|
||||
msgid "Create a link to share watch config with others"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/blueprint/watchlist/templates/watch-overview.html
|
||||
msgid "Detecting restock and price"
|
||||
msgstr ""
|
||||
@@ -1946,7 +1890,6 @@ msgid "Queued"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/blueprint/watchlist/templates/watch-overview.html
|
||||
#: changedetectionio/processors/text_json_diff/difference.py
|
||||
msgid "History"
|
||||
msgstr ""
|
||||
|
||||
@@ -1980,168 +1923,6 @@ msgstr ""
|
||||
msgid "in '%(title)s'"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/conditions/__init__.py
|
||||
msgid "Choose one - Operator"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/conditions/__init__.py
|
||||
msgid "Greater Than"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/conditions/__init__.py
|
||||
msgid "Less Than"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/conditions/__init__.py
|
||||
msgid "Greater Than or Equal To"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/conditions/__init__.py
|
||||
msgid "Less Than or Equal To"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/conditions/__init__.py
|
||||
msgid "Equals"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/conditions/__init__.py
|
||||
msgid "Not Equals"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/conditions/__init__.py
|
||||
msgid "Contains"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/conditions/__init__.py
|
||||
msgid "Choose one - Field"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/conditions/default_plugin.py
|
||||
msgid "Does NOT Contain"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/conditions/default_plugin.py
|
||||
msgid "Text Starts With"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/conditions/default_plugin.py
|
||||
msgid "Text Ends With"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/conditions/default_plugin.py
|
||||
msgid "Length minimum"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/conditions/default_plugin.py
|
||||
msgid "Length maximum"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/conditions/default_plugin.py
|
||||
msgid "Text Matches Regex"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/conditions/default_plugin.py
|
||||
msgid "Text Does NOT Match Regex"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/conditions/default_plugin.py
|
||||
msgid "Extracted number after 'Filters & Triggers'"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/conditions/default_plugin.py
|
||||
msgid "Page text after 'Filters & Triggers'"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/conditions/form.py
|
||||
msgid "Field"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/conditions/form.py
|
||||
msgid "Operator"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/conditions/form.py
|
||||
msgid "A value"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/conditions/form.py
|
||||
msgid "Operator is required."
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/conditions/form.py
|
||||
msgid "Field is required."
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/conditions/form.py
|
||||
msgid "Value is required."
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/conditions/plugins/levenshtein_plugin.py
|
||||
msgid "Levenshtein - Text similarity ratio"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/conditions/plugins/levenshtein_plugin.py
|
||||
msgid "Levenshtein - Text change distance"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/conditions/plugins/levenshtein_plugin.py
|
||||
msgid "Not enough history to calculate Levenshtein metrics"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/conditions/plugins/levenshtein_plugin.py
|
||||
msgid "Snapshot too large for edit statistics, skipping."
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/conditions/plugins/levenshtein_plugin.py
|
||||
msgid "Unable to calculate Levenshtein metrics"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/conditions/plugins/levenshtein_plugin.py
|
||||
msgid "Levenshtein Text Similarity Details"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/conditions/plugins/levenshtein_plugin.py
|
||||
msgid "Raw distance (edits needed)"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/conditions/plugins/levenshtein_plugin.py
|
||||
msgid "Similarity ratio"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/conditions/plugins/levenshtein_plugin.py
|
||||
msgid "Percent similar"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/conditions/plugins/levenshtein_plugin.py
|
||||
msgid ""
|
||||
"Levenshtein metrics compare the last two snapshots, measuring how many character edits are needed to transform one "
|
||||
"into the other."
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/conditions/plugins/levenshtein_plugin.py
|
||||
msgid "Error calculating Levenshtein metrics"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/conditions/plugins/wordcount_plugin.py
|
||||
msgid "Word count of content"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/conditions/plugins/wordcount_plugin.py
|
||||
msgid "Content Analysis"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/conditions/plugins/wordcount_plugin.py
|
||||
msgid "Word count (latest snapshot)"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/conditions/plugins/wordcount_plugin.py
|
||||
msgid "Word count is a simple measure of content length, calculated by splitting text on whitespace."
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/content_fetchers/requests.py
|
||||
msgid "Basic fast Plaintext/HTTP Client"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/blueprint/watchlist/templates/watch-overview.html changedetectionio/flask_app.py
|
||||
#: changedetectionio/realtime/socket_server.py
|
||||
msgid "Not yet"
|
||||
@@ -2424,26 +2205,10 @@ msgstr ""
|
||||
msgid "Selector"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/forms.py
|
||||
msgid "CSS or xPath selector"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/forms.py
|
||||
msgid "value"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/conditions/form.py changedetectionio/forms.py
|
||||
msgid "Value"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/forms.py
|
||||
msgid "Web Page URL"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/forms.py
|
||||
msgid "Group Tag"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/forms.py
|
||||
msgid "Time Between Check"
|
||||
msgstr ""
|
||||
@@ -2532,8 +2297,7 @@ msgstr ""
|
||||
msgid "Execute JavaScript before change detection"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/blueprint/tags/form.py changedetectionio/blueprint/tags/templates/groups-overview.html
|
||||
#: changedetectionio/forms.py
|
||||
#: changedetectionio/blueprint/tags/templates/groups-overview.html changedetectionio/forms.py
|
||||
msgid "Save"
|
||||
msgstr ""
|
||||
|
||||
@@ -2601,7 +2365,7 @@ msgstr ""
|
||||
msgid "Invalid template syntax in \"%(header)s\" header: %(error)s"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/blueprint/tags/form.py changedetectionio/forms.py
|
||||
#: changedetectionio/forms.py
|
||||
msgid "Name"
|
||||
msgstr ""
|
||||
|
||||
@@ -2697,10 +2461,6 @@ msgstr ""
|
||||
msgid "Ignore whitespace"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/forms.py
|
||||
msgid "Screenshot: Minimum Change Percentage"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/forms.py changedetectionio/processors/image_ssim_diff/forms.py
|
||||
msgid "Must be between 0 and 100"
|
||||
msgstr ""
|
||||
@@ -2900,42 +2660,6 @@ msgstr ""
|
||||
msgid "Detects if the product goes back to in-stock"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/processors/templates/extract.html
|
||||
msgid "Screenshot"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/processors/templates/extract.html
|
||||
msgid "This tool will extract text data from all of the watch history."
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/processors/templates/extract.html
|
||||
msgid "A <strong>RegEx</strong> is a pattern that identifies exactly which part inside of the text that you want to extract."
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/processors/templates/extract.html
|
||||
msgid "For example, to extract only the numbers from text"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/processors/templates/extract.html
|
||||
msgid "Raw text"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/processors/templates/extract.html
|
||||
msgid "RegEx to extract:"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/processors/templates/extract.html
|
||||
msgid "Be sure to test your RegEx here."
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/processors/templates/extract.html
|
||||
msgid "Each RegEx group bracket"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/processors/templates/extract.html
|
||||
msgid "will be in its own column, the first column value is always the date."
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/processors/text_json_diff/processor.py
|
||||
msgid "Webpage Text/HTML, JSON and PDF changes"
|
||||
msgstr ""
|
||||
@@ -3115,7 +2839,7 @@ msgstr ""
|
||||
msgid "Use"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/templates/_common_fields.html changedetectionio/templates/edit/include_subtract.html
|
||||
#: changedetectionio/templates/_common_fields.html
|
||||
msgid "Show advanced help and tips"
|
||||
msgstr ""
|
||||
|
||||
@@ -3219,26 +2943,6 @@ msgstr ""
|
||||
msgid "Format for all notifications"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/templates/_common_fields.html
|
||||
msgid "Note"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/templates/_common_fields.html
|
||||
msgid "Discord does not render HTML — switch to"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/templates/_common_fields.html
|
||||
msgid "Plain Text"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/templates/_common_fields.html
|
||||
msgid "format to avoid"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/templates/_common_fields.html
|
||||
msgid "and other HTML entities appearing literally in your notifications."
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/templates/_helpers.html
|
||||
msgid "Entry"
|
||||
msgstr ""
|
||||
@@ -3355,18 +3059,10 @@ msgstr ""
|
||||
msgid "Blocked text"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/templates/base.html
|
||||
msgid "A new version is available"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/templates/base.html
|
||||
msgid "Search, or Use Alt+S Key"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/templates/base.html
|
||||
msgid "Share this link:"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/templates/base.html
|
||||
msgid "Real-time updates offline"
|
||||
msgstr ""
|
||||
@@ -3537,6 +3233,10 @@ msgstr ""
|
||||
msgid "Unmute notifications"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/templates/menu.html
|
||||
msgid "Mute notifications"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/templates/menu.html
|
||||
msgid "Notifications are muted - click to unmute"
|
||||
msgstr ""
|
||||
|
||||
Binary file not shown.
@@ -7,7 +7,7 @@ msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: PROJECT VERSION\n"
|
||||
"Report-Msgid-Bugs-To: https://github.com/dgtlmoon/changedetection.io\n"
|
||||
"POT-Creation-Date: 2026-04-15 03:04+0900\n"
|
||||
"POT-Creation-Date: 2026-04-11 04:15+0200\n"
|
||||
"PO-Revision-Date: 2026-01-12 16:37+0100\n"
|
||||
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
|
||||
"Language: en_US\n"
|
||||
@@ -590,15 +590,15 @@ msgstr ""
|
||||
msgid "Changing this could affect the content of your existing watches, possibly trigger alerts etc."
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/blueprint/settings/templates/settings.html changedetectionio/templates/edit/include_subtract.html
|
||||
#: changedetectionio/blueprint/settings/templates/settings.html
|
||||
msgid "Remove HTML element(s) by CSS and XPath selectors before text conversion."
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/blueprint/settings/templates/settings.html changedetectionio/templates/edit/include_subtract.html
|
||||
#: changedetectionio/blueprint/settings/templates/settings.html
|
||||
msgid "Don't paste HTML here, use only CSS and XPath selectors"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/blueprint/settings/templates/settings.html changedetectionio/templates/edit/include_subtract.html
|
||||
#: changedetectionio/blueprint/settings/templates/settings.html
|
||||
msgid "Add multiple elements, CSS or XPath selectors per line to ignore multiple parts of the HTML."
|
||||
msgstr ""
|
||||
|
||||
@@ -829,26 +829,6 @@ msgstr ""
|
||||
msgid "Updated"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/blueprint/tags/form.py
|
||||
msgid "Activate for individual watches in this tag/group?"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/blueprint/tags/form.py
|
||||
msgid "Auto-apply to watches with URLs matching"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/blueprint/tags/form.py
|
||||
msgid "e.g. *://example.com/* or github.com/myorg"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/blueprint/tags/form.py changedetectionio/blueprint/tags/templates/edit-tag.html
|
||||
msgid "Tag colour"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/blueprint/tags/form.py
|
||||
msgid "Tag name"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/blueprint/tags/templates/edit-tag.html changedetectionio/blueprint/ui/templates/edit.html
|
||||
msgid "Filters & Triggers"
|
||||
msgstr ""
|
||||
@@ -863,6 +843,10 @@ msgstr ""
|
||||
msgid "Currently matching watches"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/blueprint/tags/templates/edit-tag.html
|
||||
msgid "Tag colour"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/blueprint/tags/templates/edit-tag.html
|
||||
msgid "Custom colour"
|
||||
msgstr ""
|
||||
@@ -948,11 +932,7 @@ msgstr ""
|
||||
msgid "No website organisational tags/groups configured"
|
||||
msgstr "No website organizational tags/groups configured"
|
||||
|
||||
#: changedetectionio/blueprint/tags/templates/groups-overview.html changedetectionio/templates/menu.html
|
||||
msgid "Mute notifications"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/blueprint/tags/templates/groups-overview.html changedetectionio/blueprint/ui/edit.py
|
||||
#: changedetectionio/blueprint/tags/templates/groups-overview.html
|
||||
#: changedetectionio/blueprint/watchlist/templates/watch-overview.html
|
||||
msgid "Edit"
|
||||
msgstr ""
|
||||
@@ -1155,14 +1135,6 @@ msgstr ""
|
||||
msgid "Could not load '{}' processor, processor plugin might be missing."
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/blueprint/ui/edit.py
|
||||
msgid "System settings default"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/blueprint/ui/edit.py
|
||||
msgid "Default"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/blueprint/ui/edit.py
|
||||
msgid "Updated watch - unpaused!"
|
||||
msgstr ""
|
||||
@@ -1175,10 +1147,6 @@ msgstr ""
|
||||
msgid "Preview unavailable - No fetch/check completed or triggers not reached"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/blueprint/ui/preview.py
|
||||
msgid "Diff"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/blueprint/ui/templates/clear_all_history.html
|
||||
msgid "This will remove version history (snapshots) for ALL watches, but keep your list of URLs!"
|
||||
msgstr ""
|
||||
@@ -1288,17 +1256,14 @@ msgid "Jump"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/blueprint/ui/templates/diff.html changedetectionio/blueprint/ui/templates/preview.html
|
||||
#: changedetectionio/processors/templates/extract.html
|
||||
msgid "Error Text"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/blueprint/ui/templates/diff.html changedetectionio/blueprint/ui/templates/preview.html
|
||||
#: changedetectionio/processors/templates/extract.html
|
||||
msgid "Error Screenshot"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/blueprint/ui/templates/diff.html changedetectionio/blueprint/ui/templates/preview.html
|
||||
#: changedetectionio/processors/templates/extract.html
|
||||
msgid "Text"
|
||||
msgstr ""
|
||||
|
||||
@@ -1306,8 +1271,7 @@ msgstr ""
|
||||
msgid "Current screenshot"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/blueprint/ui/templates/diff.html changedetectionio/processors/extract.py
|
||||
#: changedetectionio/processors/templates/extract.html
|
||||
#: changedetectionio/blueprint/ui/templates/diff.html
|
||||
msgid "Extract Data"
|
||||
msgstr ""
|
||||
|
||||
@@ -1897,26 +1861,6 @@ msgstr ""
|
||||
msgid "import a list"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/blueprint/watchlist/templates/watch-overview.html
|
||||
msgid "Pause checks"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/blueprint/watchlist/templates/watch-overview.html
|
||||
msgid "UnPause checks"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/blueprint/watchlist/templates/watch-overview.html
|
||||
msgid "Mute notification"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/blueprint/watchlist/templates/watch-overview.html
|
||||
msgid "UnMute notification"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/blueprint/watchlist/templates/watch-overview.html
|
||||
msgid "Create a link to share watch config with others"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/blueprint/watchlist/templates/watch-overview.html
|
||||
msgid "Detecting restock and price"
|
||||
msgstr ""
|
||||
@@ -1946,7 +1890,6 @@ msgid "Queued"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/blueprint/watchlist/templates/watch-overview.html
|
||||
#: changedetectionio/processors/text_json_diff/difference.py
|
||||
msgid "History"
|
||||
msgstr ""
|
||||
|
||||
@@ -1980,168 +1923,6 @@ msgstr ""
|
||||
msgid "in '%(title)s'"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/conditions/__init__.py
|
||||
msgid "Choose one - Operator"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/conditions/__init__.py
|
||||
msgid "Greater Than"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/conditions/__init__.py
|
||||
msgid "Less Than"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/conditions/__init__.py
|
||||
msgid "Greater Than or Equal To"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/conditions/__init__.py
|
||||
msgid "Less Than or Equal To"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/conditions/__init__.py
|
||||
msgid "Equals"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/conditions/__init__.py
|
||||
msgid "Not Equals"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/conditions/__init__.py
|
||||
msgid "Contains"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/conditions/__init__.py
|
||||
msgid "Choose one - Field"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/conditions/default_plugin.py
|
||||
msgid "Does NOT Contain"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/conditions/default_plugin.py
|
||||
msgid "Text Starts With"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/conditions/default_plugin.py
|
||||
msgid "Text Ends With"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/conditions/default_plugin.py
|
||||
msgid "Length minimum"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/conditions/default_plugin.py
|
||||
msgid "Length maximum"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/conditions/default_plugin.py
|
||||
msgid "Text Matches Regex"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/conditions/default_plugin.py
|
||||
msgid "Text Does NOT Match Regex"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/conditions/default_plugin.py
|
||||
msgid "Extracted number after 'Filters & Triggers'"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/conditions/default_plugin.py
|
||||
msgid "Page text after 'Filters & Triggers'"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/conditions/form.py
|
||||
msgid "Field"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/conditions/form.py
|
||||
msgid "Operator"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/conditions/form.py
|
||||
msgid "A value"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/conditions/form.py
|
||||
msgid "Operator is required."
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/conditions/form.py
|
||||
msgid "Field is required."
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/conditions/form.py
|
||||
msgid "Value is required."
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/conditions/plugins/levenshtein_plugin.py
|
||||
msgid "Levenshtein - Text similarity ratio"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/conditions/plugins/levenshtein_plugin.py
|
||||
msgid "Levenshtein - Text change distance"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/conditions/plugins/levenshtein_plugin.py
|
||||
msgid "Not enough history to calculate Levenshtein metrics"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/conditions/plugins/levenshtein_plugin.py
|
||||
msgid "Snapshot too large for edit statistics, skipping."
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/conditions/plugins/levenshtein_plugin.py
|
||||
msgid "Unable to calculate Levenshtein metrics"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/conditions/plugins/levenshtein_plugin.py
|
||||
msgid "Levenshtein Text Similarity Details"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/conditions/plugins/levenshtein_plugin.py
|
||||
msgid "Raw distance (edits needed)"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/conditions/plugins/levenshtein_plugin.py
|
||||
msgid "Similarity ratio"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/conditions/plugins/levenshtein_plugin.py
|
||||
msgid "Percent similar"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/conditions/plugins/levenshtein_plugin.py
|
||||
msgid ""
|
||||
"Levenshtein metrics compare the last two snapshots, measuring how many character edits are needed to transform one "
|
||||
"into the other."
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/conditions/plugins/levenshtein_plugin.py
|
||||
msgid "Error calculating Levenshtein metrics"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/conditions/plugins/wordcount_plugin.py
|
||||
msgid "Word count of content"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/conditions/plugins/wordcount_plugin.py
|
||||
msgid "Content Analysis"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/conditions/plugins/wordcount_plugin.py
|
||||
msgid "Word count (latest snapshot)"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/conditions/plugins/wordcount_plugin.py
|
||||
msgid "Word count is a simple measure of content length, calculated by splitting text on whitespace."
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/content_fetchers/requests.py
|
||||
msgid "Basic fast Plaintext/HTTP Client"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/blueprint/watchlist/templates/watch-overview.html changedetectionio/flask_app.py
|
||||
#: changedetectionio/realtime/socket_server.py
|
||||
msgid "Not yet"
|
||||
@@ -2424,26 +2205,10 @@ msgstr ""
|
||||
msgid "Selector"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/forms.py
|
||||
msgid "CSS or xPath selector"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/forms.py
|
||||
msgid "value"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/conditions/form.py changedetectionio/forms.py
|
||||
msgid "Value"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/forms.py
|
||||
msgid "Web Page URL"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/forms.py
|
||||
msgid "Group Tag"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/forms.py
|
||||
msgid "Time Between Check"
|
||||
msgstr ""
|
||||
@@ -2532,8 +2297,7 @@ msgstr ""
|
||||
msgid "Execute JavaScript before change detection"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/blueprint/tags/form.py changedetectionio/blueprint/tags/templates/groups-overview.html
|
||||
#: changedetectionio/forms.py
|
||||
#: changedetectionio/blueprint/tags/templates/groups-overview.html changedetectionio/forms.py
|
||||
msgid "Save"
|
||||
msgstr ""
|
||||
|
||||
@@ -2601,7 +2365,7 @@ msgstr ""
|
||||
msgid "Invalid template syntax in \"%(header)s\" header: %(error)s"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/blueprint/tags/form.py changedetectionio/forms.py
|
||||
#: changedetectionio/forms.py
|
||||
msgid "Name"
|
||||
msgstr ""
|
||||
|
||||
@@ -2697,10 +2461,6 @@ msgstr ""
|
||||
msgid "Ignore whitespace"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/forms.py
|
||||
msgid "Screenshot: Minimum Change Percentage"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/forms.py changedetectionio/processors/image_ssim_diff/forms.py
|
||||
msgid "Must be between 0 and 100"
|
||||
msgstr ""
|
||||
@@ -2900,42 +2660,6 @@ msgstr ""
|
||||
msgid "Detects if the product goes back to in-stock"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/processors/templates/extract.html
|
||||
msgid "Screenshot"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/processors/templates/extract.html
|
||||
msgid "This tool will extract text data from all of the watch history."
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/processors/templates/extract.html
|
||||
msgid "A <strong>RegEx</strong> is a pattern that identifies exactly which part inside of the text that you want to extract."
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/processors/templates/extract.html
|
||||
msgid "For example, to extract only the numbers from text"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/processors/templates/extract.html
|
||||
msgid "Raw text"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/processors/templates/extract.html
|
||||
msgid "RegEx to extract:"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/processors/templates/extract.html
|
||||
msgid "Be sure to test your RegEx here."
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/processors/templates/extract.html
|
||||
msgid "Each RegEx group bracket"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/processors/templates/extract.html
|
||||
msgid "will be in its own column, the first column value is always the date."
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/processors/text_json_diff/processor.py
|
||||
msgid "Webpage Text/HTML, JSON and PDF changes"
|
||||
msgstr ""
|
||||
@@ -3115,7 +2839,7 @@ msgstr ""
|
||||
msgid "Use"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/templates/_common_fields.html changedetectionio/templates/edit/include_subtract.html
|
||||
#: changedetectionio/templates/_common_fields.html
|
||||
msgid "Show advanced help and tips"
|
||||
msgstr ""
|
||||
|
||||
@@ -3219,26 +2943,6 @@ msgstr ""
|
||||
msgid "Format for all notifications"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/templates/_common_fields.html
|
||||
msgid "Note"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/templates/_common_fields.html
|
||||
msgid "Discord does not render HTML — switch to"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/templates/_common_fields.html
|
||||
msgid "Plain Text"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/templates/_common_fields.html
|
||||
msgid "format to avoid"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/templates/_common_fields.html
|
||||
msgid "and other HTML entities appearing literally in your notifications."
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/templates/_helpers.html
|
||||
msgid "Entry"
|
||||
msgstr ""
|
||||
@@ -3355,18 +3059,10 @@ msgstr ""
|
||||
msgid "Blocked text"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/templates/base.html
|
||||
msgid "A new version is available"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/templates/base.html
|
||||
msgid "Search, or Use Alt+S Key"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/templates/base.html
|
||||
msgid "Share this link:"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/templates/base.html
|
||||
msgid "Real-time updates offline"
|
||||
msgstr ""
|
||||
@@ -3537,6 +3233,10 @@ msgstr ""
|
||||
msgid "Unmute notifications"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/templates/menu.html
|
||||
msgid "Mute notifications"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/templates/menu.html
|
||||
msgid "Notifications are muted - click to unmute"
|
||||
msgstr ""
|
||||
|
||||
Binary file not shown.
@@ -3,7 +3,7 @@ msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: changedetection.io 0.53.6\n"
|
||||
"Report-Msgid-Bugs-To: EMAIL@ADDRESS\n"
|
||||
"POT-Creation-Date: 2026-04-15 03:04+0900\n"
|
||||
"POT-Creation-Date: 2026-04-11 04:15+0200\n"
|
||||
"PO-Revision-Date: 2026-03-20 18:13+0100\n"
|
||||
"Last-Translator: Adrian Gonzalez <adrian@example.com>\n"
|
||||
"Language: es\n"
|
||||
@@ -608,15 +608,15 @@ msgstr ""
|
||||
msgid "Changing this could affect the content of your existing watches, possibly trigger alerts etc."
|
||||
msgstr "Cambiar esto podría afectar el contenido de sus monitores existentes, posiblemente activar alertas, etc."
|
||||
|
||||
#: changedetectionio/blueprint/settings/templates/settings.html changedetectionio/templates/edit/include_subtract.html
|
||||
#: changedetectionio/blueprint/settings/templates/settings.html
|
||||
msgid "Remove HTML element(s) by CSS and XPath selectors before text conversion."
|
||||
msgstr "Elimine los elementos HTML mediante los selectores CSS y XPath antes de la conversión de texto."
|
||||
|
||||
#: changedetectionio/blueprint/settings/templates/settings.html changedetectionio/templates/edit/include_subtract.html
|
||||
#: changedetectionio/blueprint/settings/templates/settings.html
|
||||
msgid "Don't paste HTML here, use only CSS and XPath selectors"
|
||||
msgstr "No pegue HTML aquí, use solo selectores CSS y XPath"
|
||||
|
||||
#: changedetectionio/blueprint/settings/templates/settings.html changedetectionio/templates/edit/include_subtract.html
|
||||
#: changedetectionio/blueprint/settings/templates/settings.html
|
||||
msgid "Add multiple elements, CSS or XPath selectors per line to ignore multiple parts of the HTML."
|
||||
msgstr "Agregue múltiples elementos, selectores CSS o XPath por línea para ignorar múltiples partes del HTML."
|
||||
|
||||
@@ -861,26 +861,6 @@ msgstr "Etiqueta no encontrada"
|
||||
msgid "Updated"
|
||||
msgstr "Actualizado"
|
||||
|
||||
#: changedetectionio/blueprint/tags/form.py
|
||||
msgid "Activate for individual watches in this tag/group?"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/blueprint/tags/form.py
|
||||
msgid "Auto-apply to watches with URLs matching"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/blueprint/tags/form.py
|
||||
msgid "e.g. *://example.com/* or github.com/myorg"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/blueprint/tags/form.py changedetectionio/blueprint/tags/templates/edit-tag.html
|
||||
msgid "Tag colour"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/blueprint/tags/form.py
|
||||
msgid "Tag name"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/blueprint/tags/templates/edit-tag.html changedetectionio/blueprint/ui/templates/edit.html
|
||||
msgid "Filters & Triggers"
|
||||
msgstr "Filtros y activadores"
|
||||
@@ -895,6 +875,10 @@ msgstr ""
|
||||
msgid "Currently matching watches"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/blueprint/tags/templates/edit-tag.html
|
||||
msgid "Tag colour"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/blueprint/tags/templates/edit-tag.html
|
||||
msgid "Custom colour"
|
||||
msgstr ""
|
||||
@@ -982,11 +966,7 @@ msgstr "Nombre de etiqueta/etiqueta"
|
||||
msgid "No website organisational tags/groups configured"
|
||||
msgstr "No hay etiquetas/grupos organizativos del sitio web configurados"
|
||||
|
||||
#: changedetectionio/blueprint/tags/templates/groups-overview.html changedetectionio/templates/menu.html
|
||||
msgid "Mute notifications"
|
||||
msgstr "Silenciar notificaciones"
|
||||
|
||||
#: changedetectionio/blueprint/tags/templates/groups-overview.html changedetectionio/blueprint/ui/edit.py
|
||||
#: changedetectionio/blueprint/tags/templates/groups-overview.html
|
||||
#: changedetectionio/blueprint/watchlist/templates/watch-overview.html
|
||||
msgid "Edit"
|
||||
msgstr "Editar"
|
||||
@@ -1195,14 +1175,6 @@ msgstr ""
|
||||
msgid "Could not load '{}' processor, processor plugin might be missing."
|
||||
msgstr "No se pudo cargar el procesador '{}'; es posible que falte el complemento del procesador."
|
||||
|
||||
#: changedetectionio/blueprint/ui/edit.py
|
||||
msgid "System settings default"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/blueprint/ui/edit.py
|
||||
msgid "Default"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/blueprint/ui/edit.py
|
||||
msgid "Updated watch - unpaused!"
|
||||
msgstr "Monitor actualizado: ¡sin pausa!"
|
||||
@@ -1215,10 +1187,6 @@ msgstr "Monitor actualizado."
|
||||
msgid "Preview unavailable - No fetch/check completed or triggers not reached"
|
||||
msgstr "Vista previa no disponible: no se completó la búsqueda/verificación o no se alcanzaron los activadores"
|
||||
|
||||
#: changedetectionio/blueprint/ui/preview.py
|
||||
msgid "Diff"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/blueprint/ui/templates/clear_all_history.html
|
||||
msgid "This will remove version history (snapshots) for ALL watches, but keep your list of URLs!"
|
||||
msgstr "Esto eliminará el historial de versiones (instantáneas) de TODOS los monitores, ¡pero mantendrá su lista de URL!"
|
||||
@@ -1328,17 +1296,14 @@ msgid "Jump"
|
||||
msgstr "Saltar"
|
||||
|
||||
#: changedetectionio/blueprint/ui/templates/diff.html changedetectionio/blueprint/ui/templates/preview.html
|
||||
#: changedetectionio/processors/templates/extract.html
|
||||
msgid "Error Text"
|
||||
msgstr "Texto de error"
|
||||
|
||||
#: changedetectionio/blueprint/ui/templates/diff.html changedetectionio/blueprint/ui/templates/preview.html
|
||||
#: changedetectionio/processors/templates/extract.html
|
||||
msgid "Error Screenshot"
|
||||
msgstr "Captura de pantalla de error"
|
||||
|
||||
#: changedetectionio/blueprint/ui/templates/diff.html changedetectionio/blueprint/ui/templates/preview.html
|
||||
#: changedetectionio/processors/templates/extract.html
|
||||
msgid "Text"
|
||||
msgstr "Texto"
|
||||
|
||||
@@ -1346,8 +1311,7 @@ msgstr "Texto"
|
||||
msgid "Current screenshot"
|
||||
msgstr "Captura de pantalla actual"
|
||||
|
||||
#: changedetectionio/blueprint/ui/templates/diff.html changedetectionio/processors/extract.py
|
||||
#: changedetectionio/processors/templates/extract.html
|
||||
#: changedetectionio/blueprint/ui/templates/diff.html
|
||||
msgid "Extract Data"
|
||||
msgstr "Extraer datos"
|
||||
|
||||
@@ -1955,26 +1919,6 @@ msgstr "No hay monitores de detección de cambios de página web configuradas; a
|
||||
msgid "import a list"
|
||||
msgstr "importar una lista"
|
||||
|
||||
#: changedetectionio/blueprint/watchlist/templates/watch-overview.html
|
||||
msgid "Pause checks"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/blueprint/watchlist/templates/watch-overview.html
|
||||
msgid "UnPause checks"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/blueprint/watchlist/templates/watch-overview.html
|
||||
msgid "Mute notification"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/blueprint/watchlist/templates/watch-overview.html
|
||||
msgid "UnMute notification"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/blueprint/watchlist/templates/watch-overview.html
|
||||
msgid "Create a link to share watch config with others"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/blueprint/watchlist/templates/watch-overview.html
|
||||
msgid "Detecting restock and price"
|
||||
msgstr "Detección de reabastecimiento y precio"
|
||||
@@ -2004,7 +1948,6 @@ msgid "Queued"
|
||||
msgstr "En cola"
|
||||
|
||||
#: changedetectionio/blueprint/watchlist/templates/watch-overview.html
|
||||
#: changedetectionio/processors/text_json_diff/difference.py
|
||||
msgid "History"
|
||||
msgstr "Historia"
|
||||
|
||||
@@ -2038,168 +1981,6 @@ msgstr "Vuelva a comprobar todo"
|
||||
msgid "in '%(title)s'"
|
||||
msgstr "en '%(title)s'"
|
||||
|
||||
#: changedetectionio/conditions/__init__.py
|
||||
msgid "Choose one - Operator"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/conditions/__init__.py
|
||||
msgid "Greater Than"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/conditions/__init__.py
|
||||
msgid "Less Than"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/conditions/__init__.py
|
||||
msgid "Greater Than or Equal To"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/conditions/__init__.py
|
||||
msgid "Less Than or Equal To"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/conditions/__init__.py
|
||||
msgid "Equals"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/conditions/__init__.py
|
||||
msgid "Not Equals"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/conditions/__init__.py
|
||||
msgid "Contains"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/conditions/__init__.py
|
||||
msgid "Choose one - Field"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/conditions/default_plugin.py
|
||||
msgid "Does NOT Contain"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/conditions/default_plugin.py
|
||||
msgid "Text Starts With"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/conditions/default_plugin.py
|
||||
msgid "Text Ends With"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/conditions/default_plugin.py
|
||||
msgid "Length minimum"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/conditions/default_plugin.py
|
||||
msgid "Length maximum"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/conditions/default_plugin.py
|
||||
msgid "Text Matches Regex"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/conditions/default_plugin.py
|
||||
msgid "Text Does NOT Match Regex"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/conditions/default_plugin.py
|
||||
msgid "Extracted number after 'Filters & Triggers'"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/conditions/default_plugin.py
|
||||
msgid "Page text after 'Filters & Triggers'"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/conditions/form.py
|
||||
msgid "Field"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/conditions/form.py
|
||||
msgid "Operator"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/conditions/form.py
|
||||
msgid "A value"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/conditions/form.py
|
||||
msgid "Operator is required."
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/conditions/form.py
|
||||
msgid "Field is required."
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/conditions/form.py
|
||||
msgid "Value is required."
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/conditions/plugins/levenshtein_plugin.py
|
||||
msgid "Levenshtein - Text similarity ratio"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/conditions/plugins/levenshtein_plugin.py
|
||||
msgid "Levenshtein - Text change distance"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/conditions/plugins/levenshtein_plugin.py
|
||||
msgid "Not enough history to calculate Levenshtein metrics"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/conditions/plugins/levenshtein_plugin.py
|
||||
msgid "Snapshot too large for edit statistics, skipping."
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/conditions/plugins/levenshtein_plugin.py
|
||||
msgid "Unable to calculate Levenshtein metrics"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/conditions/plugins/levenshtein_plugin.py
|
||||
msgid "Levenshtein Text Similarity Details"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/conditions/plugins/levenshtein_plugin.py
|
||||
msgid "Raw distance (edits needed)"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/conditions/plugins/levenshtein_plugin.py
|
||||
msgid "Similarity ratio"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/conditions/plugins/levenshtein_plugin.py
|
||||
msgid "Percent similar"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/conditions/plugins/levenshtein_plugin.py
|
||||
msgid ""
|
||||
"Levenshtein metrics compare the last two snapshots, measuring how many character edits are needed to transform one "
|
||||
"into the other."
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/conditions/plugins/levenshtein_plugin.py
|
||||
msgid "Error calculating Levenshtein metrics"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/conditions/plugins/wordcount_plugin.py
|
||||
msgid "Word count of content"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/conditions/plugins/wordcount_plugin.py
|
||||
msgid "Content Analysis"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/conditions/plugins/wordcount_plugin.py
|
||||
msgid "Word count (latest snapshot)"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/conditions/plugins/wordcount_plugin.py
|
||||
msgid "Word count is a simple measure of content length, calculated by splitting text on whitespace."
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/content_fetchers/requests.py
|
||||
msgid "Basic fast Plaintext/HTTP Client"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/blueprint/watchlist/templates/watch-overview.html changedetectionio/flask_app.py
|
||||
#: changedetectionio/realtime/socket_server.py
|
||||
msgid "Not yet"
|
||||
@@ -2484,26 +2265,10 @@ msgstr "Operación"
|
||||
msgid "Selector"
|
||||
msgstr "Selector"
|
||||
|
||||
#: changedetectionio/forms.py
|
||||
msgid "CSS or xPath selector"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/forms.py
|
||||
msgid "value"
|
||||
msgstr "valor"
|
||||
|
||||
#: changedetectionio/conditions/form.py changedetectionio/forms.py
|
||||
msgid "Value"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/forms.py
|
||||
msgid "Web Page URL"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/forms.py
|
||||
msgid "Group Tag"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/forms.py
|
||||
msgid "Time Between Check"
|
||||
msgstr "Tiempo entre comprobaciones"
|
||||
@@ -2592,8 +2357,7 @@ msgstr "Bloquear la detección de cambios mientras el texto coincide"
|
||||
msgid "Execute JavaScript before change detection"
|
||||
msgstr "Ejecute JavaScript antes de la detección de cambios"
|
||||
|
||||
#: changedetectionio/blueprint/tags/form.py changedetectionio/blueprint/tags/templates/groups-overview.html
|
||||
#: changedetectionio/forms.py
|
||||
#: changedetectionio/blueprint/tags/templates/groups-overview.html changedetectionio/forms.py
|
||||
msgid "Save"
|
||||
msgstr "Guardar"
|
||||
|
||||
@@ -2661,7 +2425,7 @@ msgstr "Sintaxis de plantilla no válida:%(error)s"
|
||||
msgid "Invalid template syntax in \"%(header)s\" header: %(error)s"
|
||||
msgstr "Sintaxis de plantilla no válida en \"%(header)s\"encabezado:%(error)s"
|
||||
|
||||
#: changedetectionio/blueprint/tags/form.py changedetectionio/forms.py
|
||||
#: changedetectionio/forms.py
|
||||
msgid "Name"
|
||||
msgstr "Nombre"
|
||||
|
||||
@@ -2757,10 +2521,6 @@ msgstr "Ignorar texto"
|
||||
msgid "Ignore whitespace"
|
||||
msgstr "Ignorar espacios en blanco"
|
||||
|
||||
#: changedetectionio/forms.py
|
||||
msgid "Screenshot: Minimum Change Percentage"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/forms.py changedetectionio/processors/image_ssim_diff/forms.py
|
||||
msgid "Must be between 0 and 100"
|
||||
msgstr "Debe estar entre 0 y 100"
|
||||
@@ -2960,42 +2720,6 @@ msgstr "Reabastecimiento y detección de precios para páginas con un ÚNICO pro
|
||||
msgid "Detects if the product goes back to in-stock"
|
||||
msgstr "Detecta si el producto vuelve a estar en stock"
|
||||
|
||||
#: changedetectionio/processors/templates/extract.html
|
||||
msgid "Screenshot"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/processors/templates/extract.html
|
||||
msgid "This tool will extract text data from all of the watch history."
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/processors/templates/extract.html
|
||||
msgid "A <strong>RegEx</strong> is a pattern that identifies exactly which part inside of the text that you want to extract."
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/processors/templates/extract.html
|
||||
msgid "For example, to extract only the numbers from text"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/processors/templates/extract.html
|
||||
msgid "Raw text"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/processors/templates/extract.html
|
||||
msgid "RegEx to extract:"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/processors/templates/extract.html
|
||||
msgid "Be sure to test your RegEx here."
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/processors/templates/extract.html
|
||||
msgid "Each RegEx group bracket"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/processors/templates/extract.html
|
||||
msgid "will be in its own column, the first column value is always the date."
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/processors/text_json_diff/processor.py
|
||||
msgid "Webpage Text/HTML, JSON and PDF changes"
|
||||
msgstr "Cambios en el texto/HTML, JSON y PDF de la página web"
|
||||
@@ -3175,7 +2899,7 @@ msgstr "Lea la wiki de servicios de notificación aquí para obtener notas de co
|
||||
msgid "Use"
|
||||
msgstr "Usar"
|
||||
|
||||
#: changedetectionio/templates/_common_fields.html changedetectionio/templates/edit/include_subtract.html
|
||||
#: changedetectionio/templates/_common_fields.html
|
||||
msgid "Show advanced help and tips"
|
||||
msgstr "Mostrar ayuda y consejos avanzados"
|
||||
|
||||
@@ -3281,26 +3005,6 @@ msgstr "Para obtener una referencia completa de todos los filtros integrados de
|
||||
msgid "Format for all notifications"
|
||||
msgstr "Formato para todas las notificaciones"
|
||||
|
||||
#: changedetectionio/templates/_common_fields.html
|
||||
msgid "Note"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/templates/_common_fields.html
|
||||
msgid "Discord does not render HTML — switch to"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/templates/_common_fields.html
|
||||
msgid "Plain Text"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/templates/_common_fields.html
|
||||
msgid "format to avoid"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/templates/_common_fields.html
|
||||
msgid "and other HTML entities appearing literally in your notifications."
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/templates/_helpers.html
|
||||
msgid "Entry"
|
||||
msgstr "Entrada"
|
||||
@@ -3419,18 +3123,10 @@ msgstr "No se producirá ninguna detección de cambios porque este texto existe.
|
||||
msgid "Blocked text"
|
||||
msgstr "Texto bloqueado"
|
||||
|
||||
#: changedetectionio/templates/base.html
|
||||
msgid "A new version is available"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/templates/base.html
|
||||
msgid "Search, or Use Alt+S Key"
|
||||
msgstr "Buscar o usar la tecla Alt+S"
|
||||
|
||||
#: changedetectionio/templates/base.html
|
||||
msgid "Share this link:"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/templates/base.html
|
||||
msgid "Real-time updates offline"
|
||||
msgstr "Actualizaciones en tiempo real sin conexión"
|
||||
@@ -3610,6 +3306,10 @@ msgstr "La programación está en pausa: haga clic para reanudar"
|
||||
msgid "Unmute notifications"
|
||||
msgstr "Dejar de silenciar notificaciones"
|
||||
|
||||
#: changedetectionio/templates/menu.html
|
||||
msgid "Mute notifications"
|
||||
msgstr "Silenciar notificaciones"
|
||||
|
||||
#: changedetectionio/templates/menu.html
|
||||
msgid "Notifications are muted - click to unmute"
|
||||
msgstr "Las notificaciones están silenciadas: haga clic para activar el silencio"
|
||||
|
||||
Binary file not shown.
@@ -7,7 +7,7 @@ msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: PROJECT VERSION\n"
|
||||
"Report-Msgid-Bugs-To: EMAIL@ADDRESS\n"
|
||||
"POT-Creation-Date: 2026-04-15 03:04+0900\n"
|
||||
"POT-Creation-Date: 2026-04-11 04:15+0200\n"
|
||||
"PO-Revision-Date: 2026-01-02 11:40+0100\n"
|
||||
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
|
||||
"Language: fr\n"
|
||||
@@ -594,15 +594,15 @@ msgstr ""
|
||||
msgid "Changing this could affect the content of your existing watches, possibly trigger alerts etc."
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/blueprint/settings/templates/settings.html changedetectionio/templates/edit/include_subtract.html
|
||||
#: changedetectionio/blueprint/settings/templates/settings.html
|
||||
msgid "Remove HTML element(s) by CSS and XPath selectors before text conversion."
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/blueprint/settings/templates/settings.html changedetectionio/templates/edit/include_subtract.html
|
||||
#: changedetectionio/blueprint/settings/templates/settings.html
|
||||
msgid "Don't paste HTML here, use only CSS and XPath selectors"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/blueprint/settings/templates/settings.html changedetectionio/templates/edit/include_subtract.html
|
||||
#: changedetectionio/blueprint/settings/templates/settings.html
|
||||
msgid "Add multiple elements, CSS or XPath selectors per line to ignore multiple parts of the HTML."
|
||||
msgstr ""
|
||||
|
||||
@@ -833,26 +833,6 @@ msgstr ""
|
||||
msgid "Updated"
|
||||
msgstr "Muet"
|
||||
|
||||
#: changedetectionio/blueprint/tags/form.py
|
||||
msgid "Activate for individual watches in this tag/group?"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/blueprint/tags/form.py
|
||||
msgid "Auto-apply to watches with URLs matching"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/blueprint/tags/form.py
|
||||
msgid "e.g. *://example.com/* or github.com/myorg"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/blueprint/tags/form.py changedetectionio/blueprint/tags/templates/edit-tag.html
|
||||
msgid "Tag colour"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/blueprint/tags/form.py
|
||||
msgid "Tag name"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/blueprint/tags/templates/edit-tag.html changedetectionio/blueprint/ui/templates/edit.html
|
||||
msgid "Filters & Triggers"
|
||||
msgstr "Filtres et déclencheurs"
|
||||
@@ -867,6 +847,10 @@ msgstr ""
|
||||
msgid "Currently matching watches"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/blueprint/tags/templates/edit-tag.html
|
||||
msgid "Tag colour"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/blueprint/tags/templates/edit-tag.html
|
||||
msgid "Custom colour"
|
||||
msgstr ""
|
||||
@@ -952,11 +936,7 @@ msgstr "Nom de l'étiquette/de l'étiquette"
|
||||
msgid "No website organisational tags/groups configured"
|
||||
msgstr "Aucun groupe/étiquette configuré"
|
||||
|
||||
#: changedetectionio/blueprint/tags/templates/groups-overview.html changedetectionio/templates/menu.html
|
||||
msgid "Mute notifications"
|
||||
msgstr "Désactiver les notifications"
|
||||
|
||||
#: changedetectionio/blueprint/tags/templates/groups-overview.html changedetectionio/blueprint/ui/edit.py
|
||||
#: changedetectionio/blueprint/tags/templates/groups-overview.html
|
||||
#: changedetectionio/blueprint/watchlist/templates/watch-overview.html
|
||||
msgid "Edit"
|
||||
msgstr "Modifier"
|
||||
@@ -1159,14 +1139,6 @@ msgstr ""
|
||||
msgid "Could not load '{}' processor, processor plugin might be missing."
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/blueprint/ui/edit.py
|
||||
msgid "System settings default"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/blueprint/ui/edit.py
|
||||
msgid "Default"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/blueprint/ui/edit.py
|
||||
msgid "Updated watch - unpaused!"
|
||||
msgstr ""
|
||||
@@ -1179,10 +1151,6 @@ msgstr "Supprimer les montres ?"
|
||||
msgid "Preview unavailable - No fetch/check completed or triggers not reached"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/blueprint/ui/preview.py
|
||||
msgid "Diff"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/blueprint/ui/templates/clear_all_history.html
|
||||
msgid "This will remove version history (snapshots) for ALL watches, but keep your list of URLs!"
|
||||
msgstr ""
|
||||
@@ -1292,17 +1260,14 @@ msgid "Jump"
|
||||
msgstr "Saut"
|
||||
|
||||
#: changedetectionio/blueprint/ui/templates/diff.html changedetectionio/blueprint/ui/templates/preview.html
|
||||
#: changedetectionio/processors/templates/extract.html
|
||||
msgid "Error Text"
|
||||
msgstr "Texte d'erreur"
|
||||
|
||||
#: changedetectionio/blueprint/ui/templates/diff.html changedetectionio/blueprint/ui/templates/preview.html
|
||||
#: changedetectionio/processors/templates/extract.html
|
||||
msgid "Error Screenshot"
|
||||
msgstr "Capture d'écran d'erreur"
|
||||
|
||||
#: changedetectionio/blueprint/ui/templates/diff.html changedetectionio/blueprint/ui/templates/preview.html
|
||||
#: changedetectionio/processors/templates/extract.html
|
||||
msgid "Text"
|
||||
msgstr "Texte"
|
||||
|
||||
@@ -1310,8 +1275,7 @@ msgstr "Texte"
|
||||
msgid "Current screenshot"
|
||||
msgstr "Capture d'écran actuelle"
|
||||
|
||||
#: changedetectionio/blueprint/ui/templates/diff.html changedetectionio/processors/extract.py
|
||||
#: changedetectionio/processors/templates/extract.html
|
||||
#: changedetectionio/blueprint/ui/templates/diff.html
|
||||
msgid "Extract Data"
|
||||
msgstr "Extraire des données"
|
||||
|
||||
@@ -1903,26 +1867,6 @@ msgstr "Aucune surveillance de site Web configurée, veuillez ajouter une URL da
|
||||
msgid "import a list"
|
||||
msgstr "importer une liste"
|
||||
|
||||
#: changedetectionio/blueprint/watchlist/templates/watch-overview.html
|
||||
msgid "Pause checks"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/blueprint/watchlist/templates/watch-overview.html
|
||||
msgid "UnPause checks"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/blueprint/watchlist/templates/watch-overview.html
|
||||
msgid "Mute notification"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/blueprint/watchlist/templates/watch-overview.html
|
||||
msgid "UnMute notification"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/blueprint/watchlist/templates/watch-overview.html
|
||||
msgid "Create a link to share watch config with others"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/blueprint/watchlist/templates/watch-overview.html
|
||||
msgid "Detecting restock and price"
|
||||
msgstr "Détection du réapprovisionnement et du prix"
|
||||
@@ -1952,7 +1896,6 @@ msgid "Queued"
|
||||
msgstr "En file d'attente"
|
||||
|
||||
#: changedetectionio/blueprint/watchlist/templates/watch-overview.html
|
||||
#: changedetectionio/processors/text_json_diff/difference.py
|
||||
msgid "History"
|
||||
msgstr "Historique"
|
||||
|
||||
@@ -1986,168 +1929,6 @@ msgstr "Revérifiez tout"
|
||||
msgid "in '%(title)s'"
|
||||
msgstr "dans '%(title)s'"
|
||||
|
||||
#: changedetectionio/conditions/__init__.py
|
||||
msgid "Choose one - Operator"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/conditions/__init__.py
|
||||
msgid "Greater Than"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/conditions/__init__.py
|
||||
msgid "Less Than"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/conditions/__init__.py
|
||||
msgid "Greater Than or Equal To"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/conditions/__init__.py
|
||||
msgid "Less Than or Equal To"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/conditions/__init__.py
|
||||
msgid "Equals"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/conditions/__init__.py
|
||||
msgid "Not Equals"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/conditions/__init__.py
|
||||
msgid "Contains"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/conditions/__init__.py
|
||||
msgid "Choose one - Field"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/conditions/default_plugin.py
|
||||
msgid "Does NOT Contain"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/conditions/default_plugin.py
|
||||
msgid "Text Starts With"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/conditions/default_plugin.py
|
||||
msgid "Text Ends With"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/conditions/default_plugin.py
|
||||
msgid "Length minimum"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/conditions/default_plugin.py
|
||||
msgid "Length maximum"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/conditions/default_plugin.py
|
||||
msgid "Text Matches Regex"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/conditions/default_plugin.py
|
||||
msgid "Text Does NOT Match Regex"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/conditions/default_plugin.py
|
||||
msgid "Extracted number after 'Filters & Triggers'"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/conditions/default_plugin.py
|
||||
msgid "Page text after 'Filters & Triggers'"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/conditions/form.py
|
||||
msgid "Field"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/conditions/form.py
|
||||
msgid "Operator"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/conditions/form.py
|
||||
msgid "A value"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/conditions/form.py
|
||||
msgid "Operator is required."
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/conditions/form.py
|
||||
msgid "Field is required."
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/conditions/form.py
|
||||
msgid "Value is required."
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/conditions/plugins/levenshtein_plugin.py
|
||||
msgid "Levenshtein - Text similarity ratio"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/conditions/plugins/levenshtein_plugin.py
|
||||
msgid "Levenshtein - Text change distance"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/conditions/plugins/levenshtein_plugin.py
|
||||
msgid "Not enough history to calculate Levenshtein metrics"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/conditions/plugins/levenshtein_plugin.py
|
||||
msgid "Snapshot too large for edit statistics, skipping."
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/conditions/plugins/levenshtein_plugin.py
|
||||
msgid "Unable to calculate Levenshtein metrics"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/conditions/plugins/levenshtein_plugin.py
|
||||
msgid "Levenshtein Text Similarity Details"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/conditions/plugins/levenshtein_plugin.py
|
||||
msgid "Raw distance (edits needed)"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/conditions/plugins/levenshtein_plugin.py
|
||||
msgid "Similarity ratio"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/conditions/plugins/levenshtein_plugin.py
|
||||
msgid "Percent similar"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/conditions/plugins/levenshtein_plugin.py
|
||||
msgid ""
|
||||
"Levenshtein metrics compare the last two snapshots, measuring how many character edits are needed to transform one "
|
||||
"into the other."
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/conditions/plugins/levenshtein_plugin.py
|
||||
msgid "Error calculating Levenshtein metrics"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/conditions/plugins/wordcount_plugin.py
|
||||
msgid "Word count of content"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/conditions/plugins/wordcount_plugin.py
|
||||
msgid "Content Analysis"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/conditions/plugins/wordcount_plugin.py
|
||||
msgid "Word count (latest snapshot)"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/conditions/plugins/wordcount_plugin.py
|
||||
msgid "Word count is a simple measure of content length, calculated by splitting text on whitespace."
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/content_fetchers/requests.py
|
||||
msgid "Basic fast Plaintext/HTTP Client"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/blueprint/watchlist/templates/watch-overview.html changedetectionio/flask_app.py
|
||||
#: changedetectionio/realtime/socket_server.py
|
||||
msgid "Not yet"
|
||||
@@ -2432,26 +2213,10 @@ msgstr "Options de l'interface utilisateur"
|
||||
msgid "Selector"
|
||||
msgstr "Mode de sélection :"
|
||||
|
||||
#: changedetectionio/forms.py
|
||||
msgid "CSS or xPath selector"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/forms.py
|
||||
msgid "value"
|
||||
msgstr "Pause"
|
||||
|
||||
#: changedetectionio/conditions/form.py changedetectionio/forms.py
|
||||
msgid "Value"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/forms.py
|
||||
msgid "Web Page URL"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/forms.py
|
||||
msgid "Group Tag"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/forms.py
|
||||
msgid "Time Between Check"
|
||||
msgstr "Intervalle de vérification"
|
||||
@@ -2540,8 +2305,7 @@ msgstr "Bloquer la détection des modifications lorsque le texte correspond"
|
||||
msgid "Execute JavaScript before change detection"
|
||||
msgstr "Exécuter JavaScript avant la détection des modifications"
|
||||
|
||||
#: changedetectionio/blueprint/tags/form.py changedetectionio/blueprint/tags/templates/groups-overview.html
|
||||
#: changedetectionio/forms.py
|
||||
#: changedetectionio/blueprint/tags/templates/groups-overview.html changedetectionio/forms.py
|
||||
msgid "Save"
|
||||
msgstr "Sauvegarder"
|
||||
|
||||
@@ -2609,7 +2373,7 @@ msgstr "Syntaxe de modèle non valide : %(error)s"
|
||||
msgid "Invalid template syntax in \"%(header)s\" header: %(error)s"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/blueprint/tags/form.py changedetectionio/forms.py
|
||||
#: changedetectionio/forms.py
|
||||
msgid "Name"
|
||||
msgstr "Nom"
|
||||
|
||||
@@ -2705,10 +2469,6 @@ msgstr "Ignorer le texte"
|
||||
msgid "Ignore whitespace"
|
||||
msgstr "Ignorer les espaces"
|
||||
|
||||
#: changedetectionio/forms.py
|
||||
msgid "Screenshot: Minimum Change Percentage"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/forms.py changedetectionio/processors/image_ssim_diff/forms.py
|
||||
msgid "Must be between 0 and 100"
|
||||
msgstr "Doit être compris entre 0 et 100"
|
||||
@@ -2908,42 +2668,6 @@ msgstr "Détection de réapprovisionnement et de prix pour les pages avec un SEU
|
||||
msgid "Detects if the product goes back to in-stock"
|
||||
msgstr "Détecte si le produit revient en stock"
|
||||
|
||||
#: changedetectionio/processors/templates/extract.html
|
||||
msgid "Screenshot"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/processors/templates/extract.html
|
||||
msgid "This tool will extract text data from all of the watch history."
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/processors/templates/extract.html
|
||||
msgid "A <strong>RegEx</strong> is a pattern that identifies exactly which part inside of the text that you want to extract."
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/processors/templates/extract.html
|
||||
msgid "For example, to extract only the numbers from text"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/processors/templates/extract.html
|
||||
msgid "Raw text"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/processors/templates/extract.html
|
||||
msgid "RegEx to extract:"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/processors/templates/extract.html
|
||||
msgid "Be sure to test your RegEx here."
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/processors/templates/extract.html
|
||||
msgid "Each RegEx group bracket"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/processors/templates/extract.html
|
||||
msgid "will be in its own column, the first column value is always the date."
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/processors/text_json_diff/processor.py
|
||||
msgid "Webpage Text/HTML, JSON and PDF changes"
|
||||
msgstr "Modifications du texte de la page Web/HTML, JSON et PDF"
|
||||
@@ -3123,7 +2847,7 @@ msgstr ""
|
||||
msgid "Use"
|
||||
msgstr "Utiliser"
|
||||
|
||||
#: changedetectionio/templates/_common_fields.html changedetectionio/templates/edit/include_subtract.html
|
||||
#: changedetectionio/templates/_common_fields.html
|
||||
msgid "Show advanced help and tips"
|
||||
msgstr "Afficher l'aide et astuces avancées"
|
||||
|
||||
@@ -3227,26 +2951,6 @@ msgstr ""
|
||||
msgid "Format for all notifications"
|
||||
msgstr "Format pour toutes les notifications"
|
||||
|
||||
#: changedetectionio/templates/_common_fields.html
|
||||
msgid "Note"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/templates/_common_fields.html
|
||||
msgid "Discord does not render HTML — switch to"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/templates/_common_fields.html
|
||||
msgid "Plain Text"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/templates/_common_fields.html
|
||||
msgid "format to avoid"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/templates/_common_fields.html
|
||||
msgid "and other HTML entities appearing literally in your notifications."
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/templates/_helpers.html
|
||||
msgid "Entry"
|
||||
msgstr ""
|
||||
@@ -3363,18 +3067,10 @@ msgstr "Aucune détection de changement si ce texte existe."
|
||||
msgid "Blocked text"
|
||||
msgstr "Texte bloqué"
|
||||
|
||||
#: changedetectionio/templates/base.html
|
||||
msgid "A new version is available"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/templates/base.html
|
||||
msgid "Search, or Use Alt+S Key"
|
||||
msgstr "Recherchez ou utilisez la touche Alt+S"
|
||||
|
||||
#: changedetectionio/templates/base.html
|
||||
msgid "Share this link:"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/templates/base.html
|
||||
msgid "Real-time updates offline"
|
||||
msgstr "Mises à jour en temps réel hors ligne"
|
||||
@@ -3547,6 +3243,10 @@ msgstr ""
|
||||
msgid "Unmute notifications"
|
||||
msgstr "Réactiver les notifications"
|
||||
|
||||
#: changedetectionio/templates/menu.html
|
||||
msgid "Mute notifications"
|
||||
msgstr "Désactiver les notifications"
|
||||
|
||||
#: changedetectionio/templates/menu.html
|
||||
msgid "Notifications are muted - click to unmute"
|
||||
msgstr "Notifications désactivées - cliquez pour réactiver"
|
||||
|
||||
Binary file not shown.
@@ -7,7 +7,7 @@ msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: PROJECT VERSION\n"
|
||||
"Report-Msgid-Bugs-To: EMAIL@ADDRESS\n"
|
||||
"POT-Creation-Date: 2026-04-15 03:04+0900\n"
|
||||
"POT-Creation-Date: 2026-04-11 04:15+0200\n"
|
||||
"PO-Revision-Date: 2026-01-02 15:32+0100\n"
|
||||
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
|
||||
"Language: it\n"
|
||||
@@ -592,15 +592,15 @@ msgstr ""
|
||||
msgid "Changing this could affect the content of your existing watches, possibly trigger alerts etc."
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/blueprint/settings/templates/settings.html changedetectionio/templates/edit/include_subtract.html
|
||||
#: changedetectionio/blueprint/settings/templates/settings.html
|
||||
msgid "Remove HTML element(s) by CSS and XPath selectors before text conversion."
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/blueprint/settings/templates/settings.html changedetectionio/templates/edit/include_subtract.html
|
||||
#: changedetectionio/blueprint/settings/templates/settings.html
|
||||
msgid "Don't paste HTML here, use only CSS and XPath selectors"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/blueprint/settings/templates/settings.html changedetectionio/templates/edit/include_subtract.html
|
||||
#: changedetectionio/blueprint/settings/templates/settings.html
|
||||
msgid "Add multiple elements, CSS or XPath selectors per line to ignore multiple parts of the HTML."
|
||||
msgstr ""
|
||||
|
||||
@@ -831,26 +831,6 @@ msgstr ""
|
||||
msgid "Updated"
|
||||
msgstr "Aggiornato"
|
||||
|
||||
#: changedetectionio/blueprint/tags/form.py
|
||||
msgid "Activate for individual watches in this tag/group?"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/blueprint/tags/form.py
|
||||
msgid "Auto-apply to watches with URLs matching"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/blueprint/tags/form.py
|
||||
msgid "e.g. *://example.com/* or github.com/myorg"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/blueprint/tags/form.py changedetectionio/blueprint/tags/templates/edit-tag.html
|
||||
msgid "Tag colour"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/blueprint/tags/form.py
|
||||
msgid "Tag name"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/blueprint/tags/templates/edit-tag.html changedetectionio/blueprint/ui/templates/edit.html
|
||||
msgid "Filters & Triggers"
|
||||
msgstr ""
|
||||
@@ -865,6 +845,10 @@ msgstr ""
|
||||
msgid "Currently matching watches"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/blueprint/tags/templates/edit-tag.html
|
||||
msgid "Tag colour"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/blueprint/tags/templates/edit-tag.html
|
||||
msgid "Custom colour"
|
||||
msgstr ""
|
||||
@@ -950,11 +934,7 @@ msgstr ""
|
||||
msgid "No website organisational tags/groups configured"
|
||||
msgstr "Nessun gruppo/etichetta configurato"
|
||||
|
||||
#: changedetectionio/blueprint/tags/templates/groups-overview.html changedetectionio/templates/menu.html
|
||||
msgid "Mute notifications"
|
||||
msgstr "Disattiva notifiche"
|
||||
|
||||
#: changedetectionio/blueprint/tags/templates/groups-overview.html changedetectionio/blueprint/ui/edit.py
|
||||
#: changedetectionio/blueprint/tags/templates/groups-overview.html
|
||||
#: changedetectionio/blueprint/watchlist/templates/watch-overview.html
|
||||
msgid "Edit"
|
||||
msgstr "Modifica"
|
||||
@@ -1157,14 +1137,6 @@ msgstr ""
|
||||
msgid "Could not load '{}' processor, processor plugin might be missing."
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/blueprint/ui/edit.py
|
||||
msgid "System settings default"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/blueprint/ui/edit.py
|
||||
msgid "Default"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/blueprint/ui/edit.py
|
||||
msgid "Updated watch - unpaused!"
|
||||
msgstr ""
|
||||
@@ -1177,10 +1149,6 @@ msgstr ""
|
||||
msgid "Preview unavailable - No fetch/check completed or triggers not reached"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/blueprint/ui/preview.py
|
||||
msgid "Diff"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/blueprint/ui/templates/clear_all_history.html
|
||||
msgid "This will remove version history (snapshots) for ALL watches, but keep your list of URLs!"
|
||||
msgstr ""
|
||||
@@ -1290,17 +1258,14 @@ msgid "Jump"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/blueprint/ui/templates/diff.html changedetectionio/blueprint/ui/templates/preview.html
|
||||
#: changedetectionio/processors/templates/extract.html
|
||||
msgid "Error Text"
|
||||
msgstr "Testo dell'errore"
|
||||
|
||||
#: changedetectionio/blueprint/ui/templates/diff.html changedetectionio/blueprint/ui/templates/preview.html
|
||||
#: changedetectionio/processors/templates/extract.html
|
||||
msgid "Error Screenshot"
|
||||
msgstr "Screenshot dell'errore"
|
||||
|
||||
#: changedetectionio/blueprint/ui/templates/diff.html changedetectionio/blueprint/ui/templates/preview.html
|
||||
#: changedetectionio/processors/templates/extract.html
|
||||
msgid "Text"
|
||||
msgstr "Testo"
|
||||
|
||||
@@ -1308,8 +1273,7 @@ msgstr "Testo"
|
||||
msgid "Current screenshot"
|
||||
msgstr "Screenshot corrente"
|
||||
|
||||
#: changedetectionio/blueprint/ui/templates/diff.html changedetectionio/processors/extract.py
|
||||
#: changedetectionio/processors/templates/extract.html
|
||||
#: changedetectionio/blueprint/ui/templates/diff.html
|
||||
msgid "Extract Data"
|
||||
msgstr ""
|
||||
|
||||
@@ -1899,26 +1863,6 @@ msgstr "Nessun monitoraggio configurato, aggiungi un URL nella casella sopra, op
|
||||
msgid "import a list"
|
||||
msgstr "importa una lista"
|
||||
|
||||
#: changedetectionio/blueprint/watchlist/templates/watch-overview.html
|
||||
msgid "Pause checks"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/blueprint/watchlist/templates/watch-overview.html
|
||||
msgid "UnPause checks"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/blueprint/watchlist/templates/watch-overview.html
|
||||
msgid "Mute notification"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/blueprint/watchlist/templates/watch-overview.html
|
||||
msgid "UnMute notification"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/blueprint/watchlist/templates/watch-overview.html
|
||||
msgid "Create a link to share watch config with others"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/blueprint/watchlist/templates/watch-overview.html
|
||||
msgid "Detecting restock and price"
|
||||
msgstr ""
|
||||
@@ -1948,7 +1892,6 @@ msgid "Queued"
|
||||
msgstr "In coda"
|
||||
|
||||
#: changedetectionio/blueprint/watchlist/templates/watch-overview.html
|
||||
#: changedetectionio/processors/text_json_diff/difference.py
|
||||
msgid "History"
|
||||
msgstr "Cronologia"
|
||||
|
||||
@@ -1982,168 +1925,6 @@ msgstr "Controlla tutti"
|
||||
msgid "in '%(title)s'"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/conditions/__init__.py
|
||||
msgid "Choose one - Operator"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/conditions/__init__.py
|
||||
msgid "Greater Than"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/conditions/__init__.py
|
||||
msgid "Less Than"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/conditions/__init__.py
|
||||
msgid "Greater Than or Equal To"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/conditions/__init__.py
|
||||
msgid "Less Than or Equal To"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/conditions/__init__.py
|
||||
msgid "Equals"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/conditions/__init__.py
|
||||
msgid "Not Equals"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/conditions/__init__.py
|
||||
msgid "Contains"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/conditions/__init__.py
|
||||
msgid "Choose one - Field"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/conditions/default_plugin.py
|
||||
msgid "Does NOT Contain"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/conditions/default_plugin.py
|
||||
msgid "Text Starts With"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/conditions/default_plugin.py
|
||||
msgid "Text Ends With"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/conditions/default_plugin.py
|
||||
msgid "Length minimum"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/conditions/default_plugin.py
|
||||
msgid "Length maximum"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/conditions/default_plugin.py
|
||||
msgid "Text Matches Regex"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/conditions/default_plugin.py
|
||||
msgid "Text Does NOT Match Regex"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/conditions/default_plugin.py
|
||||
msgid "Extracted number after 'Filters & Triggers'"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/conditions/default_plugin.py
|
||||
msgid "Page text after 'Filters & Triggers'"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/conditions/form.py
|
||||
msgid "Field"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/conditions/form.py
|
||||
msgid "Operator"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/conditions/form.py
|
||||
msgid "A value"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/conditions/form.py
|
||||
msgid "Operator is required."
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/conditions/form.py
|
||||
msgid "Field is required."
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/conditions/form.py
|
||||
msgid "Value is required."
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/conditions/plugins/levenshtein_plugin.py
|
||||
msgid "Levenshtein - Text similarity ratio"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/conditions/plugins/levenshtein_plugin.py
|
||||
msgid "Levenshtein - Text change distance"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/conditions/plugins/levenshtein_plugin.py
|
||||
msgid "Not enough history to calculate Levenshtein metrics"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/conditions/plugins/levenshtein_plugin.py
|
||||
msgid "Snapshot too large for edit statistics, skipping."
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/conditions/plugins/levenshtein_plugin.py
|
||||
msgid "Unable to calculate Levenshtein metrics"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/conditions/plugins/levenshtein_plugin.py
|
||||
msgid "Levenshtein Text Similarity Details"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/conditions/plugins/levenshtein_plugin.py
|
||||
msgid "Raw distance (edits needed)"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/conditions/plugins/levenshtein_plugin.py
|
||||
msgid "Similarity ratio"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/conditions/plugins/levenshtein_plugin.py
|
||||
msgid "Percent similar"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/conditions/plugins/levenshtein_plugin.py
|
||||
msgid ""
|
||||
"Levenshtein metrics compare the last two snapshots, measuring how many character edits are needed to transform one "
|
||||
"into the other."
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/conditions/plugins/levenshtein_plugin.py
|
||||
msgid "Error calculating Levenshtein metrics"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/conditions/plugins/wordcount_plugin.py
|
||||
msgid "Word count of content"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/conditions/plugins/wordcount_plugin.py
|
||||
msgid "Content Analysis"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/conditions/plugins/wordcount_plugin.py
|
||||
msgid "Word count (latest snapshot)"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/conditions/plugins/wordcount_plugin.py
|
||||
msgid "Word count is a simple measure of content length, calculated by splitting text on whitespace."
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/content_fetchers/requests.py
|
||||
msgid "Basic fast Plaintext/HTTP Client"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/blueprint/watchlist/templates/watch-overview.html changedetectionio/flask_app.py
|
||||
#: changedetectionio/realtime/socket_server.py
|
||||
msgid "Not yet"
|
||||
@@ -2426,26 +2207,10 @@ msgstr "Operazione"
|
||||
msgid "Selector"
|
||||
msgstr "Selettore"
|
||||
|
||||
#: changedetectionio/forms.py
|
||||
msgid "CSS or xPath selector"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/forms.py
|
||||
msgid "value"
|
||||
msgstr "valore"
|
||||
|
||||
#: changedetectionio/conditions/form.py changedetectionio/forms.py
|
||||
msgid "Value"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/forms.py
|
||||
msgid "Web Page URL"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/forms.py
|
||||
msgid "Group Tag"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/forms.py
|
||||
msgid "Time Between Check"
|
||||
msgstr "Intervallo tra controlli"
|
||||
@@ -2534,8 +2299,7 @@ msgstr "Blocca rilevamento modifiche quando il testo corrisponde"
|
||||
msgid "Execute JavaScript before change detection"
|
||||
msgstr "Esegui JavaScript prima del rilevamento"
|
||||
|
||||
#: changedetectionio/blueprint/tags/form.py changedetectionio/blueprint/tags/templates/groups-overview.html
|
||||
#: changedetectionio/forms.py
|
||||
#: changedetectionio/blueprint/tags/templates/groups-overview.html changedetectionio/forms.py
|
||||
msgid "Save"
|
||||
msgstr "Salva"
|
||||
|
||||
@@ -2603,7 +2367,7 @@ msgstr "Sintassi template non valida: %(error)s"
|
||||
msgid "Invalid template syntax in \"%(header)s\" header: %(error)s"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/blueprint/tags/form.py changedetectionio/forms.py
|
||||
#: changedetectionio/forms.py
|
||||
msgid "Name"
|
||||
msgstr "Nome"
|
||||
|
||||
@@ -2699,10 +2463,6 @@ msgstr "Ignora testo"
|
||||
msgid "Ignore whitespace"
|
||||
msgstr "Ignora spazi"
|
||||
|
||||
#: changedetectionio/forms.py
|
||||
msgid "Screenshot: Minimum Change Percentage"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/forms.py changedetectionio/processors/image_ssim_diff/forms.py
|
||||
msgid "Must be between 0 and 100"
|
||||
msgstr "Deve essere tra 0 e 100"
|
||||
@@ -2902,42 +2662,6 @@ msgstr "Rilevamento disponibilità e prezzi per pagine con UN SINGOLO prodotto"
|
||||
msgid "Detects if the product goes back to in-stock"
|
||||
msgstr "Rileva se il prodotto torna disponibile"
|
||||
|
||||
#: changedetectionio/processors/templates/extract.html
|
||||
msgid "Screenshot"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/processors/templates/extract.html
|
||||
msgid "This tool will extract text data from all of the watch history."
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/processors/templates/extract.html
|
||||
msgid "A <strong>RegEx</strong> is a pattern that identifies exactly which part inside of the text that you want to extract."
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/processors/templates/extract.html
|
||||
msgid "For example, to extract only the numbers from text"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/processors/templates/extract.html
|
||||
msgid "Raw text"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/processors/templates/extract.html
|
||||
msgid "RegEx to extract:"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/processors/templates/extract.html
|
||||
msgid "Be sure to test your RegEx here."
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/processors/templates/extract.html
|
||||
msgid "Each RegEx group bracket"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/processors/templates/extract.html
|
||||
msgid "will be in its own column, the first column value is always the date."
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/processors/text_json_diff/processor.py
|
||||
msgid "Webpage Text/HTML, JSON and PDF changes"
|
||||
msgstr "Modifiche testo/HTML, JSON e PDF"
|
||||
@@ -3117,7 +2841,7 @@ msgstr ""
|
||||
msgid "Use"
|
||||
msgstr "Usa"
|
||||
|
||||
#: changedetectionio/templates/_common_fields.html changedetectionio/templates/edit/include_subtract.html
|
||||
#: changedetectionio/templates/_common_fields.html
|
||||
msgid "Show advanced help and tips"
|
||||
msgstr ""
|
||||
|
||||
@@ -3221,26 +2945,6 @@ msgstr ""
|
||||
msgid "Format for all notifications"
|
||||
msgstr "Formato per tutte le notifiche"
|
||||
|
||||
#: changedetectionio/templates/_common_fields.html
|
||||
msgid "Note"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/templates/_common_fields.html
|
||||
msgid "Discord does not render HTML — switch to"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/templates/_common_fields.html
|
||||
msgid "Plain Text"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/templates/_common_fields.html
|
||||
msgid "format to avoid"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/templates/_common_fields.html
|
||||
msgid "and other HTML entities appearing literally in your notifications."
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/templates/_helpers.html
|
||||
msgid "Entry"
|
||||
msgstr ""
|
||||
@@ -3357,18 +3061,10 @@ msgstr "Nessuna rilevazione se questo testo esiste."
|
||||
msgid "Blocked text"
|
||||
msgstr "Testo bloccato"
|
||||
|
||||
#: changedetectionio/templates/base.html
|
||||
msgid "A new version is available"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/templates/base.html
|
||||
msgid "Search, or Use Alt+S Key"
|
||||
msgstr "Cerca, o usa il tasto Alt+S"
|
||||
|
||||
#: changedetectionio/templates/base.html
|
||||
msgid "Share this link:"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/templates/base.html
|
||||
msgid "Real-time updates offline"
|
||||
msgstr ""
|
||||
@@ -3539,6 +3235,10 @@ msgstr ""
|
||||
msgid "Unmute notifications"
|
||||
msgstr "Riattiva notifiche"
|
||||
|
||||
#: changedetectionio/templates/menu.html
|
||||
msgid "Mute notifications"
|
||||
msgstr "Disattiva notifiche"
|
||||
|
||||
#: changedetectionio/templates/menu.html
|
||||
msgid "Notifications are muted - click to unmute"
|
||||
msgstr ""
|
||||
|
||||
Binary file not shown.
@@ -8,7 +8,7 @@ msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: changedetection.io 0.53.6\n"
|
||||
"Report-Msgid-Bugs-To: EMAIL@ADDRESS\n"
|
||||
"POT-Creation-Date: 2026-04-15 03:04+0900\n"
|
||||
"POT-Creation-Date: 2026-04-11 04:15+0200\n"
|
||||
"PO-Revision-Date: 2026-03-31 23:52+0900\n"
|
||||
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
|
||||
"Language: ja\n"
|
||||
@@ -600,15 +600,15 @@ msgstr "アンカータグのコンテンツをレンダリング(デフォル
|
||||
msgid "Changing this could affect the content of your existing watches, possibly trigger alerts etc."
|
||||
msgstr "これを変更すると、既存のウォッチのコンテンツに影響し、アラートが発生する可能性があります。"
|
||||
|
||||
#: changedetectionio/blueprint/settings/templates/settings.html changedetectionio/templates/edit/include_subtract.html
|
||||
#: changedetectionio/blueprint/settings/templates/settings.html
|
||||
msgid "Remove HTML element(s) by CSS and XPath selectors before text conversion."
|
||||
msgstr "テキスト変換前に CSS および XPath セレクターで HTML 要素を削除します。"
|
||||
|
||||
#: changedetectionio/blueprint/settings/templates/settings.html changedetectionio/templates/edit/include_subtract.html
|
||||
#: changedetectionio/blueprint/settings/templates/settings.html
|
||||
msgid "Don't paste HTML here, use only CSS and XPath selectors"
|
||||
msgstr "ここにHTMLを貼り付けないでください。CSSとXPathセレクターのみを使用してください。"
|
||||
|
||||
#: changedetectionio/blueprint/settings/templates/settings.html changedetectionio/templates/edit/include_subtract.html
|
||||
#: changedetectionio/blueprint/settings/templates/settings.html
|
||||
msgid "Add multiple elements, CSS or XPath selectors per line to ignore multiple parts of the HTML."
|
||||
msgstr "1行に1つのCSSまたはXPathセレクターを追加して、HTMLの複数の部分を無視できます。"
|
||||
|
||||
@@ -839,26 +839,6 @@ msgstr "タグが見つかりません"
|
||||
msgid "Updated"
|
||||
msgstr "更新しました"
|
||||
|
||||
#: changedetectionio/blueprint/tags/form.py
|
||||
msgid "Activate for individual watches in this tag/group?"
|
||||
msgstr "このタグ/グループ内の個別ウォッチに対して有効にしますか?"
|
||||
|
||||
#: changedetectionio/blueprint/tags/form.py
|
||||
msgid "Auto-apply to watches with URLs matching"
|
||||
msgstr "URLが一致するウォッチに自動適用"
|
||||
|
||||
#: changedetectionio/blueprint/tags/form.py
|
||||
msgid "e.g. *://example.com/* or github.com/myorg"
|
||||
msgstr "例: *://example.com/* や github.com/myorg"
|
||||
|
||||
#: changedetectionio/blueprint/tags/form.py changedetectionio/blueprint/tags/templates/edit-tag.html
|
||||
msgid "Tag colour"
|
||||
msgstr "タグの色"
|
||||
|
||||
#: changedetectionio/blueprint/tags/form.py
|
||||
msgid "Tag name"
|
||||
msgstr "タグ名"
|
||||
|
||||
#: changedetectionio/blueprint/tags/templates/edit-tag.html changedetectionio/blueprint/ui/templates/edit.html
|
||||
msgid "Filters & Triggers"
|
||||
msgstr "フィルタとトリガー"
|
||||
@@ -873,6 +853,10 @@ msgstr "URLが一致するウォッチにこのタグを自動適用します。
|
||||
msgid "Currently matching watches"
|
||||
msgstr "現在マッチしているウォッチ"
|
||||
|
||||
#: changedetectionio/blueprint/tags/templates/edit-tag.html
|
||||
msgid "Tag colour"
|
||||
msgstr "タグの色"
|
||||
|
||||
#: changedetectionio/blueprint/tags/templates/edit-tag.html
|
||||
msgid "Custom colour"
|
||||
msgstr "カスタム色"
|
||||
@@ -917,7 +901,7 @@ msgid "Lookout!"
|
||||
msgstr "注意!"
|
||||
|
||||
# 訳注: "There are" + <a>"system-wide notification URLs enabled"</a> + ", " + ...
|
||||
# → 「システム全体の通知URLが有効化されています、...」
|
||||
# → 「 件のシステム全体の通知URLが有効化されています、...」
|
||||
# 前半は日本語では不要なため、空白1文字で非表示にする(空文字は英語にフォールバックするため)
|
||||
#: changedetectionio/blueprint/tags/templates/edit-tag.html changedetectionio/blueprint/ui/templates/edit.html
|
||||
msgid "There are"
|
||||
@@ -925,7 +909,7 @@ msgstr " "
|
||||
|
||||
#: changedetectionio/blueprint/tags/templates/edit-tag.html changedetectionio/blueprint/ui/templates/edit.html
|
||||
msgid "system-wide notification URLs enabled"
|
||||
msgstr "システム全体の通知URLが有効化されています"
|
||||
msgstr "件のシステム全体の通知URLが有効化されています"
|
||||
|
||||
#: changedetectionio/blueprint/tags/templates/edit-tag.html changedetectionio/blueprint/ui/templates/edit.html
|
||||
msgid "this form will override notification settings for this watch only"
|
||||
@@ -964,11 +948,7 @@ msgstr "タグ / ラベル名"
|
||||
msgid "No website organisational tags/groups configured"
|
||||
msgstr "ウェブサイトの組織タグ/グループが設定されていません"
|
||||
|
||||
#: changedetectionio/blueprint/tags/templates/groups-overview.html changedetectionio/templates/menu.html
|
||||
msgid "Mute notifications"
|
||||
msgstr "通知をミュート"
|
||||
|
||||
#: changedetectionio/blueprint/tags/templates/groups-overview.html changedetectionio/blueprint/ui/edit.py
|
||||
#: changedetectionio/blueprint/tags/templates/groups-overview.html
|
||||
#: changedetectionio/blueprint/watchlist/templates/watch-overview.html
|
||||
msgid "Edit"
|
||||
msgstr "編集"
|
||||
@@ -1171,14 +1151,6 @@ msgstr "「{}」プロセッサーを読み込めませんでした。プロセ
|
||||
msgid "Could not load '{}' processor, processor plugin might be missing."
|
||||
msgstr "「{}」プロセッサーを読み込めませんでした。プロセッサープラグインが見つからない可能性があります。"
|
||||
|
||||
#: changedetectionio/blueprint/ui/edit.py
|
||||
msgid "System settings default"
|
||||
msgstr "システム設定のデフォルト"
|
||||
|
||||
#: changedetectionio/blueprint/ui/edit.py
|
||||
msgid "Default"
|
||||
msgstr "デフォルト"
|
||||
|
||||
#: changedetectionio/blueprint/ui/edit.py
|
||||
msgid "Updated watch - unpaused!"
|
||||
msgstr "ウォッチを更新しました - 一時停止を解除しました!"
|
||||
@@ -1191,10 +1163,6 @@ msgstr "ウォッチを更新しました。"
|
||||
msgid "Preview unavailable - No fetch/check completed or triggers not reached"
|
||||
msgstr "プレビューを表示できません - 取得/チェックが完了していないか、トリガーに達していません"
|
||||
|
||||
#: changedetectionio/blueprint/ui/preview.py
|
||||
msgid "Diff"
|
||||
msgstr "差分"
|
||||
|
||||
#: changedetectionio/blueprint/ui/templates/clear_all_history.html
|
||||
msgid "This will remove version history (snapshots) for ALL watches, but keep your list of URLs!"
|
||||
msgstr "これにより、すべてのウォッチのバージョン履歴(スナップショット)が削除されますが、URLのリストは保持されます!"
|
||||
@@ -1307,17 +1275,14 @@ msgid "Jump"
|
||||
msgstr "ジャンプ"
|
||||
|
||||
#: changedetectionio/blueprint/ui/templates/diff.html changedetectionio/blueprint/ui/templates/preview.html
|
||||
#: changedetectionio/processors/templates/extract.html
|
||||
msgid "Error Text"
|
||||
msgstr "エラーテキスト"
|
||||
|
||||
#: changedetectionio/blueprint/ui/templates/diff.html changedetectionio/blueprint/ui/templates/preview.html
|
||||
#: changedetectionio/processors/templates/extract.html
|
||||
msgid "Error Screenshot"
|
||||
msgstr "エラースクリーンショット"
|
||||
|
||||
#: changedetectionio/blueprint/ui/templates/diff.html changedetectionio/blueprint/ui/templates/preview.html
|
||||
#: changedetectionio/processors/templates/extract.html
|
||||
msgid "Text"
|
||||
msgstr "テキスト"
|
||||
|
||||
@@ -1325,8 +1290,7 @@ msgstr "テキスト"
|
||||
msgid "Current screenshot"
|
||||
msgstr "現在のスクリーンショット"
|
||||
|
||||
#: changedetectionio/blueprint/ui/templates/diff.html changedetectionio/processors/extract.py
|
||||
#: changedetectionio/processors/templates/extract.html
|
||||
#: changedetectionio/blueprint/ui/templates/diff.html
|
||||
msgid "Extract Data"
|
||||
msgstr "データを抽出"
|
||||
|
||||
@@ -1926,26 +1890,6 @@ msgstr "ウェブページ変更検知ウォッチが設定されていません
|
||||
msgid "import a list"
|
||||
msgstr "リストをインポート"
|
||||
|
||||
#: changedetectionio/blueprint/watchlist/templates/watch-overview.html
|
||||
msgid "Pause checks"
|
||||
msgstr "チェックを一時停止"
|
||||
|
||||
#: changedetectionio/blueprint/watchlist/templates/watch-overview.html
|
||||
msgid "UnPause checks"
|
||||
msgstr "チェックを再開"
|
||||
|
||||
#: changedetectionio/blueprint/watchlist/templates/watch-overview.html
|
||||
msgid "Mute notification"
|
||||
msgstr "通知をミュート"
|
||||
|
||||
#: changedetectionio/blueprint/watchlist/templates/watch-overview.html
|
||||
msgid "UnMute notification"
|
||||
msgstr "通知のミュートを解除"
|
||||
|
||||
#: changedetectionio/blueprint/watchlist/templates/watch-overview.html
|
||||
msgid "Create a link to share watch config with others"
|
||||
msgstr "ウォッチ設定を他の人と共有するリンクを作成"
|
||||
|
||||
#: changedetectionio/blueprint/watchlist/templates/watch-overview.html
|
||||
msgid "Detecting restock and price"
|
||||
msgstr "在庫補充と価格を検知中"
|
||||
@@ -1975,7 +1919,6 @@ msgid "Queued"
|
||||
msgstr "キュー済み"
|
||||
|
||||
#: changedetectionio/blueprint/watchlist/templates/watch-overview.html
|
||||
#: changedetectionio/processors/text_json_diff/difference.py
|
||||
msgid "History"
|
||||
msgstr "履歴"
|
||||
|
||||
@@ -2009,168 +1952,6 @@ msgstr "すべて再チェック"
|
||||
msgid "in '%(title)s'"
|
||||
msgstr "'%(title)s' 内"
|
||||
|
||||
#: changedetectionio/conditions/__init__.py
|
||||
msgid "Choose one - Operator"
|
||||
msgstr "選択してください - 演算子"
|
||||
|
||||
#: changedetectionio/conditions/__init__.py
|
||||
msgid "Greater Than"
|
||||
msgstr "より大きい"
|
||||
|
||||
#: changedetectionio/conditions/__init__.py
|
||||
msgid "Less Than"
|
||||
msgstr "より小さい"
|
||||
|
||||
#: changedetectionio/conditions/__init__.py
|
||||
msgid "Greater Than or Equal To"
|
||||
msgstr "以上"
|
||||
|
||||
#: changedetectionio/conditions/__init__.py
|
||||
msgid "Less Than or Equal To"
|
||||
msgstr "以下"
|
||||
|
||||
#: changedetectionio/conditions/__init__.py
|
||||
msgid "Equals"
|
||||
msgstr "等しい"
|
||||
|
||||
#: changedetectionio/conditions/__init__.py
|
||||
msgid "Not Equals"
|
||||
msgstr "等しくない"
|
||||
|
||||
#: changedetectionio/conditions/__init__.py
|
||||
msgid "Contains"
|
||||
msgstr "含む"
|
||||
|
||||
#: changedetectionio/conditions/__init__.py
|
||||
msgid "Choose one - Field"
|
||||
msgstr "選択してください - フィールド"
|
||||
|
||||
#: changedetectionio/conditions/default_plugin.py
|
||||
msgid "Does NOT Contain"
|
||||
msgstr "含まない"
|
||||
|
||||
#: changedetectionio/conditions/default_plugin.py
|
||||
msgid "Text Starts With"
|
||||
msgstr "テキストが次で始まる"
|
||||
|
||||
#: changedetectionio/conditions/default_plugin.py
|
||||
msgid "Text Ends With"
|
||||
msgstr "テキストが次で終わる"
|
||||
|
||||
#: changedetectionio/conditions/default_plugin.py
|
||||
msgid "Length minimum"
|
||||
msgstr "文字数の最小値"
|
||||
|
||||
#: changedetectionio/conditions/default_plugin.py
|
||||
msgid "Length maximum"
|
||||
msgstr "文字数の最大値"
|
||||
|
||||
#: changedetectionio/conditions/default_plugin.py
|
||||
msgid "Text Matches Regex"
|
||||
msgstr "テキストが正規表現にマッチ"
|
||||
|
||||
#: changedetectionio/conditions/default_plugin.py
|
||||
msgid "Text Does NOT Match Regex"
|
||||
msgstr "テキストが正規表現にマッチしない"
|
||||
|
||||
#: changedetectionio/conditions/default_plugin.py
|
||||
msgid "Extracted number after 'Filters & Triggers'"
|
||||
msgstr "'フィルタとトリガー' 後に抽出された数値"
|
||||
|
||||
#: changedetectionio/conditions/default_plugin.py
|
||||
msgid "Page text after 'Filters & Triggers'"
|
||||
msgstr "'フィルタとトリガー' 後のページテキスト"
|
||||
|
||||
#: changedetectionio/conditions/form.py
|
||||
msgid "Field"
|
||||
msgstr "フィールド"
|
||||
|
||||
#: changedetectionio/conditions/form.py
|
||||
msgid "Operator"
|
||||
msgstr "演算子"
|
||||
|
||||
#: changedetectionio/conditions/form.py
|
||||
msgid "A value"
|
||||
msgstr "値"
|
||||
|
||||
#: changedetectionio/conditions/form.py
|
||||
msgid "Operator is required."
|
||||
msgstr "演算子は必須です。"
|
||||
|
||||
#: changedetectionio/conditions/form.py
|
||||
msgid "Field is required."
|
||||
msgstr "フィールドは必須です。"
|
||||
|
||||
#: changedetectionio/conditions/form.py
|
||||
msgid "Value is required."
|
||||
msgstr "値は必須です。"
|
||||
|
||||
#: changedetectionio/conditions/plugins/levenshtein_plugin.py
|
||||
msgid "Levenshtein - Text similarity ratio"
|
||||
msgstr "レーベンシュタイン - テキスト類似度"
|
||||
|
||||
#: changedetectionio/conditions/plugins/levenshtein_plugin.py
|
||||
msgid "Levenshtein - Text change distance"
|
||||
msgstr "レーベンシュタイン - テキスト変化距離"
|
||||
|
||||
#: changedetectionio/conditions/plugins/levenshtein_plugin.py
|
||||
msgid "Not enough history to calculate Levenshtein metrics"
|
||||
msgstr "レーベンシュタイン指標を計算するのに十分な履歴がありません"
|
||||
|
||||
#: changedetectionio/conditions/plugins/levenshtein_plugin.py
|
||||
msgid "Snapshot too large for edit statistics, skipping."
|
||||
msgstr "スナップショットが大きすぎるため、編集統計をスキップします。"
|
||||
|
||||
#: changedetectionio/conditions/plugins/levenshtein_plugin.py
|
||||
msgid "Unable to calculate Levenshtein metrics"
|
||||
msgstr "レーベンシュタイン指標を計算できません"
|
||||
|
||||
#: changedetectionio/conditions/plugins/levenshtein_plugin.py
|
||||
msgid "Levenshtein Text Similarity Details"
|
||||
msgstr "レーベンシュタイン テキスト類似度の詳細"
|
||||
|
||||
#: changedetectionio/conditions/plugins/levenshtein_plugin.py
|
||||
msgid "Raw distance (edits needed)"
|
||||
msgstr "編集距離(必要な編集回数)"
|
||||
|
||||
#: changedetectionio/conditions/plugins/levenshtein_plugin.py
|
||||
msgid "Similarity ratio"
|
||||
msgstr "類似度"
|
||||
|
||||
#: changedetectionio/conditions/plugins/levenshtein_plugin.py
|
||||
msgid "Percent similar"
|
||||
msgstr "類似度(%)"
|
||||
|
||||
#: changedetectionio/conditions/plugins/levenshtein_plugin.py
|
||||
msgid ""
|
||||
"Levenshtein metrics compare the last two snapshots, measuring how many character edits are needed to transform one "
|
||||
"into the other."
|
||||
msgstr "レーベンシュタイン指標は直近2つのスナップショットを比較し、一方を他方に変換するために必要な文字編集回数を測定します。"
|
||||
|
||||
#: changedetectionio/conditions/plugins/levenshtein_plugin.py
|
||||
msgid "Error calculating Levenshtein metrics"
|
||||
msgstr "レーベンシュタイン指標の計算中にエラーが発生しました"
|
||||
|
||||
#: changedetectionio/conditions/plugins/wordcount_plugin.py
|
||||
msgid "Word count of content"
|
||||
msgstr "コンテンツの単語数"
|
||||
|
||||
#: changedetectionio/conditions/plugins/wordcount_plugin.py
|
||||
msgid "Content Analysis"
|
||||
msgstr "コンテンツ分析"
|
||||
|
||||
#: changedetectionio/conditions/plugins/wordcount_plugin.py
|
||||
msgid "Word count (latest snapshot)"
|
||||
msgstr "単語数(最新のスナップショット)"
|
||||
|
||||
#: changedetectionio/conditions/plugins/wordcount_plugin.py
|
||||
msgid "Word count is a simple measure of content length, calculated by splitting text on whitespace."
|
||||
msgstr "単語数はコンテンツの長さを示す簡易指標で、テキストを空白で分割して計算されます。"
|
||||
|
||||
#: changedetectionio/content_fetchers/requests.py
|
||||
msgid "Basic fast Plaintext/HTTP Client"
|
||||
msgstr "シンプルで高速なプレーンテキスト/HTTPクライアント"
|
||||
|
||||
#: changedetectionio/blueprint/watchlist/templates/watch-overview.html changedetectionio/flask_app.py
|
||||
#: changedetectionio/realtime/socket_server.py
|
||||
msgid "Not yet"
|
||||
@@ -2453,26 +2234,10 @@ msgstr "操作"
|
||||
msgid "Selector"
|
||||
msgstr "セレクター"
|
||||
|
||||
#: changedetectionio/forms.py
|
||||
msgid "CSS or xPath selector"
|
||||
msgstr "CSS または xPath セレクター"
|
||||
|
||||
#: changedetectionio/forms.py
|
||||
msgid "value"
|
||||
msgstr "値"
|
||||
|
||||
#: changedetectionio/conditions/form.py changedetectionio/forms.py
|
||||
msgid "Value"
|
||||
msgstr "値"
|
||||
|
||||
#: changedetectionio/forms.py
|
||||
msgid "Web Page URL"
|
||||
msgstr "ウェブページ URL"
|
||||
|
||||
#: changedetectionio/forms.py
|
||||
msgid "Group Tag"
|
||||
msgstr "グループタグ"
|
||||
|
||||
#: changedetectionio/forms.py
|
||||
msgid "Time Between Check"
|
||||
msgstr "チェック間隔"
|
||||
@@ -2561,8 +2326,7 @@ msgstr "テキストが一致している間は変更検知をブロック"
|
||||
msgid "Execute JavaScript before change detection"
|
||||
msgstr "変更検知前にJavaScriptを実行"
|
||||
|
||||
#: changedetectionio/blueprint/tags/form.py changedetectionio/blueprint/tags/templates/groups-overview.html
|
||||
#: changedetectionio/forms.py
|
||||
#: changedetectionio/blueprint/tags/templates/groups-overview.html changedetectionio/forms.py
|
||||
msgid "Save"
|
||||
msgstr "保存"
|
||||
|
||||
@@ -2630,7 +2394,7 @@ msgstr "無効なテンプレート構文: %(error)s"
|
||||
msgid "Invalid template syntax in \"%(header)s\" header: %(error)s"
|
||||
msgstr "\"%(header)s\" ヘッダーのテンプレート構文が無効です: %(error)s"
|
||||
|
||||
#: changedetectionio/blueprint/tags/form.py changedetectionio/forms.py
|
||||
#: changedetectionio/forms.py
|
||||
msgid "Name"
|
||||
msgstr "名前"
|
||||
|
||||
@@ -2726,10 +2490,6 @@ msgstr "無視するテキスト"
|
||||
msgid "Ignore whitespace"
|
||||
msgstr "空白を無視"
|
||||
|
||||
#: changedetectionio/forms.py
|
||||
msgid "Screenshot: Minimum Change Percentage"
|
||||
msgstr "スクリーンショット: 最小変化率"
|
||||
|
||||
#: changedetectionio/forms.py changedetectionio/processors/image_ssim_diff/forms.py
|
||||
msgid "Must be between 0 and 100"
|
||||
msgstr "0から100の間で指定してください"
|
||||
@@ -2929,42 +2689,6 @@ msgstr "単一製品ページの在庫補充&価格検知"
|
||||
msgid "Detects if the product goes back to in-stock"
|
||||
msgstr "製品が在庫ありに戻ったかどうかを検知します"
|
||||
|
||||
#: changedetectionio/processors/templates/extract.html
|
||||
msgid "Screenshot"
|
||||
msgstr "スクリーンショット"
|
||||
|
||||
#: changedetectionio/processors/templates/extract.html
|
||||
msgid "This tool will extract text data from all of the watch history."
|
||||
msgstr "このツールはすべてのウォッチ履歴からテキストデータを抽出します。"
|
||||
|
||||
#: changedetectionio/processors/templates/extract.html
|
||||
msgid "A <strong>RegEx</strong> is a pattern that identifies exactly which part inside of the text that you want to extract."
|
||||
msgstr "<strong>正規表現</strong> は、抽出したいテキスト内の部分を正確に特定するためのパターンです。"
|
||||
|
||||
#: changedetectionio/processors/templates/extract.html
|
||||
msgid "For example, to extract only the numbers from text"
|
||||
msgstr "例えば、テキストから数値のみを抽出するには"
|
||||
|
||||
#: changedetectionio/processors/templates/extract.html
|
||||
msgid "Raw text"
|
||||
msgstr "元のテキスト"
|
||||
|
||||
#: changedetectionio/processors/templates/extract.html
|
||||
msgid "RegEx to extract:"
|
||||
msgstr "抽出用の正規表現:"
|
||||
|
||||
#: changedetectionio/processors/templates/extract.html
|
||||
msgid "Be sure to test your RegEx here."
|
||||
msgstr "ここで正規表現をテストしてください。"
|
||||
|
||||
#: changedetectionio/processors/templates/extract.html
|
||||
msgid "Each RegEx group bracket"
|
||||
msgstr "正規表現の各グループ括弧"
|
||||
|
||||
#: changedetectionio/processors/templates/extract.html
|
||||
msgid "will be in its own column, the first column value is always the date."
|
||||
msgstr "は独立したカラムとなり、最初のカラムの値は常に日付です。"
|
||||
|
||||
#: changedetectionio/processors/text_json_diff/processor.py
|
||||
msgid "Webpage Text/HTML, JSON and PDF changes"
|
||||
msgstr "ウェブページのテキスト/HTML、JSONおよびPDFの変更"
|
||||
@@ -3156,7 +2880,7 @@ msgstr "重要な設定に関するメモについては、通知サービスの
|
||||
msgid "Use"
|
||||
msgstr " "
|
||||
|
||||
#: changedetectionio/templates/_common_fields.html changedetectionio/templates/edit/include_subtract.html
|
||||
#: changedetectionio/templates/_common_fields.html
|
||||
msgid "Show advanced help and tips"
|
||||
msgstr "詳細なヘルプとヒントを表示"
|
||||
|
||||
@@ -3267,30 +2991,6 @@ msgstr "すべての Jinja2 組み込みフィルタの完全なリファレン
|
||||
msgid "Format for all notifications"
|
||||
msgstr "すべての通知のフォーマット"
|
||||
|
||||
#: changedetectionio/templates/_common_fields.html
|
||||
msgid "Note"
|
||||
msgstr "注意"
|
||||
|
||||
# 訳注: "Discord does not render HTML — switch to <Plain Text> format to avoid < > and other HTML entities appearing
|
||||
# literally in your notifications."
|
||||
# → 「Discord は HTML をレンダリングしません。プレーンテキスト形式に切り替えて、通知で などの HTML エンティティがそのまま表示されるのを回避してください。」
|
||||
# 3分割された文をプレースホルダー位置に合わせて再配分
|
||||
#: changedetectionio/templates/_common_fields.html
|
||||
msgid "Discord does not render HTML — switch to"
|
||||
msgstr "Discord は HTML をレンダリングしません。"
|
||||
|
||||
#: changedetectionio/templates/_common_fields.html
|
||||
msgid "Plain Text"
|
||||
msgstr "プレーンテキスト"
|
||||
|
||||
#: changedetectionio/templates/_common_fields.html
|
||||
msgid "format to avoid"
|
||||
msgstr " 形式に切り替えて、通知で "
|
||||
|
||||
#: changedetectionio/templates/_common_fields.html
|
||||
msgid "and other HTML entities appearing literally in your notifications."
|
||||
msgstr " などの HTML エンティティがそのまま表示されるのを回避してください。"
|
||||
|
||||
#: changedetectionio/templates/_helpers.html
|
||||
msgid "Entry"
|
||||
msgstr "エントリー"
|
||||
@@ -3410,18 +3110,10 @@ msgstr "このテキストが存在するため、変更検知は実行されま
|
||||
msgid "Blocked text"
|
||||
msgstr "ブロックされたテキスト"
|
||||
|
||||
#: changedetectionio/templates/base.html
|
||||
msgid "A new version is available"
|
||||
msgstr "新しいバージョンが利用可能です"
|
||||
|
||||
#: changedetectionio/templates/base.html
|
||||
msgid "Search, or Use Alt+S Key"
|
||||
msgstr "検索、またはAlt+Sキーを使用"
|
||||
|
||||
#: changedetectionio/templates/base.html
|
||||
msgid "Share this link:"
|
||||
msgstr "このリンクを共有:"
|
||||
|
||||
#: changedetectionio/templates/base.html
|
||||
msgid "Real-time updates offline"
|
||||
msgstr "リアルタイム更新オフライン"
|
||||
@@ -3597,6 +3289,10 @@ msgstr "スケジューリングは一時停止中 - クリックして再開"
|
||||
msgid "Unmute notifications"
|
||||
msgstr "通知のミュートを解除"
|
||||
|
||||
#: changedetectionio/templates/menu.html
|
||||
msgid "Mute notifications"
|
||||
msgstr "通知をミュート"
|
||||
|
||||
#: changedetectionio/templates/menu.html
|
||||
msgid "Notifications are muted - click to unmute"
|
||||
msgstr "通知はミュート中 - クリックして解除"
|
||||
|
||||
Binary file not shown.
@@ -7,7 +7,7 @@ msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: PROJECT VERSION\n"
|
||||
"Report-Msgid-Bugs-To: EMAIL@ADDRESS\n"
|
||||
"POT-Creation-Date: 2026-04-15 03:04+0900\n"
|
||||
"POT-Creation-Date: 2026-04-11 04:15+0200\n"
|
||||
"PO-Revision-Date: 2026-01-02 11:40+0100\n"
|
||||
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
|
||||
"Language: ko\n"
|
||||
@@ -590,15 +590,15 @@ msgstr ""
|
||||
msgid "Changing this could affect the content of your existing watches, possibly trigger alerts etc."
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/blueprint/settings/templates/settings.html changedetectionio/templates/edit/include_subtract.html
|
||||
#: changedetectionio/blueprint/settings/templates/settings.html
|
||||
msgid "Remove HTML element(s) by CSS and XPath selectors before text conversion."
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/blueprint/settings/templates/settings.html changedetectionio/templates/edit/include_subtract.html
|
||||
#: changedetectionio/blueprint/settings/templates/settings.html
|
||||
msgid "Don't paste HTML here, use only CSS and XPath selectors"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/blueprint/settings/templates/settings.html changedetectionio/templates/edit/include_subtract.html
|
||||
#: changedetectionio/blueprint/settings/templates/settings.html
|
||||
msgid "Add multiple elements, CSS or XPath selectors per line to ignore multiple parts of the HTML."
|
||||
msgstr ""
|
||||
|
||||
@@ -829,26 +829,6 @@ msgstr ""
|
||||
msgid "Updated"
|
||||
msgstr "업데이트됨"
|
||||
|
||||
#: changedetectionio/blueprint/tags/form.py
|
||||
msgid "Activate for individual watches in this tag/group?"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/blueprint/tags/form.py
|
||||
msgid "Auto-apply to watches with URLs matching"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/blueprint/tags/form.py
|
||||
msgid "e.g. *://example.com/* or github.com/myorg"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/blueprint/tags/form.py changedetectionio/blueprint/tags/templates/edit-tag.html
|
||||
msgid "Tag colour"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/blueprint/tags/form.py
|
||||
msgid "Tag name"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/blueprint/tags/templates/edit-tag.html changedetectionio/blueprint/ui/templates/edit.html
|
||||
msgid "Filters & Triggers"
|
||||
msgstr "필터 및 트리거"
|
||||
@@ -863,6 +843,10 @@ msgstr ""
|
||||
msgid "Currently matching watches"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/blueprint/tags/templates/edit-tag.html
|
||||
msgid "Tag colour"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/blueprint/tags/templates/edit-tag.html
|
||||
msgid "Custom colour"
|
||||
msgstr ""
|
||||
@@ -948,11 +932,7 @@ msgstr "태그/라벨 이름"
|
||||
msgid "No website organisational tags/groups configured"
|
||||
msgstr "구성된 그룹/태그 없음"
|
||||
|
||||
#: changedetectionio/blueprint/tags/templates/groups-overview.html changedetectionio/templates/menu.html
|
||||
msgid "Mute notifications"
|
||||
msgstr "알림 음소거"
|
||||
|
||||
#: changedetectionio/blueprint/tags/templates/groups-overview.html changedetectionio/blueprint/ui/edit.py
|
||||
#: changedetectionio/blueprint/tags/templates/groups-overview.html
|
||||
#: changedetectionio/blueprint/watchlist/templates/watch-overview.html
|
||||
msgid "Edit"
|
||||
msgstr "편집하다"
|
||||
@@ -1155,14 +1135,6 @@ msgstr ""
|
||||
msgid "Could not load '{}' processor, processor plugin might be missing."
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/blueprint/ui/edit.py
|
||||
msgid "System settings default"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/blueprint/ui/edit.py
|
||||
msgid "Default"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/blueprint/ui/edit.py
|
||||
msgid "Updated watch - unpaused!"
|
||||
msgstr ""
|
||||
@@ -1175,10 +1147,6 @@ msgstr "모니터가 업데이트되었습니다."
|
||||
msgid "Preview unavailable - No fetch/check completed or triggers not reached"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/blueprint/ui/preview.py
|
||||
msgid "Diff"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/blueprint/ui/templates/clear_all_history.html
|
||||
msgid "This will remove version history (snapshots) for ALL watches, but keep your list of URLs!"
|
||||
msgstr ""
|
||||
@@ -1288,17 +1256,14 @@ msgid "Jump"
|
||||
msgstr "도약"
|
||||
|
||||
#: changedetectionio/blueprint/ui/templates/diff.html changedetectionio/blueprint/ui/templates/preview.html
|
||||
#: changedetectionio/processors/templates/extract.html
|
||||
msgid "Error Text"
|
||||
msgstr "오류 텍스트"
|
||||
|
||||
#: changedetectionio/blueprint/ui/templates/diff.html changedetectionio/blueprint/ui/templates/preview.html
|
||||
#: changedetectionio/processors/templates/extract.html
|
||||
msgid "Error Screenshot"
|
||||
msgstr "오류 스크린샷"
|
||||
|
||||
#: changedetectionio/blueprint/ui/templates/diff.html changedetectionio/blueprint/ui/templates/preview.html
|
||||
#: changedetectionio/processors/templates/extract.html
|
||||
msgid "Text"
|
||||
msgstr "텍스트"
|
||||
|
||||
@@ -1306,8 +1271,7 @@ msgstr "텍스트"
|
||||
msgid "Current screenshot"
|
||||
msgstr "현재 스크린샷"
|
||||
|
||||
#: changedetectionio/blueprint/ui/templates/diff.html changedetectionio/processors/extract.py
|
||||
#: changedetectionio/processors/templates/extract.html
|
||||
#: changedetectionio/blueprint/ui/templates/diff.html
|
||||
msgid "Extract Data"
|
||||
msgstr "데이터 추출"
|
||||
|
||||
@@ -1897,26 +1861,6 @@ msgstr "구성된 웹사이트 시계가 없습니다. 위 상자에 URL을 추
|
||||
msgid "import a list"
|
||||
msgstr "목록 가져오기"
|
||||
|
||||
#: changedetectionio/blueprint/watchlist/templates/watch-overview.html
|
||||
msgid "Pause checks"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/blueprint/watchlist/templates/watch-overview.html
|
||||
msgid "UnPause checks"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/blueprint/watchlist/templates/watch-overview.html
|
||||
msgid "Mute notification"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/blueprint/watchlist/templates/watch-overview.html
|
||||
msgid "UnMute notification"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/blueprint/watchlist/templates/watch-overview.html
|
||||
msgid "Create a link to share watch config with others"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/blueprint/watchlist/templates/watch-overview.html
|
||||
msgid "Detecting restock and price"
|
||||
msgstr "재입고 및 가격 감지"
|
||||
@@ -1946,7 +1890,6 @@ msgid "Queued"
|
||||
msgstr "대기 중"
|
||||
|
||||
#: changedetectionio/blueprint/watchlist/templates/watch-overview.html
|
||||
#: changedetectionio/processors/text_json_diff/difference.py
|
||||
msgid "History"
|
||||
msgstr "기록"
|
||||
|
||||
@@ -1980,168 +1923,6 @@ msgstr "모두 다시 확인하세요"
|
||||
msgid "in '%(title)s'"
|
||||
msgstr "'%(title)s'에서"
|
||||
|
||||
#: changedetectionio/conditions/__init__.py
|
||||
msgid "Choose one - Operator"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/conditions/__init__.py
|
||||
msgid "Greater Than"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/conditions/__init__.py
|
||||
msgid "Less Than"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/conditions/__init__.py
|
||||
msgid "Greater Than or Equal To"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/conditions/__init__.py
|
||||
msgid "Less Than or Equal To"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/conditions/__init__.py
|
||||
msgid "Equals"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/conditions/__init__.py
|
||||
msgid "Not Equals"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/conditions/__init__.py
|
||||
msgid "Contains"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/conditions/__init__.py
|
||||
msgid "Choose one - Field"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/conditions/default_plugin.py
|
||||
msgid "Does NOT Contain"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/conditions/default_plugin.py
|
||||
msgid "Text Starts With"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/conditions/default_plugin.py
|
||||
msgid "Text Ends With"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/conditions/default_plugin.py
|
||||
msgid "Length minimum"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/conditions/default_plugin.py
|
||||
msgid "Length maximum"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/conditions/default_plugin.py
|
||||
msgid "Text Matches Regex"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/conditions/default_plugin.py
|
||||
msgid "Text Does NOT Match Regex"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/conditions/default_plugin.py
|
||||
msgid "Extracted number after 'Filters & Triggers'"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/conditions/default_plugin.py
|
||||
msgid "Page text after 'Filters & Triggers'"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/conditions/form.py
|
||||
msgid "Field"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/conditions/form.py
|
||||
msgid "Operator"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/conditions/form.py
|
||||
msgid "A value"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/conditions/form.py
|
||||
msgid "Operator is required."
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/conditions/form.py
|
||||
msgid "Field is required."
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/conditions/form.py
|
||||
msgid "Value is required."
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/conditions/plugins/levenshtein_plugin.py
|
||||
msgid "Levenshtein - Text similarity ratio"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/conditions/plugins/levenshtein_plugin.py
|
||||
msgid "Levenshtein - Text change distance"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/conditions/plugins/levenshtein_plugin.py
|
||||
msgid "Not enough history to calculate Levenshtein metrics"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/conditions/plugins/levenshtein_plugin.py
|
||||
msgid "Snapshot too large for edit statistics, skipping."
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/conditions/plugins/levenshtein_plugin.py
|
||||
msgid "Unable to calculate Levenshtein metrics"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/conditions/plugins/levenshtein_plugin.py
|
||||
msgid "Levenshtein Text Similarity Details"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/conditions/plugins/levenshtein_plugin.py
|
||||
msgid "Raw distance (edits needed)"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/conditions/plugins/levenshtein_plugin.py
|
||||
msgid "Similarity ratio"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/conditions/plugins/levenshtein_plugin.py
|
||||
msgid "Percent similar"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/conditions/plugins/levenshtein_plugin.py
|
||||
msgid ""
|
||||
"Levenshtein metrics compare the last two snapshots, measuring how many character edits are needed to transform one "
|
||||
"into the other."
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/conditions/plugins/levenshtein_plugin.py
|
||||
msgid "Error calculating Levenshtein metrics"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/conditions/plugins/wordcount_plugin.py
|
||||
msgid "Word count of content"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/conditions/plugins/wordcount_plugin.py
|
||||
msgid "Content Analysis"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/conditions/plugins/wordcount_plugin.py
|
||||
msgid "Word count (latest snapshot)"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/conditions/plugins/wordcount_plugin.py
|
||||
msgid "Word count is a simple measure of content length, calculated by splitting text on whitespace."
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/content_fetchers/requests.py
|
||||
msgid "Basic fast Plaintext/HTTP Client"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/blueprint/watchlist/templates/watch-overview.html changedetectionio/flask_app.py
|
||||
#: changedetectionio/realtime/socket_server.py
|
||||
msgid "Not yet"
|
||||
@@ -2424,26 +2205,10 @@ msgstr "작업"
|
||||
msgid "Selector"
|
||||
msgstr "선택자"
|
||||
|
||||
#: changedetectionio/forms.py
|
||||
msgid "CSS or xPath selector"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/forms.py
|
||||
msgid "value"
|
||||
msgstr "값"
|
||||
|
||||
#: changedetectionio/conditions/form.py changedetectionio/forms.py
|
||||
msgid "Value"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/forms.py
|
||||
msgid "Web Page URL"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/forms.py
|
||||
msgid "Group Tag"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/forms.py
|
||||
msgid "Time Between Check"
|
||||
msgstr "확인 간격"
|
||||
@@ -2532,8 +2297,7 @@ msgstr "텍스트가 일치하는 동안 변경 감지 차단"
|
||||
msgid "Execute JavaScript before change detection"
|
||||
msgstr "변경 감지 전에 JavaScript 실행"
|
||||
|
||||
#: changedetectionio/blueprint/tags/form.py changedetectionio/blueprint/tags/templates/groups-overview.html
|
||||
#: changedetectionio/forms.py
|
||||
#: changedetectionio/blueprint/tags/templates/groups-overview.html changedetectionio/forms.py
|
||||
msgid "Save"
|
||||
msgstr "구하다"
|
||||
|
||||
@@ -2601,7 +2365,7 @@ msgstr "잘못된 템플릿 구문: %(error)s"
|
||||
msgid "Invalid template syntax in \"%(header)s\" header: %(error)s"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/blueprint/tags/form.py changedetectionio/forms.py
|
||||
#: changedetectionio/forms.py
|
||||
msgid "Name"
|
||||
msgstr "이름"
|
||||
|
||||
@@ -2697,10 +2461,6 @@ msgstr "텍스트 무시"
|
||||
msgid "Ignore whitespace"
|
||||
msgstr "공백 무시"
|
||||
|
||||
#: changedetectionio/forms.py
|
||||
msgid "Screenshot: Minimum Change Percentage"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/forms.py changedetectionio/processors/image_ssim_diff/forms.py
|
||||
msgid "Must be between 0 and 100"
|
||||
msgstr "0에서 100 사이여야 합니다."
|
||||
@@ -2900,42 +2660,6 @@ msgstr "단일 제품이 포함된 페이지의 재입고 및 가격 감지"
|
||||
msgid "Detects if the product goes back to in-stock"
|
||||
msgstr "제품이 다시 재고로 돌아왔는지 감지합니다."
|
||||
|
||||
#: changedetectionio/processors/templates/extract.html
|
||||
msgid "Screenshot"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/processors/templates/extract.html
|
||||
msgid "This tool will extract text data from all of the watch history."
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/processors/templates/extract.html
|
||||
msgid "A <strong>RegEx</strong> is a pattern that identifies exactly which part inside of the text that you want to extract."
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/processors/templates/extract.html
|
||||
msgid "For example, to extract only the numbers from text"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/processors/templates/extract.html
|
||||
msgid "Raw text"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/processors/templates/extract.html
|
||||
msgid "RegEx to extract:"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/processors/templates/extract.html
|
||||
msgid "Be sure to test your RegEx here."
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/processors/templates/extract.html
|
||||
msgid "Each RegEx group bracket"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/processors/templates/extract.html
|
||||
msgid "will be in its own column, the first column value is always the date."
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/processors/text_json_diff/processor.py
|
||||
msgid "Webpage Text/HTML, JSON and PDF changes"
|
||||
msgstr "웹페이지 텍스트/HTML, JSON 및 PDF 변경"
|
||||
@@ -3115,7 +2839,7 @@ msgstr ""
|
||||
msgid "Use"
|
||||
msgstr "사용"
|
||||
|
||||
#: changedetectionio/templates/_common_fields.html changedetectionio/templates/edit/include_subtract.html
|
||||
#: changedetectionio/templates/_common_fields.html
|
||||
msgid "Show advanced help and tips"
|
||||
msgstr "고급 도움말 표시"
|
||||
|
||||
@@ -3219,26 +2943,6 @@ msgstr ""
|
||||
msgid "Format for all notifications"
|
||||
msgstr "모든 알림 형식"
|
||||
|
||||
#: changedetectionio/templates/_common_fields.html
|
||||
msgid "Note"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/templates/_common_fields.html
|
||||
msgid "Discord does not render HTML — switch to"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/templates/_common_fields.html
|
||||
msgid "Plain Text"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/templates/_common_fields.html
|
||||
msgid "format to avoid"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/templates/_common_fields.html
|
||||
msgid "and other HTML entities appearing literally in your notifications."
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/templates/_helpers.html
|
||||
msgid "Entry"
|
||||
msgstr ""
|
||||
@@ -3355,18 +3059,10 @@ msgstr "이 텍스트 존재 시 변경 감지 안 함."
|
||||
msgid "Blocked text"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/templates/base.html
|
||||
msgid "A new version is available"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/templates/base.html
|
||||
msgid "Search, or Use Alt+S Key"
|
||||
msgstr "검색 또는 Alt+S 키 사용"
|
||||
|
||||
#: changedetectionio/templates/base.html
|
||||
msgid "Share this link:"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/templates/base.html
|
||||
msgid "Real-time updates offline"
|
||||
msgstr "실시간 업데이트 오프라인"
|
||||
@@ -3537,6 +3233,10 @@ msgstr ""
|
||||
msgid "Unmute notifications"
|
||||
msgstr "알림 음소거 해제"
|
||||
|
||||
#: changedetectionio/templates/menu.html
|
||||
msgid "Mute notifications"
|
||||
msgstr "알림 음소거"
|
||||
|
||||
#: changedetectionio/templates/menu.html
|
||||
msgid "Notifications are muted - click to unmute"
|
||||
msgstr "알림 음소거됨 - 클릭하여 해제"
|
||||
|
||||
@@ -6,9 +6,9 @@
|
||||
#, fuzzy
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: changedetection.io 0.54.9\n"
|
||||
"Project-Id-Version: changedetection.io 0.54.8\n"
|
||||
"Report-Msgid-Bugs-To: EMAIL@ADDRESS\n"
|
||||
"POT-Creation-Date: 2026-04-15 03:04+0900\n"
|
||||
"POT-Creation-Date: 2026-04-11 04:15+0200\n"
|
||||
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
|
||||
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
|
||||
"Language-Team: LANGUAGE <LL@li.org>\n"
|
||||
@@ -589,15 +589,15 @@ msgstr ""
|
||||
msgid "Changing this could affect the content of your existing watches, possibly trigger alerts etc."
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/blueprint/settings/templates/settings.html changedetectionio/templates/edit/include_subtract.html
|
||||
#: changedetectionio/blueprint/settings/templates/settings.html
|
||||
msgid "Remove HTML element(s) by CSS and XPath selectors before text conversion."
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/blueprint/settings/templates/settings.html changedetectionio/templates/edit/include_subtract.html
|
||||
#: changedetectionio/blueprint/settings/templates/settings.html
|
||||
msgid "Don't paste HTML here, use only CSS and XPath selectors"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/blueprint/settings/templates/settings.html changedetectionio/templates/edit/include_subtract.html
|
||||
#: changedetectionio/blueprint/settings/templates/settings.html
|
||||
msgid "Add multiple elements, CSS or XPath selectors per line to ignore multiple parts of the HTML."
|
||||
msgstr ""
|
||||
|
||||
@@ -828,26 +828,6 @@ msgstr ""
|
||||
msgid "Updated"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/blueprint/tags/form.py
|
||||
msgid "Activate for individual watches in this tag/group?"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/blueprint/tags/form.py
|
||||
msgid "Auto-apply to watches with URLs matching"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/blueprint/tags/form.py
|
||||
msgid "e.g. *://example.com/* or github.com/myorg"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/blueprint/tags/form.py changedetectionio/blueprint/tags/templates/edit-tag.html
|
||||
msgid "Tag colour"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/blueprint/tags/form.py
|
||||
msgid "Tag name"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/blueprint/tags/templates/edit-tag.html changedetectionio/blueprint/ui/templates/edit.html
|
||||
msgid "Filters & Triggers"
|
||||
msgstr ""
|
||||
@@ -862,6 +842,10 @@ msgstr ""
|
||||
msgid "Currently matching watches"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/blueprint/tags/templates/edit-tag.html
|
||||
msgid "Tag colour"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/blueprint/tags/templates/edit-tag.html
|
||||
msgid "Custom colour"
|
||||
msgstr ""
|
||||
@@ -947,11 +931,7 @@ msgstr ""
|
||||
msgid "No website organisational tags/groups configured"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/blueprint/tags/templates/groups-overview.html changedetectionio/templates/menu.html
|
||||
msgid "Mute notifications"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/blueprint/tags/templates/groups-overview.html changedetectionio/blueprint/ui/edit.py
|
||||
#: changedetectionio/blueprint/tags/templates/groups-overview.html
|
||||
#: changedetectionio/blueprint/watchlist/templates/watch-overview.html
|
||||
msgid "Edit"
|
||||
msgstr ""
|
||||
@@ -1154,14 +1134,6 @@ msgstr ""
|
||||
msgid "Could not load '{}' processor, processor plugin might be missing."
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/blueprint/ui/edit.py
|
||||
msgid "System settings default"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/blueprint/ui/edit.py
|
||||
msgid "Default"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/blueprint/ui/edit.py
|
||||
msgid "Updated watch - unpaused!"
|
||||
msgstr ""
|
||||
@@ -1174,10 +1146,6 @@ msgstr ""
|
||||
msgid "Preview unavailable - No fetch/check completed or triggers not reached"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/blueprint/ui/preview.py
|
||||
msgid "Diff"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/blueprint/ui/templates/clear_all_history.html
|
||||
msgid "This will remove version history (snapshots) for ALL watches, but keep your list of URLs!"
|
||||
msgstr ""
|
||||
@@ -1287,17 +1255,14 @@ msgid "Jump"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/blueprint/ui/templates/diff.html changedetectionio/blueprint/ui/templates/preview.html
|
||||
#: changedetectionio/processors/templates/extract.html
|
||||
msgid "Error Text"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/blueprint/ui/templates/diff.html changedetectionio/blueprint/ui/templates/preview.html
|
||||
#: changedetectionio/processors/templates/extract.html
|
||||
msgid "Error Screenshot"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/blueprint/ui/templates/diff.html changedetectionio/blueprint/ui/templates/preview.html
|
||||
#: changedetectionio/processors/templates/extract.html
|
||||
msgid "Text"
|
||||
msgstr ""
|
||||
|
||||
@@ -1305,8 +1270,7 @@ msgstr ""
|
||||
msgid "Current screenshot"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/blueprint/ui/templates/diff.html changedetectionio/processors/extract.py
|
||||
#: changedetectionio/processors/templates/extract.html
|
||||
#: changedetectionio/blueprint/ui/templates/diff.html
|
||||
msgid "Extract Data"
|
||||
msgstr ""
|
||||
|
||||
@@ -1896,26 +1860,6 @@ msgstr ""
|
||||
msgid "import a list"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/blueprint/watchlist/templates/watch-overview.html
|
||||
msgid "Pause checks"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/blueprint/watchlist/templates/watch-overview.html
|
||||
msgid "UnPause checks"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/blueprint/watchlist/templates/watch-overview.html
|
||||
msgid "Mute notification"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/blueprint/watchlist/templates/watch-overview.html
|
||||
msgid "UnMute notification"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/blueprint/watchlist/templates/watch-overview.html
|
||||
msgid "Create a link to share watch config with others"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/blueprint/watchlist/templates/watch-overview.html
|
||||
msgid "Detecting restock and price"
|
||||
msgstr ""
|
||||
@@ -1945,7 +1889,6 @@ msgid "Queued"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/blueprint/watchlist/templates/watch-overview.html
|
||||
#: changedetectionio/processors/text_json_diff/difference.py
|
||||
msgid "History"
|
||||
msgstr ""
|
||||
|
||||
@@ -1979,168 +1922,6 @@ msgstr ""
|
||||
msgid "in '%(title)s'"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/conditions/__init__.py
|
||||
msgid "Choose one - Operator"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/conditions/__init__.py
|
||||
msgid "Greater Than"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/conditions/__init__.py
|
||||
msgid "Less Than"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/conditions/__init__.py
|
||||
msgid "Greater Than or Equal To"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/conditions/__init__.py
|
||||
msgid "Less Than or Equal To"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/conditions/__init__.py
|
||||
msgid "Equals"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/conditions/__init__.py
|
||||
msgid "Not Equals"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/conditions/__init__.py
|
||||
msgid "Contains"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/conditions/__init__.py
|
||||
msgid "Choose one - Field"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/conditions/default_plugin.py
|
||||
msgid "Does NOT Contain"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/conditions/default_plugin.py
|
||||
msgid "Text Starts With"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/conditions/default_plugin.py
|
||||
msgid "Text Ends With"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/conditions/default_plugin.py
|
||||
msgid "Length minimum"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/conditions/default_plugin.py
|
||||
msgid "Length maximum"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/conditions/default_plugin.py
|
||||
msgid "Text Matches Regex"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/conditions/default_plugin.py
|
||||
msgid "Text Does NOT Match Regex"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/conditions/default_plugin.py
|
||||
msgid "Extracted number after 'Filters & Triggers'"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/conditions/default_plugin.py
|
||||
msgid "Page text after 'Filters & Triggers'"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/conditions/form.py
|
||||
msgid "Field"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/conditions/form.py
|
||||
msgid "Operator"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/conditions/form.py
|
||||
msgid "A value"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/conditions/form.py
|
||||
msgid "Operator is required."
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/conditions/form.py
|
||||
msgid "Field is required."
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/conditions/form.py
|
||||
msgid "Value is required."
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/conditions/plugins/levenshtein_plugin.py
|
||||
msgid "Levenshtein - Text similarity ratio"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/conditions/plugins/levenshtein_plugin.py
|
||||
msgid "Levenshtein - Text change distance"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/conditions/plugins/levenshtein_plugin.py
|
||||
msgid "Not enough history to calculate Levenshtein metrics"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/conditions/plugins/levenshtein_plugin.py
|
||||
msgid "Snapshot too large for edit statistics, skipping."
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/conditions/plugins/levenshtein_plugin.py
|
||||
msgid "Unable to calculate Levenshtein metrics"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/conditions/plugins/levenshtein_plugin.py
|
||||
msgid "Levenshtein Text Similarity Details"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/conditions/plugins/levenshtein_plugin.py
|
||||
msgid "Raw distance (edits needed)"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/conditions/plugins/levenshtein_plugin.py
|
||||
msgid "Similarity ratio"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/conditions/plugins/levenshtein_plugin.py
|
||||
msgid "Percent similar"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/conditions/plugins/levenshtein_plugin.py
|
||||
msgid ""
|
||||
"Levenshtein metrics compare the last two snapshots, measuring how many character edits are needed to transform one "
|
||||
"into the other."
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/conditions/plugins/levenshtein_plugin.py
|
||||
msgid "Error calculating Levenshtein metrics"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/conditions/plugins/wordcount_plugin.py
|
||||
msgid "Word count of content"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/conditions/plugins/wordcount_plugin.py
|
||||
msgid "Content Analysis"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/conditions/plugins/wordcount_plugin.py
|
||||
msgid "Word count (latest snapshot)"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/conditions/plugins/wordcount_plugin.py
|
||||
msgid "Word count is a simple measure of content length, calculated by splitting text on whitespace."
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/content_fetchers/requests.py
|
||||
msgid "Basic fast Plaintext/HTTP Client"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/blueprint/watchlist/templates/watch-overview.html changedetectionio/flask_app.py
|
||||
#: changedetectionio/realtime/socket_server.py
|
||||
msgid "Not yet"
|
||||
@@ -2423,26 +2204,10 @@ msgstr ""
|
||||
msgid "Selector"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/forms.py
|
||||
msgid "CSS or xPath selector"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/forms.py
|
||||
msgid "value"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/conditions/form.py changedetectionio/forms.py
|
||||
msgid "Value"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/forms.py
|
||||
msgid "Web Page URL"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/forms.py
|
||||
msgid "Group Tag"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/forms.py
|
||||
msgid "Time Between Check"
|
||||
msgstr ""
|
||||
@@ -2531,8 +2296,7 @@ msgstr ""
|
||||
msgid "Execute JavaScript before change detection"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/blueprint/tags/form.py changedetectionio/blueprint/tags/templates/groups-overview.html
|
||||
#: changedetectionio/forms.py
|
||||
#: changedetectionio/blueprint/tags/templates/groups-overview.html changedetectionio/forms.py
|
||||
msgid "Save"
|
||||
msgstr ""
|
||||
|
||||
@@ -2600,7 +2364,7 @@ msgstr ""
|
||||
msgid "Invalid template syntax in \"%(header)s\" header: %(error)s"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/blueprint/tags/form.py changedetectionio/forms.py
|
||||
#: changedetectionio/forms.py
|
||||
msgid "Name"
|
||||
msgstr ""
|
||||
|
||||
@@ -2696,10 +2460,6 @@ msgstr ""
|
||||
msgid "Ignore whitespace"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/forms.py
|
||||
msgid "Screenshot: Minimum Change Percentage"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/forms.py changedetectionio/processors/image_ssim_diff/forms.py
|
||||
msgid "Must be between 0 and 100"
|
||||
msgstr ""
|
||||
@@ -2899,42 +2659,6 @@ msgstr ""
|
||||
msgid "Detects if the product goes back to in-stock"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/processors/templates/extract.html
|
||||
msgid "Screenshot"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/processors/templates/extract.html
|
||||
msgid "This tool will extract text data from all of the watch history."
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/processors/templates/extract.html
|
||||
msgid "A <strong>RegEx</strong> is a pattern that identifies exactly which part inside of the text that you want to extract."
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/processors/templates/extract.html
|
||||
msgid "For example, to extract only the numbers from text"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/processors/templates/extract.html
|
||||
msgid "Raw text"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/processors/templates/extract.html
|
||||
msgid "RegEx to extract:"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/processors/templates/extract.html
|
||||
msgid "Be sure to test your RegEx here."
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/processors/templates/extract.html
|
||||
msgid "Each RegEx group bracket"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/processors/templates/extract.html
|
||||
msgid "will be in its own column, the first column value is always the date."
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/processors/text_json_diff/processor.py
|
||||
msgid "Webpage Text/HTML, JSON and PDF changes"
|
||||
msgstr ""
|
||||
@@ -3114,7 +2838,7 @@ msgstr ""
|
||||
msgid "Use"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/templates/_common_fields.html changedetectionio/templates/edit/include_subtract.html
|
||||
#: changedetectionio/templates/_common_fields.html
|
||||
msgid "Show advanced help and tips"
|
||||
msgstr ""
|
||||
|
||||
@@ -3218,26 +2942,6 @@ msgstr ""
|
||||
msgid "Format for all notifications"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/templates/_common_fields.html
|
||||
msgid "Note"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/templates/_common_fields.html
|
||||
msgid "Discord does not render HTML — switch to"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/templates/_common_fields.html
|
||||
msgid "Plain Text"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/templates/_common_fields.html
|
||||
msgid "format to avoid"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/templates/_common_fields.html
|
||||
msgid "and other HTML entities appearing literally in your notifications."
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/templates/_helpers.html
|
||||
msgid "Entry"
|
||||
msgstr ""
|
||||
@@ -3354,18 +3058,10 @@ msgstr ""
|
||||
msgid "Blocked text"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/templates/base.html
|
||||
msgid "A new version is available"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/templates/base.html
|
||||
msgid "Search, or Use Alt+S Key"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/templates/base.html
|
||||
msgid "Share this link:"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/templates/base.html
|
||||
msgid "Real-time updates offline"
|
||||
msgstr ""
|
||||
@@ -3536,6 +3232,10 @@ msgstr ""
|
||||
msgid "Unmute notifications"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/templates/menu.html
|
||||
msgid "Mute notifications"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/templates/menu.html
|
||||
msgid "Notifications are muted - click to unmute"
|
||||
msgstr ""
|
||||
|
||||
Binary file not shown.
@@ -8,7 +8,7 @@ msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: changedetection.io 0.54.8\n"
|
||||
"Report-Msgid-Bugs-To: mstrey@gmail.com\n"
|
||||
"POT-Creation-Date: 2026-04-15 03:04+0900\n"
|
||||
"POT-Creation-Date: 2026-04-11 04:15+0200\n"
|
||||
"PO-Revision-Date: 2026-04-07 22:00-0300\n"
|
||||
"Last-Translator: Gemini AI\n"
|
||||
"Language: pt_BR\n"
|
||||
@@ -599,15 +599,15 @@ msgstr "Renderizar conteúdo da tag âncora, desativado por padrão. Se ativado,
|
||||
msgid "Changing this could affect the content of your existing watches, possibly trigger alerts etc."
|
||||
msgstr "Alterar isso pode afetar o conteúdo dos seus monitoramentos existentes, possivelmente disparando alertas, etc."
|
||||
|
||||
#: changedetectionio/blueprint/settings/templates/settings.html changedetectionio/templates/edit/include_subtract.html
|
||||
#: changedetectionio/blueprint/settings/templates/settings.html
|
||||
msgid "Remove HTML element(s) by CSS and XPath selectors before text conversion."
|
||||
msgstr "Remover elementos HTML por seletores CSS e XPath antes da conversão de texto."
|
||||
|
||||
#: changedetectionio/blueprint/settings/templates/settings.html changedetectionio/templates/edit/include_subtract.html
|
||||
#: changedetectionio/blueprint/settings/templates/settings.html
|
||||
msgid "Don't paste HTML here, use only CSS and XPath selectors"
|
||||
msgstr "Não cole HTML aqui, use apenas seletores CSS e XPath"
|
||||
|
||||
#: changedetectionio/blueprint/settings/templates/settings.html changedetectionio/templates/edit/include_subtract.html
|
||||
#: changedetectionio/blueprint/settings/templates/settings.html
|
||||
msgid "Add multiple elements, CSS or XPath selectors per line to ignore multiple parts of the HTML."
|
||||
msgstr "Adicione múltiplos elementos, seletores CSS ou XPath por linha para ignorar várias partes do HTML."
|
||||
|
||||
@@ -848,26 +848,6 @@ msgstr "Tag não encontrada"
|
||||
msgid "Updated"
|
||||
msgstr "Atualizado"
|
||||
|
||||
#: changedetectionio/blueprint/tags/form.py
|
||||
msgid "Activate for individual watches in this tag/group?"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/blueprint/tags/form.py
|
||||
msgid "Auto-apply to watches with URLs matching"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/blueprint/tags/form.py
|
||||
msgid "e.g. *://example.com/* or github.com/myorg"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/blueprint/tags/form.py changedetectionio/blueprint/tags/templates/edit-tag.html
|
||||
msgid "Tag colour"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/blueprint/tags/form.py
|
||||
msgid "Tag name"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/blueprint/tags/templates/edit-tag.html changedetectionio/blueprint/ui/templates/edit.html
|
||||
msgid "Filters & Triggers"
|
||||
msgstr "Filtros e Gatilhos"
|
||||
@@ -882,6 +862,10 @@ msgstr ""
|
||||
msgid "Currently matching watches"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/blueprint/tags/templates/edit-tag.html
|
||||
msgid "Tag colour"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/blueprint/tags/templates/edit-tag.html
|
||||
msgid "Custom colour"
|
||||
msgstr ""
|
||||
@@ -969,11 +953,7 @@ msgstr "Nome da Tag / Rótulo"
|
||||
msgid "No website organisational tags/groups configured"
|
||||
msgstr "Nenhum grupo ou tag organizacional configurado"
|
||||
|
||||
#: changedetectionio/blueprint/tags/templates/groups-overview.html changedetectionio/templates/menu.html
|
||||
msgid "Mute notifications"
|
||||
msgstr "Silenciar notificações"
|
||||
|
||||
#: changedetectionio/blueprint/tags/templates/groups-overview.html changedetectionio/blueprint/ui/edit.py
|
||||
#: changedetectionio/blueprint/tags/templates/groups-overview.html
|
||||
#: changedetectionio/blueprint/watchlist/templates/watch-overview.html
|
||||
msgid "Edit"
|
||||
msgstr "Editar"
|
||||
@@ -1180,14 +1160,6 @@ msgstr ""
|
||||
msgid "Could not load '{}' processor, processor plugin might be missing."
|
||||
msgstr "Não foi possível carregar o processador '{}', o plugin pode estar faltando."
|
||||
|
||||
#: changedetectionio/blueprint/ui/edit.py
|
||||
msgid "System settings default"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/blueprint/ui/edit.py
|
||||
msgid "Default"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/blueprint/ui/edit.py
|
||||
msgid "Updated watch - unpaused!"
|
||||
msgstr "Monitoramento atualizado - retomado!"
|
||||
@@ -1200,10 +1172,6 @@ msgstr "Monitoramento atualizado."
|
||||
msgid "Preview unavailable - No fetch/check completed or triggers not reached"
|
||||
msgstr "Pré-visualização indisponível - Nenhuma busca concluída ou gatilhos não atingidos"
|
||||
|
||||
#: changedetectionio/blueprint/ui/preview.py
|
||||
msgid "Diff"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/blueprint/ui/templates/clear_all_history.html
|
||||
msgid "This will remove version history (snapshots) for ALL watches, but keep your list of URLs!"
|
||||
msgstr "Isso removerá o histórico de versões (instantâneos) para TODOS os monitoramentos, mas manterá sua lista de URLs!"
|
||||
@@ -1313,17 +1281,14 @@ msgid "Jump"
|
||||
msgstr "Pular"
|
||||
|
||||
#: changedetectionio/blueprint/ui/templates/diff.html changedetectionio/blueprint/ui/templates/preview.html
|
||||
#: changedetectionio/processors/templates/extract.html
|
||||
msgid "Error Text"
|
||||
msgstr "Texto de Erro"
|
||||
|
||||
#: changedetectionio/blueprint/ui/templates/diff.html changedetectionio/blueprint/ui/templates/preview.html
|
||||
#: changedetectionio/processors/templates/extract.html
|
||||
msgid "Error Screenshot"
|
||||
msgstr "Screenshot de Erro"
|
||||
|
||||
#: changedetectionio/blueprint/ui/templates/diff.html changedetectionio/blueprint/ui/templates/preview.html
|
||||
#: changedetectionio/processors/templates/extract.html
|
||||
msgid "Text"
|
||||
msgstr "Texto"
|
||||
|
||||
@@ -1331,8 +1296,7 @@ msgstr "Texto"
|
||||
msgid "Current screenshot"
|
||||
msgstr "Screenshot atual"
|
||||
|
||||
#: changedetectionio/blueprint/ui/templates/diff.html changedetectionio/processors/extract.py
|
||||
#: changedetectionio/processors/templates/extract.html
|
||||
#: changedetectionio/blueprint/ui/templates/diff.html
|
||||
msgid "Extract Data"
|
||||
msgstr "Extrair Dados"
|
||||
|
||||
@@ -1930,26 +1894,6 @@ msgstr "Nenhum monitoramento configurado, adicione uma URL na caixa acima ou"
|
||||
msgid "import a list"
|
||||
msgstr "importe uma lista"
|
||||
|
||||
#: changedetectionio/blueprint/watchlist/templates/watch-overview.html
|
||||
msgid "Pause checks"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/blueprint/watchlist/templates/watch-overview.html
|
||||
msgid "UnPause checks"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/blueprint/watchlist/templates/watch-overview.html
|
||||
msgid "Mute notification"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/blueprint/watchlist/templates/watch-overview.html
|
||||
msgid "UnMute notification"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/blueprint/watchlist/templates/watch-overview.html
|
||||
msgid "Create a link to share watch config with others"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/blueprint/watchlist/templates/watch-overview.html
|
||||
msgid "Detecting restock and price"
|
||||
msgstr "Detectando estoque e preço"
|
||||
@@ -1979,7 +1923,6 @@ msgid "Queued"
|
||||
msgstr "Enfileirado"
|
||||
|
||||
#: changedetectionio/blueprint/watchlist/templates/watch-overview.html
|
||||
#: changedetectionio/processors/text_json_diff/difference.py
|
||||
msgid "History"
|
||||
msgstr "Histórico"
|
||||
|
||||
@@ -2013,168 +1956,6 @@ msgstr "Rechecar todos"
|
||||
msgid "in '%(title)s'"
|
||||
msgstr "em '%(title)s'"
|
||||
|
||||
#: changedetectionio/conditions/__init__.py
|
||||
msgid "Choose one - Operator"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/conditions/__init__.py
|
||||
msgid "Greater Than"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/conditions/__init__.py
|
||||
msgid "Less Than"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/conditions/__init__.py
|
||||
msgid "Greater Than or Equal To"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/conditions/__init__.py
|
||||
msgid "Less Than or Equal To"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/conditions/__init__.py
|
||||
msgid "Equals"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/conditions/__init__.py
|
||||
msgid "Not Equals"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/conditions/__init__.py
|
||||
msgid "Contains"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/conditions/__init__.py
|
||||
msgid "Choose one - Field"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/conditions/default_plugin.py
|
||||
msgid "Does NOT Contain"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/conditions/default_plugin.py
|
||||
msgid "Text Starts With"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/conditions/default_plugin.py
|
||||
msgid "Text Ends With"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/conditions/default_plugin.py
|
||||
msgid "Length minimum"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/conditions/default_plugin.py
|
||||
msgid "Length maximum"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/conditions/default_plugin.py
|
||||
msgid "Text Matches Regex"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/conditions/default_plugin.py
|
||||
msgid "Text Does NOT Match Regex"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/conditions/default_plugin.py
|
||||
msgid "Extracted number after 'Filters & Triggers'"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/conditions/default_plugin.py
|
||||
msgid "Page text after 'Filters & Triggers'"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/conditions/form.py
|
||||
msgid "Field"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/conditions/form.py
|
||||
msgid "Operator"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/conditions/form.py
|
||||
msgid "A value"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/conditions/form.py
|
||||
msgid "Operator is required."
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/conditions/form.py
|
||||
msgid "Field is required."
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/conditions/form.py
|
||||
msgid "Value is required."
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/conditions/plugins/levenshtein_plugin.py
|
||||
msgid "Levenshtein - Text similarity ratio"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/conditions/plugins/levenshtein_plugin.py
|
||||
msgid "Levenshtein - Text change distance"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/conditions/plugins/levenshtein_plugin.py
|
||||
msgid "Not enough history to calculate Levenshtein metrics"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/conditions/plugins/levenshtein_plugin.py
|
||||
msgid "Snapshot too large for edit statistics, skipping."
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/conditions/plugins/levenshtein_plugin.py
|
||||
msgid "Unable to calculate Levenshtein metrics"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/conditions/plugins/levenshtein_plugin.py
|
||||
msgid "Levenshtein Text Similarity Details"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/conditions/plugins/levenshtein_plugin.py
|
||||
msgid "Raw distance (edits needed)"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/conditions/plugins/levenshtein_plugin.py
|
||||
msgid "Similarity ratio"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/conditions/plugins/levenshtein_plugin.py
|
||||
msgid "Percent similar"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/conditions/plugins/levenshtein_plugin.py
|
||||
msgid ""
|
||||
"Levenshtein metrics compare the last two snapshots, measuring how many character edits are needed to transform one "
|
||||
"into the other."
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/conditions/plugins/levenshtein_plugin.py
|
||||
msgid "Error calculating Levenshtein metrics"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/conditions/plugins/wordcount_plugin.py
|
||||
msgid "Word count of content"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/conditions/plugins/wordcount_plugin.py
|
||||
msgid "Content Analysis"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/conditions/plugins/wordcount_plugin.py
|
||||
msgid "Word count (latest snapshot)"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/conditions/plugins/wordcount_plugin.py
|
||||
msgid "Word count is a simple measure of content length, calculated by splitting text on whitespace."
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/content_fetchers/requests.py
|
||||
msgid "Basic fast Plaintext/HTTP Client"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/blueprint/watchlist/templates/watch-overview.html changedetectionio/flask_app.py
|
||||
#: changedetectionio/realtime/socket_server.py
|
||||
msgid "Not yet"
|
||||
@@ -2457,26 +2238,10 @@ msgstr "Operação"
|
||||
msgid "Selector"
|
||||
msgstr "Seletor"
|
||||
|
||||
#: changedetectionio/forms.py
|
||||
msgid "CSS or xPath selector"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/forms.py
|
||||
msgid "value"
|
||||
msgstr "valor"
|
||||
|
||||
#: changedetectionio/conditions/form.py changedetectionio/forms.py
|
||||
msgid "Value"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/forms.py
|
||||
msgid "Web Page URL"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/forms.py
|
||||
msgid "Group Tag"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/forms.py
|
||||
msgid "Time Between Check"
|
||||
msgstr "Tempo Entre Verificações"
|
||||
@@ -2565,8 +2330,7 @@ msgstr "Bloquear detecção de mudança enquanto o texto corresponder"
|
||||
msgid "Execute JavaScript before change detection"
|
||||
msgstr "Executar JavaScript antes da detecção de mudanças"
|
||||
|
||||
#: changedetectionio/blueprint/tags/form.py changedetectionio/blueprint/tags/templates/groups-overview.html
|
||||
#: changedetectionio/forms.py
|
||||
#: changedetectionio/blueprint/tags/templates/groups-overview.html changedetectionio/forms.py
|
||||
msgid "Save"
|
||||
msgstr "Salvar"
|
||||
|
||||
@@ -2634,7 +2398,7 @@ msgstr "Sintaxe de modelo inválida: %(error)s"
|
||||
msgid "Invalid template syntax in \"%(header)s\" header: %(error)s"
|
||||
msgstr "Sintaxe de modelo inválida no cabeçalho \"%(header)s\": %(error)s"
|
||||
|
||||
#: changedetectionio/blueprint/tags/form.py changedetectionio/forms.py
|
||||
#: changedetectionio/forms.py
|
||||
msgid "Name"
|
||||
msgstr "Nome"
|
||||
|
||||
@@ -2730,10 +2494,6 @@ msgstr "Ignorar Texto"
|
||||
msgid "Ignore whitespace"
|
||||
msgstr "Ignorar espaços"
|
||||
|
||||
#: changedetectionio/forms.py
|
||||
msgid "Screenshot: Minimum Change Percentage"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/forms.py changedetectionio/processors/image_ssim_diff/forms.py
|
||||
msgid "Must be between 0 and 100"
|
||||
msgstr "Deve estar entre 0 e 100"
|
||||
@@ -2933,42 +2693,6 @@ msgstr "Detecção de Estoque e Preço para páginas com um ÚNICO produto"
|
||||
msgid "Detects if the product goes back to in-stock"
|
||||
msgstr "Detecta se o produto volta ao estoque"
|
||||
|
||||
#: changedetectionio/processors/templates/extract.html
|
||||
msgid "Screenshot"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/processors/templates/extract.html
|
||||
msgid "This tool will extract text data from all of the watch history."
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/processors/templates/extract.html
|
||||
msgid "A <strong>RegEx</strong> is a pattern that identifies exactly which part inside of the text that you want to extract."
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/processors/templates/extract.html
|
||||
msgid "For example, to extract only the numbers from text"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/processors/templates/extract.html
|
||||
msgid "Raw text"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/processors/templates/extract.html
|
||||
msgid "RegEx to extract:"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/processors/templates/extract.html
|
||||
msgid "Be sure to test your RegEx here."
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/processors/templates/extract.html
|
||||
msgid "Each RegEx group bracket"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/processors/templates/extract.html
|
||||
msgid "will be in its own column, the first column value is always the date."
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/processors/text_json_diff/processor.py
|
||||
msgid "Webpage Text/HTML, JSON and PDF changes"
|
||||
msgstr "Mudanças em Texto/HTML de páginas, JSON e PDF"
|
||||
@@ -3148,7 +2872,7 @@ msgstr "Por favor, leia a wiki dos serviços de notificação aqui para notas im
|
||||
msgid "Use"
|
||||
msgstr "Use"
|
||||
|
||||
#: changedetectionio/templates/_common_fields.html changedetectionio/templates/edit/include_subtract.html
|
||||
#: changedetectionio/templates/_common_fields.html
|
||||
msgid "Show advanced help and tips"
|
||||
msgstr "Mostrar ajuda avançada e dicas"
|
||||
|
||||
@@ -3252,26 +2976,6 @@ msgstr "Para uma referência completa de todos os filtros nativos do Jinja2, os
|
||||
msgid "Format for all notifications"
|
||||
msgstr "Formato para todas as notificações"
|
||||
|
||||
#: changedetectionio/templates/_common_fields.html
|
||||
msgid "Note"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/templates/_common_fields.html
|
||||
msgid "Discord does not render HTML — switch to"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/templates/_common_fields.html
|
||||
msgid "Plain Text"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/templates/_common_fields.html
|
||||
msgid "format to avoid"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/templates/_common_fields.html
|
||||
msgid "and other HTML entities appearing literally in your notifications."
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/templates/_helpers.html
|
||||
msgid "Entry"
|
||||
msgstr "Entrada"
|
||||
@@ -3390,18 +3094,10 @@ msgstr "Nenhuma detecção de mudança ocorrerá porque este texto existe."
|
||||
msgid "Blocked text"
|
||||
msgstr "Texto bloqueado"
|
||||
|
||||
#: changedetectionio/templates/base.html
|
||||
msgid "A new version is available"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/templates/base.html
|
||||
msgid "Search, or Use Alt+S Key"
|
||||
msgstr "Pesquisar, ou use a tecla Alt+S"
|
||||
|
||||
#: changedetectionio/templates/base.html
|
||||
msgid "Share this link:"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/templates/base.html
|
||||
msgid "Real-time updates offline"
|
||||
msgstr "Atualizações em tempo real offline"
|
||||
@@ -3578,6 +3274,10 @@ msgstr "O agendamento está pausado - clique para retomar"
|
||||
msgid "Unmute notifications"
|
||||
msgstr "Reativar notificações"
|
||||
|
||||
#: changedetectionio/templates/menu.html
|
||||
msgid "Mute notifications"
|
||||
msgstr "Silenciar notificações"
|
||||
|
||||
#: changedetectionio/templates/menu.html
|
||||
msgid "Notifications are muted - click to unmute"
|
||||
msgstr "As notificações estão silenciadas - clique para reativar"
|
||||
|
||||
Binary file not shown.
@@ -8,7 +8,7 @@ msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: changedetection.io 0.53.6\n"
|
||||
"Report-Msgid-Bugs-To: EMAIL@ADDRESS\n"
|
||||
"POT-Creation-Date: 2026-04-15 03:04+0900\n"
|
||||
"POT-Creation-Date: 2026-04-11 04:15+0200\n"
|
||||
"PO-Revision-Date: 2026-04-10 20:38+0300\n"
|
||||
"Last-Translator: \n"
|
||||
"Language: tr\n"
|
||||
@@ -607,15 +607,15 @@ msgstr ""
|
||||
msgid "Changing this could affect the content of your existing watches, possibly trigger alerts etc."
|
||||
msgstr "Bunu değiştirmek mevcut izleyicilerinizin içeriğini etkileyebilir, muhtemelen uyarıları vb. tetikleyebilir."
|
||||
|
||||
#: changedetectionio/blueprint/settings/templates/settings.html changedetectionio/templates/edit/include_subtract.html
|
||||
#: changedetectionio/blueprint/settings/templates/settings.html
|
||||
msgid "Remove HTML element(s) by CSS and XPath selectors before text conversion."
|
||||
msgstr "Metin dönüştürmeden önce HTML öğelerini CSS ve XPath seçicilere göre kaldırın."
|
||||
|
||||
#: changedetectionio/blueprint/settings/templates/settings.html changedetectionio/templates/edit/include_subtract.html
|
||||
#: changedetectionio/blueprint/settings/templates/settings.html
|
||||
msgid "Don't paste HTML here, use only CSS and XPath selectors"
|
||||
msgstr "Buraya HTML yapıştırmayın, yalnızca CSS ve XPath seçicilerini kullanın"
|
||||
|
||||
#: changedetectionio/blueprint/settings/templates/settings.html changedetectionio/templates/edit/include_subtract.html
|
||||
#: changedetectionio/blueprint/settings/templates/settings.html
|
||||
msgid "Add multiple elements, CSS or XPath selectors per line to ignore multiple parts of the HTML."
|
||||
msgstr "HTML'nin birden çok bölümünü yoksaymak için satır başına birden çok öğe, CSS veya XPath seçici ekleyin."
|
||||
|
||||
@@ -856,26 +856,6 @@ msgstr "Etiket bulunamadı"
|
||||
msgid "Updated"
|
||||
msgstr "Güncellendi"
|
||||
|
||||
#: changedetectionio/blueprint/tags/form.py
|
||||
msgid "Activate for individual watches in this tag/group?"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/blueprint/tags/form.py
|
||||
msgid "Auto-apply to watches with URLs matching"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/blueprint/tags/form.py
|
||||
msgid "e.g. *://example.com/* or github.com/myorg"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/blueprint/tags/form.py changedetectionio/blueprint/tags/templates/edit-tag.html
|
||||
msgid "Tag colour"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/blueprint/tags/form.py
|
||||
msgid "Tag name"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/blueprint/tags/templates/edit-tag.html changedetectionio/blueprint/ui/templates/edit.html
|
||||
msgid "Filters & Triggers"
|
||||
msgstr "Filtreler ve Tetikleyiciler"
|
||||
@@ -890,6 +870,10 @@ msgstr ""
|
||||
msgid "Currently matching watches"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/blueprint/tags/templates/edit-tag.html
|
||||
msgid "Tag colour"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/blueprint/tags/templates/edit-tag.html
|
||||
msgid "Custom colour"
|
||||
msgstr ""
|
||||
@@ -975,11 +959,7 @@ msgstr "Etiket / İsim adı"
|
||||
msgid "No website organisational tags/groups configured"
|
||||
msgstr "Yapılandırılmış web sitesi organizasyonel etiketi/grubu yok"
|
||||
|
||||
#: changedetectionio/blueprint/tags/templates/groups-overview.html changedetectionio/templates/menu.html
|
||||
msgid "Mute notifications"
|
||||
msgstr "Bildirimleri sessize al"
|
||||
|
||||
#: changedetectionio/blueprint/tags/templates/groups-overview.html changedetectionio/blueprint/ui/edit.py
|
||||
#: changedetectionio/blueprint/tags/templates/groups-overview.html
|
||||
#: changedetectionio/blueprint/watchlist/templates/watch-overview.html
|
||||
msgid "Edit"
|
||||
msgstr "Düzenle"
|
||||
@@ -1184,14 +1164,6 @@ msgstr "'{}' işlemcisi yüklenemedi, işlemci eklentisi eksik olabilir. Lütfen
|
||||
msgid "Could not load '{}' processor, processor plugin might be missing."
|
||||
msgstr "'{}' işlemcisi yüklenemedi, işlemci eklentisi eksik olabilir."
|
||||
|
||||
#: changedetectionio/blueprint/ui/edit.py
|
||||
msgid "System settings default"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/blueprint/ui/edit.py
|
||||
msgid "Default"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/blueprint/ui/edit.py
|
||||
msgid "Updated watch - unpaused!"
|
||||
msgstr "İzleyici güncellendi - duraklatma iptal edildi!"
|
||||
@@ -1204,10 +1176,6 @@ msgstr "İzleyici güncellendi."
|
||||
msgid "Preview unavailable - No fetch/check completed or triggers not reached"
|
||||
msgstr "Önizleme kullanılamıyor - Getirme/kontrol tamamlanmadı veya tetikleyicilere ulaşılamadı"
|
||||
|
||||
#: changedetectionio/blueprint/ui/preview.py
|
||||
msgid "Diff"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/blueprint/ui/templates/clear_all_history.html
|
||||
msgid "This will remove version history (snapshots) for ALL watches, but keep your list of URLs!"
|
||||
msgstr "Bu işlem TÜM izleyiciler için sürüm geçmişini (anlık görüntüleri) kaldıracak, ancak URL listenizi koruyacaktır!"
|
||||
@@ -1317,17 +1285,14 @@ msgid "Jump"
|
||||
msgstr "Atla"
|
||||
|
||||
#: changedetectionio/blueprint/ui/templates/diff.html changedetectionio/blueprint/ui/templates/preview.html
|
||||
#: changedetectionio/processors/templates/extract.html
|
||||
msgid "Error Text"
|
||||
msgstr "Hata Metni"
|
||||
|
||||
#: changedetectionio/blueprint/ui/templates/diff.html changedetectionio/blueprint/ui/templates/preview.html
|
||||
#: changedetectionio/processors/templates/extract.html
|
||||
msgid "Error Screenshot"
|
||||
msgstr "Hata Ekran Görüntüsü"
|
||||
|
||||
#: changedetectionio/blueprint/ui/templates/diff.html changedetectionio/blueprint/ui/templates/preview.html
|
||||
#: changedetectionio/processors/templates/extract.html
|
||||
msgid "Text"
|
||||
msgstr "Metin"
|
||||
|
||||
@@ -1335,8 +1300,7 @@ msgstr "Metin"
|
||||
msgid "Current screenshot"
|
||||
msgstr "Mevcut ekran görüntüsü"
|
||||
|
||||
#: changedetectionio/blueprint/ui/templates/diff.html changedetectionio/processors/extract.py
|
||||
#: changedetectionio/processors/templates/extract.html
|
||||
#: changedetectionio/blueprint/ui/templates/diff.html
|
||||
msgid "Extract Data"
|
||||
msgstr "Veriyi Çıkar"
|
||||
|
||||
@@ -1940,26 +1904,6 @@ msgstr "Yapılandırılmış web sayfası değişiklik tespiti izleyicisi yok, l
|
||||
msgid "import a list"
|
||||
msgstr "bir listeyi içe aktarın"
|
||||
|
||||
#: changedetectionio/blueprint/watchlist/templates/watch-overview.html
|
||||
msgid "Pause checks"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/blueprint/watchlist/templates/watch-overview.html
|
||||
msgid "UnPause checks"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/blueprint/watchlist/templates/watch-overview.html
|
||||
msgid "Mute notification"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/blueprint/watchlist/templates/watch-overview.html
|
||||
msgid "UnMute notification"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/blueprint/watchlist/templates/watch-overview.html
|
||||
msgid "Create a link to share watch config with others"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/blueprint/watchlist/templates/watch-overview.html
|
||||
msgid "Detecting restock and price"
|
||||
msgstr "Yeniden stoklama ve fiyat tespiti"
|
||||
@@ -1989,7 +1933,6 @@ msgid "Queued"
|
||||
msgstr "Sırada"
|
||||
|
||||
#: changedetectionio/blueprint/watchlist/templates/watch-overview.html
|
||||
#: changedetectionio/processors/text_json_diff/difference.py
|
||||
msgid "History"
|
||||
msgstr "Geçmiş"
|
||||
|
||||
@@ -2023,168 +1966,6 @@ msgstr "Tümünü yeniden kontrol et"
|
||||
msgid "in '%(title)s'"
|
||||
msgstr "'%(title)s' içinde"
|
||||
|
||||
#: changedetectionio/conditions/__init__.py
|
||||
msgid "Choose one - Operator"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/conditions/__init__.py
|
||||
msgid "Greater Than"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/conditions/__init__.py
|
||||
msgid "Less Than"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/conditions/__init__.py
|
||||
msgid "Greater Than or Equal To"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/conditions/__init__.py
|
||||
msgid "Less Than or Equal To"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/conditions/__init__.py
|
||||
msgid "Equals"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/conditions/__init__.py
|
||||
msgid "Not Equals"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/conditions/__init__.py
|
||||
msgid "Contains"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/conditions/__init__.py
|
||||
msgid "Choose one - Field"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/conditions/default_plugin.py
|
||||
msgid "Does NOT Contain"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/conditions/default_plugin.py
|
||||
msgid "Text Starts With"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/conditions/default_plugin.py
|
||||
msgid "Text Ends With"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/conditions/default_plugin.py
|
||||
msgid "Length minimum"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/conditions/default_plugin.py
|
||||
msgid "Length maximum"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/conditions/default_plugin.py
|
||||
msgid "Text Matches Regex"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/conditions/default_plugin.py
|
||||
msgid "Text Does NOT Match Regex"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/conditions/default_plugin.py
|
||||
msgid "Extracted number after 'Filters & Triggers'"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/conditions/default_plugin.py
|
||||
msgid "Page text after 'Filters & Triggers'"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/conditions/form.py
|
||||
msgid "Field"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/conditions/form.py
|
||||
msgid "Operator"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/conditions/form.py
|
||||
msgid "A value"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/conditions/form.py
|
||||
msgid "Operator is required."
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/conditions/form.py
|
||||
msgid "Field is required."
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/conditions/form.py
|
||||
msgid "Value is required."
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/conditions/plugins/levenshtein_plugin.py
|
||||
msgid "Levenshtein - Text similarity ratio"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/conditions/plugins/levenshtein_plugin.py
|
||||
msgid "Levenshtein - Text change distance"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/conditions/plugins/levenshtein_plugin.py
|
||||
msgid "Not enough history to calculate Levenshtein metrics"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/conditions/plugins/levenshtein_plugin.py
|
||||
msgid "Snapshot too large for edit statistics, skipping."
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/conditions/plugins/levenshtein_plugin.py
|
||||
msgid "Unable to calculate Levenshtein metrics"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/conditions/plugins/levenshtein_plugin.py
|
||||
msgid "Levenshtein Text Similarity Details"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/conditions/plugins/levenshtein_plugin.py
|
||||
msgid "Raw distance (edits needed)"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/conditions/plugins/levenshtein_plugin.py
|
||||
msgid "Similarity ratio"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/conditions/plugins/levenshtein_plugin.py
|
||||
msgid "Percent similar"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/conditions/plugins/levenshtein_plugin.py
|
||||
msgid ""
|
||||
"Levenshtein metrics compare the last two snapshots, measuring how many character edits are needed to transform one "
|
||||
"into the other."
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/conditions/plugins/levenshtein_plugin.py
|
||||
msgid "Error calculating Levenshtein metrics"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/conditions/plugins/wordcount_plugin.py
|
||||
msgid "Word count of content"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/conditions/plugins/wordcount_plugin.py
|
||||
msgid "Content Analysis"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/conditions/plugins/wordcount_plugin.py
|
||||
msgid "Word count (latest snapshot)"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/conditions/plugins/wordcount_plugin.py
|
||||
msgid "Word count is a simple measure of content length, calculated by splitting text on whitespace."
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/content_fetchers/requests.py
|
||||
msgid "Basic fast Plaintext/HTTP Client"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/blueprint/watchlist/templates/watch-overview.html changedetectionio/flask_app.py
|
||||
#: changedetectionio/realtime/socket_server.py
|
||||
msgid "Not yet"
|
||||
@@ -2467,26 +2248,10 @@ msgstr "Operasyon"
|
||||
msgid "Selector"
|
||||
msgstr "Seçici"
|
||||
|
||||
#: changedetectionio/forms.py
|
||||
msgid "CSS or xPath selector"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/forms.py
|
||||
msgid "value"
|
||||
msgstr "değer"
|
||||
|
||||
#: changedetectionio/conditions/form.py changedetectionio/forms.py
|
||||
msgid "Value"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/forms.py
|
||||
msgid "Web Page URL"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/forms.py
|
||||
msgid "Group Tag"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/forms.py
|
||||
msgid "Time Between Check"
|
||||
msgstr "Kontrol Arasındaki Süre"
|
||||
@@ -2575,8 +2340,7 @@ msgstr "Metin eşleşirken değişiklik tespitini engelle"
|
||||
msgid "Execute JavaScript before change detection"
|
||||
msgstr "Değişiklik tespitinden önce JavaScript'i çalıştır"
|
||||
|
||||
#: changedetectionio/blueprint/tags/form.py changedetectionio/blueprint/tags/templates/groups-overview.html
|
||||
#: changedetectionio/forms.py
|
||||
#: changedetectionio/blueprint/tags/templates/groups-overview.html changedetectionio/forms.py
|
||||
msgid "Save"
|
||||
msgstr "Kaydet"
|
||||
|
||||
@@ -2644,7 +2408,7 @@ msgstr "Geçersiz şablon sözdizimi: %(error)s"
|
||||
msgid "Invalid template syntax in \"%(header)s\" header: %(error)s"
|
||||
msgstr "\"%(header)s\" başlığında geçersiz şablon sözdizimi: %(error)s"
|
||||
|
||||
#: changedetectionio/blueprint/tags/form.py changedetectionio/forms.py
|
||||
#: changedetectionio/forms.py
|
||||
msgid "Name"
|
||||
msgstr "İsim"
|
||||
|
||||
@@ -2740,10 +2504,6 @@ msgstr "Metni Yoksay"
|
||||
msgid "Ignore whitespace"
|
||||
msgstr "Boşlukları yoksay"
|
||||
|
||||
#: changedetectionio/forms.py
|
||||
msgid "Screenshot: Minimum Change Percentage"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/forms.py changedetectionio/processors/image_ssim_diff/forms.py
|
||||
msgid "Must be between 0 and 100"
|
||||
msgstr "0 ile 100 arasında olmalıdır"
|
||||
@@ -2943,42 +2703,6 @@ msgstr "TEK bir ürüne sahip sayfalar için Yeniden Stoklama ve Fiyat tespiti"
|
||||
msgid "Detects if the product goes back to in-stock"
|
||||
msgstr "Ürünün tekrar stoka girip girmediğini tespit eder"
|
||||
|
||||
#: changedetectionio/processors/templates/extract.html
|
||||
msgid "Screenshot"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/processors/templates/extract.html
|
||||
msgid "This tool will extract text data from all of the watch history."
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/processors/templates/extract.html
|
||||
msgid "A <strong>RegEx</strong> is a pattern that identifies exactly which part inside of the text that you want to extract."
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/processors/templates/extract.html
|
||||
msgid "For example, to extract only the numbers from text"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/processors/templates/extract.html
|
||||
msgid "Raw text"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/processors/templates/extract.html
|
||||
msgid "RegEx to extract:"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/processors/templates/extract.html
|
||||
msgid "Be sure to test your RegEx here."
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/processors/templates/extract.html
|
||||
msgid "Each RegEx group bracket"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/processors/templates/extract.html
|
||||
msgid "will be in its own column, the first column value is always the date."
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/processors/text_json_diff/processor.py
|
||||
msgid "Webpage Text/HTML, JSON and PDF changes"
|
||||
msgstr "Web Sayfası Metin/HTML, JSON ve PDF değişiklikleri"
|
||||
@@ -3158,7 +2882,7 @@ msgstr "Önemli yapılandırma notları için lütfen buradaki bildirim hizmetle
|
||||
msgid "Use"
|
||||
msgstr "Şu"
|
||||
|
||||
#: changedetectionio/templates/_common_fields.html changedetectionio/templates/edit/include_subtract.html
|
||||
#: changedetectionio/templates/_common_fields.html
|
||||
msgid "Show advanced help and tips"
|
||||
msgstr "Gelişmiş yardım ve ipuçlarını göster"
|
||||
|
||||
@@ -3262,26 +2986,6 @@ msgstr "Tüm Jinja2 yerleşik filtrelerinin tam bir referansı için kullanıcı
|
||||
msgid "Format for all notifications"
|
||||
msgstr "Tüm bildirimler için format"
|
||||
|
||||
#: changedetectionio/templates/_common_fields.html
|
||||
msgid "Note"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/templates/_common_fields.html
|
||||
msgid "Discord does not render HTML — switch to"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/templates/_common_fields.html
|
||||
msgid "Plain Text"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/templates/_common_fields.html
|
||||
msgid "format to avoid"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/templates/_common_fields.html
|
||||
msgid "and other HTML entities appearing literally in your notifications."
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/templates/_helpers.html
|
||||
msgid "Entry"
|
||||
msgstr "Giriş"
|
||||
@@ -3400,18 +3104,10 @@ msgstr "Bu metin var olduğu için değişiklik tespiti yapılmayacaktır."
|
||||
msgid "Blocked text"
|
||||
msgstr "Engellenen metin"
|
||||
|
||||
#: changedetectionio/templates/base.html
|
||||
msgid "A new version is available"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/templates/base.html
|
||||
msgid "Search, or Use Alt+S Key"
|
||||
msgstr "Arayın veya Alt+S Tuşunu Kullanın"
|
||||
|
||||
#: changedetectionio/templates/base.html
|
||||
msgid "Share this link:"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/templates/base.html
|
||||
msgid "Real-time updates offline"
|
||||
msgstr "Gerçek zamanlı güncellemeler çevrimdışı"
|
||||
@@ -3590,6 +3286,10 @@ msgstr "Zamanlama duraklatıldı - devam etmek için tıklayın"
|
||||
msgid "Unmute notifications"
|
||||
msgstr "Bildirimlerin sesini aç"
|
||||
|
||||
#: changedetectionio/templates/menu.html
|
||||
msgid "Mute notifications"
|
||||
msgstr "Bildirimleri sessize al"
|
||||
|
||||
#: changedetectionio/templates/menu.html
|
||||
msgid "Notifications are muted - click to unmute"
|
||||
msgstr "Bildirimler sessize alındı - sesini açmak için tıklayın"
|
||||
|
||||
Binary file not shown.
@@ -6,7 +6,7 @@ msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: changedetection.io\n"
|
||||
"Report-Msgid-Bugs-To: https://github.com/dgtlmoon/changedetection.io\n"
|
||||
"POT-Creation-Date: 2026-04-15 03:04+0900\n"
|
||||
"POT-Creation-Date: 2026-04-11 04:15+0200\n"
|
||||
"PO-Revision-Date: 2026-02-19 12:30+0100\n"
|
||||
"Last-Translator: \n"
|
||||
"Language: uk\n"
|
||||
@@ -589,15 +589,15 @@ msgstr "Відображати вміст тегів посилань. За за
|
||||
msgid "Changing this could affect the content of your existing watches, possibly trigger alerts etc."
|
||||
msgstr "Зміна цього параметра може вплинути на вміст ваших завдань та викликати хибні спрацювання."
|
||||
|
||||
#: changedetectionio/blueprint/settings/templates/settings.html changedetectionio/templates/edit/include_subtract.html
|
||||
#: changedetectionio/blueprint/settings/templates/settings.html
|
||||
msgid "Remove HTML element(s) by CSS and XPath selectors before text conversion."
|
||||
msgstr "Видалити HTML-елемент(и) за допомогою CSS та XPath селекторів перед перетворенням у текст."
|
||||
|
||||
#: changedetectionio/blueprint/settings/templates/settings.html changedetectionio/templates/edit/include_subtract.html
|
||||
#: changedetectionio/blueprint/settings/templates/settings.html
|
||||
msgid "Don't paste HTML here, use only CSS and XPath selectors"
|
||||
msgstr "Не вставляйте сюди HTML, використовуйте лише CSS та XPath селектори"
|
||||
|
||||
#: changedetectionio/blueprint/settings/templates/settings.html changedetectionio/templates/edit/include_subtract.html
|
||||
#: changedetectionio/blueprint/settings/templates/settings.html
|
||||
msgid "Add multiple elements, CSS or XPath selectors per line to ignore multiple parts of the HTML."
|
||||
msgstr "Додайте кілька елементів, CSS або XPath селекторів (по одному на рядок), щоб ігнорувати частини HTML."
|
||||
|
||||
@@ -836,26 +836,6 @@ msgstr "Тег не знайдено"
|
||||
msgid "Updated"
|
||||
msgstr "Оновлено"
|
||||
|
||||
#: changedetectionio/blueprint/tags/form.py
|
||||
msgid "Activate for individual watches in this tag/group?"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/blueprint/tags/form.py
|
||||
msgid "Auto-apply to watches with URLs matching"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/blueprint/tags/form.py
|
||||
msgid "e.g. *://example.com/* or github.com/myorg"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/blueprint/tags/form.py changedetectionio/blueprint/tags/templates/edit-tag.html
|
||||
msgid "Tag colour"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/blueprint/tags/form.py
|
||||
msgid "Tag name"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/blueprint/tags/templates/edit-tag.html changedetectionio/blueprint/ui/templates/edit.html
|
||||
msgid "Filters & Triggers"
|
||||
msgstr "Фільтри та Тригери"
|
||||
@@ -870,6 +850,10 @@ msgstr ""
|
||||
msgid "Currently matching watches"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/blueprint/tags/templates/edit-tag.html
|
||||
msgid "Tag colour"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/blueprint/tags/templates/edit-tag.html
|
||||
msgid "Custom colour"
|
||||
msgstr ""
|
||||
@@ -955,11 +939,7 @@ msgstr "Ім'я Тегу / Мітки"
|
||||
msgid "No website organisational tags/groups configured"
|
||||
msgstr "Організаційні теги/групи сайтів не налаштовані"
|
||||
|
||||
#: changedetectionio/blueprint/tags/templates/groups-overview.html changedetectionio/templates/menu.html
|
||||
msgid "Mute notifications"
|
||||
msgstr "Вимкнути сповіщення"
|
||||
|
||||
#: changedetectionio/blueprint/tags/templates/groups-overview.html changedetectionio/blueprint/ui/edit.py
|
||||
#: changedetectionio/blueprint/tags/templates/groups-overview.html
|
||||
#: changedetectionio/blueprint/watchlist/templates/watch-overview.html
|
||||
msgid "Edit"
|
||||
msgstr "Редагувати"
|
||||
@@ -1164,14 +1144,6 @@ msgstr "Не вдалося завантажити процесор '{}', мож
|
||||
msgid "Could not load '{}' processor, processor plugin might be missing."
|
||||
msgstr "Не вдалося завантажити процесор '{}', можливо плагін відсутній."
|
||||
|
||||
#: changedetectionio/blueprint/ui/edit.py
|
||||
msgid "System settings default"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/blueprint/ui/edit.py
|
||||
msgid "Default"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/blueprint/ui/edit.py
|
||||
msgid "Updated watch - unpaused!"
|
||||
msgstr "Завдання оновлено — знято з паузи!"
|
||||
@@ -1184,10 +1156,6 @@ msgstr "Завдання оновлено."
|
||||
msgid "Preview unavailable - No fetch/check completed or triggers not reached"
|
||||
msgstr "Попередній перегляд недоступний — перевірка не завершена або тригери не спрацювали"
|
||||
|
||||
#: changedetectionio/blueprint/ui/preview.py
|
||||
msgid "Diff"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/blueprint/ui/templates/clear_all_history.html
|
||||
msgid "This will remove version history (snapshots) for ALL watches, but keep your list of URLs!"
|
||||
msgstr "Це видалить історію версій (знімки) для ВСІХ завдань, але збереже ваш список URL!"
|
||||
@@ -1297,17 +1265,14 @@ msgid "Jump"
|
||||
msgstr "Перейти"
|
||||
|
||||
#: changedetectionio/blueprint/ui/templates/diff.html changedetectionio/blueprint/ui/templates/preview.html
|
||||
#: changedetectionio/processors/templates/extract.html
|
||||
msgid "Error Text"
|
||||
msgstr "Текст помилки"
|
||||
|
||||
#: changedetectionio/blueprint/ui/templates/diff.html changedetectionio/blueprint/ui/templates/preview.html
|
||||
#: changedetectionio/processors/templates/extract.html
|
||||
msgid "Error Screenshot"
|
||||
msgstr "Скріншот помилки"
|
||||
|
||||
#: changedetectionio/blueprint/ui/templates/diff.html changedetectionio/blueprint/ui/templates/preview.html
|
||||
#: changedetectionio/processors/templates/extract.html
|
||||
msgid "Text"
|
||||
msgstr "Текст"
|
||||
|
||||
@@ -1315,8 +1280,7 @@ msgstr "Текст"
|
||||
msgid "Current screenshot"
|
||||
msgstr "Поточний скріншот"
|
||||
|
||||
#: changedetectionio/blueprint/ui/templates/diff.html changedetectionio/processors/extract.py
|
||||
#: changedetectionio/processors/templates/extract.html
|
||||
#: changedetectionio/blueprint/ui/templates/diff.html
|
||||
msgid "Extract Data"
|
||||
msgstr "Вилучити дані"
|
||||
|
||||
@@ -1914,26 +1878,6 @@ msgstr "Немає налаштованих завдань для відстеж
|
||||
msgid "import a list"
|
||||
msgstr "імпортуйте список"
|
||||
|
||||
#: changedetectionio/blueprint/watchlist/templates/watch-overview.html
|
||||
msgid "Pause checks"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/blueprint/watchlist/templates/watch-overview.html
|
||||
msgid "UnPause checks"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/blueprint/watchlist/templates/watch-overview.html
|
||||
msgid "Mute notification"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/blueprint/watchlist/templates/watch-overview.html
|
||||
msgid "UnMute notification"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/blueprint/watchlist/templates/watch-overview.html
|
||||
msgid "Create a link to share watch config with others"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/blueprint/watchlist/templates/watch-overview.html
|
||||
msgid "Detecting restock and price"
|
||||
msgstr "Визначення наявності та ціни"
|
||||
@@ -1963,7 +1907,6 @@ msgid "Queued"
|
||||
msgstr "В черзі"
|
||||
|
||||
#: changedetectionio/blueprint/watchlist/templates/watch-overview.html
|
||||
#: changedetectionio/processors/text_json_diff/difference.py
|
||||
msgid "History"
|
||||
msgstr "Історія"
|
||||
|
||||
@@ -1997,168 +1940,6 @@ msgstr "Перевірити всі"
|
||||
msgid "in '%(title)s'"
|
||||
msgstr "в '%(title)s'"
|
||||
|
||||
#: changedetectionio/conditions/__init__.py
|
||||
msgid "Choose one - Operator"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/conditions/__init__.py
|
||||
msgid "Greater Than"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/conditions/__init__.py
|
||||
msgid "Less Than"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/conditions/__init__.py
|
||||
msgid "Greater Than or Equal To"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/conditions/__init__.py
|
||||
msgid "Less Than or Equal To"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/conditions/__init__.py
|
||||
msgid "Equals"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/conditions/__init__.py
|
||||
msgid "Not Equals"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/conditions/__init__.py
|
||||
msgid "Contains"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/conditions/__init__.py
|
||||
msgid "Choose one - Field"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/conditions/default_plugin.py
|
||||
msgid "Does NOT Contain"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/conditions/default_plugin.py
|
||||
msgid "Text Starts With"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/conditions/default_plugin.py
|
||||
msgid "Text Ends With"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/conditions/default_plugin.py
|
||||
msgid "Length minimum"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/conditions/default_plugin.py
|
||||
msgid "Length maximum"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/conditions/default_plugin.py
|
||||
msgid "Text Matches Regex"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/conditions/default_plugin.py
|
||||
msgid "Text Does NOT Match Regex"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/conditions/default_plugin.py
|
||||
msgid "Extracted number after 'Filters & Triggers'"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/conditions/default_plugin.py
|
||||
msgid "Page text after 'Filters & Triggers'"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/conditions/form.py
|
||||
msgid "Field"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/conditions/form.py
|
||||
msgid "Operator"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/conditions/form.py
|
||||
msgid "A value"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/conditions/form.py
|
||||
msgid "Operator is required."
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/conditions/form.py
|
||||
msgid "Field is required."
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/conditions/form.py
|
||||
msgid "Value is required."
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/conditions/plugins/levenshtein_plugin.py
|
||||
msgid "Levenshtein - Text similarity ratio"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/conditions/plugins/levenshtein_plugin.py
|
||||
msgid "Levenshtein - Text change distance"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/conditions/plugins/levenshtein_plugin.py
|
||||
msgid "Not enough history to calculate Levenshtein metrics"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/conditions/plugins/levenshtein_plugin.py
|
||||
msgid "Snapshot too large for edit statistics, skipping."
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/conditions/plugins/levenshtein_plugin.py
|
||||
msgid "Unable to calculate Levenshtein metrics"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/conditions/plugins/levenshtein_plugin.py
|
||||
msgid "Levenshtein Text Similarity Details"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/conditions/plugins/levenshtein_plugin.py
|
||||
msgid "Raw distance (edits needed)"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/conditions/plugins/levenshtein_plugin.py
|
||||
msgid "Similarity ratio"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/conditions/plugins/levenshtein_plugin.py
|
||||
msgid "Percent similar"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/conditions/plugins/levenshtein_plugin.py
|
||||
msgid ""
|
||||
"Levenshtein metrics compare the last two snapshots, measuring how many character edits are needed to transform one "
|
||||
"into the other."
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/conditions/plugins/levenshtein_plugin.py
|
||||
msgid "Error calculating Levenshtein metrics"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/conditions/plugins/wordcount_plugin.py
|
||||
msgid "Word count of content"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/conditions/plugins/wordcount_plugin.py
|
||||
msgid "Content Analysis"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/conditions/plugins/wordcount_plugin.py
|
||||
msgid "Word count (latest snapshot)"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/conditions/plugins/wordcount_plugin.py
|
||||
msgid "Word count is a simple measure of content length, calculated by splitting text on whitespace."
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/content_fetchers/requests.py
|
||||
msgid "Basic fast Plaintext/HTTP Client"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/blueprint/watchlist/templates/watch-overview.html changedetectionio/flask_app.py
|
||||
#: changedetectionio/realtime/socket_server.py
|
||||
msgid "Not yet"
|
||||
@@ -2441,26 +2222,10 @@ msgstr "Операція"
|
||||
msgid "Selector"
|
||||
msgstr "Селектор"
|
||||
|
||||
#: changedetectionio/forms.py
|
||||
msgid "CSS or xPath selector"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/forms.py
|
||||
msgid "value"
|
||||
msgstr "значення"
|
||||
|
||||
#: changedetectionio/conditions/form.py changedetectionio/forms.py
|
||||
msgid "Value"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/forms.py
|
||||
msgid "Web Page URL"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/forms.py
|
||||
msgid "Group Tag"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/forms.py
|
||||
msgid "Time Between Check"
|
||||
msgstr "Час між перевірками"
|
||||
@@ -2549,8 +2314,7 @@ msgstr "Блокувати виявлення змін, поки текст зб
|
||||
msgid "Execute JavaScript before change detection"
|
||||
msgstr "Виконати JavaScript перед виявленням змін"
|
||||
|
||||
#: changedetectionio/blueprint/tags/form.py changedetectionio/blueprint/tags/templates/groups-overview.html
|
||||
#: changedetectionio/forms.py
|
||||
#: changedetectionio/blueprint/tags/templates/groups-overview.html changedetectionio/forms.py
|
||||
msgid "Save"
|
||||
msgstr "Зберегти"
|
||||
|
||||
@@ -2618,7 +2382,7 @@ msgstr "Невірний синтаксис шаблону: %(error)s"
|
||||
msgid "Invalid template syntax in \"%(header)s\" header: %(error)s"
|
||||
msgstr "Невірний синтаксис шаблону в заголовку \"%(header)s\": %(error)s"
|
||||
|
||||
#: changedetectionio/blueprint/tags/form.py changedetectionio/forms.py
|
||||
#: changedetectionio/forms.py
|
||||
msgid "Name"
|
||||
msgstr "Ім'я"
|
||||
|
||||
@@ -2714,10 +2478,6 @@ msgstr "Ігнорувати текст"
|
||||
msgid "Ignore whitespace"
|
||||
msgstr "Ігнорувати пробіли"
|
||||
|
||||
#: changedetectionio/forms.py
|
||||
msgid "Screenshot: Minimum Change Percentage"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/forms.py changedetectionio/processors/image_ssim_diff/forms.py
|
||||
msgid "Must be between 0 and 100"
|
||||
msgstr "Має бути між 0 та 100"
|
||||
@@ -2917,42 +2677,6 @@ msgstr "Виявлення поповнення та ціни для сторі
|
||||
msgid "Detects if the product goes back to in-stock"
|
||||
msgstr "Визначає, чи повернувся товар у наявність"
|
||||
|
||||
#: changedetectionio/processors/templates/extract.html
|
||||
msgid "Screenshot"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/processors/templates/extract.html
|
||||
msgid "This tool will extract text data from all of the watch history."
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/processors/templates/extract.html
|
||||
msgid "A <strong>RegEx</strong> is a pattern that identifies exactly which part inside of the text that you want to extract."
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/processors/templates/extract.html
|
||||
msgid "For example, to extract only the numbers from text"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/processors/templates/extract.html
|
||||
msgid "Raw text"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/processors/templates/extract.html
|
||||
msgid "RegEx to extract:"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/processors/templates/extract.html
|
||||
msgid "Be sure to test your RegEx here."
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/processors/templates/extract.html
|
||||
msgid "Each RegEx group bracket"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/processors/templates/extract.html
|
||||
msgid "will be in its own column, the first column value is always the date."
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/processors/text_json_diff/processor.py
|
||||
msgid "Webpage Text/HTML, JSON and PDF changes"
|
||||
msgstr "Зміни тексту веб-сторінки/HTML, JSON та PDF"
|
||||
@@ -3132,7 +2856,7 @@ msgstr "Будь ласка, прочитайте вікі по сервісах
|
||||
msgid "Use"
|
||||
msgstr "Використовуйте"
|
||||
|
||||
#: changedetectionio/templates/_common_fields.html changedetectionio/templates/edit/include_subtract.html
|
||||
#: changedetectionio/templates/_common_fields.html
|
||||
msgid "Show advanced help and tips"
|
||||
msgstr "Показати розширену довідку та поради"
|
||||
|
||||
@@ -3236,26 +2960,6 @@ msgstr "Для повного довідника по всіх вбудован
|
||||
msgid "Format for all notifications"
|
||||
msgstr "Формат для всіх сповіщень"
|
||||
|
||||
#: changedetectionio/templates/_common_fields.html
|
||||
msgid "Note"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/templates/_common_fields.html
|
||||
msgid "Discord does not render HTML — switch to"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/templates/_common_fields.html
|
||||
msgid "Plain Text"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/templates/_common_fields.html
|
||||
msgid "format to avoid"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/templates/_common_fields.html
|
||||
msgid "and other HTML entities appearing literally in your notifications."
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/templates/_helpers.html
|
||||
msgid "Entry"
|
||||
msgstr "Запис"
|
||||
@@ -3372,18 +3076,10 @@ msgstr "Виявлення змін не відбудеться, оскільк
|
||||
msgid "Blocked text"
|
||||
msgstr "Блокуючий текст"
|
||||
|
||||
#: changedetectionio/templates/base.html
|
||||
msgid "A new version is available"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/templates/base.html
|
||||
msgid "Search, or Use Alt+S Key"
|
||||
msgstr "Пошук, або використовуйте Alt+S"
|
||||
|
||||
#: changedetectionio/templates/base.html
|
||||
msgid "Share this link:"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/templates/base.html
|
||||
msgid "Real-time updates offline"
|
||||
msgstr "Оновлення в реальному часі вимкнено"
|
||||
@@ -3558,6 +3254,10 @@ msgstr "Планування на паузі - натисніть для від
|
||||
msgid "Unmute notifications"
|
||||
msgstr "Увімкнути сповіщення"
|
||||
|
||||
#: changedetectionio/templates/menu.html
|
||||
msgid "Mute notifications"
|
||||
msgstr "Вимкнути сповіщення"
|
||||
|
||||
#: changedetectionio/templates/menu.html
|
||||
msgid "Notifications are muted - click to unmute"
|
||||
msgstr "Сповіщення вимкнено - натисніть для увімкнення"
|
||||
|
||||
Binary file not shown.
@@ -7,7 +7,7 @@ msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: PROJECT VERSION\n"
|
||||
"Report-Msgid-Bugs-To: EMAIL@ADDRESS\n"
|
||||
"POT-Creation-Date: 2026-04-15 03:04+0900\n"
|
||||
"POT-Creation-Date: 2026-04-11 04:15+0200\n"
|
||||
"PO-Revision-Date: 2026-01-18 21:31+0800\n"
|
||||
"Last-Translator: 吾爱分享 <admin@wuaishare.cn>\n"
|
||||
"Language: zh\n"
|
||||
@@ -590,15 +590,15 @@ msgstr "渲染 a 标签内容,默认关闭,开启后链接会呈现为"
|
||||
msgid "Changing this could affect the content of your existing watches, possibly trigger alerts etc."
|
||||
msgstr "更改此项可能影响现有监控项内容,可能触发警报等。"
|
||||
|
||||
#: changedetectionio/blueprint/settings/templates/settings.html changedetectionio/templates/edit/include_subtract.html
|
||||
#: changedetectionio/blueprint/settings/templates/settings.html
|
||||
msgid "Remove HTML element(s) by CSS and XPath selectors before text conversion."
|
||||
msgstr "在文本转换前通过 CSS 和 XPath 选择器移除 HTML 元素。"
|
||||
|
||||
#: changedetectionio/blueprint/settings/templates/settings.html changedetectionio/templates/edit/include_subtract.html
|
||||
#: changedetectionio/blueprint/settings/templates/settings.html
|
||||
msgid "Don't paste HTML here, use only CSS and XPath selectors"
|
||||
msgstr "不要在此粘贴 HTML,仅使用 CSS 和 XPath 选择器"
|
||||
|
||||
#: changedetectionio/blueprint/settings/templates/settings.html changedetectionio/templates/edit/include_subtract.html
|
||||
#: changedetectionio/blueprint/settings/templates/settings.html
|
||||
msgid "Add multiple elements, CSS or XPath selectors per line to ignore multiple parts of the HTML."
|
||||
msgstr "每行添加多个元素、CSS 或 XPath 选择器,用于忽略 HTML 的多个部分。"
|
||||
|
||||
@@ -829,26 +829,6 @@ msgstr "未找到标签"
|
||||
msgid "Updated"
|
||||
msgstr "已更新"
|
||||
|
||||
#: changedetectionio/blueprint/tags/form.py
|
||||
msgid "Activate for individual watches in this tag/group?"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/blueprint/tags/form.py
|
||||
msgid "Auto-apply to watches with URLs matching"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/blueprint/tags/form.py
|
||||
msgid "e.g. *://example.com/* or github.com/myorg"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/blueprint/tags/form.py changedetectionio/blueprint/tags/templates/edit-tag.html
|
||||
msgid "Tag colour"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/blueprint/tags/form.py
|
||||
msgid "Tag name"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/blueprint/tags/templates/edit-tag.html changedetectionio/blueprint/ui/templates/edit.html
|
||||
msgid "Filters & Triggers"
|
||||
msgstr "过滤器与触发器"
|
||||
@@ -863,6 +843,10 @@ msgstr ""
|
||||
msgid "Currently matching watches"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/blueprint/tags/templates/edit-tag.html
|
||||
msgid "Tag colour"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/blueprint/tags/templates/edit-tag.html
|
||||
msgid "Custom colour"
|
||||
msgstr ""
|
||||
@@ -948,11 +932,7 @@ msgstr "标签/名称"
|
||||
msgid "No website organisational tags/groups configured"
|
||||
msgstr "未配置分组/标签"
|
||||
|
||||
#: changedetectionio/blueprint/tags/templates/groups-overview.html changedetectionio/templates/menu.html
|
||||
msgid "Mute notifications"
|
||||
msgstr "静音通知"
|
||||
|
||||
#: changedetectionio/blueprint/tags/templates/groups-overview.html changedetectionio/blueprint/ui/edit.py
|
||||
#: changedetectionio/blueprint/tags/templates/groups-overview.html
|
||||
#: changedetectionio/blueprint/watchlist/templates/watch-overview.html
|
||||
msgid "Edit"
|
||||
msgstr "编辑"
|
||||
@@ -1155,14 +1135,6 @@ msgstr ""
|
||||
msgid "Could not load '{}' processor, processor plugin might be missing."
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/blueprint/ui/edit.py
|
||||
msgid "System settings default"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/blueprint/ui/edit.py
|
||||
msgid "Default"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/blueprint/ui/edit.py
|
||||
msgid "Updated watch - unpaused!"
|
||||
msgstr "监控项已更新并取消暂停!"
|
||||
@@ -1175,10 +1147,6 @@ msgstr "监控项已更新。"
|
||||
msgid "Preview unavailable - No fetch/check completed or triggers not reached"
|
||||
msgstr "无法预览 - 尚未完成抓取/检查或未满足触发条件"
|
||||
|
||||
#: changedetectionio/blueprint/ui/preview.py
|
||||
msgid "Diff"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/blueprint/ui/templates/clear_all_history.html
|
||||
msgid "This will remove version history (snapshots) for ALL watches, but keep your list of URLs!"
|
||||
msgstr "这将删除所有监控项的版本历史(快照),但会保留 URL 列表!"
|
||||
@@ -1288,17 +1256,14 @@ msgid "Jump"
|
||||
msgstr "跳转"
|
||||
|
||||
#: changedetectionio/blueprint/ui/templates/diff.html changedetectionio/blueprint/ui/templates/preview.html
|
||||
#: changedetectionio/processors/templates/extract.html
|
||||
msgid "Error Text"
|
||||
msgstr "错误文本"
|
||||
|
||||
#: changedetectionio/blueprint/ui/templates/diff.html changedetectionio/blueprint/ui/templates/preview.html
|
||||
#: changedetectionio/processors/templates/extract.html
|
||||
msgid "Error Screenshot"
|
||||
msgstr "错误截图"
|
||||
|
||||
#: changedetectionio/blueprint/ui/templates/diff.html changedetectionio/blueprint/ui/templates/preview.html
|
||||
#: changedetectionio/processors/templates/extract.html
|
||||
msgid "Text"
|
||||
msgstr "文本"
|
||||
|
||||
@@ -1306,8 +1271,7 @@ msgstr "文本"
|
||||
msgid "Current screenshot"
|
||||
msgstr "当前截图"
|
||||
|
||||
#: changedetectionio/blueprint/ui/templates/diff.html changedetectionio/processors/extract.py
|
||||
#: changedetectionio/processors/templates/extract.html
|
||||
#: changedetectionio/blueprint/ui/templates/diff.html
|
||||
msgid "Extract Data"
|
||||
msgstr "提取数据"
|
||||
|
||||
@@ -1897,26 +1861,6 @@ msgstr "尚未配置网站监控项,请在上方输入 URL 或"
|
||||
msgid "import a list"
|
||||
msgstr "导入列表"
|
||||
|
||||
#: changedetectionio/blueprint/watchlist/templates/watch-overview.html
|
||||
msgid "Pause checks"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/blueprint/watchlist/templates/watch-overview.html
|
||||
msgid "UnPause checks"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/blueprint/watchlist/templates/watch-overview.html
|
||||
msgid "Mute notification"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/blueprint/watchlist/templates/watch-overview.html
|
||||
msgid "UnMute notification"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/blueprint/watchlist/templates/watch-overview.html
|
||||
msgid "Create a link to share watch config with others"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/blueprint/watchlist/templates/watch-overview.html
|
||||
msgid "Detecting restock and price"
|
||||
msgstr "检测补货与价格"
|
||||
@@ -1946,7 +1890,6 @@ msgid "Queued"
|
||||
msgstr "队列中"
|
||||
|
||||
#: changedetectionio/blueprint/watchlist/templates/watch-overview.html
|
||||
#: changedetectionio/processors/text_json_diff/difference.py
|
||||
msgid "History"
|
||||
msgstr "历史"
|
||||
|
||||
@@ -1980,168 +1923,6 @@ msgstr "重新检查全部"
|
||||
msgid "in '%(title)s'"
|
||||
msgstr "(“%(title)s”中)"
|
||||
|
||||
#: changedetectionio/conditions/__init__.py
|
||||
msgid "Choose one - Operator"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/conditions/__init__.py
|
||||
msgid "Greater Than"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/conditions/__init__.py
|
||||
msgid "Less Than"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/conditions/__init__.py
|
||||
msgid "Greater Than or Equal To"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/conditions/__init__.py
|
||||
msgid "Less Than or Equal To"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/conditions/__init__.py
|
||||
msgid "Equals"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/conditions/__init__.py
|
||||
msgid "Not Equals"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/conditions/__init__.py
|
||||
msgid "Contains"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/conditions/__init__.py
|
||||
msgid "Choose one - Field"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/conditions/default_plugin.py
|
||||
msgid "Does NOT Contain"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/conditions/default_plugin.py
|
||||
msgid "Text Starts With"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/conditions/default_plugin.py
|
||||
msgid "Text Ends With"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/conditions/default_plugin.py
|
||||
msgid "Length minimum"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/conditions/default_plugin.py
|
||||
msgid "Length maximum"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/conditions/default_plugin.py
|
||||
msgid "Text Matches Regex"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/conditions/default_plugin.py
|
||||
msgid "Text Does NOT Match Regex"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/conditions/default_plugin.py
|
||||
msgid "Extracted number after 'Filters & Triggers'"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/conditions/default_plugin.py
|
||||
msgid "Page text after 'Filters & Triggers'"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/conditions/form.py
|
||||
msgid "Field"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/conditions/form.py
|
||||
msgid "Operator"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/conditions/form.py
|
||||
msgid "A value"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/conditions/form.py
|
||||
msgid "Operator is required."
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/conditions/form.py
|
||||
msgid "Field is required."
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/conditions/form.py
|
||||
msgid "Value is required."
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/conditions/plugins/levenshtein_plugin.py
|
||||
msgid "Levenshtein - Text similarity ratio"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/conditions/plugins/levenshtein_plugin.py
|
||||
msgid "Levenshtein - Text change distance"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/conditions/plugins/levenshtein_plugin.py
|
||||
msgid "Not enough history to calculate Levenshtein metrics"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/conditions/plugins/levenshtein_plugin.py
|
||||
msgid "Snapshot too large for edit statistics, skipping."
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/conditions/plugins/levenshtein_plugin.py
|
||||
msgid "Unable to calculate Levenshtein metrics"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/conditions/plugins/levenshtein_plugin.py
|
||||
msgid "Levenshtein Text Similarity Details"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/conditions/plugins/levenshtein_plugin.py
|
||||
msgid "Raw distance (edits needed)"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/conditions/plugins/levenshtein_plugin.py
|
||||
msgid "Similarity ratio"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/conditions/plugins/levenshtein_plugin.py
|
||||
msgid "Percent similar"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/conditions/plugins/levenshtein_plugin.py
|
||||
msgid ""
|
||||
"Levenshtein metrics compare the last two snapshots, measuring how many character edits are needed to transform one "
|
||||
"into the other."
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/conditions/plugins/levenshtein_plugin.py
|
||||
msgid "Error calculating Levenshtein metrics"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/conditions/plugins/wordcount_plugin.py
|
||||
msgid "Word count of content"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/conditions/plugins/wordcount_plugin.py
|
||||
msgid "Content Analysis"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/conditions/plugins/wordcount_plugin.py
|
||||
msgid "Word count (latest snapshot)"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/conditions/plugins/wordcount_plugin.py
|
||||
msgid "Word count is a simple measure of content length, calculated by splitting text on whitespace."
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/content_fetchers/requests.py
|
||||
msgid "Basic fast Plaintext/HTTP Client"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/blueprint/watchlist/templates/watch-overview.html changedetectionio/flask_app.py
|
||||
#: changedetectionio/realtime/socket_server.py
|
||||
msgid "Not yet"
|
||||
@@ -2424,26 +2205,10 @@ msgstr "操作"
|
||||
msgid "Selector"
|
||||
msgstr "选择器"
|
||||
|
||||
#: changedetectionio/forms.py
|
||||
msgid "CSS or xPath selector"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/forms.py
|
||||
msgid "value"
|
||||
msgstr "值"
|
||||
|
||||
#: changedetectionio/conditions/form.py changedetectionio/forms.py
|
||||
msgid "Value"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/forms.py
|
||||
msgid "Web Page URL"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/forms.py
|
||||
msgid "Group Tag"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/forms.py
|
||||
msgid "Time Between Check"
|
||||
msgstr "检查间隔"
|
||||
@@ -2532,8 +2297,7 @@ msgstr "文本匹配时阻止变更检测"
|
||||
msgid "Execute JavaScript before change detection"
|
||||
msgstr "在变更检测前执行 JavaScript"
|
||||
|
||||
#: changedetectionio/blueprint/tags/form.py changedetectionio/blueprint/tags/templates/groups-overview.html
|
||||
#: changedetectionio/forms.py
|
||||
#: changedetectionio/blueprint/tags/templates/groups-overview.html changedetectionio/forms.py
|
||||
msgid "Save"
|
||||
msgstr "保存"
|
||||
|
||||
@@ -2601,7 +2365,7 @@ msgstr "模板语法无效:%(error)s"
|
||||
msgid "Invalid template syntax in \"%(header)s\" header: %(error)s"
|
||||
msgstr "“%(header)s”请求头中的模板语法无效:%(error)s"
|
||||
|
||||
#: changedetectionio/blueprint/tags/form.py changedetectionio/forms.py
|
||||
#: changedetectionio/forms.py
|
||||
msgid "Name"
|
||||
msgstr "名称"
|
||||
|
||||
@@ -2697,10 +2461,6 @@ msgstr "忽略文本"
|
||||
msgid "Ignore whitespace"
|
||||
msgstr "忽略空白"
|
||||
|
||||
#: changedetectionio/forms.py
|
||||
msgid "Screenshot: Minimum Change Percentage"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/forms.py changedetectionio/processors/image_ssim_diff/forms.py
|
||||
msgid "Must be between 0 and 100"
|
||||
msgstr "必须介于 0 到 100 之间"
|
||||
@@ -2900,42 +2660,6 @@ msgstr "适用于单一商品页面的补货与价格检测"
|
||||
msgid "Detects if the product goes back to in-stock"
|
||||
msgstr "检测商品是否恢复有库存"
|
||||
|
||||
#: changedetectionio/processors/templates/extract.html
|
||||
msgid "Screenshot"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/processors/templates/extract.html
|
||||
msgid "This tool will extract text data from all of the watch history."
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/processors/templates/extract.html
|
||||
msgid "A <strong>RegEx</strong> is a pattern that identifies exactly which part inside of the text that you want to extract."
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/processors/templates/extract.html
|
||||
msgid "For example, to extract only the numbers from text"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/processors/templates/extract.html
|
||||
msgid "Raw text"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/processors/templates/extract.html
|
||||
msgid "RegEx to extract:"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/processors/templates/extract.html
|
||||
msgid "Be sure to test your RegEx here."
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/processors/templates/extract.html
|
||||
msgid "Each RegEx group bracket"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/processors/templates/extract.html
|
||||
msgid "will be in its own column, the first column value is always the date."
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/processors/text_json_diff/processor.py
|
||||
msgid "Webpage Text/HTML, JSON and PDF changes"
|
||||
msgstr "网页文本/HTML、JSON 和 PDF 变更"
|
||||
@@ -3115,7 +2839,7 @@ msgstr "请阅读通知服务 Wiki 以了解重要配置说明"
|
||||
msgid "Use"
|
||||
msgstr "使用"
|
||||
|
||||
#: changedetectionio/templates/_common_fields.html changedetectionio/templates/edit/include_subtract.html
|
||||
#: changedetectionio/templates/_common_fields.html
|
||||
msgid "Show advanced help and tips"
|
||||
msgstr "显示高级帮助和提示"
|
||||
|
||||
@@ -3219,26 +2943,6 @@ msgstr "关于 Jinja2 内置过滤器的完整参考,请见"
|
||||
msgid "Format for all notifications"
|
||||
msgstr "所有通知的格式"
|
||||
|
||||
#: changedetectionio/templates/_common_fields.html
|
||||
msgid "Note"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/templates/_common_fields.html
|
||||
msgid "Discord does not render HTML — switch to"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/templates/_common_fields.html
|
||||
msgid "Plain Text"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/templates/_common_fields.html
|
||||
msgid "format to avoid"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/templates/_common_fields.html
|
||||
msgid "and other HTML entities appearing literally in your notifications."
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/templates/_helpers.html
|
||||
msgid "Entry"
|
||||
msgstr "条目"
|
||||
@@ -3355,18 +3059,10 @@ msgstr "此文本存在时将不会进行变更检测。"
|
||||
msgid "Blocked text"
|
||||
msgstr "阻止文本"
|
||||
|
||||
#: changedetectionio/templates/base.html
|
||||
msgid "A new version is available"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/templates/base.html
|
||||
msgid "Search, or Use Alt+S Key"
|
||||
msgstr "搜索,或使用 Alt+S 快捷键"
|
||||
|
||||
#: changedetectionio/templates/base.html
|
||||
msgid "Share this link:"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/templates/base.html
|
||||
msgid "Real-time updates offline"
|
||||
msgstr "实时更新离线"
|
||||
@@ -3537,6 +3233,10 @@ msgstr "调度已暂停 - 点击恢复"
|
||||
msgid "Unmute notifications"
|
||||
msgstr "取消静音通知"
|
||||
|
||||
#: changedetectionio/templates/menu.html
|
||||
msgid "Mute notifications"
|
||||
msgstr "静音通知"
|
||||
|
||||
#: changedetectionio/templates/menu.html
|
||||
msgid "Notifications are muted - click to unmute"
|
||||
msgstr "通知已静音 - 点击取消静音"
|
||||
|
||||
Binary file not shown.
@@ -7,7 +7,7 @@ msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: PROJECT VERSION\n"
|
||||
"Report-Msgid-Bugs-To: EMAIL@ADDRESS\n"
|
||||
"POT-Creation-Date: 2026-04-15 03:04+0900\n"
|
||||
"POT-Creation-Date: 2026-04-11 04:15+0200\n"
|
||||
"PO-Revision-Date: 2026-01-15 12:00+0800\n"
|
||||
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
|
||||
"Language: zh_Hant_TW\n"
|
||||
@@ -590,15 +590,15 @@ msgstr ""
|
||||
msgid "Changing this could affect the content of your existing watches, possibly trigger alerts etc."
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/blueprint/settings/templates/settings.html changedetectionio/templates/edit/include_subtract.html
|
||||
#: changedetectionio/blueprint/settings/templates/settings.html
|
||||
msgid "Remove HTML element(s) by CSS and XPath selectors before text conversion."
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/blueprint/settings/templates/settings.html changedetectionio/templates/edit/include_subtract.html
|
||||
#: changedetectionio/blueprint/settings/templates/settings.html
|
||||
msgid "Don't paste HTML here, use only CSS and XPath selectors"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/blueprint/settings/templates/settings.html changedetectionio/templates/edit/include_subtract.html
|
||||
#: changedetectionio/blueprint/settings/templates/settings.html
|
||||
msgid "Add multiple elements, CSS or XPath selectors per line to ignore multiple parts of the HTML."
|
||||
msgstr ""
|
||||
|
||||
@@ -829,26 +829,6 @@ msgstr "找不到標籤"
|
||||
msgid "Updated"
|
||||
msgstr "已更新"
|
||||
|
||||
#: changedetectionio/blueprint/tags/form.py
|
||||
msgid "Activate for individual watches in this tag/group?"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/blueprint/tags/form.py
|
||||
msgid "Auto-apply to watches with URLs matching"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/blueprint/tags/form.py
|
||||
msgid "e.g. *://example.com/* or github.com/myorg"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/blueprint/tags/form.py changedetectionio/blueprint/tags/templates/edit-tag.html
|
||||
msgid "Tag colour"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/blueprint/tags/form.py
|
||||
msgid "Tag name"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/blueprint/tags/templates/edit-tag.html changedetectionio/blueprint/ui/templates/edit.html
|
||||
msgid "Filters & Triggers"
|
||||
msgstr "過濾器與觸發器"
|
||||
@@ -863,6 +843,10 @@ msgstr ""
|
||||
msgid "Currently matching watches"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/blueprint/tags/templates/edit-tag.html
|
||||
msgid "Tag colour"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/blueprint/tags/templates/edit-tag.html
|
||||
msgid "Custom colour"
|
||||
msgstr ""
|
||||
@@ -948,11 +932,7 @@ msgstr "標籤 / 名稱"
|
||||
msgid "No website organisational tags/groups configured"
|
||||
msgstr "未設定群組/標籤"
|
||||
|
||||
#: changedetectionio/blueprint/tags/templates/groups-overview.html changedetectionio/templates/menu.html
|
||||
msgid "Mute notifications"
|
||||
msgstr "靜音通知"
|
||||
|
||||
#: changedetectionio/blueprint/tags/templates/groups-overview.html changedetectionio/blueprint/ui/edit.py
|
||||
#: changedetectionio/blueprint/tags/templates/groups-overview.html
|
||||
#: changedetectionio/blueprint/watchlist/templates/watch-overview.html
|
||||
msgid "Edit"
|
||||
msgstr "編輯"
|
||||
@@ -1155,14 +1135,6 @@ msgstr ""
|
||||
msgid "Could not load '{}' processor, processor plugin might be missing."
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/blueprint/ui/edit.py
|
||||
msgid "System settings default"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/blueprint/ui/edit.py
|
||||
msgid "Default"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/blueprint/ui/edit.py
|
||||
msgid "Updated watch - unpaused!"
|
||||
msgstr "已更新監測任務 - 已取消暫停!"
|
||||
@@ -1175,10 +1147,6 @@ msgstr "已更新監測任務。"
|
||||
msgid "Preview unavailable - No fetch/check completed or triggers not reached"
|
||||
msgstr "預覽無法使用 - 未完成抓取 / 檢查或未觸發"
|
||||
|
||||
#: changedetectionio/blueprint/ui/preview.py
|
||||
msgid "Diff"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/blueprint/ui/templates/clear_all_history.html
|
||||
msgid "This will remove version history (snapshots) for ALL watches, but keep your list of URLs!"
|
||||
msgstr "這將移除「所有」監測任務的版本歷史記錄(快照),但保留您的 URL 列表!"
|
||||
@@ -1288,17 +1256,14 @@ msgid "Jump"
|
||||
msgstr "跳轉"
|
||||
|
||||
#: changedetectionio/blueprint/ui/templates/diff.html changedetectionio/blueprint/ui/templates/preview.html
|
||||
#: changedetectionio/processors/templates/extract.html
|
||||
msgid "Error Text"
|
||||
msgstr "錯誤文字"
|
||||
|
||||
#: changedetectionio/blueprint/ui/templates/diff.html changedetectionio/blueprint/ui/templates/preview.html
|
||||
#: changedetectionio/processors/templates/extract.html
|
||||
msgid "Error Screenshot"
|
||||
msgstr "錯誤截圖"
|
||||
|
||||
#: changedetectionio/blueprint/ui/templates/diff.html changedetectionio/blueprint/ui/templates/preview.html
|
||||
#: changedetectionio/processors/templates/extract.html
|
||||
msgid "Text"
|
||||
msgstr "文字"
|
||||
|
||||
@@ -1306,8 +1271,7 @@ msgstr "文字"
|
||||
msgid "Current screenshot"
|
||||
msgstr "目前截圖"
|
||||
|
||||
#: changedetectionio/blueprint/ui/templates/diff.html changedetectionio/processors/extract.py
|
||||
#: changedetectionio/processors/templates/extract.html
|
||||
#: changedetectionio/blueprint/ui/templates/diff.html
|
||||
msgid "Extract Data"
|
||||
msgstr "提取資料"
|
||||
|
||||
@@ -1897,26 +1861,6 @@ msgstr "未設定網站監測任務,請在上方欄位新增 URL,或"
|
||||
msgid "import a list"
|
||||
msgstr "匯入列表"
|
||||
|
||||
#: changedetectionio/blueprint/watchlist/templates/watch-overview.html
|
||||
msgid "Pause checks"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/blueprint/watchlist/templates/watch-overview.html
|
||||
msgid "UnPause checks"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/blueprint/watchlist/templates/watch-overview.html
|
||||
msgid "Mute notification"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/blueprint/watchlist/templates/watch-overview.html
|
||||
msgid "UnMute notification"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/blueprint/watchlist/templates/watch-overview.html
|
||||
msgid "Create a link to share watch config with others"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/blueprint/watchlist/templates/watch-overview.html
|
||||
msgid "Detecting restock and price"
|
||||
msgstr "檢測補貨與價格"
|
||||
@@ -1946,7 +1890,6 @@ msgid "Queued"
|
||||
msgstr "已排程"
|
||||
|
||||
#: changedetectionio/blueprint/watchlist/templates/watch-overview.html
|
||||
#: changedetectionio/processors/text_json_diff/difference.py
|
||||
msgid "History"
|
||||
msgstr "歷史記錄"
|
||||
|
||||
@@ -1980,168 +1923,6 @@ msgstr "複查全部"
|
||||
msgid "in '%(title)s'"
|
||||
msgstr "於 '%(title)s'"
|
||||
|
||||
#: changedetectionio/conditions/__init__.py
|
||||
msgid "Choose one - Operator"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/conditions/__init__.py
|
||||
msgid "Greater Than"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/conditions/__init__.py
|
||||
msgid "Less Than"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/conditions/__init__.py
|
||||
msgid "Greater Than or Equal To"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/conditions/__init__.py
|
||||
msgid "Less Than or Equal To"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/conditions/__init__.py
|
||||
msgid "Equals"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/conditions/__init__.py
|
||||
msgid "Not Equals"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/conditions/__init__.py
|
||||
msgid "Contains"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/conditions/__init__.py
|
||||
msgid "Choose one - Field"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/conditions/default_plugin.py
|
||||
msgid "Does NOT Contain"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/conditions/default_plugin.py
|
||||
msgid "Text Starts With"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/conditions/default_plugin.py
|
||||
msgid "Text Ends With"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/conditions/default_plugin.py
|
||||
msgid "Length minimum"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/conditions/default_plugin.py
|
||||
msgid "Length maximum"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/conditions/default_plugin.py
|
||||
msgid "Text Matches Regex"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/conditions/default_plugin.py
|
||||
msgid "Text Does NOT Match Regex"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/conditions/default_plugin.py
|
||||
msgid "Extracted number after 'Filters & Triggers'"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/conditions/default_plugin.py
|
||||
msgid "Page text after 'Filters & Triggers'"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/conditions/form.py
|
||||
msgid "Field"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/conditions/form.py
|
||||
msgid "Operator"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/conditions/form.py
|
||||
msgid "A value"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/conditions/form.py
|
||||
msgid "Operator is required."
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/conditions/form.py
|
||||
msgid "Field is required."
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/conditions/form.py
|
||||
msgid "Value is required."
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/conditions/plugins/levenshtein_plugin.py
|
||||
msgid "Levenshtein - Text similarity ratio"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/conditions/plugins/levenshtein_plugin.py
|
||||
msgid "Levenshtein - Text change distance"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/conditions/plugins/levenshtein_plugin.py
|
||||
msgid "Not enough history to calculate Levenshtein metrics"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/conditions/plugins/levenshtein_plugin.py
|
||||
msgid "Snapshot too large for edit statistics, skipping."
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/conditions/plugins/levenshtein_plugin.py
|
||||
msgid "Unable to calculate Levenshtein metrics"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/conditions/plugins/levenshtein_plugin.py
|
||||
msgid "Levenshtein Text Similarity Details"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/conditions/plugins/levenshtein_plugin.py
|
||||
msgid "Raw distance (edits needed)"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/conditions/plugins/levenshtein_plugin.py
|
||||
msgid "Similarity ratio"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/conditions/plugins/levenshtein_plugin.py
|
||||
msgid "Percent similar"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/conditions/plugins/levenshtein_plugin.py
|
||||
msgid ""
|
||||
"Levenshtein metrics compare the last two snapshots, measuring how many character edits are needed to transform one "
|
||||
"into the other."
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/conditions/plugins/levenshtein_plugin.py
|
||||
msgid "Error calculating Levenshtein metrics"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/conditions/plugins/wordcount_plugin.py
|
||||
msgid "Word count of content"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/conditions/plugins/wordcount_plugin.py
|
||||
msgid "Content Analysis"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/conditions/plugins/wordcount_plugin.py
|
||||
msgid "Word count (latest snapshot)"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/conditions/plugins/wordcount_plugin.py
|
||||
msgid "Word count is a simple measure of content length, calculated by splitting text on whitespace."
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/content_fetchers/requests.py
|
||||
msgid "Basic fast Plaintext/HTTP Client"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/blueprint/watchlist/templates/watch-overview.html changedetectionio/flask_app.py
|
||||
#: changedetectionio/realtime/socket_server.py
|
||||
msgid "Not yet"
|
||||
@@ -2424,26 +2205,10 @@ msgstr "操作"
|
||||
msgid "Selector"
|
||||
msgstr "選擇器"
|
||||
|
||||
#: changedetectionio/forms.py
|
||||
msgid "CSS or xPath selector"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/forms.py
|
||||
msgid "value"
|
||||
msgstr "值"
|
||||
|
||||
#: changedetectionio/conditions/form.py changedetectionio/forms.py
|
||||
msgid "Value"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/forms.py
|
||||
msgid "Web Page URL"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/forms.py
|
||||
msgid "Group Tag"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/forms.py
|
||||
msgid "Time Between Check"
|
||||
msgstr "檢查間隔"
|
||||
@@ -2532,8 +2297,7 @@ msgstr "當文字符合時,阻擋變更檢測"
|
||||
msgid "Execute JavaScript before change detection"
|
||||
msgstr "在變更檢測前執行 JavaScript"
|
||||
|
||||
#: changedetectionio/blueprint/tags/form.py changedetectionio/blueprint/tags/templates/groups-overview.html
|
||||
#: changedetectionio/forms.py
|
||||
#: changedetectionio/blueprint/tags/templates/groups-overview.html changedetectionio/forms.py
|
||||
msgid "Save"
|
||||
msgstr "儲存"
|
||||
|
||||
@@ -2601,7 +2365,7 @@ msgstr "無效的範本語法:%(error)s"
|
||||
msgid "Invalid template syntax in \"%(header)s\" header: %(error)s"
|
||||
msgstr "「%(header)s」標頭中的範本語法無效:%(error)s"
|
||||
|
||||
#: changedetectionio/blueprint/tags/form.py changedetectionio/forms.py
|
||||
#: changedetectionio/forms.py
|
||||
msgid "Name"
|
||||
msgstr "名稱"
|
||||
|
||||
@@ -2697,10 +2461,6 @@ msgstr "忽略文字"
|
||||
msgid "Ignore whitespace"
|
||||
msgstr "忽略空白"
|
||||
|
||||
#: changedetectionio/forms.py
|
||||
msgid "Screenshot: Minimum Change Percentage"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/forms.py changedetectionio/processors/image_ssim_diff/forms.py
|
||||
msgid "Must be between 0 and 100"
|
||||
msgstr "必須介於 0 到 100 之間"
|
||||
@@ -2900,42 +2660,6 @@ msgstr "針對單一產品頁面的補貨與價格檢測"
|
||||
msgid "Detects if the product goes back to in-stock"
|
||||
msgstr "檢測產品是否恢復庫存"
|
||||
|
||||
#: changedetectionio/processors/templates/extract.html
|
||||
msgid "Screenshot"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/processors/templates/extract.html
|
||||
msgid "This tool will extract text data from all of the watch history."
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/processors/templates/extract.html
|
||||
msgid "A <strong>RegEx</strong> is a pattern that identifies exactly which part inside of the text that you want to extract."
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/processors/templates/extract.html
|
||||
msgid "For example, to extract only the numbers from text"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/processors/templates/extract.html
|
||||
msgid "Raw text"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/processors/templates/extract.html
|
||||
msgid "RegEx to extract:"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/processors/templates/extract.html
|
||||
msgid "Be sure to test your RegEx here."
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/processors/templates/extract.html
|
||||
msgid "Each RegEx group bracket"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/processors/templates/extract.html
|
||||
msgid "will be in its own column, the first column value is always the date."
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/processors/text_json_diff/processor.py
|
||||
msgid "Webpage Text/HTML, JSON and PDF changes"
|
||||
msgstr "網頁文字 / HTML、JSON 和 PDF 變更"
|
||||
@@ -3115,7 +2839,7 @@ msgstr ""
|
||||
msgid "Use"
|
||||
msgstr "使用"
|
||||
|
||||
#: changedetectionio/templates/_common_fields.html changedetectionio/templates/edit/include_subtract.html
|
||||
#: changedetectionio/templates/_common_fields.html
|
||||
msgid "Show advanced help and tips"
|
||||
msgstr "顯示進階說明與提示"
|
||||
|
||||
@@ -3219,26 +2943,6 @@ msgstr ""
|
||||
msgid "Format for all notifications"
|
||||
msgstr "所有通知的格式"
|
||||
|
||||
#: changedetectionio/templates/_common_fields.html
|
||||
msgid "Note"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/templates/_common_fields.html
|
||||
msgid "Discord does not render HTML — switch to"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/templates/_common_fields.html
|
||||
msgid "Plain Text"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/templates/_common_fields.html
|
||||
msgid "format to avoid"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/templates/_common_fields.html
|
||||
msgid "and other HTML entities appearing literally in your notifications."
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/templates/_helpers.html
|
||||
msgid "Entry"
|
||||
msgstr ""
|
||||
@@ -3355,18 +3059,10 @@ msgstr "當文字符合時,阻擋變更檢測"
|
||||
msgid "Blocked text"
|
||||
msgstr "阻擋文字"
|
||||
|
||||
#: changedetectionio/templates/base.html
|
||||
msgid "A new version is available"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/templates/base.html
|
||||
msgid "Search, or Use Alt+S Key"
|
||||
msgstr "搜尋,或使用 Alt+S 鍵"
|
||||
|
||||
#: changedetectionio/templates/base.html
|
||||
msgid "Share this link:"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/templates/base.html
|
||||
msgid "Real-time updates offline"
|
||||
msgstr "離線即時更新"
|
||||
@@ -3537,6 +3233,10 @@ msgstr ""
|
||||
msgid "Unmute notifications"
|
||||
msgstr "取消靜音通知"
|
||||
|
||||
#: changedetectionio/templates/menu.html
|
||||
msgid "Mute notifications"
|
||||
msgstr "靜音通知"
|
||||
|
||||
#: changedetectionio/templates/menu.html
|
||||
msgid "Notifications are muted - click to unmute"
|
||||
msgstr "通知已靜音 - 點擊以取消靜音"
|
||||
|
||||
@@ -405,6 +405,74 @@ async def async_update_worker(worker_id, q, notification_q, app, datastore, exec
|
||||
try:
|
||||
# Reset the edited flag BEFORE update_watch (which calls watch.update() and would set it again)
|
||||
watch.reset_watch_edited_flag()
|
||||
|
||||
# LLM evaluation — intent filtering + change summary
|
||||
update_obj['_llm_result'] = None
|
||||
update_obj['_llm_intent'] = ''
|
||||
update_obj['_llm_change_summary'] = ''
|
||||
if changed_detected:
|
||||
try:
|
||||
from changedetectionio.llm.evaluator import (
|
||||
evaluate_change, resolve_intent, resolve_llm_field,
|
||||
summarise_change, get_llm_config,
|
||||
get_effective_summary_prompt, compute_summary_cache_key,
|
||||
)
|
||||
_llm_cfg = get_llm_config(datastore)
|
||||
if _llm_cfg:
|
||||
# Compute unified diff once — used by both intent and summary
|
||||
_watch_dates = list(watch.history.keys())
|
||||
if _watch_dates:
|
||||
_prev_text = watch.get_history_snapshot(timestamp=_watch_dates[-1]) or ''
|
||||
from difflib import unified_diff as _unified_diff
|
||||
_diff_lines = list(_unified_diff(
|
||||
_prev_text.splitlines(keepends=True),
|
||||
contents.splitlines(keepends=True),
|
||||
lineterm='',
|
||||
n=3
|
||||
))
|
||||
_diff_text = ''.join(_diff_lines) if _diff_lines else contents
|
||||
else:
|
||||
_diff_text = contents
|
||||
|
||||
# Step 1: AI Change Intent — may suppress notification
|
||||
_llm_intent, _llm_intent_source = resolve_intent(watch, datastore)
|
||||
if _llm_intent:
|
||||
_llm_result = await loop.run_in_executor(
|
||||
executor,
|
||||
lambda diff=_diff_text, snap=contents: evaluate_change(
|
||||
watch, datastore, diff=diff, current_snapshot=snap
|
||||
)
|
||||
)
|
||||
update_obj['_llm_result'] = _llm_result
|
||||
update_obj['_llm_intent'] = _llm_intent
|
||||
|
||||
if _llm_result and not _llm_result.get('important', True):
|
||||
changed_detected = False
|
||||
logger.info(
|
||||
f"LLM filtered out change for {uuid} "
|
||||
f"(intent from {_llm_intent_source}): "
|
||||
f"{_llm_result.get('summary', '')[:80]}"
|
||||
)
|
||||
|
||||
# Step 2: AI Change Summary — runs for any LLM-configured watch with a change
|
||||
if changed_detected:
|
||||
_effective_prompt = get_effective_summary_prompt(watch, datastore)
|
||||
_summary_cache_key = compute_summary_cache_key(_diff_text, _effective_prompt)
|
||||
_change_summary = await loop.run_in_executor(
|
||||
executor,
|
||||
lambda diff=_diff_text, snap=contents: summarise_change(
|
||||
watch, datastore, diff=diff, current_snapshot=snap
|
||||
)
|
||||
)
|
||||
if _change_summary:
|
||||
update_obj['_llm_change_summary'] = _change_summary
|
||||
try:
|
||||
watch.save_llm_diff_summary(_change_summary, cache_key=_summary_cache_key)
|
||||
except Exception as _fe:
|
||||
logger.warning(f"Could not write last-llm-diff-summary.txt for {uuid}: {_fe}")
|
||||
except Exception as e:
|
||||
logger.warning(f"LLM evaluation error for {uuid}: {e}")
|
||||
|
||||
datastore.update_watch(uuid=uuid, update_obj=update_obj)
|
||||
|
||||
if changed_detected or not watch.history_n:
|
||||
|
||||
@@ -140,6 +140,11 @@ tzdata
|
||||
|
||||
pluggy ~= 1.6
|
||||
|
||||
# LLM intent-based change evaluation (multi-provider via litellm)
|
||||
litellm>=1.40.0
|
||||
# BM25 relevance trimming for large snapshots (pure Python, no ML)
|
||||
rank-bm25>=0.2.2
|
||||
|
||||
# Needed for testing, cross-platform for process and system monitoring
|
||||
psutil==7.2.2
|
||||
|
||||
|
||||
Reference in New Issue
Block a user