-
-
Notifications
You must be signed in to change notification settings - Fork 5
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #14 from avixiii-dev/develop
Develop
- Loading branch information
Showing
16 changed files
with
1,192 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,20 @@ | ||
[run] | ||
source = seo_optimizer | ||
omit = | ||
*/migrations/* | ||
*/tests/* | ||
*/admin.py | ||
*/apps.py | ||
*/urls.py | ||
manage.py | ||
setup.py | ||
|
||
[report] | ||
exclude_lines = | ||
pragma: no cover | ||
def __repr__ | ||
if self.debug: | ||
raise NotImplementedError | ||
if __name__ == .__main__.: | ||
pass | ||
raise ImportError |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,14 @@ | ||
.PHONY: test coverage performance clean | ||
|
||
test: | ||
pytest | ||
|
||
coverage: | ||
pytest --cov=seo_optimizer --cov-report=html | ||
open htmlcov/index.html | ||
|
||
performance: | ||
locust -f tests/performance/locustfile.py | ||
|
||
clean: | ||
rm -rf .coverage htmlcov .pytest_cache __pycache__ .benchmarks |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,4 @@ | ||
[pytest] | ||
DJANGO_SETTINGS_MODULE = seo_optimizer.settings | ||
python_files = tests.py test_*.py *_tests.py | ||
addopts = --cov=seo_optimizer --cov-report=html --cov-report=term-missing -v |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,14 @@ | ||
Django>=3.2,<4.0 | ||
beautifulsoup4>=4.9.3 | ||
requests>=2.26.0 | ||
pytz>=2021.1 | ||
|
||
# Testing dependencies | ||
pytest>=7.0.0 | ||
pytest-django>=4.5.0 | ||
pytest-cov>=4.0.0 | ||
pytest-benchmark>=4.0.0 | ||
pytest-mock>=3.10.0 | ||
factory-boy>=3.2.0 | ||
coverage>=7.0.0 | ||
locust>=2.15.0 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,186 @@ | ||
""" | ||
Internationalization support for Django SEO Optimizer | ||
Created by avixiii (https://avixiii.com) | ||
""" | ||
from typing import Dict, List, Optional, Any | ||
from dataclasses import dataclass | ||
from django.conf import settings | ||
from django.utils import timezone, translation | ||
from django.urls import reverse | ||
from django.core.cache import cache | ||
from django.utils.translation import gettext_lazy as _ | ||
|
||
|
||
class I18nConfig: | ||
"""Configuration for internationalization""" | ||
CACHE_TIMEOUT = getattr(settings, 'SEO_I18N_CACHE_TIMEOUT', 3600) | ||
DEFAULT_LANGUAGE = getattr(settings, 'LANGUAGE_CODE', 'en') | ||
SUPPORTED_LANGUAGES = getattr(settings, 'LANGUAGES', [('en', 'English')]) | ||
URL_TYPE = getattr(settings, 'SEO_I18N_URL_TYPE', 'prefix') # prefix or domain | ||
DOMAIN_MAPPING = getattr(settings, 'SEO_I18N_DOMAIN_MAPPING', {}) | ||
|
||
|
||
@dataclass | ||
class LocalizedMetadata: | ||
"""Container for localized metadata""" | ||
language: str | ||
title: str | ||
description: str | ||
keywords: List[str] | ||
canonical_url: str | ||
og_title: str | ||
og_description: str | ||
og_image: str | ||
twitter_title: str | ||
twitter_description: str | ||
twitter_image: str | ||
|
||
|
||
class I18nMetadataManager: | ||
"""Manager for handling localized metadata""" | ||
|
||
def __init__(self): | ||
self.cache_timeout = I18nConfig.CACHE_TIMEOUT | ||
|
||
def get_metadata(self, path: str, language: Optional[str] = None) -> LocalizedMetadata: | ||
""" | ||
Get localized metadata for a path | ||
Args: | ||
path: URL path | ||
language: Language code (if None, uses current active language) | ||
Returns: | ||
LocalizedMetadata object | ||
""" | ||
if language is None: | ||
language = translation.get_language() or I18nConfig.DEFAULT_LANGUAGE | ||
|
||
cache_key = f'i18n_metadata_{path}_{language}' | ||
cached_data = cache.get(cache_key) | ||
if cached_data: | ||
return cached_data | ||
|
||
# Get base metadata and localize it | ||
metadata = self._get_base_metadata(path) | ||
localized = self._localize_metadata(metadata, language) | ||
|
||
cache.set(cache_key, localized, timeout=self.cache_timeout) | ||
return localized | ||
|
||
def _get_base_metadata(self, path: str) -> Dict[str, Any]: | ||
"""Get base metadata before localization""" | ||
# Implement base metadata retrieval logic | ||
return {} | ||
|
||
def _localize_metadata(self, metadata: Dict[str, Any], | ||
language: str) -> LocalizedMetadata: | ||
"""Localize metadata for a specific language""" | ||
with translation.override(language): | ||
# Implement metadata localization logic | ||
return LocalizedMetadata( | ||
language=language, | ||
title=translation.gettext(metadata.get('title', '')), | ||
description=translation.gettext(metadata.get('description', '')), | ||
keywords=metadata.get('keywords', []), | ||
canonical_url=self._get_localized_url( | ||
metadata.get('canonical_url', ''), | ||
language | ||
), | ||
og_title=translation.gettext(metadata.get('og_title', '')), | ||
og_description=translation.gettext(metadata.get('og_description', '')), | ||
og_image=metadata.get('og_image', ''), | ||
twitter_title=translation.gettext(metadata.get('twitter_title', '')), | ||
twitter_description=translation.gettext( | ||
metadata.get('twitter_description', '') | ||
), | ||
twitter_image=metadata.get('twitter_image', '') | ||
) | ||
|
||
|
||
class LocalizedURLManager: | ||
"""Manager for handling localized URLs""" | ||
|
||
@staticmethod | ||
def get_language_url(url: str, language: str) -> str: | ||
"""Get URL for a specific language""" | ||
if I18nConfig.URL_TYPE == 'domain': | ||
return LocalizedURLManager._get_domain_url(url, language) | ||
return LocalizedURLManager._get_prefix_url(url, language) | ||
|
||
@staticmethod | ||
def _get_domain_url(url: str, language: str) -> str: | ||
"""Get domain-based language URL""" | ||
domain = I18nConfig.DOMAIN_MAPPING.get(language) | ||
if not domain: | ||
return url | ||
return f'https://{domain}{url}' | ||
|
||
@staticmethod | ||
def _get_prefix_url(url: str, language: str) -> str: | ||
"""Get prefix-based language URL""" | ||
if language == I18nConfig.DEFAULT_LANGUAGE: | ||
return url | ||
return f'/{language}{url}' | ||
|
||
|
||
class HrefLangGenerator: | ||
"""Generator for hreflang tags""" | ||
|
||
def __init__(self, url: str): | ||
self.url = url | ||
self.url_manager = LocalizedURLManager() | ||
|
||
def generate_tags(self) -> List[Dict[str, str]]: | ||
"""Generate hreflang tags for all supported languages""" | ||
tags = [] | ||
|
||
for lang_code, lang_name in I18nConfig.SUPPORTED_LANGUAGES: | ||
tags.append({ | ||
'hreflang': lang_code, | ||
'href': self.url_manager.get_language_url(self.url, lang_code) | ||
}) | ||
|
||
# Add x-default tag for default language | ||
if lang_code == I18nConfig.DEFAULT_LANGUAGE: | ||
tags.append({ | ||
'hreflang': 'x-default', | ||
'href': self.url_manager.get_language_url( | ||
self.url, | ||
I18nConfig.DEFAULT_LANGUAGE | ||
) | ||
}) | ||
|
||
return tags | ||
|
||
|
||
class TimezoneManager: | ||
"""Manager for handling timezone-specific content""" | ||
|
||
@staticmethod | ||
def get_user_timezone(request) -> str: | ||
"""Get user's timezone from request""" | ||
# Try to get from session | ||
tz = request.session.get('user_timezone') | ||
if tz: | ||
return tz | ||
|
||
# Try to get from Accept-Language header | ||
accept_lang = request.META.get('HTTP_ACCEPT_LANGUAGE', '') | ||
if accept_lang: | ||
try: | ||
# Parse timezone from Accept-Language | ||
# This is a simplified example | ||
return 'UTC' | ||
except Exception: | ||
pass | ||
|
||
return settings.TIME_ZONE | ||
|
||
@staticmethod | ||
def format_datetime(dt, tz: Optional[str] = None): | ||
"""Format datetime in user's timezone""" | ||
if tz: | ||
user_tz = timezone.pytz.timezone(tz) | ||
dt = timezone.localtime(dt, user_tz) | ||
return dt.isoformat() |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,35 @@ | ||
# SOME DESCRIPTIVE TITLE. | ||
# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER | ||
# This file is distributed under the same license as the PACKAGE package. | ||
# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR. | ||
# | ||
#, fuzzy | ||
msgid "" | ||
msgstr "" | ||
"Project-Id-Version: django-seo-optimizer\n" | ||
"Report-Msgid-Bugs-To: \n" | ||
"POT-Creation-Date: 2024-12-25 14:30+0000\n" | ||
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" | ||
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n" | ||
"Language-Team: LANGUAGE <[email protected]>\n" | ||
"Language: en\n" | ||
"MIME-Version: 1.0\n" | ||
"Content-Type: text/plain; charset=UTF-8\n" | ||
"Content-Transfer-Encoding: 8bit\n" | ||
"Plural-Forms: nplurals=2; plural=(n != 1);\n" | ||
|
||
#: seo_optimizer/i18n.py:15 | ||
msgid "Default language" | ||
msgstr "Default language" | ||
|
||
#: seo_optimizer/i18n.py:16 | ||
msgid "Supported languages" | ||
msgstr "Supported languages" | ||
|
||
#: seo_optimizer/i18n.py:17 | ||
msgid "URL type" | ||
msgstr "URL type" | ||
|
||
#: seo_optimizer/i18n.py:18 | ||
msgid "Domain mapping" | ||
msgstr "Domain mapping" |
Oops, something went wrong.