Skip to main content

Grammate

Grammate is a lightweight Python module for localization based on formal grammars.


Installation

You can install Grammate via pip:

pip install grammate

Features

  • Custom Formatters For Classes.
  • Complex Grammatical Rules.
  • YAML-based Configuration.
  • Inheritance Support: Locales can inherit settings from other locales, making it easy to define regional variations or dialects.
  • Proxy Locale Support: Allows dynamic locale selection in applications (e.g., Flask).

Limitations

Grammate is not a comprehensive localization library and does not contain any localization knowledge out of the box, such as currencies, number and date formats. You need to implement grammar formatters and modifiers in your code and configuration files.


Basic Usage

YAML Files for Localization

Grammate uses YAML files to store your translations for different locales.

locales/en.yaml

This file contains the translation for the English locale:

greeting: "Hello"

locales/ar.yaml

This file contains the translation for the Arabic locale:

greeting: "أهلاً"

example.py

from grammate import get_locale, setup_locale, get_text

# Set up default locale (English)
setup_locale()

# Get greeting text in the default locale (English)
print(get_text('greeting'))  # Output: Hello

# Get greeting text in Arabic
print(get_locale('ar').get_text('greeting'))  # Output: أهلاً

# Use placeholders in text
print(get_text('[greeting] {name}!', name='Ahmed'))  # Output: Hello Ahmed!

Advanced Usage

Proxy Locale for Dynamic Locale Resolution (e.g., Flask)

You can set up a proxy locale to dynamically resolve the locale for a user, for instance, based on a session in a Flask application.

from grammate import get_locale, setup_locale, get_text, ProxyLocale
from flask import session

class FlaskSessionLocale(ProxyLocale):
    def get_locale(s) -> Locale:
        # Resolve the current locale based on Flask session
        return get_locale(session.get('locale') or 'ar')

# Set up locale with dynamic resolution using Flask's session
setup_locale(FlaskSessionLocale(), default_locale='ar', locales_dir=None)

In this example, FlaskSessionLocale overrides the get_locale method to dynamically return the locale based on the session's locale key, with a fallback to 'ar' if not specified.

Custom Formatters

Grammate supports custom formatters any class. This allows you to localize your own classes. The following example shows how to format a custom HijriDate that implements the __localized_format__ method:

example.py (Custom Date Formatter)

We can create a custom HijriDate class that implements the __localized_format__ method to format Hijri dates.

from grammate import get_locale, setup_locale, get_text
from dataclasses import dataclass

@dataclass
class HijriDate:
    year: int
    month: int
    day: int

    @property
    def weekday(self):
        return 0  # Always Monday :)

    def __localized_format__(self, locale, fmt='long'):
        key = f"date.format.long" if fmt == 'long' else 'date.format.short'
        fmt = locale.get(key, default='%Y-%m-%d')

        fmt = fmt.replace('%A', locale.get(f'date.week_day.{self.weekday}', default='None'))  # Full weekday name
        fmt = fmt.replace('%B', locale.get(f'hijridate.months.{self.month}', default='None'))  # Full month name
        fmt = fmt.replace('%Y', str(self.year))
        fmt = fmt.replace('%m', str(self.month))
        fmt = fmt.replace('%d', str(self.day))

        return fmt

setup_locale()

result = get_locale('ar').get_text("{date:long}", date=HijriDate(1442, 9, 21))
print(result)  # الإثنين، 21 رمضان 1442

locales/ar.yaml (Custom Date Formatter)

date:
  format:
    long: '%A، %d %B %Y'  # Example: الإثنين، 01 يناير 2024
    short: '%d/%m/%Y'     # Example: 01/01/2024
  months:
    1: يناير
    2: فبراير
    3: مارس
  week_day:
    0: الإثنين
    1: الثلاثاء
hijridate:
  months:
    1: محرم
    2: صفر

Custom Formatters for Imported Classes

Grammate also allows you to define formatters for imported classes, like date. You can use the @formatter decorator to create custom formatters for these classes.

from grammate import formatter
from datetime import date

@formatter(date)
def date_formatter(dt, locale, fmt='long'):
    key = f"date.format.long" if fmt == 'long' else 'date.format.short'
    fmt = locale.get(key, default='%Y-%m-%d')

    fmt = fmt.replace('%A', locale.get(f'date.week_day.{dt.weekday()}', default='None'))  # Full weekday name
    fmt = fmt.replace('%B', locale.get(f'date.months.{dt.month}', default='None'))  # Full month name

    return dt.strftime(fmt)

result = get_locale('ar').get_text("{date:long}", date=date(2021, 5, 4))
print(result)  # الثلاثاء، 04 مايو 2021

Configuration Inheritance

By default, a locale such as ar_MA (for Morocco) inherits from a parent locale like ar (Arabic). All locales inherit from the default locale unless otherwise specified.

You can override specific parts of the locale configuration. In the example below, ar_MA.yaml inherits from ar.yaml but customizes the month names for the Moroccan variant.

locales/ar_MA.yaml (inherits from ar.yaml by default)

date:
  months:
    5: ماي
    6: يونيو
    7: يوليوز
    8: غشت
    9: شتنبر
    11: نونبر
    12: دجنبر

Example usage:

result = get_locale('ar_MA').get_text("{date:long}", date=date(2021, 5, 4))
print(result)  # الثلاثاء، 04 ماي 2021

You can also define custom inheritance using the key $extends for the entire configuration or for specific subkeys. For instance:

locales/ur.yaml

$extends: 'en'
hijridate:
  months:
    $extends: 'ar'

Example usage:

result = get_locale('ur').get_text("{date:long}", date=HijriDate(1442, 9, 21))
print(result)  # Monday, رمضان 21, 1442

In this example, the Urdu locale inherits from English, except for the Hijri date months, which are inherited from Arabic.


Custom Grammar Rules

Custom grammar rules can be defined as modifiers. The expression format is [!modifier_id:args], where args are YAML/JSON lists without the brackets. If an argument starts with a $, it is searched in the provider's kwargs.

Example of a simple plural modifier:

@modifier('plural', locale='en')
def plural_en(locale, singular, value, *args):
    form = locale.get(singular, default=singular)
    if value != 1:
        # find the plural form in the locale config or add 's' if not found
        form = locale.get(singular + '.plural', default=None) or f'{form}s'

    return f'{value} {form}'

Usage example:

result = get_text("I have [!plural:apple,$count]!", count=1)
print(result)  # I have 1 apple!

result = get_text("I have [!plural:apple,$count]!", count=2)
print(result)  # I have 2 apples!

Modifiers can be as simple or as complex as needed, and they can request locale configurations based on the passed arguments.

It is also possible to chain modifiers by returning a valid get_text expression. For example:

@modifier('adj', locale='ar')  # Gender should be specified for agreement
def adj_ar(locale, word, adj, *args):
    word = locale.get(word, default=word)
    adj_forms = locale.get(f'{adj}.forms', default={})
    adj = locale.get(adj, default=adj)
    if 'fem' in args:
        adj = adj_forms.get('fem') or adj
    else:
        adj = adj_forms.get('masc') or adj

    return f'{word} {adj}'


@modifier('adj2', locale='ar')  # Gender is resolved from locale configuration then passed to the first modifier
def adj2_ar(locale, word, adj, *args):  # Find gender
    rules = locale.get(f'{word}.rules', default={})
    gender = rules.get('gender', 'masc')
    args = [word, adj, gender]
    return f"[!adj:{json.dumps(args)[1:-1]}]"

result = get_text("عندي [!adj2:$thing,$adj]!", thing='apple', adj='red')
print(result) # عندي تفاحة حمراء!
result = get_text("عندي [!adj2:$thing,$adj]!", thing='notebook', adj='red')
print(result) # عندي دفتر أحمر!

In this example, the adj2 modifier resolves thing's gender from the locale configuration and passes it to the adj modifier, enabling gender-based adjective agreement in the final output.


License

Grammate is released under the MIT License. See the LICENSE file for more details.

Metadata

Release files for grammate 1.0.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for grammate 1.0.0
File Size Uploaded Provenance
grammate-1.0.0.tar.gz 11.9 kB Without provenance Details

Release files / grammate-1.0.0.tar.gz

Download URL grammate-1.0.0.tar.gz
Size 11.9 kB
Tags Source
SHA-256 checksum
How to use checksums
5e4d2feee527720b7bf1ef4e27dcd62c260b2c3d5f06fabefc11109f94b11b2f
BLAKE2b-256 checksum
How to use checksums
ffff6271cbb98c093bdf7a4cba8968d9a707874c92802aad2f242383484cb968
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/6.1.0 CPython/3.10.12

Security for grammate 1.0.0

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

No provenance found

None of the files in this release were uploaded with provenance metadata.

A note on package security

PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.

Release history Release notifications | RSS feed

This release

1.0.0 This release

1 release file No provenance

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page