Polska odmiana rzeczowników w Hugo
• tech • 1281 słów • 7 minut czytania
Po ogarnięciu polskich nazw miesięcy w Hugo przyszła pora na odmiany rzeczowników przy liczebnikach i odpowiednią formę liczby mnogiej. Poza wsparciem tego w tłumaczeniach, Hugo nie posiada żadnych innych pomocnych mechanizmów. Żeby ogarnąć poprawną formę i odmianę dla naszego rodzimego języku to trzeba trochę pokombinować…
Na początku miałem problem z przypomnieniem sobie fachowej nazwę tej reguły, aby o to zapytać wyszukiwarkę. Wiedziałem tylko, że kiedyś coś takiego widziałem przy tłumaczeniach z wykorzystaniem gettetxt
-a i plików PO. W końcu trafiłem na poszukiwane informacje o “składni liczebników” na stronie Rady Języka Polskiego:
[…] liczebniki 2, 3, 4 oraz liczebniki, których ostatnim członem jest 2, 3, 4 (czyli np. 22, 23, 24, 152, 153, 154 itd.) łączą się z rzeczownikami w mianowniku liczby mnogiej, np. trzy koty, dwadzieścia cztery koty, sto pięćdziesiąt dwa koty. Liczebniki od 5 do 21 i te, które są zakończone na 5-9 (np. 25, 36, 27, 58, 69), łączą się z rzeczownikiem w dopełniaczu liczby mnogiej, np. pięć kotów, siedemnaście kotów, sto siedemdziesiąt siedem kotów […]
Niechciałem samemu przekładać tego na kod, aby nie popełnić czasem jakiegoś głupiego błędu lub nie pominąć szczególnego przypadku. Posłużyłem się znów wspomnianym gettextem. Tam w regułach Plural-Forms można znaleźć taki oto kod dla języka polskiego:
Plural-Forms: nplurals=3; \
plural=n==1 ? 0 : \
n%10>=2 && n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2;
Tłumacząc ten warunek na bardziej czytelny zapis funkcyjny w C++ otrzymujemy:
enum PluralForms { One, Few, Many };
PluralForms GetPluralForm(int n) {
if (n == 1)
return One;
if (n % 10 >= 2 && n % 10 <= 4 && (n % 100 < 10 || n % 100 >= 20))
return Few;
return Many;
}
W takiej formie postanowiłem zasymulować to jako partial-function w szablonach Hugo.
Logikę udało mi się w miarę łatwo przetransformować pomimo ograniczonych zasobów szablonowych Go. Zawartość pliku partials/func/GetPluralForm.html
przedstawia się nastepująco:
{{/*
Get the correct plural form according to the rules of Polish language
usage: $plural := partial "func/GetPluralForm" <num>
return 0 /one/, 1 /few/ or 2 /many/
*/}}
{{ $ret := 2 }}
{{ $val := . }}
{{ if eq $val 1 }}
{{ $ret = 0 }} {{/* one */}}
{{ else }}
{{/* val % 10 >= 2 && val % 10 <= 4 && (val % 100 < 10 || val % 100 >= 20) */}}
{{ $mod10 := mod $val 10 }}
{{ $mod100 := mod $val 100 }}
{{ if and (and (ge $mod10 2) (le $mod10 4)) (or (lt $mod100 10) (ge $mod100 20)) }}
{{ $ret = 1 }} {{/* few */}}
{{ end }}
{{/* else: 2 */}} {{/* many */}}
{{ end }}
{{ return $ret }}
Taki szablon cząstkowy zwraca identyfikator poprawnej formy odmiany dla podanej liczby - wartość 0
(one), 1
(few) i 2
(many). Wykorzystać to można w szablonie do warunkowego renderowania zawartości strony:
{{- $plural := partial "func/GetPluralForm" .ReadingTime }}
{{- if eq $plural 0 }} {{/* one */}}
{{ .ReadingTime }} minuta
{{- else if eq $plural 1 }} {{/* few */}}
{{ .ReadingTime }} minuty
{{- else }} {{/* many */}}
{{ .ReadingTime }} minut
{{- end }}
Powyższy kod może nie jest dobrym przykładem, ale w rozbudowanych szablonach, gdy dla różnych form przewidujemy większe zmiany w designie taka mozliwość może być przydatna. Chociaż w większości przypadków użycie będzie ograniczone do wyboru odpowiedniej wersji stringa. Wtedy można zapisać to w nieco mniej “rozlazłej” postaci:
{{- $plurals := slice "minuta" "minuty" "minut" }}
{{- $plural := partial "func/GetPluralForm" .ReadingTime }}
{{ .ReadingTime }} {{ index $plurals $plural }}
Całość można jeszcze bardziej skompresować definiując niektóre elementy inline w miejscu wywołania… ale to i tak nie spełnia moich oczekiwań. Do wyboru będę miał dwa warianty - długa i mało czytelna (na pierwszy rzut oka) linijka kodu, albo czytelny kod z dodatkowymi zmiennymi w “szablonowej” przestrzeni nazw. A ja chciałbym uniknąć obu tych opcji.
Chcę mieć krótki i zwięzły kawałek kodu w szablonie, bo w końcu będę “odmieniał” tylko kilka słówek. To wymaga napisania sobie dodatkowego helper-a. Przejmie on na siebie całą logikę i formatowanie stringa, aby w szablonie, w miejscu renderowania można było ograniczyć się do przekazania tylko niezbędnych parametrów:
{{ partial "func/PluralFormat" (slice .WordCount "%d słowo" "%d słowa" "%d słów") }}
Implementacja takiej funkcji (partial-a) jest prosta. Mój plik partials/func/PluralFormat.html
:
{{/*
Return formatted string with the correct plural form...
usage: partial "func/PluralFormat" <slice num "one" "few" "many">
*/}}
{{ $num := index . 0 }}
{{ $plr := partial "func/GetPluralForm" $num }}
{{ $fmt := index . (add $plr 1) }}
{{ return printf $fmt $num }}
Wynikowy string formatowany jest za pomocą funkcji printf
, zatem przekazywane literały muszą zawierać jedno pole formatujące liczbę - na przykład %d
. Wynika to ze sposobu obsługi błędów w funkcjach formatujących IO z pakietu fmt języka Go, z których korzysta Hugo. W razie błędu do wynikowego stringa doklejone zostaną dodatkowe informacje.
Dotyczy to także niezgodności liczby przekazanych argumentów ze specyfikowanymi polami w ciągu formatującym. Nie zostaną zignorowane nadmiarowe argumenty i w przypadku takiego wywołania:
fmt.Printf("Za dużo słów...", num) // num := 123
zostanie wyplute coś w stylu:
Za dużo słów...%!(EXTRA int=123)
Ta “drobna” niedogodność mi nie przeszkadza, bo aktualnie i tak zawsze wyświetlam liczbę wraz z poprawnie odmienionym rzeczownikiem. Ale gdyby ktoś miał inne potrzeby to należy ten przypadek jakoś oprogramować lub po prostu użyć warunkowego renderowania ;)
Teraz zasadnicze pytanie: po co się tak meczę zamiast skorzystać z i18n? Na to odpowiedź można po części znaleźć w poprzednim wpisie. Ta strona zawsze bedzie prowadzona w ojczystym języku, więc użycie jakiegokolwiek modułu do lokalizacji, aby dokonać odmiany kilku słówek w szablonie jest moim zdaniem wielkim overkill-em.
Sprawa przedstawia się zgoła inaczej, gdy ktoś prowadzi wielojęzyczną stronę lub bloga, czy to z tłumaczonym kontentem, czy tylko wielojęzykowym interfejsem, to oczywiste dla niego powinno być wykorzystanie lokalizacji.
Hugo używa go-i18n do tłumaczeń stringów, a moduł ten radzi sobie bardzo dobrze z liczbą mnogą w ponad 200 językach. Mając w polskim pliku tłumaczeń (i18n/pl.yaml
) zapisane odpowiednie formy dla danego identyfikatora:
- id: words
translation:
one: "{{ .Count }} słowo"
few: "{{ .Count }} słowa"
many: "{{ .Count }} słów"
w szablonie wystarczy skorzystać z funkcji i18n
:
{{ i18n "words" .WordCount }}
aby w rezultacie otrzymać to samo co robią moje genialne szablony cząstkowe…
[dodano 2020-07-31 17:00]
Znalazłem mały błąd w obsłudze partial templates przy zwracaniu wartości w Hugo, który występuje przy przekazywaniu wartości 0
jako argumentu szablonu. Szczegóły w opisie zgłoszonego błędu (#7528).
Problem ten psuje przedstawione tutaj “funkcje”. Dla przypadków z liczebnikiem 0
zwracana wartość będzie nil
zamiast spodziewanej 2
(dla liczby 0
poprawna forma to ta sama co dla “wiele”). Tymczasowo, do czasu naprawy błędu, można zastosować mały workaround na zwracaną wartość z GetPluralForm
:
{{ $plr := partial "func/GetPluralForm" $num }}
{{/* workaround for bug #7528 */}}
{{ $plr = cond (eq $plr nil) 2 $plr }}
Mam jednak nadzieję, że szybko pojawi się fix, bo to nieco psuje moją wizję krótkiego i zwięzłego kodu ;)
[dodano 2020-09-20 18:00]
W nowej wersji Hugo (0.75) dodano zwracanie ERROR
-a w stylu “error calling partial: partials that returns a value needs a non-zero argument” w sytuacji przekazania 0 do partial templates. Zatem, aby budowanie strony przebiegło pomyślnie należy zastosować zaktualizowany workaround:
{{/* workaround for bug #7528 */}}
{{ $tmp := cond (eq $num 0) 5 $num }}
{{ $plr := partial "func/GetPluralForm" $tmp }}
Po prostu podmieniamy wartość liczby z 0
na 5
, aby nie przekazywać zera do funkcji. Finalnie i tak otrzymamy poprawny wynik, bo obie te liczby używają formy “wiele”.
[dodano 2021-12-30 19:30]
Wydano Hugo w wersji 0.91, w której wreszcie naprawiono błąd #7528 i nie trzeba już stosować hacka, bo przekazywane argumenty poprawnie są “interpretowane”. W kodzie bloga usunąłem już obejście tego problemu.
Komentarze (0)