Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Develop #14

Merged
merged 9 commits into from
Dec 25, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 20 additions & 0 deletions .coveragerc
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
14 changes: 14 additions & 0 deletions Makefile
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
4 changes: 4 additions & 0 deletions pytest.ini
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
14 changes: 14 additions & 0 deletions requirements.txt
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
186 changes: 186 additions & 0 deletions seo_optimizer/i18n.py
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()
35 changes: 35 additions & 0 deletions seo_optimizer/locale/en/LC_MESSAGES/django.po
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"
Loading
Loading