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

1""" 

2Implements Unicode CLDR pluralization rules, used by i18next. 

3See https://cldr.unicode.org/index/cldr-spec/plural-rules 

4""" 

5 

6from collections.abc import Callable 

7from enum import Enum 

8 

9 

10class PluralCategory(Enum): 

11 """ 

12 Unicode CLDR plural categories, as defined on 

13 https://cldr.unicode.org/index/cldr-spec/plural-rules 

14 """ 

15 

16 ZERO = "zero" 

17 ONE = "one" 

18 TWO = "two" 

19 FEW = "few" 

20 MANY = "many" 

21 OTHER = "other" 

22 

23 

24type PluralRule = Callable[[int], PluralCategory] 

25"""Selects the plural category for a given count.""" 

26 

27 

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 """ 

33 

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) 

40 

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 

45 

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 

55 

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 

60 

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 

68 

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 

78 

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 

87 

88 return PluralCategory.OTHER # 2 pommes 

89 

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 

99 

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 

107 

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 

112 

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 

117 

118 @staticmethod 

119 def ja(count: int) -> PluralCategory: 

120 """Gets the Japanese plural category for a given count.""" 

121 return PluralCategory.OTHER 

122 

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 

127 

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 

137 

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 

142 

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. 

152 

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 

157 

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 

162 

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 

167 

168 @staticmethod 

169 def zh(count: int) -> PluralCategory: 

170 """Gets the Chinese plural category for a given count.""" 

171 return PluralCategory.OTHER