$1 is invalid. It should be an integer.",
+ "error-no-url": "Compare mode requires a URL to be entered. Enter one in the text box below, or choose copyvio search mode to look for content similar to the article elsewhere on the web.",
+ "error-unsupported-uri": "Unsupported URI scheme: $1.",
+ "error-no-data": "Couldn't find any text in $1. Note: only HTML documents, plain text pages, and PDFs are supported, and content generated by JavaScript or found inside iframes is ignored.",
+ "error-timeout": "The URL $1 timed out before any data could be retrieved.",
+ "error-search-error": "An error occurred while using the search engine ($1). Note: there is a daily limit on the number of search queries the tool is allowed to make. You may $2.",
+ "error-unknown": "An unknown error occurred.",
+ "error-bad-site": "The given site (project=$1, language=$2) doesn't seem to exist. It may also be closed or private. $3",
+ "error-revision-not-found": "The revision ID couldn't be found: $1.",
+ "error-page-not-found": "The page couldn't be found: $1.",
+ "intro-paragraph-1": "This tool attempts to detect $1 in articles. In search mode, it will check for similar content elsewhere on the web using Google, external links present in the text of the page, or Turnitin (via EranBot), depending on which options are selected. In compare mode, the tool will compare the article to a specific webpage without making additional searches, like the Duplication Detector.",
+ "intro-paragraph-1-copyvio-url": "https://en.wikipedia.org/wiki/WP:COPYVIO",
+ "intro-copyright-violations": "copyright violations",
+ "intro-paragraph-2": "Running a full check can take up to a minute if other websites are slow or if the tool is under heavy use. Please be patient. If you get a timeout, wait a moment and refresh the page.",
+ "intro-paragraph-3": "Be aware that other websites can copy from Wikipedia, so check the results carefully, especially for older or well-developed articles. Specific websites can be skipped by adding them to the $1.",
+ "intro-exclusions-list": "excluded URL list",
+ "cached-line": "Results cached from $2. Originally generated in $3 seconds.",
+ "cached-line-search": "Results cached from $2. Originally generated in $3 seconds using {{PLURAL:$4|$4 query|$4 queries}}.",
+ "cached-tooltip": "To save time (and money), this tool will retain the results of checks for up to 72 hours. This includes the URLs of the checked sources, but neither their content nor the content of the article. Future checks on the same page (assuming it remains unchanged) will not involve additional search queries, but a fresh comparison against the source URL will be made. If the page is modified, a new check will be run.",
+ "check-results-time": "Results generated in $1 seconds.",
+ "check-results-time-search": "Results generated in $1 seconds using {{PLURAL:$2|$2 query|$2 queries}}.",
+ "permalink": "Permalink.",
+ "violation-suspected": "Violation suspected",
+ "violation-possible": "Violation possible",
+ "violation-unlikely": "Violation unlikely",
+ "no-violation": "No violation",
+ "similarity": "similarity",
+ "no-matches-found": "No matches found.",
+ "redirected-from": "Redirected from $1. $2.",
+ "redirected-from-check-original": "Check original.",
+ "attribution-warning": "This article contains an attribution template: {{$1}}. Please verify that any potential copyvios are not from properly attributed sources.",
+ "turnitin-results": "Turnitin Results",
+ "turnitin-no-matches": "No matching sources found.",
+ "turnitin-report": "Report $1",
+ "turnitin-report-link": "Report $1 for text added at $2:",
+ "turnitin-source": "$1% of revision text ($2 words) found at $3",
+ "checked-sources": "Checked Sources",
+ "table-url": "URL",
+ "table-similarity": "Similarity",
+ "table-compare": "Compare",
+ "source-excluded": "Excluded",
+ "source-skipped": "Skipped",
+ "no-sources-checked": "No sources checked.",
+ "urls-hidden": "{{PLURAL:$1|$1 URL with lower similarity hidden.|$1 URLs with lower similarity hidden.}}",
+ "urls-hidden-show": "Show them.",
+ "search-ended-early": "The search ended early because a match was found with high similarity. $1",
+ "search-ended-early-retry": "Do a complete check.",
+ "article-label": "Article:",
+ "source-label": "Source:",
+ "compare-action": "Compare",
+ "ago-seconds": "{{PLURAL:$1|$1 second|$1 seconds}} ago",
+ "ago-minutes": "{{PLURAL:$1|$1 minute|$1 minutes}} ago",
+ "ago-hours": "{{PLURAL:$1|$1 hour|$1 hours}} ago"
+}
diff --git a/i18n/qqq.json b/i18n/qqq.json
new file mode 100644
index 0000000..007f86f
--- /dev/null
+++ b/i18n/qqq.json
@@ -0,0 +1,118 @@
+{
+ "@metadata": {
+ "authors": [
+ "Ben Kurtovic"
+ ]
+ },
+ "site-title": "Title of the tool shown in the browser tab",
+ "site-header-title": "Header title of the tool, shown at the top of every page. \"Earwig's\" is the author's username. \"Copyvio Detector\" should be bolded.",
+ "log-in": "Label for the login link in the header",
+ "log-out": "Title attribute for the logout link in the header",
+ "settings": "Label for the settings link in the header",
+ "maintained-by": "Footer text with a link to the author's user page. $1 is the complete HTML link to the author's user page.",
+ "api": "Link to the API documentation page",
+ "source-code": "Link to the source code repository",
+ "background-image": "Link to the background image description page",
+ "page-title-search": "Suffix for the browser tab title when viewing a search result",
+ "page-title-compare": "Suffix for the browser tab title when viewing a compare result",
+ "page-title-settings": "Suffix for the browser tab title on the settings page",
+ "page-title-login": "Suffix for the browser tab title on the login page",
+ "page-title-logout": "Suffix for the browser tab title on the logout page",
+ "page-title-error": "Suffix for the browser tab title on the error page",
+ "login-required": "Message explaining that the user needs to log in via OAuth to use the search engine",
+ "login-error": "Error message displayed when login fails. $1 is the error message.",
+ "login-heading": "Heading for the login page",
+ "after-logging-in": "Beginning of a sentence explaining what happens after login",
+ "check-will-be-run": "Part of a sentence, follows 'after-logging-in'. Used when the user was about to run a search.",
+ "will-be-redirected-to": "Part of a sentence, follows 'after-logging-in'. Used when the user was on another page.",
+ "login-button": "Label for the login button",
+ "logout-heading": "Heading for the logout page",
+ "logout-message": "Explanation of what logging out does",
+ "logout-button": "Label for the logout button",
+ "error-heading": "Heading for the error page",
+ "error-description": "Instructions for the user when an unexpected error occurs. Contains links to GitHub issues and email. $1 is the GitHub issues list URL, $2 is the new issue URL, $3 is the author's email URL.",
+ "error-bad-site-confirm": "Link text for confirming a wiki site's URL.",
+ "search-error-retry": "Link text for retrying a search without the search engine.",
+ "settings-heading": "Heading for the settings page",
+ "settings-description": "Explanation of the settings page",
+ "settings-default-site": "Section heading for default wiki site selection",
+ "settings-background": "Section heading for background image selection",
+ "settings-background-list": "Description for the 'list' background option. Contains several links. $1 is the POTD subset URL, $2 is the Wikimedia Commons URL, $3 is the Pictures of the Day URL.",
+ "settings-background-potd": "Description for the 'POTD' (Picture of the Day) background option",
+ "settings-background-plain": "Description for the 'plain' background option",
+ "settings-save": "Label for the save settings button",
+ "settings-updated": "Confirmation message shown after settings are saved. $1 is a comma-separated list of changed settings.",
+ "form-site": "Label for the site selection dropdown in the search form",
+ "form-language": "Title attribute for the language dropdown",
+ "form-project": "Title attribute for the project dropdown",
+ "form-page": "Label for the page title input",
+ "form-title-placeholder": "Placeholder text for the page title input",
+ "form-title-tooltip": "Tooltip for the page title input",
+ "form-or": "Separator label between the title and revision ID inputs",
+ "form-revision-id-placeholder": "Placeholder text for the revision ID input",
+ "form-revision-id-tooltip": "Tooltip for the revision ID input",
+ "form-url-placeholder": "Placeholder text for the URL input",
+ "form-url-tooltip": "Tooltip for the URL input in compare mode",
+ "form-copyvio-search": "Label for the 'search' radio button, selecting copyvio search mode",
+ "form-copyvio-compare": "Label for the 'compare' radio button, selecting copyvio compare mode",
+ "form-use-search-engine": "Label for the checkbox to enable/disable Google search engine",
+ "form-use-links": "Label for the checkbox to enable/disable checking external links in the page",
+ "form-use-turnitin": "Label for the checkbox to enable/disable Turnitin integration (currently disabled)",
+ "form-bypass-cache": "Label for the checkbox to bypass cached results",
+ "form-submit": "Label for the submit/search button",
+ "error-bad-action": "Error shown when an unknown action is submitted. $1 is the action name.",
+ "error-no-search-method": "Error shown when neither search engine nor page links are selected for checking",
+ "error-invalid-oldid": "Error shown when the revision ID is not a valid integer. $1 is the invalid ID.",
+ "error-no-url": "Error shown when no URL is provided for compare mode",
+ "error-unsupported-uri": "Error shown when the URL has an unsupported URI scheme. $1 is the URL.",
+ "error-no-data": "Error shown when no text could be extracted from a URL. $1 is the URL.",
+ "error-timeout": "Error shown when a URL times out. $1 is the URL.",
+ "error-search-error": "Error shown when the search engine fails. $1 is the error cause. $2 is a URL to retry without search engine.",
+ "error-unknown": "Fallback error message for unrecognized error codes",
+ "error-bad-site": "Error shown when the wiki site does not exist. $1 is the project code, $2 is the language code, $3 is a URL to confirm.",
+ "error-revision-not-found": "Error shown when the revision ID is not found. $1 is the URL, $2 is the revision ID.",
+ "error-page-not-found": "Error shown when the page is not found. $1 is the URL, $2 is the page title.",
+ "intro-paragraph-1": "First introductory paragraph explaining what the tool does (search mode vs compare mode). $1 is a link to the copyright violations page. Contains several hardcoded links: Google, Turnitin, EranBot, Duplication Detector.",
+ "intro-paragraph-1-copyvio-url": "URL of the copyright violations page on the local Wikipedia.",
+ "intro-copyright-violations": "Link text for the copyright violations page (WP:COPYVIO).",
+ "intro-paragraph-2": "Second introductory paragraph about performance and timeouts",
+ "intro-paragraph-3": "Third introductory paragraph explaining caution with results and the excluded URL list. $1 is a link to the excluded URL list page.",
+ "intro-exclusions-list": "Link text for the excluded URL list page.",
+ "cached-line": "Cached results sentence (compare mode). $1 is the tooltip text for the around 'cached'. $2 is a complete '' with the time difference (e.g. '1 hour ago'). $3 is the generation time in seconds.",
+ "cached-line-search": "Cached results sentence (search mode). $1 is the tooltip text for the around 'cached'. $2 is a complete '' with the time difference (e.g. '1 hour ago'). $3 is the generation time. $4 is the search query count (PLURAL).",
+ "cached-tooltip": "Tooltip text for the 'cached' element in the cached results sentence.",
+ "check-results-time": "Generation time sentence (compare mode). $1 is the time in seconds.",
+ "check-results-time-search": "Generation time sentence (search mode). $1 is the time. $2 is the search query count (PLURAL).",
+ "permalink": "Label for a permanent link to the check results",
+ "violation-suspected": "Label indicating a copyright violation is suspected",
+ "violation-possible": "Label indicating a copyright violation is possible",
+ "violation-unlikely": "Label indicating a copyright violation is unlikely",
+ "no-violation": "Label indicating no copyright violation was found",
+ "similarity": "Label below the similarity percentage score",
+ "no-matches-found": "Text shown when no matching sources were found",
+ "redirected-from": "Notice that the page was redirected from another title. $1 is the redirect page URL, $2 is the original title, $3 is the URL to check the original.",
+ "redirected-from-check-original": "Link text for checking the original (non-redirected) page.",
+ "attribution-warning": "Warning when the article contains an attribution template. $1 is the URL to the template, $2 is the template name.",
+ "turnitin-results": "Section heading for Turnitin results",
+ "turnitin-no-matches": "Text shown when Turnitin found no matching sources",
+ "turnitin-report": "Label for a Turnitin report link. $1 is the report ID.",
+ "turnitin-report-link": "Turnitin report link with timestamp. $1 is the report ID, $2 is the diff URL, $3 is the timestamp string.",
+ "turnitin-source": "Turnitin source match description. $1 is the percentage, $2 is the word count, $3 is the source URL.",
+ "checked-sources": "Section heading for the list of checked sources",
+ "table-url": "Column header for the URL column in the sources table",
+ "table-similarity": "Column header for the similarity column in the sources table",
+ "table-compare": "Column header for the compare action column in the sources table",
+ "source-excluded": "Label shown next to a source that was excluded from checking",
+ "source-skipped": "Label shown next to a source that was skipped",
+ "no-sources-checked": "Text shown when no sources were checked",
+ "urls-hidden": "Notice that some URLs are hidden. $1 is the count of hidden URLs. Followed by a separate 'Show them.' link.",
+ "urls-hidden-show": "Link text to reveal hidden URLs.",
+ "search-ended-early": "Notice that the search ended early due to a high-similarity match. $1 is a URL to re-run with a complete check.",
+ "search-ended-early-retry": "Link text for re-running with a complete check after the search ended early.",
+ "article-label": "Label for the article text in the comparison view",
+ "source-label": "Label for the source text in the comparison view",
+ "compare-action": "Link text to compare a specific source URL",
+ "ago-seconds": "Time duration in seconds. $1 is the count (PLURAL). Example: '5 seconds ago'.",
+ "ago-minutes": "Time duration in minutes. $1 is the count (PLURAL). Example: '2 minutes ago'.",
+ "ago-hours": "Time duration in hours. $1 is the count (PLURAL). Example: '1 hour ago'."
+}
diff --git a/pyproject.toml b/pyproject.toml
index 014d4a4..a2c74d9 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -15,6 +15,7 @@ dependencies = [
"pydantic >= 2.9.2",
"SQLAlchemy >= 2.0.32",
"mwoauth >= 0.4.0",
+ "toolforge_i18n[Flask] >= 0.1",
]
[project.urls]
diff --git a/src/copyvios/__init__.py b/src/copyvios/__init__.py
index ff8281a..57f0ea6 100644
--- a/src/copyvios/__init__.py
+++ b/src/copyvios/__init__.py
@@ -3,8 +3,9 @@ import logging
import os
from logging.handlers import TimedRotatingFileHandler
-from flask import Flask, request
+from flask import Flask, g, request
from flask.sessions import SecureCookieSessionInterface, SessionMixin
+from toolforge_i18n import ToolforgeI18n, set_user_agent
class CopyviosSessionInterface(SecureCookieSessionInterface):
@@ -34,6 +35,9 @@ app.config["SESSION_COOKIE_SECURE"] = True
app.config["SESSION_COOKIE_SAMESITE"] = "Lax"
app.config["PERMANENT_SESSION_LIFETIME"] = datetime.timedelta(days=365)
+set_user_agent("Copyvios/1.0 (https://github.com/earwig/copyvios; tools.copyvios@toolforge.org)")
+i18n = ToolforgeI18n(app)
+
app.jinja_options["trim_blocks"] = True
app.jinja_options["lstrip_blocks"] = True
diff --git a/src/copyvios/checker.py b/src/copyvios/checker.py
index fa3a161..27d6876 100644
--- a/src/copyvios/checker.py
+++ b/src/copyvios/checker.py
@@ -14,8 +14,9 @@ from earwigbot.wiki.copyvios import CopyvioChecker, workers
from earwigbot.wiki.copyvios.markov import DEFAULT_DEGREE, EMPTY
from earwigbot.wiki.copyvios.result import CopyvioCheckResult, CopyvioSource
from earwigbot.wiki.copyvios.workers import CopyvioWorkspace
-from flask import g, session
+from flask import current_app, g, session
from sqlalchemy import PoolProxiedConnection
+from toolforge_i18n._flask import message as _i18n_message
from .cache import cache
from .misc import get_sql_error, sql_dialect
@@ -177,7 +178,8 @@ def _perform_check(
if not result:
is_logged_in = session.get("username")
- if query.use_engine and not is_logged_in and not isinstance(query, APIQuery):
+ bypass = current_app.config.get("COPYVIOS_BYPASS_LOGIN", False)
+ if query.use_engine and not is_logged_in and not isinstance(query, APIQuery) and not bypass:
raise CopyvioCheckError(ErrorCode.NOT_LOGGED_IN)
try:
@@ -285,16 +287,13 @@ def _get_cached_results(
def _format_date(cache_time: datetime) -> str:
- def formatter(val: float, unit: str):
- return f"{int(val)} {unit}{'' if val == 1 else 's'}"
-
diff = datetime.now(UTC) - cache_time
total_seconds = diff.days * 86400 + diff.seconds
- if total_seconds > 3600:
- return formatter(total_seconds / 3600, "hour")
- if total_seconds > 60:
- return formatter(total_seconds / 60, "minute")
- return formatter(total_seconds, "second")
+ if total_seconds >= 3600:
+ return _i18n_message("ago-hours", num=int(total_seconds / 3600))
+ if total_seconds >= 60:
+ return _i18n_message("ago-minutes", num=int(total_seconds / 60))
+ return str(_i18n_message("ago-seconds", num=total_seconds))
def _cache_result(
diff --git a/src/copyvios/templates/error.html.jinja b/src/copyvios/templates/error.html.jinja
index 883055a..01a504d 100644
--- a/src/copyvios/templates/error.html.jinja
+++ b/src/copyvios/templates/error.html.jinja
@@ -1,10 +1,10 @@
{% extends "support/base.html.jinja" %}
{% block title %}
- Error! - {{ super() }}
+ {{ message('page-title-error') | striptags }} - {{ super() }}
{% endblock %}
{% block content %}
- An error occurred. If it hasn't been reported (try to check), please file an issue or email me. Include the following information:
+{{ message('error-description', issues_url='https://github.com/earwig/copyvios/issues', new_issue_url='https://github.com/earwig/copyvios/issues/new', email_url='mailto:wikipedia.earwig@gmail.com') }}
{{ traceback | trim | e }}
{% if error.code == ErrorCode.BAD_ACTION %}
- Unknown action: {{ query.action | e }}.
+ {{ message('error-bad-action', action=query.action) }}
{% elif error.code == ErrorCode.NO_SEARCH_METHOD %}
- No copyvio search methods were selected. A check can only be made using the search engine, links present in the page, or some combination of these.
+ {{ message('error-no-search-method') }}
{% elif error.code == ErrorCode.BAD_OLDID %}
- The revision ID {{ query.oldid | e }} is invalid. It should be an integer.
+ {{ message('error-invalid-oldid', oldid=query.oldid) }}
{% elif error.code == ErrorCode.NO_URL %}
- Compare mode requires a URL to be entered. Enter one in the text box below, or choose copyvio search mode to look for content similar to the article elsewhere on the web.
+ {{ message('error-no-url') }}
{% elif error.code == ErrorCode.BAD_URI %}
- Unsupported URI scheme: {{ query.url | e }}.
+ {% set url_link = '' ~ (query.url | e) ~ '' %}
+ {{ message('error-unsupported-uri', target_url=url_link | safe) }}
{% elif error.code == ErrorCode.NO_DATA %}
- Couldn't find any text in {{ query.url | e }}. Note: only HTML documents, plain text pages, and PDFs are supported, and content generated by JavaScript or found inside iframes is ignored.
+ {% set url_link = '' ~ (query.url | e) ~ '' %}
+ {{ message('error-no-data', target_url=url_link | safe) }}
{% elif error.code == ErrorCode.TIMEOUT %}
- The URL {{ query.url | e }} timed out before any data could be retrieved.
+ {% set url_link = '' ~ (query.url | e) ~ '' %}
+ {{ message('error-timeout', target_url=url_link | safe) }}
{% elif error.code == ErrorCode.SEARCH_ERROR %}
- An error occurred while using the search engine ({{ error.__cause__ }}). Note: there is a daily limit on the number of search queries the tool is allowed to make. You may repeat the check without using the search engine.
+ {% set retry_url = ((request.url | httpsfix) + '&use_engine=0') | e %}
+ {% set retry_link = '' ~ (message('search-error-retry') | striptags) ~ '' %}
+ {{ message('error-search-error', error_cause=error.__cause__, no_engine_url=retry_link | safe) }}
{% else %}
- An unknown error occurred.
+ {{ message('error-unknown') }}
{% endif %}
The given site (project={{ query.project | e }}, language={{ query.lang | e }}) doesn't seem to exist. It may also be closed or private. Confirm its URL.
+ {% set site_confirm_link = '' ~ (message('error-bad-site-confirm') | striptags) ~ '' %} +{{ message('error-bad-site', project=query.project, lang=query.lang, site_url=site_confirm_link | safe) }}
The revision ID couldn't be found: {{ query.oldid | e }}.
+ {% set rev_link = '' ~ (query.oldid | e) ~ '' %} +{{ message('error-revision-not-found', revision_url=rev_link | safe) }}
The page couldn't be found: {{ g.page.title | e }}.
+ {% set page_link = '' ~ (g.page.title | e) ~ '' %} +{{ message('error-page-not-found', page_url=page_link | safe) }}
This tool attempts to detect copyright violations in articles. In search mode, it will check for similar content elsewhere on the web using Google, external links present in the text of the page, or Turnitin (via EranBot), depending on which options are selected. In compare mode, the tool will compare the article to a specific webpage without making additional searches, like the Duplication Detector.
-Running a full check can take up to a minute if other websites are slow or if the tool is under heavy use. Please be patient. If you get a timeout, wait a moment and refresh the page.
-Be aware that other websites can copy from Wikipedia, so check the results carefully, especially for older or well-developed articles. Specific websites can be skipped by adding them to the excluded URL list.
+ {% set ui_lang = query.lang or g.lang_code or 'en' %} +{{ message('intro-paragraph-1', + copyright_url=('' ~ (message('intro-copyright-violations') | striptags) ~ '') | safe) }}
+{{ message('intro-paragraph-2') }}
+{{ message('intro-paragraph-3', exclusions_url=('' ~ (message('intro-exclusions-list') | striptags) ~ '') | safe) }}
{% include "support/cv_form.html.jinja" %} {% if result %} {% include "support/cv_result.html.jinja" %} diff --git a/src/copyvios/templates/login.html.jinja b/src/copyvios/templates/login.html.jinja index f23ca13..4356b32 100644 --- a/src/copyvios/templates/login.html.jinja +++ b/src/copyvios/templates/login.html.jinja @@ -1,22 +1,22 @@ {% extends "support/base.html.jinja" %} {% block title %} - Login - {{ super() }} + {{ message('page-title-login') | striptags }} - {{ super() }} {% endblock %} {% block content %} {% if error %}Error trying to log in: {{ error | safe }}
+{{ message('login-error', error_text=error) }}
You are required to log in with your Wikimedia account to perform checks with the search engine.
+{{ message('login-required') }}
{% if request.args.get("next") %}- After logging in, + {{ message('after-logging-in') }} {% if is_search %} - your check will be run. + {{ message('check-will-be-run') }} {% else %} - you will be redirected to: {{ request.args["next"] | safe }} + {{ message('will-be-redirected-to') }} {{ request.args["next"] | safe }} {% endif %}
{% endif %} @@ -27,7 +27,7 @@ diff --git a/src/copyvios/templates/logout.html.jinja b/src/copyvios/templates/logout.html.jinja index 3fbe114..c308faf 100644 --- a/src/copyvios/templates/logout.html.jinja +++ b/src/copyvios/templates/logout.html.jinja @@ -1,17 +1,17 @@ {% extends "support/base.html.jinja" %} {% block title %} - Logout - {{ super() }} + {{ message('page-title-logout') | striptags }} - {{ super() }} {% endblock %} {% block content %} -Logging out will prevent you from making search engine checks.
+{{ message('logout-message') }}