Coverage for app / backend / src / couchers / i18n / plurals.py: 44%
110 statements
« prev ^ index » next coverage.py v7.13.2, created at 2026-02-03 06:18 +0000
« prev ^ index » next coverage.py v7.13.2, created at 2026-02-03 06:18 +0000
1"""
2Implements Unicode CLDR pluralization rules, used by i18next.
3See https://cldr.unicode.org/index/cldr-spec/plural-rules
4"""
6from collections.abc import Callable
7from enum import Enum
10class PluralCategory(Enum):
11 """
12 Unicode CLDR plural categories, as defined on
13 https://cldr.unicode.org/index/cldr-spec/plural-rules
14 """
16 ZERO = "zero"
17 ONE = "one"
18 TWO = "two"
19 FEW = "few"
20 MANY = "many"
21 OTHER = "other"
24type PluralRule = Callable[[int], PluralCategory]
25"""Selects the plural category for a given count."""
28class PluralRules:
29 """
30 Implements Unicode CLDR language rules for known languages.
31 See https://www.unicode.org/cldr/charts/48/supplemental/language_plural_rules.html
32 """
34 @staticmethod
35 def for_language(lang: str) -> PluralRule | None:
36 separator_index = lang.find("-")
37 lang_family = lang if separator_index == -1 else lang[0:separator_index]
38 # Look up one of the plural rule functions defined below
39 return getattr(PluralRules, lang_family, None)
41 @staticmethod
42 def ca(count: int) -> PluralCategory:
43 """Gets the Catalan plural category for a given count."""
44 return PluralRules.es(count) # Same as ES
46 @staticmethod
47 def cs(count: int) -> PluralCategory:
48 """Gets the Czech plural category for a given count."""
49 count = abs(count)
50 if count == 1:
51 return PluralCategory.ONE
52 if count >= 2 and count <= 4:
53 return PluralCategory.FEW
54 return PluralCategory.OTHER
56 @staticmethod
57 def de(count: int) -> PluralCategory:
58 """Gets the German plural category for a given count."""
59 return PluralRules.en(count) # Same as EN
61 @staticmethod
62 def en(count: int) -> PluralCategory:
63 """Gets the English plural category for a given count."""
64 count = abs(count)
65 if count == 1:
66 return PluralCategory.ONE # 1 apple
67 return PluralCategory.OTHER # 2 apples
69 @staticmethod
70 def es(count: int) -> PluralCategory:
71 """Gets the Spanish plural category for a given count."""
72 count = abs(count)
73 if count == 1:
74 return PluralCategory.ONE # 1 manzana
75 if count > 0 and count % 1_000_000 == 0:
76 return PluralCategory.MANY # 1000000 de manzanas
77 return PluralCategory.OTHER # 2 manzanas
79 @staticmethod
80 def fr(count: int) -> PluralCategory:
81 """Gets the French plural category for a given count."""
82 count = abs(count)
83 if count == 0 or count == 1:
84 return PluralCategory.ONE # 0 pomme, 1 pomme
85 if count % 1_000_000 == 0:
86 return PluralCategory.MANY # 1000000 de pommes
88 return PluralCategory.OTHER # 2 pommes
90 @staticmethod
91 def he(count: int) -> PluralCategory:
92 """Gets the Hebrew plural category for a given count."""
93 count = abs(count)
94 if count == 1:
95 return PluralCategory.ONE
96 if count == 2:
97 return PluralCategory.TWO
98 return PluralCategory.OTHER
100 @staticmethod
101 def hi(count: int) -> PluralCategory:
102 """Gets the Hindi plural category for a given count."""
103 count = abs(count)
104 if count <= 1:
105 return PluralCategory.ONE
106 return PluralCategory.OTHER
108 @staticmethod
109 def hu(count: int) -> PluralCategory:
110 """Gets the Hungarian plural category for a given count."""
111 return PluralRules.en(count) # Same as EN
113 @staticmethod
114 def it(count: int) -> PluralCategory:
115 """Gets the Italian plural category for a given count."""
116 return PluralRules.es(count) # Same as ES
118 @staticmethod
119 def ja(count: int) -> PluralCategory:
120 """Gets the Japanese plural category for a given count."""
121 return PluralCategory.OTHER
123 @staticmethod
124 def nl(count: int) -> PluralCategory:
125 """Gets the Dutch plural category for a given count."""
126 return PluralRules.en(count) # Same as EN
128 @staticmethod
129 def pl(count: int) -> PluralCategory:
130 """Gets the Polish plural category for a given count."""
131 count = abs(count)
132 if count == 1:
133 return PluralCategory.ONE
134 if count % 10 in range(2, 5) and count % 100 not in range(12, 15):
135 return PluralCategory.FEW
136 return PluralCategory.MANY # "Other" is reserved for decimals
138 @staticmethod
139 def pt(count: int) -> PluralCategory:
140 """Gets the Portuguese plural category for a given count."""
141 return PluralRules.fr(count) # Same as FR
143 @staticmethod
144 def ru(count: int) -> PluralCategory:
145 """Gets the Russian plural category for a given count."""
146 count = abs(count)
147 if count % 10 == 1 and count % 100 != 11:
148 return PluralCategory.ONE
149 elif count % 10 in range(2, 5) and count % 100 not in range(12, 15):
150 return PluralCategory.FEW
151 return PluralCategory.MANY # OTHER is only for numbers with decimal separator.
153 @staticmethod
154 def sv(count: int) -> PluralCategory:
155 """Gets the Swedish plural category for a given count."""
156 return PluralRules.en(count) # Same as EN
158 @staticmethod
159 def tr(count: int) -> PluralCategory:
160 """Gets the Turkish plural category for a given count."""
161 return PluralRules.en(count) # Same as EN
163 @staticmethod
164 def uk(count: int) -> PluralCategory:
165 """Gets the Ukrainian plural category for a given count."""
166 return PluralRules.ru(count) # Same as RU
168 @staticmethod
169 def zh(count: int) -> PluralCategory:
170 """Gets the Chinese plural category for a given count."""
171 return PluralCategory.OTHER