mirror of
https://github.com/rendercv/rendercv.git
synced 2026-04-25 01:23:43 -04:00
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:
committed by
Sina Atalay
parent
8961744ebd
commit
c3ae80c54d
690
schema.json
690
schema.json
File diff suppressed because it is too large
Load Diff
@@ -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)
|
||||
|
||||
57
src/rendercv/renderer/templater/plural_rules.py
Normal file
57
src/rendercv/renderer/templater/plural_rules.py
Normal 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)
|
||||
@@ -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
|
||||
|
||||
41
src/rendercv/schema/models/locale/other_locales/polish.yaml
Normal file
41
src/rendercv/schema/models/locale/other_locales/polish.yaml
Normal 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ń
|
||||
Reference in New Issue
Block a user