feat: add Polish and pluralization support

- Update EnglishLocale schema to support dictionary-based pluralization for months and years using 'str | dict[str, str]'.
- Implement a centralized plural rule registry in plural_rules.py following Unicode CLDR standards.
- Refactor compute_time_span_string in date.py to select unit labels based on language-specific numeric rules.
- Add the Polish locale with grammatically correct paucal (few) and genitive plural (many) forms.
This commit is contained in:
Julia Rzymowska
2026-03-01 14:01:05 +01:00
committed by Sina Atalay
parent 8961744ebd
commit c3ae80c54d
5 changed files with 770 additions and 107 deletions

View File

File diff suppressed because it is too large Load Diff

View File

@@ -6,6 +6,7 @@ from rendercv.schema.models.cv.entries.bases.entry_with_complex_fields import (
)
from rendercv.schema.models.locale.locale import Locale
from .plural_rules import get_plural_rules
from .string_processor import substitute_placeholders
@@ -228,6 +229,50 @@ def compute_time_span_string(
Returns:
Formatted time span string with years and months.
"""
def _get_localized_label(
count: int,
singular_label: str,
plural_data: str | dict[str, str],
lang_iso: str,
) -> str:
"""Select the correct localized label based on count and language plural rules.
Why:
This helper function returns the appropriate singular, plural, or language-specific
plural form of a label based on the count value and the target language's pluralization rules.
Args:
count: The quantity used to determine which plural form to use.
singular_label: The label to return when count equals 1.
plural_data: Either a string (for simple plural forms) or a dictionary mapping
plural categories ('one', 'few', 'many', etc.) to their localized labels.
lang_iso: ISO 639-1 language code (e.g., 'en', 'de', 'ru') used to determine
plural rules and categories.
Returns:
The appropriate localized label (string) for the given count and language.
Returns an empty string if count is 0.
Returns singular_label if count is 1.
Returns the language-specific plural form from plural_data if count > 1.
"""
if count == 0:
return ""
if count == 1:
return singular_label
if isinstance(plural_data, dict):
# Determine the category tag (one, few, many)
category = get_plural_rules(count, lang_iso)
# Return the specific form, or 'many' as a fallback
return plural_data.get(category, plural_data.get("many", ""))
# Fallback for standard string-based locales (english, german, etc.)
return plural_data
lang_iso = locale.language_iso_639_1
if isinstance(start_date, int) or isinstance(end_date, int):
# Then it means one of the dates is year, so time span cannot be more
# specific than years.
@@ -236,15 +281,12 @@ def compute_time_span_string(
time_span_in_years = end_year - start_year
if time_span_in_years < 2:
how_many_years = "1"
locale_years = locale.year
else:
how_many_years = str(time_span_in_years)
locale_years = locale.years
locale_years = _get_localized_label(
time_span_in_years, locale.year, locale.years, lang_iso
)
placeholders: dict[str, str] = {
"HOW_MANY_YEARS": how_many_years,
"HOW_MANY_YEARS": str(time_span_in_years),
"YEARS": locale_years,
"HOW_MANY_MONTHS": "",
"MONTHS": "",
@@ -268,31 +310,23 @@ def compute_time_span_string(
how_many_months %= 12
# Format the number of years and months between start_date and end_date:
if how_many_years == 0:
locale_years = _get_localized_label(
how_many_years, locale.year, locale.years, lang_iso
)
if locale_years == "":
how_many_years = ""
locale_years = ""
elif how_many_years == 1:
how_many_years = "1"
locale_years = locale.year
else:
how_many_years = str(how_many_years)
locale_years = locale.years
# Format the number of months between start_date and end_date:
if how_many_months == 0:
locale_months = _get_localized_label(
how_many_months, locale.month, locale.months, lang_iso
)
if locale_months == "":
how_many_months = ""
locale_months = ""
elif how_many_months == 1:
how_many_months = "1"
locale_months = locale.month
else:
how_many_months = str(how_many_months)
locale_months = locale.months
placeholders = {
"HOW_MANY_YEARS": how_many_years,
"HOW_MANY_YEARS": str(how_many_years),
"YEARS": locale_years,
"HOW_MANY_MONTHS": how_many_months,
"HOW_MANY_MONTHS": str(how_many_months),
"MONTHS": locale_months,
}
return substitute_placeholders(time_span_template, placeholders)

View File

@@ -0,0 +1,57 @@
# https://www.unicode.org/cldr/charts/48/supplemental/language_plural_rules.html
def polish_rule(count: int) -> str:
if count == 1:
return "one"
if 2 <= count % 10 <= 4 and not (12 <= count % 100 <= 14):
return "few"
return "many"
# Registry mapping ISO codes to rule functions
PLURAL_RULES = {
"pl": polish_rule,
# add here new set of rules
}
def default_rule(n: int) -> str:
"""Fallback rule for simple singular/plural languages."""
return "one" if n == 1 else "other"
def get_plural_rules(count: int, language_code: str):
"""Determine the appropriate CLDR (Unicode Common Locale Data Repository) plural category
for a given count and language code.
Why:
This function returns the grammatical plural category that should be used for a specific number in a given language. Different languages have different plural rules - for example, English has two forms (singular/plural), while Polish has three, and Arabic has six.
Example:
```py
>>> get_plural_rules(1, "en")
'one'
>>> get_plural_rules(5, "en")
'other'
```
Args:
count (int): The number for which to determine the plural category.
language_code (str): The ISO language code (e.g., 'en', 'pl', 'ar') identifying
which language's plural rules to apply.
Returns:
str: The CLDR plural category, typically one of: 'zero', 'one', 'two', 'few', 'many', or 'other'.
The exact categories available depend on the language's plural rules.
Note:
- This function relies on a PLURAL_RULES dictionary that maps language codes to their
respective plural rule functions.
- If the language_code is not found in PLURAL_RULES, a default_rule function is used
as a fallback.
- CLDR plural categories are used for proper localization and internationalization (i18n) of text that contains numbers.
"""
rule = PLURAL_RULES.get(language_code, default_rule)
return rule(count)

View File

@@ -33,7 +33,7 @@ class EnglishLocale(BaseModelWithoutExtraKeys):
default="month",
description='Translation of "month" (singular). The default value is `month`.',
)
months: str = pydantic.Field(
months: str | dict[str, str] = pydantic.Field(
default="months",
description='Translation of "months" (plural). The default value is `months`.',
)
@@ -41,7 +41,7 @@ class EnglishLocale(BaseModelWithoutExtraKeys):
default="year",
description='Translation of "year" (singular). The default value is `year`.',
)
years: str = pydantic.Field(
years: str | dict[str, str] = pydantic.Field(
default="years",
description='Translation of "years" (plural). The default value is `years`.',
)
@@ -131,6 +131,7 @@ class EnglishLocale(BaseModelWithoutExtraKeys):
"arabic": "ar",
"hebrew": "he",
"persian": "fa",
"polish": "pl",
}[self.language]
@functools.cached_property

View File

@@ -0,0 +1,41 @@
# yaml-language-server: $schema=../../../../../../schema.json
locale:
language: polish
last_updated: "Ostatnia aktualizacja"
month: "miesiąc"
months:
one: "miesiąc"
few: "miesiące"
many: "miesięcy"
year: "rok"
years:
one: "rok"
few: "lata"
many: "lat"
present: "obecnie"
month_abbreviations:
- Sty
- Lut
- Mar
- Kwi
- Maj
- Cze
- Lip
- Sie
- Wrz
- Paź
- Lis
- Gru
month_names:
- Styczeń
- Luty
- Marzec
- Kwiecień
- Maj
- Czerwiec
- Lipiec
- Sierpień
- Wrzesień
- Październik
- Listopad
- Grudzień