@mideind/netskrafl-react 1.9.0 → 2.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/esm/index.js CHANGED
@@ -27509,6 +27509,54 @@ const requestMoves = (state, options) => {
27509
27509
  });
27510
27510
  };
27511
27511
 
27512
+ /*
27513
+
27514
+ Types.ts
27515
+
27516
+ Common type definitions for the Explo/Netskrafl user interface
27517
+
27518
+ Copyright (C) 2025 Miðeind ehf.
27519
+ Author: Vilhjalmur Thorsteinsson
27520
+
27521
+ The Creative Commons Attribution-NonCommercial 4.0
27522
+ International Public License (CC-BY-NC 4.0) applies to this software.
27523
+ For further information, see https://github.com/mideind/Netskrafl
27524
+
27525
+ */
27526
+ // Global constants
27527
+ const RACK_SIZE = 7;
27528
+ const ROWIDS = 'ABCDEFGHIJKLMNO';
27529
+ const BOARD_SIZE = ROWIDS.length;
27530
+ const EXTRA_WIDE_LETTERS = 'q';
27531
+ const WIDE_LETTERS = 'zxmæ';
27532
+ const ZOOM_FACTOR = 1.5;
27533
+ const GATA_DAGSINS_MIN_DATE = '2025-11-01';
27534
+ const ERROR_MESSAGES = {
27535
+ // Translations are found in /static/assets/messages.json
27536
+ 1: 'Enginn stafur lagður niður',
27537
+ 2: 'Fyrsta orð verður að liggja um byrjunarreitinn',
27538
+ 3: 'Orð verður að vera samfellt á borðinu',
27539
+ 4: 'Orð verður að tengjast orði sem fyrir er',
27540
+ 5: 'Reitur þegar upptekinn',
27541
+ 6: 'Ekki má vera eyða í orði',
27542
+ 7: 'word_not_found',
27543
+ 8: 'word_not_found',
27544
+ 9: 'Of margir stafir lagðir niður',
27545
+ 10: 'Stafur er ekki í rekkanum',
27546
+ 11: 'Of fáir stafir eftir, skipting ekki leyfð',
27547
+ 12: 'Of mörgum stöfum skipt',
27548
+ 13: 'Leik vantar á borðið - notið F5/Refresh',
27549
+ 14: 'Notandi ekki innskráður - notið F5/Refresh',
27550
+ 15: 'Rangur eða óþekktur notandi',
27551
+ 16: 'Viðureign finnst ekki',
27552
+ 17: 'Viðureign er ekki utan tímamarka',
27553
+ 18: 'Netþjónn gat ekki tekið við leiknum - reyndu aftur',
27554
+ 19: 'Véfenging er ekki möguleg í þessari viðureign',
27555
+ 20: 'Síðasti leikur er ekki véfengjanlegur',
27556
+ 21: 'Aðeins véfenging eða pass leyfileg',
27557
+ server: 'Netþjónn gat ekki tekið við leiknum - reyndu aftur',
27558
+ };
27559
+
27512
27560
  /*
27513
27561
 
27514
27562
  Audio.ts
@@ -27636,52 +27684,2073 @@ function getAudioManager(state, soundUrls) {
27636
27684
  return audioManager;
27637
27685
  }
27638
27686
 
27687
+ var Leikir = {
27688
+ en: "Moves",
27689
+ nb: "Trekk",
27690
+ nn: "Trekk",
27691
+ pl: "Ruchy",
27692
+ ga: "Bogann"
27693
+ };
27694
+ var submit_move = {
27695
+ is: "Leika",
27696
+ en: "Move",
27697
+ nb: "Trekk",
27698
+ nn: "Trekk",
27699
+ pl: "Wykonaj ruch",
27700
+ ga: "Bog"
27701
+ };
27702
+ var Spjall = {
27703
+ en: "Chat",
27704
+ nb: "Chat",
27705
+ nn: "Chat",
27706
+ pl: "Czat",
27707
+ ga: "Comhrá"
27708
+ };
27709
+ var player_info = {
27710
+ is: "Upplýsingar um leikmann",
27711
+ en: "Player information",
27712
+ nb: "Spillerinformasjon",
27713
+ nn: "Spelarinformasjon",
27714
+ pl: "Informacje o graczu",
27715
+ ga: "Eolas Imreoir"
27716
+ };
27717
+ var explain_email = {
27718
+ is: "Ef póstfang er gefið upp mun Netskrafl geta sent tölvupóst þegar þú átt leik",
27719
+ en: "If given, Explo can send e-mail to the address when it's your turn in a game",
27720
+ nb: "Hvis oppgitt, kan Explo sende e-post til adressen når det er din tur i et spill",
27721
+ nn: "Viss oppgjeve, kan Explo sende e-post til adressa når det er din tur i eit spel",
27722
+ pl: "Jeśli podano, Explo może wysłać e-mail na ten adres, gdy nadejdzie Twoja kolej w grze",
27723
+ ga: "Más tugadh é, is féidir le Explo ríomhphost a sheoladh chuig an seoladh nuair atá sé do sheal sa chluiche"
27724
+ };
27725
+ var explain_sound = {
27726
+ is: "Stillir hvort hljóðmerki heyrast t.d. þegar andstæðingur leikur og þegar sigur vinnst",
27727
+ en: "Controls whether sounds are played, e.g. when an opponent makes a move or when you win",
27728
+ nb: "Kontrollerer om lyder spilles, f.eks. når en motstander gjør et trekk eller når du vinner",
27729
+ nn: "Kontrollerer om lydar blir spelte, t.d. når ein motstandar gjer eit trekk eller når du vinn",
27730
+ pl: "Kontroluje, czy dźwięki są odtwarzane, np. gdy przeciwnik wykonuje ruch lub gdy wygrywasz",
27731
+ ga: "Rialaíonn sé an seinntear fuaimeanna, m.sh. nuair a bhogann freasúra nó nuair a bhuaigh tú"
27732
+ };
27733
+ var Vista = {
27734
+ en: "Save",
27735
+ nb: "Lagre",
27736
+ nn: "Lagre",
27737
+ pl: "Zapisz",
27738
+ ga: "Sábháil"
27739
+ };
27740
+ var no_helpers = {
27741
+ is: "Með því að velja þessa merkingu lýsir þú því yfir að þú\nskraflir við aðra leikmenn ",
27742
+ en: "By selecting this option, you declare that you play\nagainst other humans ",
27743
+ nb: "Ved å velge dette alternativet, erklærer du at du spiller\nmot andre mennesker ",
27744
+ nn: "Ved å velje dette, stadfester du at du spelar\nmot andre menneske ",
27745
+ pl: "Wybierając tę opcję, deklarujesz, że grasz\nprzeciwko innym ludziom ",
27746
+ ga: "Trí an rogha seo a roghnú, dearbhaíonn tú go n-imríonn tú\ni gcoinne daoine eile "
27747
+ };
27748
+ var Ferill = {
27749
+ en: "History",
27750
+ nb: "Historikk",
27751
+ nn: "Historikk",
27752
+ pl: "Historia",
27753
+ ga: "Stair"
27754
+ };
27755
+ var opp_move = {
27756
+ is: "{opponent} á leik",
27757
+ en: "{opponent}'s turn",
27758
+ nb: "{opponent}s tur",
27759
+ nn: "{opponent} sin tur",
27760
+ pl: "Tura {opponent}",
27761
+ ga: "Seal {opponent}"
27762
+ };
27763
+ var Framvinda = {
27764
+ en: "Progress",
27765
+ nb: "Fremgang",
27766
+ nn: "Framgang",
27767
+ pl: "Postęp",
27768
+ ga: "Dul chun cinn"
27769
+ };
27770
+ var Keppnishamur = {
27771
+ en: "Pro mode",
27772
+ nb: "Pro-modus",
27773
+ nn: "Pro-modus",
27774
+ pl: "Tryb Pro",
27775
+ ga: "Modh Pro"
27776
+ };
27777
+ var with_clock = {
27778
+ is: "Með klukku, 2 x {duration} mínútur",
27779
+ en: "With clock, 2 x {duration} minutes",
27780
+ nb: "Med klokke, 2 x {duration} minutter",
27781
+ nn: "Med klokke, 2 x {duration} minutt",
27782
+ pl: "Z zegarem, 2 x {duration} minut",
27783
+ ga: "Le clog, 2 x {duration} nóiméad"
27784
+ };
27785
+ var Hafna = {
27786
+ en: "Decline",
27787
+ nb: "Avslå",
27788
+ nn: "Avslå",
27789
+ pl: "Odrzuć",
27790
+ ga: "Diúltaigh"
27791
+ };
27792
+ var Afturkalla = {
27793
+ en: "Retract",
27794
+ nb: "Trekk tilbake",
27795
+ nn: "Trekk tilbake",
27796
+ pl: "Wycofaj",
27797
+ ga: "Tarraing siar"
27798
+ };
27799
+ var Hvernig = {
27800
+ en: "How",
27801
+ nb: "Hvordan",
27802
+ nn: "Korleis",
27803
+ pl: "Jak",
27804
+ ga: "Conas"
27805
+ };
27806
+ var Sigur = {
27807
+ en: "Win",
27808
+ nb: "Seier",
27809
+ nn: "Siger",
27810
+ pl: "Zwycięstwo",
27811
+ ga: "Bua"
27812
+ };
27813
+ var Jafntefli = {
27814
+ en: "Draw",
27815
+ nb: "Uavgjort",
27816
+ nn: "Uavgjort",
27817
+ pl: "Remis",
27818
+ ga: "Tarraingt"
27819
+ };
27820
+ var Tap = {
27821
+ en: "Loss",
27822
+ nb: "Tap",
27823
+ nn: "Tap",
27824
+ pl: "Porażka",
27825
+ ga: "Caill"
27826
+ };
27827
+ var Elo = {
27828
+ is: "Elo",
27829
+ en: "Elo",
27830
+ nb: "Elo",
27831
+ nn: "Elo",
27832
+ pl: "Elo",
27833
+ ga: "Elo"
27834
+ };
27835
+ var Lengd = {
27836
+ en: "Duration",
27837
+ nb: "Varighet",
27838
+ nn: "Varigheit",
27839
+ pl: "Czas trwania",
27840
+ ga: "Fad"
27841
+ };
27842
+ var Einkenni = {
27843
+ en: "Identifier",
27844
+ nb: "Identifikator",
27845
+ nn: "Identifikator",
27846
+ pl: "Identyfikator",
27847
+ ga: "Aitheantóir"
27848
+ };
27849
+ var stafur = {
27850
+ en: "letter",
27851
+ nb: "bokstav",
27852
+ nn: "bokstav",
27853
+ pl: "litera",
27854
+ ga: "litir"
27855
+ };
27856
+ var Senda = {
27857
+ en: "Send",
27858
+ nb: "Send",
27859
+ nn: "Send",
27860
+ pl: "Wyślij",
27861
+ ga: "Seol"
27862
+ };
27863
+ var Pass = {
27864
+ en: "Pass",
27865
+ nb: "Pass",
27866
+ nn: "Pass",
27867
+ pl: "Pas",
27868
+ ga: "Pas"
27869
+ };
27870
+ var letter = {
27871
+ is: "staf",
27872
+ en: "letter",
27873
+ nb: "bokstav",
27874
+ nn: "bokstav",
27875
+ pl: "litera",
27876
+ ga: "litir"
27877
+ };
27878
+ var letters = {
27879
+ is: "stafi",
27880
+ en: "letters",
27881
+ nb: "bokstaver",
27882
+ nn: "bokstavar",
27883
+ pl: "litery",
27884
+ ga: "litreacha"
27885
+ };
27886
+ var exchanged = {
27887
+ is: "Skipti um {numtiles} {letters}",
27888
+ en: "Exchanged {numtiles} {letters}",
27889
+ nb: "Byttet ut {numtiles} {letters}",
27890
+ nn: "Bytte ut {numtiles} {letters}",
27891
+ pl: "Wymieniono {numtiles} {letters}",
27892
+ ga: "Mhalartaithe {numtiles} {letters}"
27893
+ };
27894
+ var click_on_game = {
27895
+ is: " - smelltu á viðureign til að skoða stöðuna og leika ef ",
27896
+ en: " - click on a game to view it and make a move if ",
27897
+ nb: " - klikk på et spill for å åpne det og gjøre et trekk hvis ",
27898
+ nn: " - klikk på eit spel for å opne det og gjere eit trekk viss ",
27899
+ pl: " - kliknij na grę, aby ją zobaczyć i wykonać ruch, jeśli ",
27900
+ ga: " - cliceáil ar chluiche chun é a fheiceáil agus bogadh má "
27901
+ };
27902
+ var click_on_challenge = {
27903
+ is: " - smelltu á áskorun til að taka henni og hefja viðureign, eða á ",
27904
+ en: " - click on a challenge to accept it and start a game, or on ",
27905
+ nb: " - klikk på en utfordring for å akseptere den og starte et spill, eller på ",
27906
+ nn: " - klikk på ei utfordring for å akseptere ho og starte eit spel, eller på ",
27907
+ pl: " - kliknij na wyzwanie, aby je zaakceptować i rozpocząć grę, lub na ",
27908
+ ga: " - cliceáil ar dhúshlán chun glacadh leis agus cluiche a thosú, nó ar "
27909
+ };
27910
+ var click_to_review = {
27911
+ is: " - smelltu á viðureign til að skoða hana og rifja upp",
27912
+ en: " - click on a game to review it",
27913
+ nb: " - klikk på et spill for å åpne det og gå gjennom det",
27914
+ nn: " - klikk på eit spel for å opne det og gå gjennom det",
27915
+ pl: " - kliknij na grę, aby ją przejrzeć",
27916
+ ga: " - cliceáil ar chluiche chun athbhreithniú a dhéanamh air"
27917
+ };
27918
+ var Loka = {
27919
+ en: "Close",
27920
+ nb: "Lukk",
27921
+ nn: "Lukk",
27922
+ pl: "Zamknij",
27923
+ ga: "Dún"
27924
+ };
27925
+ var opponent_emptied_rack = {
27926
+ is: "Andstæðingur tæmdi rekkann - þú getur véfengt eða sagt pass",
27927
+ en: "Your opponent emptied the rack - you can challenge or pass",
27928
+ nb: "Motstanderen tømte stativet - du kan utfordre eller passere",
27929
+ nn: "Motstandaren tømde stativet - du kan utfordre eller passere",
27930
+ pl: "Twój przeciwnik opróżnił stojak - możesz rzucić wyzwanie lub spasować",
27931
+ ga: "D'fholmhaigh do chéile comhraic an raca - is féidir leat dúshlán a thabhairt nó pas a fháil"
27932
+ };
27933
+ var Skipta = {
27934
+ en: "Exchange",
27935
+ nb: "Bytte",
27936
+ nn: "Byte",
27937
+ pl: "Wymiana",
27938
+ ga: "Malartú"
27939
+ };
27940
+ var elo_list_choice = {
27941
+ is: "Fólk | Allar | Keppnishamur",
27942
+ en: "Humans | All | Pro mode",
27943
+ nb: "Mennesker | Alle | Pro modus",
27944
+ nn: "Menneske | Alle | Pro-modus",
27945
+ pl: "Ludzie | Wszyscy | Tryb Pro",
27946
+ ga: "Daoine | Gach | Modh Pro"
27947
+ };
27948
+ var stats_choice = {
27949
+ is: "Með þjörkum eða án",
27950
+ en: "With or without robot games",
27951
+ nb: "Med eller uten robotspill",
27952
+ nn: "Med eller utan robotspel",
27953
+ pl: "Z grami robotów lub bez",
27954
+ ga: "Le cluichí róbón nó gan iad"
27955
+ };
27956
+ var Vinningshlutfall = {
27957
+ en: "Winning ratio",
27958
+ nb: "Vinningsforhold",
27959
+ nn: "Vinningsforhold",
27960
+ pl: "Stosunek wygranych",
27961
+ ga: "Cóimheas bua"
27962
+ };
27963
+ var word_not_found = {
27964
+ en: "'{word}' is not in the dictionary",
27965
+ is: "'{word}' finnst ekki í orðasafni",
27966
+ nb: "'{word}' finnes ikke i ordboken",
27967
+ nn: "'{word}' finst ikkje i ordboka",
27968
+ pl: "'{word}' nie znajduje się w słowniku",
27969
+ ga: "Níl '{word}' sa fhoclóir"
27970
+ };
27971
+ var welcome_2 = {
27972
+ is: [
27973
+ "Til auðkenningar tengir Netskrafl tölvupóstfang og nafn við hvern notanda. ",
27974
+ "Að öðru leyti eru ekki geymdar aðrar upplýsingar um notendur ",
27975
+ "en þær sem þeir skrá sjálfir. Annáll er haldinn um umferð um vefinn og um ",
27976
+ "aðgerðir notenda, í því skyni að endurbæta Netskrafl."
27977
+ ],
27978
+ en: [
27979
+ "For identification, Explo associates an e-mail address and a name with each user. ",
27980
+ "Apart from this, Explo only stores information which is voluntarily ",
27981
+ "entered by users themselves. A log is kept of web traffic and events within Explo, ",
27982
+ "for the purpose of improving the service."
27983
+ ],
27984
+ nb: [
27985
+ "For identifikasjon knytter Explo en e-postadresse og et navn til hver bruker. ",
27986
+ "Utover dette lagrer Explo kun informasjon som frivillig ",
27987
+ "legges inn av brukerne selv. En logg føres over webtrafikk og hendelser innen Explo, ",
27988
+ "i hensikt å forbedre tjenesten."
27989
+ ],
27990
+ nn: [
27991
+ "For identifikasjon knyter Explo ei e-postadresse og eit namn til kvar brukar. ",
27992
+ "Utover dette lagrar Explo berre informasjon som frivillig ",
27993
+ "blir lagt inn av brukarane sjølve. Ein logg blir ført over webtrafikk og hendingar innan Explo, ",
27994
+ "i føremål å forbetre tenesta."
27995
+ ],
27996
+ pl: [
27997
+ "W celu identyfikacji Explo przypisuje każdemu użytkownikowi adres e-mail i nazwę. ",
27998
+ "Poza tym Explo przechowuje tylko te informacje, które dobrowolnie wprowadzają sami ",
27999
+ "użytkownicy. Prowadzony jest rejestr ruchu internetowego i zdarzeń w Explo w celu ",
28000
+ "ulepszenia usługi."
28001
+ ],
28002
+ ga: [
28003
+ "Chun aitheantais, ceanglaíonn Explo seoladh ríomhphoist agus ainm le gach úsáideoir. ",
28004
+ "Seachas sin, ní stóráiltear ag Explo ach an fhaisnéis a chuirtear isteach go deonach ",
28005
+ "ag na húsáideoirí iad féin. Coinnítear logáil ar thrácht gréasáin agus imeachtaí laistigh de Explo, ",
28006
+ "leis an gcuspóir an tseirbhís a fheabhsú."
28007
+ ]
28008
+ };
28009
+ var welcome_1 = {
28010
+ is: [
28011
+ "Netskrafl notar Google Accounts innskráningu, þá sömu og er notuð m.a. í Gmail. ",
28012
+ "Til að auðkenna þig sem notanda og halda innskráningunni virkri ",
28013
+ "er óhjákvæmilegt að geyma þar til gerða smáköku (<i>cookie</i>) ",
28014
+ "í vafranum þínum."
28015
+ ],
28016
+ en: [
28017
+ "Explo uses Google Accounts for user authentication, similarly to e.g. Gmail. ",
28018
+ "To remember your login and maintain your session, it is necessary to store a ",
28019
+ "cookie within your browser."
28020
+ ],
28021
+ nb: [
28022
+ "Explo bruker Google-kontoer for brukerautentisering, likt som f.eks. Gmail. ",
28023
+ "For å huske din innlogging og opprettholde din økt, er det nødvendig å lagre en ",
28024
+ "informasjonskapsel i nettleseren din."
28025
+ ],
28026
+ nn: [
28027
+ "Explo brukar Google-kontoar for brukarautentisering, likt som t.d. Gmail. ",
28028
+ "For å hugse innlogginga di og halde ved like økta di, er det naudsynt å lagre ein ",
28029
+ "informasjonskapsel i nettlesaren din."
28030
+ ],
28031
+ pl: [
28032
+ "Explo używa kont Google do uwierzytelniania użytkownika, podobnie jak np. Gmail. ",
28033
+ "Aby zapamiętać twoje logowanie i utrzymać sesję, konieczne jest przechowanie ",
28034
+ "ciasteczka w twojej przeglądarce."
28035
+ ],
28036
+ ga: [
28037
+ "Úsáideann Explo Cuntais Google le haghaidh fíordheimhniú úsáideora, cosúil le Gmail, mar shampla. ",
28038
+ "Chun do logáil isteach a mheabhrú agus do sheisiún a chothabháil, tá sé riachtanach fianán a stóráil ",
28039
+ "i do bhrabhsálaí."
28040
+ ]
28041
+ };
28042
+ var welcome_0 = {
28043
+ is: [
28044
+ "Netskrafl er vettvangur ",
28045
+ "<b>yfir 40.000 íslenskra skraflara</b>",
28046
+ " á netinu."
28047
+ ],
28048
+ en: [
28049
+ "Explo is a venue for ",
28050
+ "<b>tens of thousands of crossword game enthusiasts</b> ",
28051
+ "on the Internet."
28052
+ ],
28053
+ nb: [
28054
+ "Explo er et møtested for ",
28055
+ "<b>tusenvis av kryssordspillentusiaster</b>",
28056
+ " på internett."
28057
+ ],
28058
+ nn: [
28059
+ "Explo er ein møtestad for ",
28060
+ "<b>tusenvis av kryssordspelentusiastar</b>",
28061
+ " på internett."
28062
+ ],
28063
+ pl: [
28064
+ "Explo to miejsce dla ",
28065
+ "<b>dziesiątek tysięcy entuzjastów gier krzyżówkowych</b>",
28066
+ " w Internecie."
28067
+ ],
28068
+ ga: [
28069
+ "Is ionad é Explo do ",
28070
+ "<b>na deich mílte díograiseoirí cluiche crosfhocal</b>",
28071
+ " ar an Idirlíon."
28072
+ ]
28073
+ };
28074
+ var Stigatafla = {
28075
+ en: "Leaderboard",
28076
+ nb: "Toppliste",
28077
+ nn: "Toppliste",
28078
+ pl: "Tabela wyników",
28079
+ ga: "Clár ceannairí"
28080
+ };
28081
+ var date_format = {
28082
+ is: "{day}. {month}",
28083
+ en: "{month} {day}",
28084
+ nb: "{day}. {month}",
28085
+ nn: "{day}. {month}",
28086
+ pl: "{day} {month}",
28087
+ ga: "{day} {month}"
28088
+ };
28089
+ var january = {
28090
+ is: "janúar",
28091
+ en: "January",
28092
+ nb: "januar",
28093
+ nn: "januar",
28094
+ pl: "styczeń",
28095
+ ga: "Eanáir"
28096
+ };
28097
+ var february = {
28098
+ is: "febrúar",
28099
+ en: "February",
28100
+ nb: "februar",
28101
+ nn: "februar",
28102
+ pl: "luty",
28103
+ ga: "Feabhra"
28104
+ };
28105
+ var march = {
28106
+ is: "mars",
28107
+ en: "March",
28108
+ nb: "mars",
28109
+ nn: "mars",
28110
+ pl: "marzec",
28111
+ ga: "Márta"
28112
+ };
28113
+ var april = {
28114
+ is: "apríl",
28115
+ en: "April",
28116
+ nb: "april",
28117
+ nn: "april",
28118
+ pl: "kwiecień",
28119
+ ga: "Aibreán"
28120
+ };
28121
+ var may = {
28122
+ is: "maí",
28123
+ en: "May",
28124
+ nb: "mai",
28125
+ nn: "mai",
28126
+ pl: "maj",
28127
+ ga: "Bealtaine"
28128
+ };
28129
+ var june = {
28130
+ is: "júní",
28131
+ en: "June",
28132
+ nb: "juni",
28133
+ nn: "juni",
28134
+ pl: "czerwiec",
28135
+ ga: "Meitheamh"
28136
+ };
28137
+ var july = {
28138
+ is: "júlí",
28139
+ en: "July",
28140
+ nb: "juli",
28141
+ nn: "juli",
28142
+ pl: "lipiec",
28143
+ ga: "Iúil"
28144
+ };
28145
+ var august = {
28146
+ is: "ágúst",
28147
+ en: "August",
28148
+ nb: "august",
28149
+ nn: "august",
28150
+ pl: "sierpień",
28151
+ ga: "Lúnasa"
28152
+ };
28153
+ var september = {
28154
+ is: "september",
28155
+ en: "September",
28156
+ nb: "september",
28157
+ nn: "september",
28158
+ pl: "wrzesień",
28159
+ ga: "Meán Fómhair"
28160
+ };
28161
+ var october = {
28162
+ is: "október",
28163
+ en: "October",
28164
+ nb: "oktober",
28165
+ nn: "oktober",
28166
+ pl: "październik",
28167
+ ga: "Deireadh Fómhair"
28168
+ };
28169
+ var november = {
28170
+ is: "nóvember",
28171
+ en: "November",
28172
+ nb: "november",
28173
+ nn: "november",
28174
+ pl: "listopad",
28175
+ ga: "Samhain"
28176
+ };
28177
+ var december = {
28178
+ is: "desember",
28179
+ en: "December",
28180
+ nb: "desember",
28181
+ nn: "desember",
28182
+ pl: "grudzień",
28183
+ ga: "Nollaig"
28184
+ };
28185
+ var messagesData = {
28186
+ Leikir: Leikir,
28187
+ submit_move: submit_move,
28188
+ "Viðureignir": {
28189
+ en: "Games",
28190
+ nb: "Spill",
28191
+ nn: "Spel",
28192
+ pl: "Gry",
28193
+ ga: "Cluichí"
28194
+ },
28195
+ "Borðið": {
28196
+ en: "Board",
28197
+ nb: "Brett",
28198
+ nn: "Brett",
28199
+ pl: "Plansza",
28200
+ ga: "Bord"
28201
+ },
28202
+ "Tveggja stafa orð": {
28203
+ en: "Two letter words",
28204
+ nb: "Tobokstavsord",
28205
+ nn: "Tobokstavsord",
28206
+ pl: "Dwuliterowe słowa",
28207
+ ga: "Focail dhá litir"
28208
+ },
28209
+ Spjall: Spjall,
28210
+ "Þessi vefslóð er ekki rétt": {
28211
+ en: "Unknown URL",
28212
+ nb: "Ukjent URL",
28213
+ nn: "Ukjend URL",
28214
+ pl: "Nieznany URL",
28215
+ ga: "URL Anaithnid"
28216
+ },
28217
+ player_info: player_info,
28218
+ "Einkenni:": {
28219
+ en: "User identifier:",
28220
+ nb: "Brukeridentifikator:",
28221
+ nn: "Brukaridentifikator:",
28222
+ pl: "Identyfikator użytkownika:",
28223
+ ga: "Aitheantóir Úsáideora:"
28224
+ },
28225
+ "Verður að vera útfyllt": {
28226
+ en: "Required field",
28227
+ nb: "Obligatorisk felt",
28228
+ nn: "Obligatorisk felt",
28229
+ pl: "Pole wymagane",
28230
+ ga: "Réimse Riachtanach"
28231
+ },
28232
+ "Fullt nafn:": {
28233
+ en: "Full name:",
28234
+ nb: "Fullt navn:",
28235
+ nn: "Fullt namn:",
28236
+ pl: "Pełne imię i nazwisko:",
28237
+ ga: "Ainm Iomlán:"
28238
+ },
28239
+ "Valfrjálst - sýnt í notendalistum Netskrafls": {
28240
+ en: "Optional - shown in user lists",
28241
+ nb: "Valgfritt - vist i brukerlister",
28242
+ nn: "Valfritt - vist i brukarlister",
28243
+ pl: "Opcjonalne - pokazane na listach użytkowników",
28244
+ ga: "Roghnach - le feiceáil i liostaí úsáideoirí"
28245
+ },
28246
+ "Tölvupóstfang:": {
28247
+ en: "E-mail:",
28248
+ nb: "E-post:",
28249
+ nn: "E-post:",
28250
+ pl: "E-mail:",
28251
+ ga: "Ríomhphost:"
28252
+ },
28253
+ explain_email: explain_email,
28254
+ "Hljóðmerki:": {
28255
+ en: "Sounds:",
28256
+ nb: "Lyder:",
28257
+ nn: "Lydar:",
28258
+ pl: "Dźwięki:",
28259
+ ga: "Fuaimeanna:"
28260
+ },
28261
+ "Hljóð á/af": {
28262
+ en: "Sound on/off",
28263
+ nb: "Lyd på/av",
28264
+ nn: "Lyd på/av",
28265
+ pl: "Dźwięki włącz/wyłącz",
28266
+ ga: "Fuaimeanna ar/amuigh"
28267
+ },
28268
+ "Lúðraþytur eftir sigur:": {
28269
+ en: "Fanfare after a win:",
28270
+ nb: "Trompeter etter seier:",
28271
+ nn: "Trompeter etter siger:",
28272
+ pl: "Trąbki po zwycięstwie:",
28273
+ ga: "Trumpaí tar éis bua:"
28274
+ },
28275
+ "Lúðraþytur á/af": {
28276
+ en: "Fanfare on/off",
28277
+ nb: "Trompeter på/av",
28278
+ nn: "Trompeter på/av",
28279
+ pl: "Trąbki włącz/wyłącz",
28280
+ ga: "Trumpaí ar/amuigh"
28281
+ },
28282
+ explain_sound: explain_sound,
28283
+ "Sýna reitagildi:": {
28284
+ en: "Show multipliers:",
28285
+ nb: "Vis multiplikatorer:",
28286
+ nn: "Vis multiplikatorar:",
28287
+ pl: "Pokaż mnożniki:",
28288
+ ga: "Taispeáin iolraitheoirí:"
28289
+ },
28290
+ "Nýi skraflpokinn:": {
28291
+ en: "Use the new bag:",
28292
+ nb: "Bruk den nye posen:",
28293
+ nn: "Bruk den nye posen:",
28294
+ pl: "Użyj nowego worka:",
28295
+ ga: "Bain úsáid as an mála nua:"
28296
+ },
28297
+ "Án hjálpartækja:": {
28298
+ en: "Without helpers:",
28299
+ nb: "Uten hjelpemidler:",
28300
+ nn: "Utan hjelpemiddel:",
28301
+ pl: "Bez pomocy:",
28302
+ ga: "Gan chúntóirí:"
28303
+ },
28304
+ Vista: Vista,
28305
+ "Hætta við": {
28306
+ en: "Cancel",
28307
+ nb: "Avbryt",
28308
+ nn: "Bryt av",
28309
+ pl: "Anuluj",
28310
+ ga: "Cealaigh"
28311
+ },
28312
+ "Skrá mig út": {
28313
+ en: "Log out",
28314
+ nb: "Logg ut",
28315
+ nn: "Logg ut",
28316
+ pl: "Wyloguj się",
28317
+ ga: "Logáil Amach"
28318
+ },
28319
+ "Þú ert áskrifandi!": {
28320
+ en: "You're a subscriber!",
28321
+ nb: "Du er en abonnent!",
28322
+ nn: "Du er ein abonnent!",
28323
+ pl: "Jesteś subskrybentem!",
28324
+ ga: "Is síntiúsóir tú!"
28325
+ },
28326
+ "Gerast áskrifandi": {
28327
+ en: "Subscribe",
28328
+ nb: "Abonner",
28329
+ nn: "Abonner",
28330
+ pl: "Subskrybuj",
28331
+ ga: "Liostáil"
28332
+ },
28333
+ "Tek við áskorunum!": {
28334
+ en: "Accepting challenges!",
28335
+ nb: "Aksepterer utfordringer!",
28336
+ nn: "Tek imot utfordringar!",
28337
+ pl: "Akceptuje wyzwania!",
28338
+ ga: "Ag glacadh le dúshláin!"
28339
+ },
28340
+ "Til í viðureign með klukku!": {
28341
+ en: "Ready for timed games!",
28342
+ nb: "Klar for tidsbegrensede spill!",
28343
+ nn: "Klar for tidsavgrensa spel!",
28344
+ pl: "Gotowy na gry z czasem!",
28345
+ ga: "Réidh do chluichí ama!"
28346
+ },
28347
+ "Stillir hvort ": {
28348
+ en: "Controls whether ",
28349
+ nb: "Kontrollerer om ",
28350
+ nn: "Styrer om ",
28351
+ pl: "Kontroluje czy ",
28352
+ ga: "Rialaíonn sé an "
28353
+ },
28354
+ "minnismiði": {
28355
+ en: "a memo sticker",
28356
+ nb: "en huskelapp",
28357
+ nn: "ein hugselapp",
28358
+ pl: "naklejka memo",
28359
+ ga: "nóta meabhrúcháin"
28360
+ },
28361
+ " um margföldunargildi reita er sýndur við borðið": {
28362
+ en: " with square multipliers is shown beside the board",
28363
+ nb: " med rute-multiplikatorer vises ved siden av brettet",
28364
+ nn: " med rute-multiplikatorar blir vist ved sida av brettet",
28365
+ pl: " z mnożnikami kwadratów jest pokazany obok planszy",
28366
+ ga: " le hiolraitheoirí cearnóg le feiceáil in aice leis an mbord"
28367
+ },
28368
+ "Gefur til kynna hvort þú sért reiðubúin(n) að\nskrafla með ": {
28369
+ en: "Indicates whether you are ready to play with\n",
28370
+ nb: "Indikerer om du er klar til å spille med\n",
28371
+ nn: "Indikerer om du er klar til å spele med\n",
28372
+ pl: "Wskazuje czy jesteś gotowy do gry z\n",
28373
+ ga: "Léiríonn sé an bhfuil tú ullamh chun imeartha le\n"
28374
+ },
28375
+ "nýja íslenska skraflpokanum": {
28376
+ en: "the new tile bag",
28377
+ nb: "den nye brikkeposen",
28378
+ nn: "den nye brikkeposen",
28379
+ pl: "nowym workiem z kafelkami",
28380
+ ga: "an mála tíleanna nua"
28381
+ },
28382
+ no_helpers: no_helpers,
28383
+ "án stafrænna hjálpartækja": {
28384
+ en: "without helpers or tools",
28385
+ nb: "uten hjelpemidler eller verktøy",
28386
+ nn: "utan hjelpemiddel eller verktøy",
28387
+ pl: "bez pomocy lub narzędzi",
28388
+ ga: "gan chúntóirí ná uirlisí"
28389
+ },
28390
+ " af nokkru tagi": {
28391
+ en: " of any kind",
28392
+ nb: " av noe slag",
28393
+ nn: " av noko slag",
28394
+ pl: " jakiegokolwiek rodzaju",
28395
+ ga: " de chineál ar bith"
28396
+ },
28397
+ "Áskoranir": {
28398
+ en: "Challenges",
28399
+ nb: "Utfordringer",
28400
+ nn: "Utfordringar",
28401
+ pl: "Wyzwania",
28402
+ ga: "Dúshláin"
28403
+ },
28404
+ "Andstæðingar": {
28405
+ en: "Opponents",
28406
+ nb: "Motstandere",
28407
+ nn: "Motstandarar",
28408
+ pl: "Przeciwnicy",
28409
+ ga: "Freasúra"
28410
+ },
28411
+ Ferill: Ferill,
28412
+ "Þú átt leik": {
28413
+ en: "Your turn",
28414
+ nb: "Din tur",
28415
+ nn: "Din tur",
28416
+ pl: "Twoja kolej",
28417
+ ga: "Do sheal"
28418
+ },
28419
+ "Viðureign lokið": {
28420
+ en: "Game over",
28421
+ nb: "Spillet er over",
28422
+ nn: "Spelet er over",
28423
+ pl: "Koniec gry",
28424
+ ga: "Cluiche thart"
28425
+ },
28426
+ opp_move: opp_move,
28427
+ "Átt þú leik?": {
28428
+ en: "Your turn?",
28429
+ nb: "Din tur?",
28430
+ nn: "Din tur?",
28431
+ pl: "Twoja kolej?",
28432
+ ga: "Do sheal?"
28433
+ },
28434
+ "Langt frá síðasta leik?": {
28435
+ en: "Long time elapsed since last move?",
28436
+ nb: "Lang tid siden siste trekk?",
28437
+ nn: "Lang tid sidan siste trekk?",
28438
+ pl: "Dużo czasu minęło od ostatniego ruchu?",
28439
+ ga: "An bhfuil am fada caite ón mbogadh deireanach?"
28440
+ },
28441
+ "Síðasti leikur": {
28442
+ en: "Last move",
28443
+ nb: "Siste trekk",
28444
+ nn: "Siste trekk",
28445
+ pl: "Ostatni ruch",
28446
+ ga: "Bogadh deireanach"
28447
+ },
28448
+ "Andstæðingur": {
28449
+ en: "Opponent",
28450
+ nb: "Motstander",
28451
+ nn: "Motstandar",
28452
+ pl: "Przeciwnik",
28453
+ ga: "Iomaitheoir"
28454
+ },
28455
+ "Staða": {
28456
+ en: "Score",
28457
+ nb: "Poengsum",
28458
+ nn: "Poengsum",
28459
+ pl: "Wynik",
28460
+ ga: "Scór"
28461
+ },
28462
+ Framvinda: Framvinda,
28463
+ Keppnishamur: Keppnishamur,
28464
+ "Venjuleg ótímabundin viðureign": {
28465
+ en: "Standard Mode without time limit",
28466
+ nb: "Standardmodus uten tidsbegrensning",
28467
+ nn: "Standardmodus utan tidsbegrensing",
28468
+ pl: "Tryb standardowy bez limitu czasu",
28469
+ ga: "Gnáthchluiche gan teorainn ama"
28470
+ },
28471
+ with_clock: with_clock,
28472
+ Hafna: Hafna,
28473
+ Afturkalla: Afturkalla,
28474
+ "Án hjálpartækja": {
28475
+ en: "Without helpers",
28476
+ nb: "Uten hjelpemidler",
28477
+ nn: "Utan hjelpemiddel",
28478
+ pl: "Bez pomocy",
28479
+ ga: "Gan chúntóirí"
28480
+ },
28481
+ "Skoða feril": {
28482
+ en: "View history",
28483
+ nb: "Vis historikk",
28484
+ nn: "Vis historikk",
28485
+ pl: "Zobacz historię",
28486
+ ga: "Féach ar stair"
28487
+ },
28488
+ "Gamli pokinn": {
28489
+ en: "Old tile bag",
28490
+ nb: "Gammel brikkepose",
28491
+ nn: "Gammal brikkepose",
28492
+ pl: "Stary worek z kafelkami",
28493
+ ga: "Mála tíleanna sean"
28494
+ },
28495
+ "Hvenær": {
28496
+ en: "When",
28497
+ nb: "Når",
28498
+ nn: "Når",
28499
+ pl: "Kiedy",
28500
+ ga: "Cathain"
28501
+ },
28502
+ "Áskorandi": {
28503
+ en: "Challenger",
28504
+ nb: "Utfordrer",
28505
+ nn: "Utfordrar",
28506
+ pl: "Wyzwający",
28507
+ ga: "Dúshlánóir"
28508
+ },
28509
+ Hvernig: Hvernig,
28510
+ Sigur: Sigur,
28511
+ Jafntefli: Jafntefli,
28512
+ Tap: Tap,
28513
+ "Úrslit": {
28514
+ en: "Result",
28515
+ nb: "Resultat",
28516
+ nn: "Resultat",
28517
+ pl: "Wynik",
28518
+ ga: "Toradh"
28519
+ },
28520
+ "Viðureign lauk": {
28521
+ en: "Game finished",
28522
+ nb: "Spill avsluttet",
28523
+ nn: "Spel avslutta",
28524
+ pl: "Gra zakończona",
28525
+ ga: "Cluiche críochnaithe"
28526
+ },
28527
+ "Mennskir andstæðingar": {
28528
+ en: "Human opponents",
28529
+ nb: "Menneskelige motstandere",
28530
+ nn: "Menneskelege motstandarar",
28531
+ pl: "Ludzcy przeciwnicy",
28532
+ ga: "Freasúra daonna"
28533
+ },
28534
+ "Allir andstæðingar": {
28535
+ en: "All opponents",
28536
+ nb: "Alle motstandere",
28537
+ nn: "Alle motstandarar",
28538
+ pl: "Wszyscy przeciwnicy",
28539
+ ga: "Gach freasúra"
28540
+ },
28541
+ Elo: Elo,
28542
+ Lengd: Lengd,
28543
+ "Uppáhald": {
28544
+ en_US: "Favorite",
28545
+ en: "Favourite",
28546
+ nb: "Favoritt",
28547
+ nn: "Favoritt",
28548
+ pl: "Ulubione",
28549
+ ga: "Is fearr leat"
28550
+ },
28551
+ "Uppáhalds": {
28552
+ en_US: "Favorites",
28553
+ en: "Favourites",
28554
+ nb: "Favoritter",
28555
+ nn: "Favorittar",
28556
+ pl: "Ulubione",
28557
+ ga: "Roghanna"
28558
+ },
28559
+ "Skora á": {
28560
+ en: "Challenge",
28561
+ nb: "Utfordre",
28562
+ nn: "Utfordre",
28563
+ pl: "Wyzwanie",
28564
+ ga: "Dúshlán"
28565
+ },
28566
+ Einkenni: Einkenni,
28567
+ "Nafn og merki": {
28568
+ en: "Full name and badges",
28569
+ nb: "Fullt navn og merker",
28570
+ nn: "Fullt namn og merke",
28571
+ pl: "Pełna nazwa i odznaki",
28572
+ ga: "Ainm iomlán agus suaitheantais"
28573
+ },
28574
+ " finnst ekki": {
28575
+ en: " not found",
28576
+ nb: " ikke funnet",
28577
+ nn: " ikkje funne",
28578
+ pl: " nie znaleziono",
28579
+ ga: " gan aimsiú"
28580
+ },
28581
+ "Þjarkar": {
28582
+ en: "Robots",
28583
+ nb: "Roboter",
28584
+ nn: "Robotar",
28585
+ pl: "Roboty",
28586
+ ga: "Róbónna"
28587
+ },
28588
+ "Álínis": {
28589
+ en: "Online",
28590
+ nb: "Pålogget",
28591
+ nn: "Pålogga",
28592
+ pl: "Online",
28593
+ ga: "Ar líne"
28594
+ },
28595
+ "Svipaðir": {
28596
+ en: "Similar",
28597
+ nb: "Lignende",
28598
+ nn: "Liknande",
28599
+ pl: "Podobne",
28600
+ ga: "Cosúil"
28601
+ },
28602
+ "Topp 100": {
28603
+ en: "Top 100",
28604
+ nb: "Topp 100",
28605
+ nn: "Topp 100",
28606
+ pl: "Top 100",
28607
+ ga: "Barr 100"
28608
+ },
28609
+ "orð": {
28610
+ en: "word",
28611
+ nb: "ord",
28612
+ nn: "ord",
28613
+ pl: "słowo",
28614
+ ga: "focal"
28615
+ },
28616
+ stafur: stafur,
28617
+ Senda: Senda,
28618
+ Pass: Pass,
28619
+ letter: letter,
28620
+ letters: letters,
28621
+ exchanged: exchanged,
28622
+ "Gaf viðureign": {
28623
+ en: "Resigned",
28624
+ nb: "Ga opp",
28625
+ nn: "Gav opp",
28626
+ pl: "Zrezygnował",
28627
+ ga: "D'éirigh as"
28628
+ },
28629
+ "Véfengdi lögn": {
28630
+ en: "Challenged move",
28631
+ nb: "Utfordret trekk",
28632
+ nn: "Utfordra trekk",
28633
+ pl: "Ruch zakwestionowany",
28634
+ ga: "Bogadh dúshlánach"
28635
+ },
28636
+ "Óleyfileg lögn": {
28637
+ en: "Invalid move",
28638
+ nb: "Ugyldig trekk",
28639
+ nn: "Ugyldig trekk",
28640
+ pl: "Nieprawidłowy ruch",
28641
+ ga: "Bogadh neamhbhailí"
28642
+ },
28643
+ "Röng véfenging": {
28644
+ en: "Unsuccessful challenge",
28645
+ nb: "Mislykket utfordring",
28646
+ nn: "Mislukka utfordring",
28647
+ pl: "Nieudane wyzwanie",
28648
+ ga: "Dúshlán gan rath"
28649
+ },
28650
+ "Umframtími": {
28651
+ en: "Extra time",
28652
+ nb: "Ekstra tid",
28653
+ nn: "Ekstra tid",
28654
+ pl: "Dodatkowy czas",
28655
+ ga: "Am breise"
28656
+ },
28657
+ "Stafaleif: engin": {
28658
+ en: "Rack leave: none",
28659
+ nb: "Rack forlater: ingen",
28660
+ nn: "Stativ forlèt: ingen",
28661
+ pl: "Pozostawienie na stojaku: brak",
28662
+ ga: "Fágáil raca: dada"
28663
+ },
28664
+ "Stafaleif: ": {
28665
+ en: "Rack leave: ",
28666
+ nb: "Rack forlater: ",
28667
+ nn: "Stativ forlèt: ",
28668
+ pl: "Pozostawienie na stojaku: ",
28669
+ ga: "Fágáil raca: "
28670
+ },
28671
+ "Ný áskorun": {
28672
+ en: "New challenge",
28673
+ nb: "Ny utfordring",
28674
+ nn: "Ny utfordring",
28675
+ pl: "Nowe wyzwanie",
28676
+ ga: "Dúshlán nua"
28677
+ },
28678
+ "Viðureign án klukku": {
28679
+ en: "Game with no time limit",
28680
+ nb: "Spill uten tidsbegrensning",
28681
+ nn: "Spel utan tidsbegrensing",
28682
+ pl: "Gra bez limitu czasu",
28683
+ ga: "Cluiche gan teorainn ama"
28684
+ },
28685
+ "Nota ": {
28686
+ en: "Enable ",
28687
+ nb: "Aktiver ",
28688
+ nn: "Aktiver ",
28689
+ pl: "Włącz ",
28690
+ ga: "Cumasaigh "
28691
+ },
28692
+ "handvirka véfengingu": {
28693
+ en: "manual challenges",
28694
+ nb: "manuelle utfordringer",
28695
+ nn: "manuelle utfordringar",
28696
+ pl: "ręczne wyzwania",
28697
+ ga: "dúshláin láimhe"
28698
+ },
28699
+ "(\"keppnishamur\")": {
28700
+ en: "(\"Pro mode\")",
28701
+ nb: "(\"Pro-modus\")",
28702
+ nn: "(\"Pro-modus\")",
28703
+ pl: "(\"Tryb Pro\")",
28704
+ ga: "(\"Modh Pro\")"
28705
+ },
28706
+ "Viðureignir sem standa yfir": {
28707
+ en: "Games in progress",
28708
+ nb: "Pågående spill",
28709
+ nn: "Pågåande spel",
28710
+ pl: "Trwające gry",
28711
+ ga: "Cluichí ar siúl"
28712
+ },
28713
+ click_on_game: click_on_game,
28714
+ " þú átt leik": {
28715
+ en: " it's your turn",
28716
+ nb: " det er din tur",
28717
+ nn: " det er din tur",
28718
+ pl: " to twoja kolej",
28719
+ ga: " tá sé do sheal"
28720
+ },
28721
+ "Skorað á þig": {
28722
+ en: "Other players have challenged you",
28723
+ nb: "Andre spillere har utfordret deg",
28724
+ nn: "Andre spelarar har utfordra deg",
28725
+ pl: "Inni gracze wyzwali cię",
28726
+ ga: "Tá dúshláin curtha ort ag imreoirí eile"
28727
+ },
28728
+ click_on_challenge: click_on_challenge,
28729
+ " til að hafna henni": {
28730
+ en: " to decline it",
28731
+ nb: " for å avslå det",
28732
+ nn: " for å avslå det",
28733
+ pl: " aby je odrzucić",
28734
+ ga: " chun é a dhiúltú"
28735
+ },
28736
+ "Þú skorar á aðra": {
28737
+ en: "You challenge other players",
28738
+ nb: "Du utfordrer andre spillere",
28739
+ nn: "Du utfordrar andre spelarar",
28740
+ pl: "Wyzwasz innych graczy",
28741
+ ga: "Dúshlánaíonn tú imreoirí eile"
28742
+ },
28743
+ " - smelltu á ": {
28744
+ en: " - click on ",
28745
+ nb: " - klikk på ",
28746
+ nn: " - klikk på ",
28747
+ pl: " - kliknij na ",
28748
+ ga: " - cliceáil ar "
28749
+ },
28750
+ " til að afturkalla áskorun": {
28751
+ en: " to retract an issued challenge",
28752
+ nb: " for å trekke tilbake en utfordring",
28753
+ nn: " for å trekkje tilbake ei utfordring",
28754
+ pl: " aby wycofać wydane wyzwanie",
28755
+ ga: " chun dúshlán eisithe a tharraingt siar"
28756
+ },
28757
+ "Nýlegar viðureignir þínar": {
28758
+ en: "Your recent games",
28759
+ nb: "Dine nylige spill",
28760
+ nn: "Dine nylege spel",
28761
+ pl: "Twoje ostatnie gry",
28762
+ ga: "Do chluichí le déanaí"
28763
+ },
28764
+ click_to_review: click_to_review,
28765
+ "Einkenni eða nafn": {
28766
+ en: "Identifier or name",
28767
+ nb: "Identifikator eller navn",
28768
+ nn: "Identifikator eller namn",
28769
+ pl: "Identyfikator lub nazwa",
28770
+ ga: "Aitheantóir nó ainm"
28771
+ },
28772
+ "Upplýsingar og hjálp": {
28773
+ en: "Information and help",
28774
+ nb: "Informasjon og hjelp",
28775
+ nn: "Informasjon og hjelp",
28776
+ pl: "Informacje i pomoc",
28777
+ ga: "Eolas agus cabhair"
28778
+ },
28779
+ "1 dagur": {
28780
+ en: "1 day",
28781
+ nb: "1 dag",
28782
+ nn: "1 dag",
28783
+ pl: "1 dzień",
28784
+ ga: "1 lá"
28785
+ },
28786
+ " dagar": {
28787
+ en: " days",
28788
+ nb: " dager",
28789
+ nn: " dagar",
28790
+ pl: " dni",
28791
+ ga: " laethanta"
28792
+ },
28793
+ " og ": {
28794
+ en: " and ",
28795
+ nb: " og ",
28796
+ nn: " og ",
28797
+ pl: " i ",
28798
+ ga: " agus "
28799
+ },
28800
+ "1 klst": {
28801
+ en: "1 hour",
28802
+ nb: "1 time",
28803
+ nn: "1 time",
28804
+ pl: "1 godzina",
28805
+ ga: "1 uair"
28806
+ },
28807
+ " klst": {
28808
+ en: " hours",
28809
+ nb: " timer",
28810
+ nn: " timar",
28811
+ pl: " godzin",
28812
+ ga: " uair an chloig"
28813
+ },
28814
+ "1 mínúta": {
28815
+ en: "1 minute",
28816
+ nb: "1 minutt",
28817
+ nn: "1 minutt",
28818
+ pl: "1 minuta",
28819
+ ga: "1 nóiméad"
28820
+ },
28821
+ " mínútur": {
28822
+ en: " minutes",
28823
+ nb: " minutter",
28824
+ nn: " minutt",
28825
+ pl: " minuty",
28826
+ ga: " nóiméad"
28827
+ },
28828
+ "Viðureign með klukku": {
28829
+ en: "Timed game",
28830
+ nb: "Tidsbegrenset spill",
28831
+ nn: "Tidsbegrensa spel",
28832
+ pl: "Gra zegarowa",
28833
+ ga: "Cluiche ama"
28834
+ },
28835
+ "Áskrifandi": {
28836
+ en: "Subscriber",
28837
+ nb: "Abonnent",
28838
+ nn: "Abonnent",
28839
+ pl: "Subskrybent",
28840
+ ga: "Síntiúsóir"
28841
+ },
28842
+ "Nýjustu viðureignir": {
28843
+ en: "Recent games",
28844
+ nb: "Nylige spill",
28845
+ nn: "Nylege spel",
28846
+ pl: "Najnowsze gry",
28847
+ ga: "Cluichí is déanaí"
28848
+ },
28849
+ Loka: Loka,
28850
+ "Besta orð ": {
28851
+ en: "Best word ",
28852
+ nb: "Beste ord ",
28853
+ nn: "Beste ord ",
28854
+ pl: "Najlepsze słowo ",
28855
+ ga: "An focal is fearr "
28856
+ },
28857
+ "Hæsta skor ": {
28858
+ en: "Highest score ",
28859
+ nb: "Høyeste poengsum ",
28860
+ nn: "Høgaste poengsum ",
28861
+ pl: "Najwyższy wynik ",
28862
+ ga: "An scór is airde "
28863
+ },
28864
+ " stig": {
28865
+ en: " points",
28866
+ nb: " poeng",
28867
+ nn: " poeng",
28868
+ pl: " punkty",
28869
+ ga: " pointí"
28870
+ },
28871
+ "Til hamingju með sigurinn!": {
28872
+ en: "You won - congratulations!",
28873
+ nb: "Du vant - gratulerer!",
28874
+ nn: "Du vann - gratulerer!",
28875
+ pl: "Wygrałeś - gratulacje!",
28876
+ ga: "Bhuaigh tú - comhghairdeas!"
28877
+ },
28878
+ "Viðureigninni er lokið": {
28879
+ en: "Game over",
28880
+ nb: "Spillet er over",
28881
+ nn: "Spelet er over",
28882
+ pl: "Gra zakończona",
28883
+ ga: "Cluiche thart"
28884
+ },
28885
+ opponent_emptied_rack: opponent_emptied_rack,
28886
+ "Viltu gefa leikinn?": {
28887
+ en: "Do you want to resign the game?",
28888
+ nb: "Ønsker du å gi opp spillet?",
28889
+ nn: "Ønskjer du å gje opp spelet?",
28890
+ pl: "Czy chcesz zrezygnować z gry?",
28891
+ ga: "Ar mhaith leat éirí as an gcluiche?"
28892
+ },
28893
+ " Já": {
28894
+ en: " Yes",
28895
+ nb: " Ja",
28896
+ nn: " Ja",
28897
+ pl: " Tak",
28898
+ ga: " Tá"
28899
+ },
28900
+ " Nei": {
28901
+ en: " No",
28902
+ nb: " Nei",
28903
+ nn: " Nei",
28904
+ pl: " Nie",
28905
+ ga: " Níl"
28906
+ },
28907
+ "Segja pass?": {
28908
+ en: "Pass the turn?",
28909
+ nb: "Passere turen?",
28910
+ nn: "Passere turen?",
28911
+ pl: "Spasować kolej?",
28912
+ ga: "Pas an tsealaíocht?"
28913
+ },
28914
+ "2x3 pöss í röð ljúka viðureign": {
28915
+ en: "2x3 passes in a row end the game",
28916
+ nb: "2x3 pasninger på rad avslutter spillet",
28917
+ nn: "2x3 pasningar på rad avsluttar spelet",
28918
+ pl: "2x3 pasy z rzędu kończą grę",
28919
+ ga: "Críochnaíonn 2x3 pasanna as a chéile an cluiche"
28920
+ },
28921
+ "Viðureign lýkur þar með": {
28922
+ en: "This finishes the game",
28923
+ nb: "Dette avslutter spillet",
28924
+ nn: "Dette avsluttar spelet",
28925
+ pl: "To kończy grę",
28926
+ ga: "Críochnaíonn sé seo an cluiche"
28927
+ },
28928
+ Skipta: Skipta,
28929
+ "Smelltu á flísarnar sem þú vilt skipta": {
28930
+ en: "Click on the tiles that you wish to exchange",
28931
+ nb: "Klikk på brikkene du ønsker å bytte",
28932
+ nn: "Klikk på brikkene du ønskjer å byte",
28933
+ pl: "Kliknij na kafelki, które chcesz wymienić",
28934
+ ga: "Cliceáil ar na tíleanna ar mhaith leat a mhalartú"
28935
+ },
28936
+ "Véfengja lögn?": {
28937
+ en: "Challenge move?",
28938
+ nb: "Utfordre trekk?",
28939
+ nn: "Utfordre trekk?",
28940
+ pl: "Wyzwanie ruchu?",
28941
+ ga: "Dúshlán bogadh?"
28942
+ },
28943
+ "Röng véfenging kostar 10 stig": {
28944
+ en: "Wrong challenge costs 10 points",
28945
+ nb: "Feil utfordring koster 10 poeng",
28946
+ nn: "Feil utfordring kostar 10 poeng",
28947
+ pl: "Błędne wyzwanie kosztuje 10 punktów",
28948
+ ga: "Cosnaíonn dúshlán mícheart 10 bpointe"
28949
+ },
28950
+ "Er álínis": {
28951
+ en: "Is online",
28952
+ nb: "Er pålogget",
28953
+ nn: "Er pålogga",
28954
+ pl: "Jest online",
28955
+ ga: "Ar líne"
28956
+ },
28957
+ "Álínis?": {
28958
+ en: "Online?",
28959
+ nb: "Pålogget?",
28960
+ nn: "Pålogga?",
28961
+ pl: "Online?",
28962
+ ga: "Ar líne?"
28963
+ },
28964
+ "Röð": {
28965
+ en: "Rank",
28966
+ nb: "Rangering",
28967
+ nn: "Rangering",
28968
+ pl: "Ranking",
28969
+ ga: "Rang"
28970
+ },
28971
+ "Röð í gær": {
28972
+ en: "Rank yesterday",
28973
+ nb: "Rangering i går",
28974
+ nn: "Rangering i går",
28975
+ pl: "Ranking wczoraj",
28976
+ ga: "Rang inné"
28977
+ },
28978
+ "Röð fyrir viku": {
28979
+ en: "Rank a week ago",
28980
+ nb: "Rangering for en uke siden",
28981
+ nn: "Rangering for ei veke sidan",
28982
+ pl: "Ranking tydzień temu",
28983
+ ga: "Rang seachtain ó shin"
28984
+ },
28985
+ "1d": {
28986
+ en: "1d",
28987
+ nb: "1d",
28988
+ nn: "1d",
28989
+ pl: "1d",
28990
+ ga: "1l"
28991
+ },
28992
+ "7d": {
28993
+ en: "7d",
28994
+ nb: "7d",
28995
+ nn: "7d",
28996
+ pl: "7d",
28997
+ ga: "7l"
28998
+ },
28999
+ "30d": {
29000
+ en: "30d",
29001
+ nb: "30d",
29002
+ nn: "30d",
29003
+ pl: "30d",
29004
+ ga: "30l"
29005
+ },
29006
+ "Elo-stig": {
29007
+ en: "Elo points",
29008
+ nb: "Elo-poeng",
29009
+ nn: "Elo-poeng",
29010
+ pl: "Punkty Elo",
29011
+ ga: "Pointí Elo"
29012
+ },
29013
+ "Elo-stig í gær": {
29014
+ en: "Elo points yesterday",
29015
+ nb: "Elo-poeng i går",
29016
+ nn: "Elo-poeng i går",
29017
+ pl: "Punkty Elo wczoraj",
29018
+ ga: "Pointí Elo inné"
29019
+ },
29020
+ "Elo-stig fyrir viku": {
29021
+ en: "Elo points a week ago",
29022
+ nb: "Elo-poeng for en uke siden",
29023
+ nn: "Elo-poeng for ei veke sidan",
29024
+ pl: "Punkty Elo tydzień temu",
29025
+ ga: "Pointí Elo seachtain ó shin"
29026
+ },
29027
+ "Elo-stig fyrir mánuði": {
29028
+ en: "Elo points a month ago",
29029
+ nb: "Elo-poeng for en måned siden",
29030
+ nn: "Elo-poeng for ein månad sidan",
29031
+ pl: "Punkty Elo miesiąc temu",
29032
+ ga: "Pointí Elo mí ó shin"
29033
+ },
29034
+ elo_list_choice: elo_list_choice,
29035
+ stats_choice: stats_choice,
29036
+ "Fjöldi viðureigna": {
29037
+ en: "Number of games",
29038
+ nb: "Antall spill",
29039
+ nn: "Tal på spel",
29040
+ pl: "Liczba gier",
29041
+ ga: "Líon cluichí"
29042
+ },
29043
+ Vinningshlutfall: Vinningshlutfall,
29044
+ "Meðalstigafjöldi": {
29045
+ en: "Average score",
29046
+ nb: "Gjennomsnittlig poengsum",
29047
+ nn: "Gjennomsnittleg poengsum",
29048
+ pl: "Średni wynik",
29049
+ ga: "Scór meánach"
29050
+ },
29051
+ " gegn öllum ": {
29052
+ en: " against all ",
29053
+ nb: " mot alle ",
29054
+ nn: " mot alle ",
29055
+ pl: " przeciwko wszystkim ",
29056
+ ga: " in aghaidh gach "
29057
+ },
29058
+ " gegn þér ": {
29059
+ en: " against you ",
29060
+ nb: " mot deg ",
29061
+ nn: " mot deg ",
29062
+ pl: " przeciwko tobie ",
29063
+ ga: " in aghaidh tú "
29064
+ },
29065
+ " - veldu lengd viðureignar:": {
29066
+ en: " - choose game duration:",
29067
+ nb: " - velg spillvarighet:",
29068
+ nn: " - vel spelvarigheit:",
29069
+ pl: " - wybierz czas trwania gry:",
29070
+ ga: " - roghnaigh fad an chluiche:"
29071
+ },
29072
+ "2 x 10 mínútur": {
29073
+ en: "2 x 10 minutes",
29074
+ nb: "2 x 10 minutter",
29075
+ nn: "2 x 10 minutt",
29076
+ pl: "2 x 10 minut",
29077
+ ga: "2 x 10 nóiméad"
29078
+ },
29079
+ "2 x 15 mínútur": {
29080
+ en: "2 x 15 minutes",
29081
+ nb: "2 x 15 minutter",
29082
+ nn: "2 x 15 minutt",
29083
+ pl: "2 x 15 minut",
29084
+ ga: "2 x 15 nóiméad"
29085
+ },
29086
+ "2 x 20 mínútur": {
29087
+ en: "2 x 20 minutes",
29088
+ nb: "2 x 20 minutter",
29089
+ nn: "2 x 20 minutt",
29090
+ pl: "2 x 20 minut",
29091
+ ga: "2 x 20 nóiméad"
29092
+ },
29093
+ "2 x 25 mínútur": {
29094
+ en: "2 x 25 minutes",
29095
+ nb: "2 x 25 minutter",
29096
+ nn: "2 x 25 minutt",
29097
+ pl: "2 x 25 minut",
29098
+ ga: "2 x 25 nóiméad"
29099
+ },
29100
+ "2 x 30 mínútur": {
29101
+ en: "2 x 30 minutes",
29102
+ nb: "2 x 30 minutter",
29103
+ nn: "2 x 30 minutt",
29104
+ pl: "2 x 30 minut",
29105
+ ga: "2 x 30 nóiméad"
29106
+ },
29107
+ "Báðir leikmenn lýsa því yfir að þeir skrafla ": {
29108
+ en: "Both players declare that they play ",
29109
+ nb: "Begge spillere erklærer at de spiller ",
29110
+ nn: "Begge spelarar erklærer at dei spelar ",
29111
+ pl: "Obaj gracze deklarują, że grają ",
29112
+ ga: "Dearbhaíonn an bheirt imreoirí go n-imríonn siad "
29113
+ },
29114
+ "Flísar sem eftir eru": {
29115
+ en: "Tiles remaining",
29116
+ nb: "Fliser igjen",
29117
+ nn: "Fliser igjen",
29118
+ pl: "Pozostałe kafelki",
29119
+ ga: "Tíleanna fágtha"
29120
+ },
29121
+ "Hvaða staf táknar auða flísin?": {
29122
+ en: "Which letter does the blank tile represent?",
29123
+ nb: "Hvilken bokstav representerer den blanke flisen?",
29124
+ nn: "Kva bokstav representerer den blanke flisa?",
29125
+ pl: "Jaką literę reprezentuje pusty kafelek?",
29126
+ ga: "Cén litir a léiríonn an tíl bán?"
29127
+ },
29128
+ "Þvinga til uppgjafar": {
29129
+ en: "Force to resign",
29130
+ nb: "Tving til å gi opp",
29131
+ nn: "Tving til å gje opp",
29132
+ pl: "Zmusić do rezygnacji",
29133
+ ga: "Éignigh le héirí as"
29134
+ },
29135
+ "14 dagar liðnir án leiks": {
29136
+ en: "14 days elapsed without a move",
29137
+ nb: "14 dager gått uten trekk",
29138
+ nn: "14 dagar gått utan trekk",
29139
+ pl: "Upłynęło 14 dni bez ruchu",
29140
+ ga: "14 lá caite gan bogadh"
29141
+ },
29142
+ "Gefa viðureign": {
29143
+ en: "Resign from game",
29144
+ nb: "Gi opp spillet",
29145
+ nn: "Gje opp spelet",
29146
+ pl: "Zrezygnować z gry",
29147
+ ga: "Éirigh as an gcluiche"
29148
+ },
29149
+ "Skipta stöfum": {
29150
+ en: "Exchange tiles",
29151
+ nb: "Bytt fliser",
29152
+ nn: "Byt fliser",
29153
+ pl: "Wymień kafelki",
29154
+ ga: "Malartú tíleanna"
29155
+ },
29156
+ word_not_found: word_not_found,
29157
+ "Smelltu til að fletta upp": {
29158
+ en: "Click to look up",
29159
+ nb: "Klikk for å slå opp",
29160
+ nn: "Klikk for å slå opp",
29161
+ pl: "Kliknij, aby wyszukać",
29162
+ ga: "Cliceáil chun cuardach"
29163
+ },
29164
+ "Skoða yfirlit": {
29165
+ en: "Review game",
29166
+ nb: "Gjennomgå spill",
29167
+ nn: "Gjennomgå spel",
29168
+ pl: "Przejrzyj grę",
29169
+ ga: "Athbhreithniú cluiche"
29170
+ },
29171
+ "Skraflað án hjálpartækja": {
29172
+ en: "Game without helpers or tools",
29173
+ nb: "Spill uten hjelpemidler eller verktøy",
29174
+ nn: "Spel utan hjelpemiddel eller verktøy",
29175
+ pl: "Gra bez pomocy lub narzędzi",
29176
+ ga: "Cluiche gan chúntóirí nó uirlisí"
29177
+ },
29178
+ "Skraflar án hjálpartækja": {
29179
+ en: "Plays without helpers or tools",
29180
+ nb: "Spiller uten hjelpemidler eller verktøy",
29181
+ nn: "Spelar utan hjelpemiddel eller verktøy",
29182
+ pl: "Gra bez pomocy lub narzędzi",
29183
+ ga: "Imríonn gan chúntóirí nó uirlisí"
29184
+ },
29185
+ "Til í viðureign með klukku": {
29186
+ en: "Willing to play timed games",
29187
+ nb: "Villig til å spille tidsbegrensede spill",
29188
+ nn: "Villig til å spele tidsbegrensa spel",
29189
+ pl: "Chętny do gry w gry zegarowe",
29190
+ ga: "Toilteanach cluichí ama a imirt"
29191
+ },
29192
+ "Álínis og tekur við áskorunum": {
29193
+ en: "Online and accepting challenges",
29194
+ nb: "Pålogget og aksepterer utfordringer",
29195
+ nn: "Pålogga og aksepterer utfordringar",
29196
+ pl: "Online i akceptuje wyzwania",
29197
+ ga: "Ar líne agus ag glacadh le dúshláin"
29198
+ },
29199
+ "Enginn stafur lagður niður": {
29200
+ en: "No tile played",
29201
+ nb: "Ingen flis spilt",
29202
+ nn: "Inga flis spelt",
29203
+ pl: "Żaden kafelek nie został położony",
29204
+ ga: "Níor imríodh aon tíl"
29205
+ },
29206
+ "Fyrsta orð verður að liggja um byrjunarreitinn": {
29207
+ en: "First word must cover the start square",
29208
+ nb: "Første ord må dekke startfeltet",
29209
+ nn: "Første ord må dekkje startfeltet",
29210
+ pl: "Pierwsze słowo musi pokryć pole startowe",
29211
+ ga: "Caithfidh an chéad fhocal an cearnóg tosaigh a chlúdach"
29212
+ },
29213
+ "Orð verður að vera samfellt á borðinu": {
29214
+ en: "Word must be placed consecutively on the board",
29215
+ nb: "Ord må plasseres etter hverandre på brettet",
29216
+ nn: "Ord må plasserast etter kvarandre på brettet",
29217
+ pl: "Słowo musi być umieszczone kolejno na planszy",
29218
+ ga: "Caithfear an focal a chur go leanúnach ar an mbord"
29219
+ },
29220
+ "Orð verður að tengjast orði sem fyrir er": {
29221
+ en: "Word must be connected to another word on the board",
29222
+ nb: "Ord må være koblet til et annet ord på brettet",
29223
+ nn: "Ord må vere kopla til eit anna ord på brettet",
29224
+ pl: "Słowo musi być połączone z innym słowem na planszy",
29225
+ ga: "Caithfidh an focal a bheith ceangailte le focal eile ar an mbord"
29226
+ },
29227
+ "Reitur þegar upptekinn": {
29228
+ en: "Square is already occupied",
29229
+ nb: "Feltet er allerede opptatt",
29230
+ nn: "Feltet er allereie oppteke",
29231
+ pl: "Pole jest już zajęte",
29232
+ ga: "Tá an cearnóg áitithe cheana"
29233
+ },
29234
+ "Ekki má vera eyða í orði": {
29235
+ en: "Word cannot contain a space",
29236
+ nb: "Ord kan ikke inneholde et mellomrom",
29237
+ nn: "Ord kan ikkje innehalde eit mellomrom",
29238
+ pl: "Słowo nie może zawierać spacji",
29239
+ ga: "Ní féidir spás a bheith i bhfocal"
29240
+ },
29241
+ "Of margir stafir lagðir niður": {
29242
+ en: "Too many tiles laid down",
29243
+ nb: "For mange fliser lagt ned",
29244
+ nn: "For mange fliser lagde ned",
29245
+ pl: "Położono zbyt wiele kafelków",
29246
+ ga: "Leagadh síos an iomarca tíleanna"
29247
+ },
29248
+ "Stafur er ekki í rekkanum": {
29249
+ en: "Tile is not present in the player's rack",
29250
+ nb: "Flisen er ikke til stede i spillerens stativ",
29251
+ nn: "Flisa er ikkje til stades i spelaren sitt stativ",
29252
+ pl: "Kafelek nie znajduje się na stojaku gracza",
29253
+ ga: "Níl an tíl i raca an imreora"
29254
+ },
29255
+ "Of fáir stafir eftir, skipting ekki leyfð": {
29256
+ en: "Too few tiles left in bag; exchange not permitted",
29257
+ nb: "For få fliser igjen i posen; bytte ikke tillatt",
29258
+ nn: "For få fliser igjen i posen; byte ikkje tillate",
29259
+ pl: "Zbyt mało kafelków w worku; wymiana niedozwolona",
29260
+ ga: "Ró-bheag tíleanna fágtha sa mhála; malartú toirmiscthe"
29261
+ },
29262
+ "Of mörgum stöfum skipt": {
29263
+ en: "Too many tiles exchanged",
29264
+ nb: "For mange fliser byttet",
29265
+ nn: "For mange fliser bytte",
29266
+ pl: "Wymieniono zbyt wiele kafelków",
29267
+ ga: "Malartaíodh an iomarca tíleanna"
29268
+ },
29269
+ "Leik vantar á borðið - endurglæðið vefráparann": {
29270
+ en: "Move missing from board - refresh browser",
29271
+ nb: "Trekk mangler på brettet - oppdater nettleseren",
29272
+ nn: "Trekk manglar på brettet - oppdater nettlesaren",
29273
+ pl: "Ruch nieobecny na planszy - odśwież przeglądarkę",
29274
+ ga: "Bogadh in easnamh ón mbord - athnuaigh an brabhsálaí"
29275
+ },
29276
+ "Notandi ekki innskráður - endurglæðið vefráparann": {
29277
+ en: "User not logged in - refresh browser",
29278
+ nb: "Bruker ikke logget inn - oppdater nettleseren",
29279
+ nn: "Brukar ikkje logga inn - oppdater nettlesaren",
29280
+ pl: "Użytkownik nie jest zalogowany - odśwież przeglądarkę",
29281
+ ga: "Úsáideoir gan logáil isteach - athnuaigh an brabhsálaí"
29282
+ },
29283
+ "Rangur eða óþekktur notandi": {
29284
+ en: "Wrong or unknown user",
29285
+ nb: "Feil eller ukjent bruker",
29286
+ nn: "Feil eller ukjend brukar",
29287
+ pl: "Błędny lub nieznany użytkownik",
29288
+ ga: "Úsáideoir mícheart nó anaithnid"
29289
+ },
29290
+ "Viðureign finnst ekki": {
29291
+ en: "Game not found",
29292
+ nb: "Spill ikke funnet",
29293
+ nn: "Spel ikkje funne",
29294
+ pl: "Gra nie znaleziona",
29295
+ ga: "Cluiche gan aimsiú"
29296
+ },
29297
+ "Viðureign er ekki utan tímamarka": {
29298
+ en: "Game has not exceeded time limit",
29299
+ nb: "Spillet har ikke overskredet tidsbegrensningen",
29300
+ nn: "Spelet har ikkje overskride tidsbegrensinga",
29301
+ pl: "Gra nie przekroczyła limitu czasu",
29302
+ ga: "Níor sháraigh an cluiche an teorainn ama"
29303
+ },
29304
+ "Netþjónn gat ekki tekið við leiknum - reyndu aftur": {
29305
+ en: "Server is unable to process move - try again",
29306
+ nb: "Serveren kan ikke behandle trekket - prøv igjen",
29307
+ nn: "Tenaren kan ikkje behandle trekket - prøv igjen",
29308
+ pl: "Serwer nie może przetworzyć ruchu - spróbuj ponownie",
29309
+ ga: "Ní féidir leis an bhfreastalaí an gluaiseacht a phróiseáil - bain triail eile as"
29310
+ },
29311
+ "Véfenging er ekki möguleg í þessari viðureign": {
29312
+ en: "A challenge is not possible in this game",
29313
+ nb: "En utfordring er ikke mulig i dette spillet",
29314
+ nn: "Ei utfordring er ikkje mogleg i dette spelet",
29315
+ pl: "Wyzwanie nie jest możliwe w tej grze",
29316
+ ga: "Ní féidir dúshlán a thabhairt sa chluiche seo"
29317
+ },
29318
+ "Síðasti leikur er ekki véfengjanlegur": {
29319
+ en: "The last move cannot be challenged",
29320
+ nb: "Siste trekk kan ikke utfordres",
29321
+ nn: "Siste trekk kan ikkje utfordrast",
29322
+ pl: "Ostatniego ruchu nie można zakwestionować",
29323
+ ga: "Ní féidir an gluaiseacht deireanach a dhúshlánú"
29324
+ },
29325
+ "Aðeins véfenging eða pass leyfileg": {
29326
+ en: "Only a challenge or a pass are possible",
29327
+ nb: "Bare en utfordring eller pass er mulig",
29328
+ nn: "Berre ei utfordring eller pass er mogleg",
29329
+ pl: "Możliwe jest tylko wyzwanie lub pas",
29330
+ ga: "Níl ach dúshlán nó pas indéanta"
29331
+ },
29332
+ welcome_2: welcome_2,
29333
+ welcome_1: welcome_1,
29334
+ welcome_0: welcome_0,
29335
+ "Skrái þig inn...": {
29336
+ en: "Logging you in...",
29337
+ nb: "Logger deg inn...",
29338
+ nn: "Loggar deg inn...",
29339
+ pl: "Logowanie...",
29340
+ ga: "Ag logáil isteach tú..."
29341
+ },
29342
+ "Innskrá": {
29343
+ en: "Log in",
29344
+ nb: "Logg inn",
29345
+ nn: "Logg inn",
29346
+ pl: "Zaloguj się",
29347
+ ga: "Logáil isteach"
29348
+ },
29349
+ "Smelltu til að raða eftir seinni staf": {
29350
+ en: "Click to sort by last letter",
29351
+ nb: "Klikk for å sortere etter siste bokstav",
29352
+ nn: "Klikk for å sortere etter siste bokstav",
29353
+ pl: "Kliknij, aby posortować według ostatniej litery",
29354
+ ga: "Cliceáil chun sórtáil de réir an litir deireanaigh"
29355
+ },
29356
+ "Smelltu til að raða eftir fyrri staf": {
29357
+ en: "Click to sort by first letter",
29358
+ nb: "Klikk for å sortere etter første bokstav",
29359
+ nn: "Klikk for å sortere etter første bokstav",
29360
+ pl: "Kliknij, aby posortować według pierwszej litery",
29361
+ ga: "Cliceáil chun sórtáil de réir an chéad litir"
29362
+ },
29363
+ "Hvernig reitirnir margfalda stigin": {
29364
+ en: "How squares multiply points",
29365
+ nb: "Hvordan ruter multipliserer poeng",
29366
+ nn: "Korleis ruter multipliserer poeng",
29367
+ pl: "Jak kwadraty mnożą punkty",
29368
+ ga: "Conas a iolraíonn cearnóga pointí"
29369
+ },
29370
+ "Loka þessari hjálp": {
29371
+ en: "Close this help",
29372
+ nb: "Lukk denne hjelpen",
29373
+ nn: "Lukk denne hjelpa",
29374
+ pl: "Zamknij tę pomoc",
29375
+ ga: "Dún an chabhair seo"
29376
+ },
29377
+ "Stokka upp rekka": {
29378
+ en: "Shuffle rack",
29379
+ nb: "Stokk stativet",
29380
+ nn: "Stokk stativet",
29381
+ pl: "Przetasuj stojak",
29382
+ ga: "Measc raca"
29383
+ },
29384
+ "Færa stafi aftur í rekka": {
29385
+ en: "Recall tiles into rack",
29386
+ nb: "Hent fliser tilbake til stativet",
29387
+ nn: "Hent fliser tilbake til stativet",
29388
+ pl: "Przywróć kafelki do stojaka",
29389
+ ga: "Cuimhnigh tíleanna isteach sa raca"
29390
+ },
29391
+ "Besta mögulega lögn": {
29392
+ en: "Best possible move",
29393
+ nb: "Beste mulige trekk",
29394
+ nn: "Beste mogelege trekk",
29395
+ pl: "Najlepszy możliwy ruch",
29396
+ ga: "An bogadh is fearr is féidir"
29397
+ },
29398
+ "Nei, takk": {
29399
+ en: "No, thanks",
29400
+ nb: "Nei, takk",
29401
+ nn: "Nei, takk",
29402
+ pl: "Nie, dziękuję",
29403
+ ga: "Ní hea"
29404
+ },
29405
+ "Fyrri dagur": {
29406
+ en: "Previous day",
29407
+ nb: "Forrige dag",
29408
+ nn: "Førre dag",
29409
+ pl: "Poprzedni dzień",
29410
+ ga: "An lá roimhe"
29411
+ },
29412
+ "Næsti dagur": {
29413
+ en: "Next day",
29414
+ nb: "Neste dag",
29415
+ nn: "Neste dag",
29416
+ pl: "Następny dzień",
29417
+ ga: "An lá dar gcionn"
29418
+ },
29419
+ "Frammistaða": {
29420
+ en: "Performance",
29421
+ nb: "Ytelse",
29422
+ nn: "Yting",
29423
+ pl: "Wyniki",
29424
+ ga: "Feidhmíocht"
29425
+ },
29426
+ "Tölfræði": {
29427
+ en: "Statistics",
29428
+ nb: "Statistikk",
29429
+ nn: "Statistikk",
29430
+ pl: "Statystyki",
29431
+ ga: "Staitisticí"
29432
+ },
29433
+ Stigatafla: Stigatafla,
29434
+ "Tölfræði og stigatafla": {
29435
+ en: "Stats and leaderboard",
29436
+ nb: "Statistikk og toppliste",
29437
+ nn: "Statistikk og toppliste",
29438
+ pl: "Statystyki i tabela",
29439
+ ga: "Staitisticí agus clár"
29440
+ },
29441
+ "Þín besta": {
29442
+ en: "Your best",
29443
+ nb: "Din beste",
29444
+ nn: "Di beste",
29445
+ pl: "Twój najlepszy",
29446
+ ga: "Do cheann is fearr"
29447
+ },
29448
+ "Þú leiðir!": {
29449
+ en: "You lead!",
29450
+ nb: "Du leder!",
29451
+ nn: "Du leier!",
29452
+ pl: "Prowadzisz!",
29453
+ ga: "Tá tú chun tosaigh!"
29454
+ },
29455
+ "Best til þessa": {
29456
+ en: "Best so far",
29457
+ nb: "Beste hittil",
29458
+ nn: "Beste hittil",
29459
+ pl: "Najlepszy dotąd",
29460
+ ga: "Is fearr go dtí seo"
29461
+ },
29462
+ "Sýna besta leik": {
29463
+ en: "Show best move",
29464
+ nb: "Vis beste trekk",
29465
+ nn: "Vis beste trekk",
29466
+ pl: "Pokaż najlepszy ruch",
29467
+ ga: "Taispeáin an bogadh is fearr"
29468
+ },
29469
+ "Sýna leik": {
29470
+ en: "Show move",
29471
+ nb: "Vis trekk",
29472
+ nn: "Vis trekk",
29473
+ pl: "Pokaż ruch",
29474
+ ga: "Taispeáin bogadh"
29475
+ },
29476
+ "Smelltu til að sjá lausn": {
29477
+ en: "Click to see solution",
29478
+ nb: "Klikk for å se løsning",
29479
+ nn: "Klikk for å sjå løysing",
29480
+ pl: "Kliknij, aby zobaczyć rozwiązanie",
29481
+ ga: "Cliceáil chun an réiteach a fheiceáil"
29482
+ },
29483
+ "Þú": {
29484
+ en: "You",
29485
+ nb: "Du",
29486
+ nn: "Du",
29487
+ pl: "Ty",
29488
+ ga: "Tú"
29489
+ },
29490
+ "Sæki tölfræði...": {
29491
+ en: "Loading stats...",
29492
+ nb: "Laster statistikk...",
29493
+ nn: "Lastar statistikk...",
29494
+ pl: "Ładowanie statystyk...",
29495
+ ga: "Ag lódáil staitisticí..."
29496
+ },
29497
+ "Engin tölfræði til að sýna": {
29498
+ en: "No statistics to show",
29499
+ nb: "Ingen statistikk å vise",
29500
+ nn: "Ingen statistikk å vise",
29501
+ pl: "Brak statystyk do wyświetlenia",
29502
+ ga: "Níl aon staitisticí le taispeáint"
29503
+ },
29504
+ "Núverandi striklota": {
29505
+ en: "Current streak",
29506
+ nb: "Nåværende rekke",
29507
+ nn: "Noverande rekkje",
29508
+ pl: "Bieżąca seria",
29509
+ ga: "Sraith reatha"
29510
+ },
29511
+ "Lengsta striklota": {
29512
+ en: "Longest streak",
29513
+ nb: "Lengste rekke",
29514
+ nn: "Lengste rekkje",
29515
+ pl: "Najdłuższa seria",
29516
+ ga: "An tsraith is faide"
29517
+ },
29518
+ "Hæsta skori náð": {
29519
+ en: "Best score achieved",
29520
+ nb: "Beste oppnådde poengsum",
29521
+ nn: "Beste oppnådde poengsum",
29522
+ pl: "Najwyższy wynik",
29523
+ ga: "An scór is fearr"
29524
+ },
29525
+ "Striklota hæsta skors": {
29526
+ en: "Best score streak",
29527
+ nb: "Rekke med beste poengsum",
29528
+ nn: "Rekkje med beste poengsum",
29529
+ pl: "Seria najwyższych wyników",
29530
+ ga: "Sraith scór is fearr"
29531
+ },
29532
+ "Heildarfjöldi daga": {
29533
+ en: "Total days played",
29534
+ nb: "Totalt antall dager",
29535
+ nn: "Totalt tal på dagar",
29536
+ pl: "Łączna liczba dni",
29537
+ ga: "Iomlán laethanta"
29538
+ },
29539
+ "Hleð stigatöflu...": {
29540
+ en: "Loading leaderboard...",
29541
+ nb: "Laster toppliste...",
29542
+ nn: "Lastar toppliste...",
29543
+ pl: "Ładowanie tabeli...",
29544
+ ga: "Ag lódáil an chláir..."
29545
+ },
29546
+ "Engin stig skráð enn": {
29547
+ en: "No scores yet",
29548
+ nb: "Ingen poeng ennå",
29549
+ nn: "Ingen poeng enno",
29550
+ pl: "Brak wyników",
29551
+ ga: "Gan scóir fós"
29552
+ },
29553
+ "Reyna síðar": {
29554
+ en: "Try later",
29555
+ nb: "Prøv senere",
29556
+ nn: "Prøv seinare",
29557
+ pl: "Spróbuj później",
29558
+ ga: "Bain triail níos déanaí"
29559
+ },
29560
+ " mínútur.": {
29561
+ en: " minutes.",
29562
+ nb: " minutter.",
29563
+ nn: " minutt.",
29564
+ pl: " minut.",
29565
+ ga: " nóiméad."
29566
+ },
29567
+ date_format: date_format,
29568
+ january: january,
29569
+ february: february,
29570
+ march: march,
29571
+ april: april,
29572
+ may: may,
29573
+ june: june,
29574
+ july: july,
29575
+ august: august,
29576
+ september: september,
29577
+ october: october,
29578
+ november: november,
29579
+ december: december,
29580
+ "[sentinel]": {
29581
+ }
29582
+ };
29583
+
27639
29584
  /*
27640
29585
 
27641
- Types.ts
29586
+ i18n.ts
27642
29587
 
27643
- Common type definitions for the Explo/Netskrafl user interface
29588
+ Single page UI for Netskrafl/Explo using the Mithril library
27644
29589
 
27645
29590
  Copyright (C) 2025 Miðeind ehf.
27646
- Author: Vilhjalmur Thorsteinsson
29591
+ Author: Vilhjálmur Þorsteinsson
27647
29592
 
27648
29593
  The Creative Commons Attribution-NonCommercial 4.0
27649
29594
  International Public License (CC-BY-NC 4.0) applies to this software.
27650
29595
  For further information, see https://github.com/mideind/Netskrafl
27651
29596
 
29597
+
29598
+ This module contains internationalization (i18n) utility functions,
29599
+ allowing for translation of displayed text between languages.
29600
+
29601
+ Text messages are embedded directly from the messages.json file
29602
+ at build time.
29603
+
27652
29604
  */
27653
- // Global constants
27654
- const RACK_SIZE = 7;
27655
- const ROWIDS = 'ABCDEFGHIJKLMNO';
27656
- const BOARD_SIZE = ROWIDS.length;
27657
- const EXTRA_WIDE_LETTERS = 'q';
27658
- const WIDE_LETTERS = 'zxmæ';
27659
- const ZOOM_FACTOR = 1.5;
27660
- const ERROR_MESSAGES = {
27661
- // Translations are found in /static/assets/messages.json
27662
- 1: 'Enginn stafur lagður niður',
27663
- 2: 'Fyrsta orð verður liggja um byrjunarreitinn',
27664
- 3: 'Orð verður vera samfellt á borðinu',
27665
- 4: 'Orð verður tengjast orði sem fyrir er',
27666
- 5: 'Reitur þegar upptekinn',
27667
- 6: 'Ekki má vera eyða í orði',
27668
- 7: 'word_not_found',
27669
- 8: 'word_not_found',
27670
- 9: 'Of margir stafir lagðir niður',
27671
- 10: 'Stafur er ekki í rekkanum',
27672
- 11: 'Of fáir stafir eftir, skipting ekki leyfð',
27673
- 12: 'Of mörgum stöfum skipt',
27674
- 13: 'Leik vantar á borðið - notið F5/Refresh',
27675
- 14: 'Notandi ekki innskráður - notið F5/Refresh',
27676
- 15: 'Rangur eða óþekktur notandi',
27677
- 16: 'Viðureign finnst ekki',
27678
- 17: 'Viðureign er ekki utan tímamarka',
27679
- 18: 'Netþjónn gat ekki tekið við leiknum - reyndu aftur',
27680
- 19: 'Véfenging er ekki möguleg í þessari viðureign',
27681
- 20: 'Síðasti leikur er ekki véfengjanlegur',
27682
- 21: 'Aðeins véfenging eða pass leyfileg',
27683
- server: 'Netþjónn gat ekki tekið við leiknum - reyndu aftur',
27684
- };
29605
+ // Current exact user locale and fallback locale ("en" for "en_US"/"en_GB"/...)
29606
+ // This is overwritten in setLocale()
29607
+ let currentLocale = 'is_IS';
29608
+ let currentFallback = 'is';
29609
+ // Regex that matches embedded interpolations such as "Welcome, {username}!"
29610
+ // Interpolation identifiers should only contain ASCII characters, digits and '_'
29611
+ const rex = /{\s*(\w+)\s*}/g;
29612
+ let messages = {};
29613
+ let messagesLoaded = false;
29614
+ function getLocaleFromUrl() {
29615
+ // Check for a ?lang= query parameter to override the locale
29616
+ // Accepts formats like: lang=nb, lang=nb-NO, lang=nb_NO
29617
+ const params = new URLSearchParams(window.location.search);
29618
+ const lang = params.get('lang');
29619
+ if (!lang)
29620
+ return null;
29621
+ // Normalize: nb-NO → nb_NO
29622
+ return lang.replace('-', '_');
29623
+ }
29624
+ function hasAnyTranslation(msgs, locale) {
29625
+ // Return true if any translation is available for the given locale
29626
+ for (const key in msgs) {
29627
+ if (msgs[key][locale] !== undefined)
29628
+ return true;
29629
+ }
29630
+ return false;
29631
+ }
29632
+ function setLocale(locale, msgs) {
29633
+ // Set the current i18n locale and fallback
29634
+ currentLocale = locale;
29635
+ currentFallback = locale.split('_')[0];
29636
+ // For unsupported locales, i.e. locales that have no
29637
+ // translations available for them, fall back to English (U.S.).
29638
+ if (!hasAnyTranslation(msgs, currentLocale) &&
29639
+ !hasAnyTranslation(msgs, currentFallback)) {
29640
+ currentLocale = 'en_US';
29641
+ currentFallback = 'en';
29642
+ }
29643
+ // Flatten the Messages structure, enabling long strings
29644
+ // to be represented as string arrays in the messages.json file
29645
+ messages = {};
29646
+ for (const key in msgs) {
29647
+ for (const lc in msgs[key]) {
29648
+ let s = msgs[key][lc];
29649
+ if (Array.isArray(s))
29650
+ s = s.join('');
29651
+ if (messages[key] === undefined)
29652
+ messages[key] = {};
29653
+ // If the string s contains HTML markup of the form <tag>...</tag>,
29654
+ // convert it into a list of Mithril Vnode children corresponding to
29655
+ // the text and the tags
29656
+ if (s.match(/<[a-z]+>/)) {
29657
+ // Looks like the string contains HTML markup
29658
+ const vnodes = [];
29659
+ let i = 0;
29660
+ let tagMatch = null;
29661
+ while (i < s.length &&
29662
+ // biome-ignore lint/suspicious/noAssignInExpressions: Done for efficiency
29663
+ (tagMatch = s.slice(i).match(/<[a-z]+>/)) &&
29664
+ tagMatch.index !== undefined) {
29665
+ // Found what looks like an HTML tag
29666
+ // Calculate the index of the enclosed text within s
29667
+ const tag = tagMatch[0];
29668
+ const j = i + tagMatch.index + tag.length;
29669
+ // Find the end tag
29670
+ const end = s.indexOf(`</${tag.slice(1)}`, j);
29671
+ if (end < 0) {
29672
+ // No end tag - skip past this weirdness
29673
+ i = j;
29674
+ continue;
29675
+ }
29676
+ // Add the text preceding the tag
29677
+ if (tagMatch.index > 0)
29678
+ vnodes.push(s.slice(i, i + tagMatch.index));
29679
+ // Create the Mithril node corresponding to the tag and the enclosed text
29680
+ // and add it to the list
29681
+ vnodes.push(m(tag.slice(1, -1), s.slice(j, end)));
29682
+ // Advance the index past the end of the tag
29683
+ i = end + tag.length + 1;
29684
+ }
29685
+ // Push the final text part, if any
29686
+ if (i < s.length)
29687
+ vnodes.push(s.slice(i));
29688
+ // Reassign s to the list of vnodes
29689
+ s = vnodes;
29690
+ }
29691
+ messages[key][lc] = s;
29692
+ }
29693
+ }
29694
+ messagesLoaded = true;
29695
+ }
29696
+ function initMessages(locale) {
29697
+ // Initialize the i18n messages with the embedded messages data
29698
+ // and set the user's locale. A ?lang= URL parameter overrides the locale.
29699
+ const override = getLocaleFromUrl();
29700
+ setLocale(override || locale, messagesData);
29701
+ }
29702
+ function t(key, ips = {}) {
29703
+ // Main text translation function, supporting interpolation
29704
+ // and HTML tag substitution
29705
+ const msgDict = messages[key];
29706
+ if (msgDict === undefined)
29707
+ // No dictionary for this key - may actually be a missing entry
29708
+ return messagesLoaded ? key : '';
29709
+ // Lookup exact locale, then fallback, then resort to returning the key
29710
+ const message = msgDict[currentLocale] || msgDict[currentFallback] || key;
29711
+ // If we have an interpolation object, do the interpolation first
29712
+ return Object.keys(ips).length ? interpolate(message, ips) : message;
29713
+ }
29714
+ function ts(key, ips = {}) {
29715
+ // String translation function, supporting interpolation
29716
+ // but not HTML tag substitution
29717
+ const msgDict = messages[key];
29718
+ if (msgDict === undefined)
29719
+ // No dictionary for this key - may actually be a missing entry
29720
+ return messagesLoaded ? key : '';
29721
+ // Lookup exact locale, then fallback, then resort to returning the key
29722
+ const message = msgDict[currentLocale] || msgDict[currentFallback] || key;
29723
+ if (typeof message !== 'string')
29724
+ // This is actually an error - the client should be calling t() instead
29725
+ return '';
29726
+ // If we have an interpolation object, do the interpolation first
29727
+ return Object.keys(ips).length ? interpolate_string(message, ips) : message;
29728
+ }
29729
+ function mt(cls, children) {
29730
+ // Wrapper for the Mithril m() function that auto-translates
29731
+ // string and array arguments
29732
+ if (typeof children === 'string') {
29733
+ return m(cls, t(children));
29734
+ }
29735
+ if (Array.isArray(children)) {
29736
+ return m(cls, children.map((item) => (typeof item === 'string' ? t(item) : item)));
29737
+ }
29738
+ return m(cls, children);
29739
+ }
29740
+ function interpolate(message, ips) {
29741
+ // Replace interpolation placeholders with their corresponding values
29742
+ if (typeof message === 'string') {
29743
+ return message.replace(rex, (match, key) => ips[key] || match);
29744
+ }
29745
+ if (Array.isArray(message)) {
29746
+ return message.map((item) => interpolate(item, ips));
29747
+ }
29748
+ return message;
29749
+ }
29750
+ function interpolate_string(message, ips) {
29751
+ // Replace interpolation placeholders with their corresponding values
29752
+ return message.replace(rex, (match, key) => ips[key] || match);
29753
+ }
27685
29754
 
27686
29755
  /*
27687
29756
 
@@ -27708,6 +29777,21 @@ var hasZoomed = false;
27708
29777
  // Old-style (non-single-page) game URL prefix
27709
29778
  const BOARD_PREFIX = '/board?game=';
27710
29779
  const BOARD_PREFIX_LEN = BOARD_PREFIX.length;
29780
+ // Translation keys for month names (january, february, etc.)
29781
+ const MONTH_KEYS = [
29782
+ 'january',
29783
+ 'february',
29784
+ 'march',
29785
+ 'april',
29786
+ 'may',
29787
+ 'june',
29788
+ 'july',
29789
+ 'august',
29790
+ 'september',
29791
+ 'october',
29792
+ 'november',
29793
+ 'december',
29794
+ ];
27711
29795
  function addPinchZoom(attrs, funcZoomIn, funcZoomOut) {
27712
29796
  // Install event handlers for the pointer target
27713
29797
  attrs.onpointerdown = pointerdown_handler;
@@ -27922,6 +30006,15 @@ function valueOrK(value, breakpoint = 10000) {
27922
30006
  value = Math.round(value / 1000);
27923
30007
  return `${sign}${value}K`;
27924
30008
  }
30009
+ function formatDate(dateStr) {
30010
+ // Format YYYY-MM-DD to localized date using the date_format template
30011
+ // e.g., "2. október" in Icelandic, "October 2" in English
30012
+ // Ensure date is parsed as UTC for consistent formatting
30013
+ const date = new Date(`${dateStr}T00:00:00Z`);
30014
+ const day = date.getUTCDate().toString();
30015
+ const month = ts(MONTH_KEYS[date.getUTCMonth()]);
30016
+ return ts('date_format', { day, month });
30017
+ }
27925
30018
 
27926
30019
  /*
27927
30020
 
@@ -27941,10 +30034,14 @@ function valueOrK(value, breakpoint = 10000) {
27941
30034
  */
27942
30035
  class Actions {
27943
30036
  constructor(model) {
30037
+ this.riddleLoading = false; // Prevents concurrent loadRiddle calls
27944
30038
  this.model = model;
27945
30039
  // Media and Firebase listeners will be initialized
27946
30040
  // when view is available
27947
30041
  }
30042
+ get isRiddleLoading() {
30043
+ return this.riddleLoading;
30044
+ }
27948
30045
  onNavigateTo(routeName, params, view) {
27949
30046
  // We have navigated to a new route
27950
30047
  // If navigating to something other than help,
@@ -28450,13 +30547,18 @@ class Actions {
28450
30547
  this.model.leaderboard = [];
28451
30548
  }
28452
30549
  else {
28453
- // Convert dictionary to array and sort by score (desc), then timestamp (desc)
28454
- const entries = Object.keys(json).map((userId) => ({
30550
+ const { riddle } = this.model;
30551
+ const bestPossibleScore = riddle?.bestPossibleScore ?? Number.POSITIVE_INFINITY;
30552
+ // Convert dictionary to array and filter out invalid entries
30553
+ const entries = Object.keys(json)
30554
+ .map((userId) => ({
28455
30555
  userId: json[userId].userId || userId,
28456
30556
  displayName: json[userId].displayName || '',
28457
30557
  score: json[userId].score || 0,
28458
30558
  timestamp: json[userId].timestamp || '',
28459
- }));
30559
+ }))
30560
+ // Filter out invalid entries (score > bestPossibleScore)
30561
+ .filter((entry) => entry.score <= bestPossibleScore);
28460
30562
  // Sort by score descending, then by timestamp ascending (earlier first)
28461
30563
  entries.sort((a, b) => {
28462
30564
  if (b.score !== a.score) {
@@ -28507,13 +30609,43 @@ class Actions {
28507
30609
  m.redraw();
28508
30610
  }
28509
30611
  async fetchRiddle(date, locale) {
28510
- // Create the game via model
30612
+ // Create the riddle via the model
28511
30613
  if (!this.model)
28512
30614
  throw new Error('Model is not initialized');
28513
30615
  await this.model.initRiddle(date, locale);
28514
30616
  // Attach Firebase listeners
28515
30617
  this.attachListenerToRiddle(date, locale);
28516
30618
  }
30619
+ async loadRiddle(date, locale) {
30620
+ // Load a specific riddle date, handling cleanup of previous listeners
30621
+ // Prevent concurrent loads (e.g., from rapid navigation clicks)
30622
+ if (this.riddleLoading)
30623
+ return;
30624
+ this.riddleLoading = true;
30625
+ try {
30626
+ const currentRiddle = this.model.riddle;
30627
+ const today = new Date().toISOString().split('T')[0];
30628
+ const MIN_DATE = GATA_DAGSINS_MIN_DATE; // Earliest allowed date
30629
+ let effectiveDate = date;
30630
+ if (effectiveDate < MIN_DATE || effectiveDate > today) {
30631
+ effectiveDate = today;
30632
+ // Update URL to reflect the corrected date
30633
+ const url = new URL(window.location.href);
30634
+ url.searchParams.set('date', effectiveDate);
30635
+ window.history.pushState({}, '', url.toString());
30636
+ }
30637
+ // If we have an existing riddle, detach its listeners first
30638
+ if (currentRiddle) {
30639
+ this.detachListenerFromRiddle(currentRiddle.date, currentRiddle.locale);
30640
+ }
30641
+ // Fetch and initialize the new riddle
30642
+ await this.fetchRiddle(effectiveDate, locale);
30643
+ m.redraw();
30644
+ }
30645
+ finally {
30646
+ this.riddleLoading = false;
30647
+ }
30648
+ }
28517
30649
  cleanupRiddle(date, locale) {
28518
30650
  // Detach Firebase listeners
28519
30651
  this.detachListenerFromRiddle(date, locale);
@@ -28534,181 +30666,11 @@ function createRouteResolver(actions, view) {
28534
30666
  },
28535
30667
  // Render a view on a model
28536
30668
  render: () => {
28537
- return view.appView(item.name);
28538
- },
28539
- };
28540
- return acc;
28541
- }, {});
28542
- }
28543
-
28544
- /*
28545
-
28546
- i8n.ts
28547
-
28548
- Single page UI for Netskrafl/Explo using the Mithril library
28549
-
28550
- Copyright (C) 2025 Miðeind ehf.
28551
- Author: Vilhjálmur Þorsteinsson
28552
-
28553
- The Creative Commons Attribution-NonCommercial 4.0
28554
- International Public License (CC-BY-NC 4.0) applies to this software.
28555
- For further information, see https://github.com/mideind/Netskrafl
28556
-
28557
-
28558
- This module contains internationalization (i18n) utility functions,
28559
- allowing for translation of displayed text between languages.
28560
-
28561
- Text messages for individual locales are loaded from the
28562
- /static/assets/messages.json file, which is fetched from the server.
28563
-
28564
- */
28565
- // Current exact user locale and fallback locale ("en" for "en_US"/"en_GB"/...)
28566
- // This is overwritten in setLocale()
28567
- let currentLocale = 'is_IS';
28568
- let currentFallback = 'is';
28569
- // Regex that matches embedded interpolations such as "Welcome, {username}!"
28570
- // Interpolation identifiers should only contain ASCII characters, digits and '_'
28571
- const rex = /{\s*(\w+)\s*}/g;
28572
- let messages = {};
28573
- let messagesLoaded = false;
28574
- function hasAnyTranslation(msgs, locale) {
28575
- // Return true if any translation is available for the given locale
28576
- for (const key in msgs) {
28577
- if (msgs[key][locale] !== undefined)
28578
- return true;
28579
- }
28580
- return false;
28581
- }
28582
- function setLocale(locale, msgs) {
28583
- // Set the current i18n locale and fallback
28584
- currentLocale = locale;
28585
- currentFallback = locale.split('_')[0];
28586
- // For unsupported locales, i.e. locales that have no
28587
- // translations available for them, fall back to English (U.S.).
28588
- if (!hasAnyTranslation(msgs, currentLocale) &&
28589
- !hasAnyTranslation(msgs, currentFallback)) {
28590
- currentLocale = 'en_US';
28591
- currentFallback = 'en';
28592
- }
28593
- // Flatten the Messages structure, enabling long strings
28594
- // to be represented as string arrays in the messages.json file
28595
- messages = {};
28596
- for (const key in msgs) {
28597
- for (const lc in msgs[key]) {
28598
- let s = msgs[key][lc];
28599
- if (Array.isArray(s))
28600
- s = s.join('');
28601
- if (messages[key] === undefined)
28602
- messages[key] = {};
28603
- // If the string s contains HTML markup of the form <tag>...</tag>,
28604
- // convert it into a list of Mithril Vnode children corresponding to
28605
- // the text and the tags
28606
- if (s.match(/<[a-z]+>/)) {
28607
- // Looks like the string contains HTML markup
28608
- const vnodes = [];
28609
- let i = 0;
28610
- let tagMatch = null;
28611
- while (i < s.length &&
28612
- // biome-ignore lint/suspicious/noAssignInExpressions: Done for efficiency
28613
- (tagMatch = s.slice(i).match(/<[a-z]+>/)) &&
28614
- tagMatch.index !== undefined) {
28615
- // Found what looks like an HTML tag
28616
- // Calculate the index of the enclosed text within s
28617
- const tag = tagMatch[0];
28618
- const j = i + tagMatch.index + tag.length;
28619
- // Find the end tag
28620
- const end = s.indexOf(`</${tag.slice(1)}`, j);
28621
- if (end < 0) {
28622
- // No end tag - skip past this weirdness
28623
- i = j;
28624
- continue;
28625
- }
28626
- // Add the text preceding the tag
28627
- if (tagMatch.index > 0)
28628
- vnodes.push(s.slice(i, i + tagMatch.index));
28629
- // Create the Mithril node corresponding to the tag and the enclosed text
28630
- // and add it to the list
28631
- vnodes.push(m(tag.slice(1, -1), s.slice(j, end)));
28632
- // Advance the index past the end of the tag
28633
- i = end + tag.length + 1;
28634
- }
28635
- // Push the final text part, if any
28636
- if (i < s.length)
28637
- vnodes.push(s.slice(i));
28638
- // Reassign s to the list of vnodes
28639
- s = vnodes;
28640
- }
28641
- messages[key][lc] = s;
28642
- }
28643
- }
28644
- messagesLoaded = true;
28645
- }
28646
- async function loadMessages(state, locale) {
28647
- // Load the internationalization message JSON file from the server
28648
- // and set the user's locale
28649
- try {
28650
- const messages = await requestWithoutAuth(state, {
28651
- method: 'GET',
28652
- url: '/static/assets/messages.json',
28653
- withCredentials: false, // Cookies are not allowed for CORS request
28654
- });
28655
- setLocale(locale, messages);
28656
- }
28657
- catch {
28658
- setLocale(locale, {});
28659
- }
28660
- }
28661
- function t(key, ips = {}) {
28662
- // Main text translation function, supporting interpolation
28663
- // and HTML tag substitution
28664
- const msgDict = messages[key];
28665
- if (msgDict === undefined)
28666
- // No dictionary for this key - may actually be a missing entry
28667
- return messagesLoaded ? key : '';
28668
- // Lookup exact locale, then fallback, then resort to returning the key
28669
- const message = msgDict[currentLocale] || msgDict[currentFallback] || key;
28670
- // If we have an interpolation object, do the interpolation first
28671
- return Object.keys(ips).length ? interpolate(message, ips) : message;
28672
- }
28673
- function ts(key, ips = {}) {
28674
- // String translation function, supporting interpolation
28675
- // but not HTML tag substitution
28676
- const msgDict = messages[key];
28677
- if (msgDict === undefined)
28678
- // No dictionary for this key - may actually be a missing entry
28679
- return messagesLoaded ? key : '';
28680
- // Lookup exact locale, then fallback, then resort to returning the key
28681
- const message = msgDict[currentLocale] || msgDict[currentFallback] || key;
28682
- if (typeof message !== 'string')
28683
- // This is actually an error - the client should be calling t() instead
28684
- return '';
28685
- // If we have an interpolation object, do the interpolation first
28686
- return Object.keys(ips).length ? interpolate_string(message, ips) : message;
28687
- }
28688
- function mt(cls, children) {
28689
- // Wrapper for the Mithril m() function that auto-translates
28690
- // string and array arguments
28691
- if (typeof children === 'string') {
28692
- return m(cls, t(children));
28693
- }
28694
- if (Array.isArray(children)) {
28695
- return m(cls, children.map((item) => (typeof item === 'string' ? t(item) : item)));
28696
- }
28697
- return m(cls, children);
28698
- }
28699
- function interpolate(message, ips) {
28700
- // Replace interpolation placeholders with their corresponding values
28701
- if (typeof message === 'string') {
28702
- return message.replace(rex, (match, key) => ips[key] || match);
28703
- }
28704
- if (Array.isArray(message)) {
28705
- return message.map((item) => interpolate(item, ips));
28706
- }
28707
- return message;
28708
- }
28709
- function interpolate_string(message, ips) {
28710
- // Replace interpolation placeholders with their corresponding values
28711
- return message.replace(rex, (match, key) => ips[key] || match);
30669
+ return view.appView(item.name);
30670
+ },
30671
+ };
30672
+ return acc;
30673
+ }, {});
28712
30674
  }
28713
30675
 
28714
30676
  /*
@@ -29264,6 +31226,23 @@ const TogglerFairplay = () => {
29264
31226
  const BLANK_TILES_PER_LINE = 6;
29265
31227
  const BlankDialog = () => {
29266
31228
  // A dialog for choosing the meaning of a blank tile
31229
+ function handleKeydown(game, ev) {
31230
+ // Escape key cancels the dialog
31231
+ let { key } = ev;
31232
+ if (key === 'Escape') {
31233
+ ev.preventDefault();
31234
+ game.cancelBlankDialog();
31235
+ return;
31236
+ }
31237
+ if (key.length === 1) {
31238
+ // Check if the pressed key matches a valid letter
31239
+ key = key.toLowerCase();
31240
+ if (game.alphabet.includes(key)) {
31241
+ ev.preventDefault();
31242
+ game.placeBlank(key);
31243
+ }
31244
+ }
31245
+ }
29267
31246
  function blankLetters(game) {
29268
31247
  const legalLetters = game.alphabet;
29269
31248
  let len = legalLetters.length;
@@ -29297,6 +31276,9 @@ const BlankDialog = () => {
29297
31276
  return m('.modal-dialog', {
29298
31277
  id: 'blank-dialog',
29299
31278
  style: { visibility: 'visible' },
31279
+ tabindex: -1,
31280
+ onkeydown: (ev) => handleKeydown(game, ev),
31281
+ oncreate: (vnode) => vnode.dom.focus(),
29300
31282
  }, m('.ui-widget.ui-widget-content.ui-corner-all', { id: 'blank-form' }, [
29301
31283
  mt('p', 'Hvaða staf táknar auða flísin?'),
29302
31284
  m('.rack.blank-rack', m('table.board', { id: 'blank-meaning' }, blankLetters(game))),
@@ -29525,11 +31507,11 @@ const Buttons = {
29525
31507
  game.player !== null) {
29526
31508
  // Indicate that it is the opponent's turn; offer to force a resignation
29527
31509
  // if the opponent hasn't moved for 14 days
31510
+ const opponentName = game.nickname[1 - game.player];
29528
31511
  r.push(m('.opp-turn', { style: { visibility: 'visible' } }, [
29529
31512
  m('span.move-indicator'),
29530
31513
  nbsp(),
29531
- m('strong', game.nickname[1 - game.player]),
29532
- ts(' á leik'),
31514
+ m('strong', ts('opp_move', { opponent: opponentName })),
29533
31515
  nbsp(),
29534
31516
  // The following inline button is only
29535
31517
  // displayed in the fullscreen UI
@@ -30125,76 +32107,74 @@ const BoardArea = {
30125
32107
  return m('.board-area', r);
30126
32108
  },
30127
32109
  };
30128
- const Board = (initialVnode) => {
32110
+ const Board = {
30129
32111
  // The game board, a 15x15 table plus row (A-O) and column (1-15) identifiers
30130
- const { view, game, review } = initialVnode.attrs;
30131
- function colid() {
30132
- // The column identifier row
30133
- const r = [];
30134
- r.push(m('td'));
30135
- for (let col = 1; col <= 15; col++)
30136
- r.push(m('td', col.toString()));
30137
- return m('tr.colid', r);
30138
- }
30139
- function row(rowid) {
30140
- // Each row of the board
30141
- const r = [];
30142
- r.push(m('td.rowid', { key: `R${rowid}` }, rowid));
30143
- for (let col = 1; col <= 15; col++) {
30144
- const coord = rowid + col.toString();
30145
- if (game && coord in game.tiles)
30146
- // There is a tile in this square: render it
30147
- r.push(m(TileSquare, {
30148
- view,
30149
- game,
30150
- key: coord,
30151
- coord: coord,
30152
- opponent: false,
30153
- }));
30154
- else if (review)
30155
- // Empty, inert square
30156
- r.push(m(ReviewTileSquare, {
30157
- view,
30158
- game,
30159
- key: coord,
30160
- coord: coord,
30161
- opponent: false,
30162
- }));
30163
- // Empty square which is a drop target
30164
- else
30165
- r.push(m(DropTargetSquare, {
30166
- view,
30167
- game,
30168
- key: coord,
30169
- coord: coord,
30170
- }));
30171
- }
30172
- return m('tr', r);
30173
- }
30174
- function allrows() {
30175
- // Return a list of all rows on the board
30176
- const r = [];
30177
- r.push(colid());
30178
- const rows = 'ABCDEFGHIJKLMNO';
30179
- for (const rw of rows)
30180
- r.push(row(rw));
30181
- return r;
30182
- }
30183
- return {
30184
- view: () => {
30185
- // const scale = view.boardScale || 1.0;
30186
- const attrs = {};
30187
- // Add handlers for pinch zoom functionality
30188
- // Note: resist the temptation to pass zoomIn/zoomOut directly,
30189
- // as that would not bind the 'this' pointer correctly
30190
- addPinchZoom(attrs, () => view.zoomIn(), () => view.zoomOut());
30191
- /*
32112
+ view: (vnode) => {
32113
+ const { view, game, review } = vnode.attrs;
32114
+ // const scale = view.boardScale || 1.0;
32115
+ const attrs = {};
32116
+ // Add handlers for pinch zoom functionality
32117
+ // Note: resist the temptation to pass zoomIn/zoomOut directly,
32118
+ // as that would not bind the 'this' pointer correctly
32119
+ addPinchZoom(attrs, () => view.zoomIn(), () => view.zoomOut());
32120
+ /*
30192
32121
  if (scale !== 1.0)
30193
32122
  attrs.style = `transform: scale(${scale})`;
30194
32123
  */
30195
- return m('.board', { id: 'board-parent' }, m('table.board', attrs, m('tbody', allrows())));
30196
- },
30197
- };
32124
+ function colid() {
32125
+ // The column identifier row
32126
+ const r = [];
32127
+ r.push(m('td'));
32128
+ for (let col = 1; col <= 15; col++)
32129
+ r.push(m('td', col.toString()));
32130
+ return m('tr.colid', r);
32131
+ }
32132
+ function row(rowid) {
32133
+ // Each row of the board
32134
+ const r = [];
32135
+ r.push(m('td.rowid', { key: `R${rowid}` }, rowid));
32136
+ for (let col = 1; col <= 15; col++) {
32137
+ const coord = rowid + col.toString();
32138
+ if (game && coord in game.tiles)
32139
+ // There is a tile in this square: render it
32140
+ r.push(m(TileSquare, {
32141
+ view,
32142
+ game,
32143
+ key: coord,
32144
+ coord: coord,
32145
+ opponent: false,
32146
+ }));
32147
+ else if (review)
32148
+ // Empty, inert square
32149
+ r.push(m(ReviewTileSquare, {
32150
+ view,
32151
+ game,
32152
+ key: coord,
32153
+ coord: coord,
32154
+ opponent: false,
32155
+ }));
32156
+ // Empty square which is a drop target
32157
+ else
32158
+ r.push(m(DropTargetSquare, {
32159
+ view,
32160
+ game,
32161
+ key: coord,
32162
+ coord: coord,
32163
+ }));
32164
+ }
32165
+ return m('tr', r);
32166
+ }
32167
+ function allrows() {
32168
+ // Return a list of all rows on the board
32169
+ const r = [];
32170
+ r.push(colid());
32171
+ const rows = 'ABCDEFGHIJKLMNO';
32172
+ for (const rw of rows)
32173
+ r.push(row(rw));
32174
+ return r;
32175
+ }
32176
+ return m('.board', { id: 'board-parent' }, m('table.board', attrs, m('tbody', allrows())));
32177
+ },
30198
32178
  };
30199
32179
 
30200
32180
  /*
@@ -30408,6 +32388,90 @@ const GataDagsinsHelp = {
30408
32388
  },
30409
32389
  };
30410
32390
 
32391
+ /*
32392
+
32393
+ DateNavigator.ts
32394
+
32395
+ Component for navigating between Gáta Dagsins dates
32396
+
32397
+ Copyright (C) 2025 Miðeind ehf.
32398
+ Author: Vilhjálmur Þorsteinsson
32399
+
32400
+ The Creative Commons Attribution-NonCommercial 4.0
32401
+ International Public License (CC-BY-NC 4.0) applies to this software.
32402
+ For further information, see https://github.com/mideind/Netskrafl
32403
+
32404
+ */
32405
+ const DateNavigator = {
32406
+ view: (vnode) => {
32407
+ const { view, date, locale } = vnode.attrs;
32408
+ const { actions } = view;
32409
+ // Helper to add days to a YYYY-MM-DD string
32410
+ const addDays = (dateStr, days) => {
32411
+ // Create date object in UTC to ensure calculations are timezone-agnostic
32412
+ const d = new Date(`${dateStr}T00:00:00Z`);
32413
+ d.setUTCDate(d.getUTCDate() + days);
32414
+ return d.toISOString().split('T')[0];
32415
+ };
32416
+ const today = new Date().toISOString().split('T')[0];
32417
+ const prevDate = addDays(date, -1);
32418
+ const nextDate = addDays(date, 1);
32419
+ const isLoading = actions.isRiddleLoading;
32420
+ // Disable navigation while loading or when at date boundaries
32421
+ const prevEnabled = !isLoading && prevDate >= GATA_DAGSINS_MIN_DATE;
32422
+ const nextEnabled = !isLoading && nextDate <= today;
32423
+ const navigateTo = (newDate) => {
32424
+ // Update URL without reloading the page
32425
+ const url = new URL(window.location.href);
32426
+ url.searchParams.set('date', newDate);
32427
+ window.history.pushState({}, '', url.toString());
32428
+ // Load the new riddle via the actions controller
32429
+ actions.loadRiddle(newDate, locale);
32430
+ };
32431
+ // Keyboard handler for Enter/Space key activation
32432
+ const handleKeydown = (enabled, action) => {
32433
+ if (!enabled)
32434
+ return undefined;
32435
+ return (e) => {
32436
+ if (e.key === 'Enter' || e.key === ' ') {
32437
+ e.preventDefault();
32438
+ action();
32439
+ }
32440
+ };
32441
+ };
32442
+ const tsPrevDay = ts('Fyrri dagur');
32443
+ const tsNextDay = ts('Næsti dagur');
32444
+ return m('.date-navigator', [
32445
+ // Previous day button
32446
+ m(`.nav-arrow.prev${prevEnabled ? '' : '.disabled'}`, {
32447
+ role: 'button',
32448
+ tabindex: prevEnabled ? 0 : -1,
32449
+ 'aria-disabled': !prevEnabled,
32450
+ 'aria-label': tsPrevDay,
32451
+ onclick: prevEnabled
32452
+ ? () => navigateTo(prevDate)
32453
+ : undefined,
32454
+ onkeydown: handleKeydown(prevEnabled, () => navigateTo(prevDate)),
32455
+ title: prevEnabled ? tsPrevDay : '',
32456
+ }, glyph('chevron-left')),
32457
+ // Date display
32458
+ m('.nav-date', formatDate(date)),
32459
+ // Next day button
32460
+ m(`.nav-arrow.next${nextEnabled ? '' : '.disabled'}`, {
32461
+ role: 'button',
32462
+ tabindex: nextEnabled ? 0 : -1,
32463
+ 'aria-disabled': !nextEnabled,
32464
+ 'aria-label': tsNextDay,
32465
+ onclick: nextEnabled
32466
+ ? () => navigateTo(nextDate)
32467
+ : undefined,
32468
+ onkeydown: handleKeydown(nextEnabled, () => navigateTo(nextDate)),
32469
+ title: nextEnabled ? tsNextDay : '',
32470
+ }, glyph('chevron-right')),
32471
+ ]);
32472
+ },
32473
+ };
32474
+
30411
32475
  /*
30412
32476
 
30413
32477
  SunCorona.ts
@@ -30481,6 +32545,13 @@ const SunCorona = {
30481
32545
  */
30482
32546
  // Mobile-only horizontal status display
30483
32547
  const MobileStatus = () => {
32548
+ // Keyboard handler for button activation
32549
+ const handleKeydown = (action) => (e) => {
32550
+ if (e.key === 'Enter' || e.key === ' ') {
32551
+ e.preventDefault();
32552
+ action();
32553
+ }
32554
+ };
30484
32555
  return {
30485
32556
  view: (vnode) => {
30486
32557
  const { view, bestMove, onMoveClick, onStatsClick } = vnode.attrs;
@@ -30490,6 +32561,12 @@ const MobileStatus = () => {
30490
32561
  const { bestPossibleScore, globalBestScore, personalBestScore } = riddle;
30491
32562
  // Determine if player achieved best possible score
30492
32563
  const celebrate = bestMove && bestMove.word !== '';
32564
+ const today = new Date().toISOString().split('T')[0];
32565
+ const lookingAtOldRiddle = riddle.date < today;
32566
+ // If we don't have an explicit solution, use the global best score as fallback
32567
+ const solution = lookingAtOldRiddle
32568
+ ? riddle.solution || globalBestScore
32569
+ : null;
30493
32570
  // Determine current leader score (may be this player or another)
30494
32571
  let leaderScore = 0;
30495
32572
  let isPlayerLeading = false;
@@ -30502,13 +32579,32 @@ const MobileStatus = () => {
30502
32579
  leaderScore = personalBestScore;
30503
32580
  isPlayerLeading = personalBestScore > 0;
30504
32581
  }
32582
+ // Handler for clicking on best possible score
32583
+ const isScoreClickable = !!(solution || celebrate);
32584
+ const handleScoreClick = () => {
32585
+ if (solution) {
32586
+ // Zoom out when showing solution for old riddles
32587
+ view.zoomOut();
32588
+ // Recreate the solution word on the board
32589
+ onMoveClick(solution.word, solution.coord);
32590
+ }
32591
+ else if (celebrate) {
32592
+ // Recreate the best move word on the board
32593
+ onMoveClick(bestMove.word, bestMove.coord);
32594
+ }
32595
+ };
32596
+ const tsStatsAndLeaderboard = ts('Tölfræði og stigatafla');
30505
32597
  return m('.mobile-status-container', [
30506
32598
  // Current word score (leftmost) - uses RiddleScore component in mobile mode
30507
32599
  m('.mobile-status-item.left', m(RiddleScore, { riddle, mode: 'mobile' })),
30508
32600
  // Interactive card containing player best and leader scores
30509
32601
  m('.mobile-status-card', {
32602
+ role: 'button',
32603
+ tabindex: 0,
32604
+ 'aria-label': tsStatsAndLeaderboard,
30510
32605
  onclick: onStatsClick,
30511
- title: ts('Tölfræði og stigatafla'),
32606
+ onkeydown: handleKeydown(onStatsClick),
32607
+ title: tsStatsAndLeaderboard,
30512
32608
  }, [
30513
32609
  // Player's best score
30514
32610
  m('.mobile-status-card-item.player-best', [
@@ -30529,8 +32625,15 @@ const MobileStatus = () => {
30529
32625
  // Best possible score
30530
32626
  m('.mobile-status-item.right.best-possible' +
30531
32627
  (celebrate ? '.celebrate' : ''), {
30532
- onclick: () => celebrate &&
30533
- onMoveClick(bestMove.word, bestMove.coord),
32628
+ role: isScoreClickable ? 'button' : undefined,
32629
+ tabindex: isScoreClickable ? 0 : undefined,
32630
+ 'aria-label': isScoreClickable
32631
+ ? ts('Sýna besta leik')
32632
+ : undefined,
32633
+ onclick: handleScoreClick,
32634
+ onkeydown: isScoreClickable
32635
+ ? handleKeydown(handleScoreClick)
32636
+ : undefined,
30534
32637
  }, [
30535
32638
  // Wrapper for score and corona to position them together
30536
32639
  m('.mobile-best-score-wrapper', [
@@ -30571,30 +32674,9 @@ function getMedalIcon(rank) {
30571
32674
  return null;
30572
32675
  }
30573
32676
  }
30574
- function formatDate(dateStr) {
30575
- // Format YYYY-MM-DD to Icelandic date (e.g., "2. okt.")
30576
- const date = new Date(`${dateStr}T00:00:00`);
30577
- const day = date.getDate();
30578
- const months = [
30579
- 'janúar',
30580
- 'febrúar',
30581
- 'mars',
30582
- 'apríl',
30583
- 'maí',
30584
- 'júní',
30585
- 'júlí',
30586
- 'ágúst',
30587
- 'september',
30588
- 'október',
30589
- 'nóvember',
30590
- 'desember',
30591
- ];
30592
- const month = months[date.getMonth()];
30593
- return `${day}. ${month}`;
30594
- }
30595
32677
  const LeaderboardView = {
30596
32678
  view: (vnode) => {
30597
- const { leaderboard, currentUserId, date, loading = false, } = vnode.attrs;
32679
+ const { leaderboard, currentUserId, loading = false } = vnode.attrs;
30598
32680
  if (loading) {
30599
32681
  return m('.leaderboard-view.loading', m('.loading-message', ts('Hleð stigatöflu...')));
30600
32682
  }
@@ -30602,9 +32684,6 @@ const LeaderboardView = {
30602
32684
  return m('.leaderboard-view.empty', m('.empty-message', ts('Engin stig skráð enn')));
30603
32685
  }
30604
32686
  return m('.leaderboard-view', [
30605
- m('.leaderboard-header', [
30606
- m('.leaderboard-title', formatDate(date)),
30607
- ]),
30608
32687
  m('.leaderboard-list', {
30609
32688
  // Allow touch scrolling but prevent events from bubbling to backdrop
30610
32689
  ontouchmove: (e) => {
@@ -30712,22 +32791,37 @@ const StatsView = {
30712
32791
  const TabBar = {
30713
32792
  view: (vnode) => {
30714
32793
  const { tabs, activeTab, onTabChange } = vnode.attrs;
30715
- return m('.tab-bar', tabs.map((tab) => m(`.tab-item${activeTab === tab.id ? '.active' : ''}`, {
30716
- key: tab.id,
30717
- onclick: () => onTabChange(tab.id),
30718
- }, [
30719
- m('span.tab-icon-wrapper', [
30720
- tab.iconGlyph
30721
- ? m('span.tab-icon', glyph(tab.iconGlyph))
30722
- : tab.icon
30723
- ? m('span.tab-icon', tab.icon)
32794
+ // Keyboard handler for tab activation
32795
+ const handleKeydown = (tabId) => (e) => {
32796
+ if (e.key === 'Enter' || e.key === ' ') {
32797
+ e.preventDefault();
32798
+ onTabChange(tabId);
32799
+ }
32800
+ };
32801
+ return m('.tab-bar[role=tablist]', tabs.map((tab) => {
32802
+ const isActive = activeTab === tab.id;
32803
+ return m(`.tab-item${isActive ? '.active' : ''}`, {
32804
+ key: tab.id,
32805
+ role: 'tab',
32806
+ tabindex: isActive ? 0 : -1,
32807
+ 'aria-selected': isActive,
32808
+ 'aria-label': tab.label,
32809
+ onclick: () => onTabChange(tab.id),
32810
+ onkeydown: handleKeydown(tab.id),
32811
+ }, [
32812
+ m('span.tab-icon-wrapper', [
32813
+ tab.iconGlyph
32814
+ ? m('span.tab-icon', glyph(tab.iconGlyph))
32815
+ : tab.icon
32816
+ ? m('span.tab-icon', tab.icon)
32817
+ : null,
32818
+ tab.badgeGlyph
32819
+ ? m('span.tab-badge', glyph(tab.badgeGlyph))
30724
32820
  : null,
30725
- tab.badgeGlyph
30726
- ? m('span.tab-badge', glyph(tab.badgeGlyph))
30727
- : null,
30728
- ]),
30729
- m('span.tab-label', tab.label),
30730
- ])));
32821
+ ]),
32822
+ m('span.tab-label', tab.label),
32823
+ ]);
32824
+ }));
30731
32825
  },
30732
32826
  };
30733
32827
 
@@ -30801,156 +32895,200 @@ function calculateZoneHeights(allocation) {
30801
32895
  };
30802
32896
  }
30803
32897
  // Best possible score component (green circle at top)
30804
- const BestPossibleScore = () => {
30805
- return {
30806
- view: (vnode) => {
30807
- const { score, bestMove, onMoveClick } = vnode.attrs;
30808
- // Determine the label based on achievement status
30809
- let topLabel;
30810
- if (bestMove?.word) {
30811
- // Current player achieved it - show their word
30812
- topLabel = removeBlankMarkers(bestMove.word);
32898
+ const BestPossibleScore = {
32899
+ view: (vnode) => {
32900
+ const { score, bestMove, onMoveClick, solution } = vnode.attrs;
32901
+ // Determine the label based on achievement status
32902
+ let topLabel;
32903
+ const celebrate = bestMove && bestMove.word !== '';
32904
+ if (celebrate) {
32905
+ // Current player achieved it - show their word
32906
+ topLabel = removeBlankMarkers(bestMove.word);
32907
+ }
32908
+ else {
32909
+ // Not achieved yet - show default label
32910
+ topLabel = ts('Besta mögulega lögn');
32911
+ }
32912
+ const title = solution && !celebrate ? ts('Smelltu til að sjá lausn') : '';
32913
+ const isClickable = !!(solution || celebrate);
32914
+ const handleClick = () => {
32915
+ if (solution) {
32916
+ // Recreate the solution word on the board
32917
+ onMoveClick(solution.word, solution.coord);
30813
32918
  }
30814
- else {
30815
- // Not achieved yet - show default label
30816
- topLabel = ts('Besta mögulega lögn');
32919
+ else if (celebrate) {
32920
+ // Recreate the best move word on the board
32921
+ onMoveClick(bestMove.word, bestMove.coord);
30817
32922
  }
30818
- const celebrate = bestMove && bestMove.word !== '';
30819
- return m(`.thermometer-best-score${celebrate ? '.celebrate' : ''}`, m('.thermometer-best-score-container', {
30820
- onclick: () => celebrate &&
30821
- onMoveClick(bestMove.word, bestMove.coord),
30822
- }, [
30823
- // Sun corona behind the circle when celebrating
30824
- celebrate ? m(SunCorona, { animate: true }) : null,
30825
- m('.thermometer-best-circle', score.toString()),
30826
- m('.thermometer-best-label', topLabel),
30827
- ]));
30828
- },
30829
- };
32923
+ };
32924
+ const handleKeydown = isClickable
32925
+ ? (e) => {
32926
+ if (e.key === 'Enter' || e.key === ' ') {
32927
+ e.preventDefault();
32928
+ handleClick();
32929
+ }
32930
+ }
32931
+ : undefined;
32932
+ return m(`.thermometer-best-score${celebrate ? '.celebrate' : ''}`, m('.thermometer-best-score-container', {
32933
+ role: isClickable ? 'button' : undefined,
32934
+ tabindex: isClickable ? 0 : undefined,
32935
+ 'aria-label': isClickable
32936
+ ? ts('Sýna besta leik')
32937
+ : undefined,
32938
+ onclick: handleClick,
32939
+ onkeydown: handleKeydown,
32940
+ title,
32941
+ }, [
32942
+ // Sun corona behind the circle when celebrating
32943
+ celebrate ? m(SunCorona, { animate: true }) : null,
32944
+ m('.thermometer-best-circle', score.toString()),
32945
+ m('.thermometer-best-label', topLabel),
32946
+ ]));
32947
+ },
30830
32948
  };
30831
32949
  // Current global best score component (circled score)
30832
- const GlobalBestScore = () => {
30833
- return {
30834
- view: (vnode) => {
30835
- const { score, thisPlayer } = vnode.attrs;
30836
- return m(`.thermometer-current-score${thisPlayer ? '.this-player' : ''}`, [m('.thermometer-current-circle', score.toString())]);
30837
- },
30838
- };
32950
+ const GlobalBestScore = {
32951
+ view: (vnode) => {
32952
+ const { score, thisPlayer } = vnode.attrs;
32953
+ return m(`.thermometer-current-score${thisPlayer ? '.this-player' : ''}`, [m('.thermometer-current-circle', score.toString())]);
32954
+ },
30839
32955
  };
30840
32956
  // Player moves overlay wrapper component with zone-aware positioning
30841
- const PlayerMovesOverlay = () => {
30842
- return {
30843
- view: (vnode) => {
30844
- // Zones: "hot", "warm", "cold"
30845
- // zoneHeights: How tall each zone is, as a percentage of the thermometer height
30846
- // zoneAllocation: A dictionary with the list of moves for each zone
30847
- const { zoneHeights, zoneAllocation, onMoveClick } = vnode.attrs;
30848
- // Calculate zone boundaries
30849
- const coldBottom = 0;
30850
- const coldTop = zoneHeights.cold;
30851
- const warmBottom = coldTop;
30852
- const warmTop = warmBottom + zoneHeights.warm;
30853
- const hotBottom = warmTop;
30854
- const allMoveElements = [];
30855
- function scoreDetails(move) {
30856
- if (move.isGlobalBestScore) {
30857
- if (move.word === '') {
30858
- // Another player holds the top score
30859
- return [
30860
- m(GlobalBestScore, {
30861
- thisPlayer: false,
30862
- score: move.score,
30863
- }),
30864
- ];
30865
- }
30866
- else {
30867
- // This player is the holder of the top score
30868
- return [
30869
- m(GlobalBestScore, {
30870
- thisPlayer: true,
30871
- score: move.score,
30872
- }),
30873
- m('.thermometer-move-word', removeBlankMarkers(move.word)),
30874
- m('.thermometer-move-coord', `(${move.coord})`),
30875
- ];
30876
- }
32957
+ const PlayerMovesOverlay = {
32958
+ view: (vnode) => {
32959
+ // Zones: "hot", "warm", "cold"
32960
+ // zoneHeights: How tall each zone is, as a percentage of the thermometer height
32961
+ // zoneAllocation: A dictionary with the list of moves for each zone
32962
+ const { zoneHeights, zoneAllocation, onMoveClick } = vnode.attrs;
32963
+ // Calculate zone boundaries
32964
+ const coldBottom = 0;
32965
+ const coldTop = zoneHeights.cold;
32966
+ const warmBottom = coldTop;
32967
+ const warmTop = warmBottom + zoneHeights.warm;
32968
+ const hotBottom = warmTop;
32969
+ const allMoveElements = [];
32970
+ function scoreDetails(move) {
32971
+ if (move.isGlobalBestScore) {
32972
+ if (move.word === '') {
32973
+ // Another player holds the top score
32974
+ return [
32975
+ m(GlobalBestScore, {
32976
+ thisPlayer: false,
32977
+ score: move.score,
32978
+ }),
32979
+ ];
30877
32980
  }
30878
32981
  else {
32982
+ // This player is the holder of the top score
30879
32983
  return [
30880
- m('.thermometer-move-score', move.score.toString()),
32984
+ m(GlobalBestScore, {
32985
+ thisPlayer: true,
32986
+ score: move.score,
32987
+ }),
30881
32988
  m('.thermometer-move-word', removeBlankMarkers(move.word)),
30882
32989
  m('.thermometer-move-coord', `(${move.coord})`),
30883
32990
  ];
30884
32991
  }
30885
32992
  }
30886
- function addZoneMoves(zoneMoves, zoneName, zoneStart, zoneHeight) {
30887
- const num = zoneMoves.length;
30888
- const segmentHeight = zoneHeight / (num + 1);
30889
- zoneMoves.forEach((move, index) => {
30890
- // The zoneMoves list is in descending order, and we want
30891
- // the first (highest) move to be at the top of the zone,
30892
- // i.e. with the highest percentage position, which is
30893
- // calculated from the bottom of the thermometer.
30894
- const position = zoneStart + segmentHeight * (num - index);
30895
- allMoveElements.push(m(`.thermometer-move-overlay .${zoneName}`, {
30896
- key: `${zoneName}-${index}`,
30897
- style: `--move-position: ${position.toFixed(2)}%`,
30898
- // Only call onMoveClick if the move was actually made
30899
- // by the current player
30900
- onclick: () => move.word &&
30901
- move.coord &&
30902
- onMoveClick(move.word, move.coord),
30903
- }, scoreDetails(move)));
30904
- });
32993
+ else {
32994
+ return [
32995
+ m('.thermometer-move-score', move.score.toString()),
32996
+ m('.thermometer-move-word', removeBlankMarkers(move.word)),
32997
+ m('.thermometer-move-coord', `(${move.coord})`),
32998
+ ];
30905
32999
  }
30906
- addZoneMoves(zoneAllocation.cold, 'cold', coldBottom, zoneHeights.cold);
30907
- addZoneMoves(zoneAllocation.warm, 'warm', warmBottom, zoneHeights.warm);
30908
- addZoneMoves(zoneAllocation.hot, 'hot', hotBottom, zoneHeights.hot);
30909
- return m('.thermometer-moves-overlay-wrapper', allMoveElements);
30910
- },
30911
- };
33000
+ }
33001
+ function addZoneMoves(zoneMoves, zoneName, zoneStart, zoneHeight) {
33002
+ const num = zoneMoves.length;
33003
+ const segmentHeight = zoneHeight / (num + 1);
33004
+ zoneMoves.forEach((move, index) => {
33005
+ // The zoneMoves list is in descending order, and we want
33006
+ // the first (highest) move to be at the top of the zone,
33007
+ // i.e. with the highest percentage position, which is
33008
+ // calculated from the bottom of the thermometer.
33009
+ const position = zoneStart + segmentHeight * (num - index);
33010
+ // Only clickable if the move was made by the current player
33011
+ const isClickable = !!(move.word && move.coord);
33012
+ const handleClick = () => {
33013
+ if (isClickable) {
33014
+ onMoveClick(move.word, move.coord);
33015
+ }
33016
+ };
33017
+ const handleKeydown = isClickable
33018
+ ? (e) => {
33019
+ if (e.key === 'Enter' || e.key === ' ') {
33020
+ e.preventDefault();
33021
+ handleClick();
33022
+ }
33023
+ }
33024
+ : undefined;
33025
+ allMoveElements.push(m(`.thermometer-move-overlay .${zoneName}`, {
33026
+ key: `${zoneName}-${index}`,
33027
+ style: `--move-position: ${position.toFixed(2)}%`,
33028
+ role: isClickable ? 'button' : undefined,
33029
+ tabindex: isClickable ? 0 : undefined,
33030
+ 'aria-label': isClickable
33031
+ ? `${ts('Sýna leik')}: ${removeBlankMarkers(move.word)}`
33032
+ : undefined,
33033
+ onclick: handleClick,
33034
+ onkeydown: handleKeydown,
33035
+ }, scoreDetails(move)));
33036
+ });
33037
+ }
33038
+ addZoneMoves(zoneAllocation.cold, 'cold', coldBottom, zoneHeights.cold);
33039
+ addZoneMoves(zoneAllocation.warm, 'warm', warmBottom, zoneHeights.warm);
33040
+ addZoneMoves(zoneAllocation.hot, 'hot', hotBottom, zoneHeights.hot);
33041
+ return m('.thermometer-moves-overlay-wrapper', allMoveElements);
33042
+ },
30912
33043
  };
30913
33044
  // Main thermometer component with dynamic layout algorithm
30914
- const Thermometer = () => {
30915
- return {
30916
- view: (vnode) => {
30917
- const { riddle, selectedMoves, bestMove, onMoveClick } = vnode.attrs;
30918
- const { warmBoundary, hotBoundary, bestPossibleScore } = riddle;
30919
- const zoneAllocation = allocateMovesToZones(warmBoundary, hotBoundary, selectedMoves);
30920
- const zoneHeights = calculateZoneHeights(zoneAllocation);
30921
- return m('.thermometer-container', [
30922
- // Main thermometer body with dynamic color zones and overlaid moves
30923
- m('.thermometer-body', [
30924
- // Dynamic color zones background
30925
- m('.thermometer-zones', [
30926
- m('.thermometer-zone.hot', {
30927
- style: `--zone-size: ${zoneHeights.hot + zoneHeights.warm + zoneHeights.cold}%`,
30928
- }),
30929
- m('.thermometer-zone.warm', {
30930
- style: `--zone-size: ${zoneHeights.warm + zoneHeights.cold}%`,
30931
- }),
30932
- m('.thermometer-zone.cold', {
30933
- style: `--zone-size: ${zoneHeights.cold}%`,
30934
- }),
30935
- ]),
30936
- // Player moves positioned using zone-aware algorithm
30937
- m(PlayerMovesOverlay, {
30938
- moves: selectedMoves,
30939
- maxScore: bestPossibleScore,
30940
- zoneHeights,
30941
- zoneAllocation,
30942
- onMoveClick,
33045
+ const Thermometer = {
33046
+ view: (vnode) => {
33047
+ const { riddle, selectedMoves, bestMove, onMoveClick } = vnode.attrs;
33048
+ const { warmBoundary, hotBoundary, bestPossibleScore, globalBestScore, } = riddle;
33049
+ const zoneAllocation = allocateMovesToZones(warmBoundary, hotBoundary, selectedMoves);
33050
+ const zoneHeights = calculateZoneHeights(zoneAllocation);
33051
+ const today = new Date().toISOString().split('T')[0];
33052
+ const lookingAtOldRiddle = riddle.date < today;
33053
+ // If we don't have an explicit solution, use the global best score as fallback
33054
+ const solution = lookingAtOldRiddle
33055
+ ? riddle.solution || globalBestScore
33056
+ : null;
33057
+ return m('.thermometer-container', [
33058
+ // Main thermometer body with dynamic color zones and overlaid moves
33059
+ m('.thermometer-body', [
33060
+ // Dynamic color zones background
33061
+ m('.thermometer-zones', [
33062
+ m('.thermometer-zone.hot', {
33063
+ style: `--zone-size: ${zoneHeights.hot +
33064
+ zoneHeights.warm +
33065
+ zoneHeights.cold}%`,
33066
+ }),
33067
+ m('.thermometer-zone.warm', {
33068
+ style: `--zone-size: ${zoneHeights.warm + zoneHeights.cold}%`,
33069
+ }),
33070
+ m('.thermometer-zone.cold', {
33071
+ style: `--zone-size: ${zoneHeights.cold}%`,
30943
33072
  }),
30944
33073
  ]),
30945
- // Best possible score at top
30946
- m(BestPossibleScore, {
30947
- score: bestPossibleScore,
30948
- bestMove,
33074
+ // Player moves positioned using zone-aware algorithm
33075
+ m(PlayerMovesOverlay, {
33076
+ moves: selectedMoves,
33077
+ maxScore: bestPossibleScore,
33078
+ zoneHeights,
33079
+ zoneAllocation,
30949
33080
  onMoveClick,
30950
33081
  }),
30951
- ]);
30952
- },
30953
- };
33082
+ ]),
33083
+ // Best possible score at top
33084
+ m(BestPossibleScore, {
33085
+ score: bestPossibleScore,
33086
+ bestMove,
33087
+ onMoveClick,
33088
+ solution,
33089
+ }),
33090
+ ]);
33091
+ },
30954
33092
  };
30955
33093
 
30956
33094
  /*
@@ -31027,7 +33165,6 @@ const RightSideTabs = () => {
31027
33165
  ? m(LeaderboardView, {
31028
33166
  leaderboard: view.model.leaderboard || [],
31029
33167
  currentUserId: state?.netskraflUserId || '',
31030
- date: riddle.date,
31031
33168
  loading: false,
31032
33169
  })
31033
33170
  : null,
@@ -31065,20 +33202,35 @@ const GataDagsinsRightSide = {
31065
33202
  return m('.gatadagsins-right-side-wrapper', riddle
31066
33203
  ? [
31067
33204
  // Mobile-only status bar (visible on mobile, hidden on desktop)
31068
- m('.gatadagsins-mobile-status', m(MobileStatus, {
31069
- view,
31070
- selectedMoves,
31071
- bestMove,
31072
- onMoveClick,
31073
- onStatsClick,
31074
- })),
33205
+ // We wrap the mobile content to include the date navigator
33206
+ m('.gatadagsins-mobile-wrapper', [
33207
+ m('.mobile-date-nav-container', m(DateNavigator, {
33208
+ view,
33209
+ date: riddle.date,
33210
+ locale: riddle.locale,
33211
+ })),
33212
+ m('.gatadagsins-mobile-status', m(MobileStatus, {
33213
+ view,
33214
+ selectedMoves,
33215
+ bestMove,
33216
+ onMoveClick,
33217
+ onStatsClick,
33218
+ })),
33219
+ ]),
31075
33220
  // Desktop-only tabbed view (hidden on mobile, visible on desktop)
31076
- m('.gatadagsins-thermometer-column', m(RightSideTabs, {
31077
- view,
31078
- selectedMoves,
31079
- bestMove,
31080
- onMoveClick,
31081
- })),
33221
+ m('.gatadagsins-thermometer-column', [
33222
+ m('.desktop-date-nav-container', m(DateNavigator, {
33223
+ view,
33224
+ date: riddle.date,
33225
+ locale: riddle.locale,
33226
+ })),
33227
+ m(RightSideTabs, {
33228
+ view,
33229
+ selectedMoves,
33230
+ bestMove,
33231
+ onMoveClick,
33232
+ }),
33233
+ ]),
31082
33234
  ]
31083
33235
  : null);
31084
33236
  },
@@ -31176,7 +33328,6 @@ const StatsModal = () => {
31176
33328
  ? m(LeaderboardView, {
31177
33329
  leaderboard: view.model.leaderboard || [],
31178
33330
  currentUserId: state?.netskraflUserId || '',
31179
- date: riddle.date,
31180
33331
  loading: false,
31181
33332
  })
31182
33333
  : null,
@@ -31892,9 +34043,9 @@ class BaseGame {
31892
34043
  }
31893
34044
  async updateScore() {
31894
34045
  // Re-calculate the current word score
31895
- const scoreResult = this.calcScore();
31896
34046
  this.wordGood = false;
31897
34047
  this.wordBad = false;
34048
+ const scoreResult = this.calcScore();
31898
34049
  if (scoreResult === undefined || !scoreResult.word) {
31899
34050
  this.currentScore = undefined;
31900
34051
  this.currentWord = '';
@@ -31910,8 +34061,12 @@ class BaseGame {
31910
34061
  // This is not a manual-wordcheck game:
31911
34062
  // Check the word that has been laid down
31912
34063
  const found = await wordChecker.checkWords(this.state, this.locale, scoreResult.words);
31913
- this.wordGood = found;
31914
- this.wordBad = !found;
34064
+ if (this.currentWord === scoreResult.word) {
34065
+ // Safety check: ensure that the current word has not changed
34066
+ // while the wordChecker was awaiting the server response
34067
+ this.wordGood = found;
34068
+ this.wordBad = !found;
34069
+ }
31915
34070
  }
31916
34071
  }
31917
34072
  }
@@ -33280,6 +35435,7 @@ class Riddle extends BaseGame {
33280
35435
  this.groupBestScore = null;
33281
35436
  this.personalBestScore = 0;
33282
35437
  this.playerMoves = [];
35438
+ this.solution = null;
33283
35439
  this.date = date;
33284
35440
  this.model = model;
33285
35441
  }
@@ -33309,8 +35465,6 @@ class Riddle extends BaseGame {
33309
35465
  return !this.showingDialog;
33310
35466
  }
33311
35467
  async load(date, locale) {
33312
- this.date = date;
33313
- this.locale = locale;
33314
35468
  const { state } = this;
33315
35469
  try {
33316
35470
  if (!state)
@@ -33322,6 +35476,8 @@ class Riddle extends BaseGame {
33322
35476
  body: { date, locale },
33323
35477
  });
33324
35478
  if (response.ok) {
35479
+ this.date = date;
35480
+ this.locale = locale;
33325
35481
  this.board_type = response.riddle.board_type || 'standard';
33326
35482
  this.alphabet = response.riddle.alphabet || '';
33327
35483
  this.tile_scores = response.riddle.tile_scores || {};
@@ -33332,6 +35488,7 @@ class Riddle extends BaseGame {
33332
35488
  this.globalBestScore = null;
33333
35489
  this.groupBestScore = null;
33334
35490
  this.personalBestScore = 0;
35491
+ this.solution = response.riddle.solution || null;
33335
35492
  this.warmBoundary =
33336
35493
  this.bestPossibleScore * WARM_COLD_BOUNDARY_RATIO;
33337
35494
  this.hotBoundary =
@@ -33342,16 +35499,20 @@ class Riddle extends BaseGame {
33342
35499
  if (state.netskraflUserId) {
33343
35500
  const persistedMoves = loadLocalMoves(state.netskraflUserId, date);
33344
35501
  if (persistedMoves.length > 0) {
33345
- // Convert from IPlayerMove to RiddleWord format, preserving timestamps
33346
- this.playerMoves = persistedMoves.map((move) => ({
33347
- word: move.word,
33348
- score: move.score,
33349
- coord: move.coord,
33350
- timestamp: move.timestamp || new Date().toISOString(), // Use stored timestamp or fallback
33351
- }));
33352
- // Update personal best score from persisted moves
33353
- const bestMove = persistedMoves.reduce((best, current) => current.score > best.score ? current : best);
33354
- this.personalBestScore = bestMove.score;
35502
+ // Filter out invalid moves (score > bestPossibleScore)
35503
+ const validMoves = persistedMoves.filter((move) => move.score <= this.bestPossibleScore);
35504
+ if (validMoves.length > 0) {
35505
+ // Convert from IPlayerMove to RiddleWord format, preserving timestamps
35506
+ this.playerMoves = validMoves.map((move) => ({
35507
+ word: move.word,
35508
+ score: move.score,
35509
+ coord: move.coord,
35510
+ timestamp: move.timestamp || new Date().toISOString(), // Use stored timestamp or fallback
35511
+ }));
35512
+ // Update personal best score from valid moves only
35513
+ const bestMove = validMoves.reduce((best, current) => current.score > best.score ? current : best);
35514
+ this.personalBestScore = bestMove.score;
35515
+ }
33355
35516
  }
33356
35517
  }
33357
35518
  }
@@ -33394,6 +35555,10 @@ class Riddle extends BaseGame {
33394
35555
  !this.currentCoord) {
33395
35556
  return;
33396
35557
  }
35558
+ // Sanity check: move score cannot exceed best possible score
35559
+ if (this.currentScore > this.bestPossibleScore) {
35560
+ return;
35561
+ }
33397
35562
  // Check whether the move already exists
33398
35563
  const move = {
33399
35564
  score: this.currentScore,
@@ -33424,23 +35589,27 @@ class Riddle extends BaseGame {
33424
35589
  // If the move is not valid or was already played, return
33425
35590
  if (!move)
33426
35591
  return;
33427
- const { state } = this;
35592
+ const { state, date, playerMoves } = this;
33428
35593
  if (!state || !state.netskraflUserId)
33429
35594
  return;
33430
35595
  // Save all moves to localStorage (local backup/cache)
33431
35596
  // Convert RiddleWord[] to IPlayerMove[] for persistence
33432
- const movesToSave = this.playerMoves.map((m) => ({
35597
+ const movesToSave = playerMoves.map((m) => ({
33433
35598
  score: m.score,
33434
35599
  word: m.word,
33435
35600
  coord: m.coord,
33436
35601
  timestamp: m.timestamp,
33437
35602
  }));
33438
- saveLocalMoves(state.netskraflUserId, this.date, movesToSave);
35603
+ saveLocalMoves(state.netskraflUserId, date, movesToSave);
33439
35604
  // If the move does not improve the personal best, we're done
33440
35605
  if (move.score <= this.personalBestScore)
33441
35606
  return;
33442
35607
  // This is the best score we've seen yet
33443
35608
  this.personalBestScore = move.score;
35609
+ // If this is not today's riddle, do not submit to server
35610
+ const today = timestamp.split('T')[0];
35611
+ if (date !== today)
35612
+ return;
33444
35613
  // Submit to server; the server handles all Firebase updates
33445
35614
  // (achievements, stats, global best, leaderboard)
33446
35615
  this.submitRiddleWord(move);
@@ -33448,11 +35617,19 @@ class Riddle extends BaseGame {
33448
35617
  updateGlobalBestScore(best) {
33449
35618
  // Update the global best score, typically as a result
33450
35619
  // of a Firebase notification from the server
35620
+ // Ignore if score exceeds best possible score
35621
+ if (best.score > this.bestPossibleScore) {
35622
+ return;
35623
+ }
33451
35624
  this.globalBestScore = best;
33452
35625
  }
33453
35626
  updateGroupBestScore(best) {
33454
35627
  // Update the group best score, typically as a result
33455
35628
  // of a Firebase notification from the server
35629
+ // Ignore if score exceeds best possible score
35630
+ if (best.score > this.bestPossibleScore) {
35631
+ return;
35632
+ }
33456
35633
  this.groupBestScore = best;
33457
35634
  }
33458
35635
  recreateWordOnBoard(word, startCoord) {
@@ -33716,8 +35893,8 @@ class Model {
33716
35893
  this.state = state;
33717
35894
  this.isExplo = state.isExplo;
33718
35895
  this.maxFreeGames = state.isExplo ? MAX_FREE_EXPLO : MAX_FREE_NETSKRAFL;
33719
- // Load localized text messages from the messages.json file
33720
- loadMessages(state, state.locale);
35896
+ // Initialize localized text messages from the embedded messages.json
35897
+ initMessages(state.locale);
33721
35898
  }
33722
35899
  // Simple POST request with JSON body (most common case)
33723
35900
  async post(url, body) {
@@ -38642,60 +40819,34 @@ async function main(state, container) {
38642
40819
  }
38643
40820
  return 'success';
38644
40821
  }
40822
+ function unmount(container) {
40823
+ // Unmount the Mithril UI completely
40824
+ // First unmount via m.mount to clean up Mithril's internal state
40825
+ m.mount(container, null);
40826
+ // Then clear the container to ensure a clean slate for next mount
40827
+ container.innerHTML = '';
40828
+ }
38645
40829
 
38646
- const mountForUser = async (state) => {
38647
- // Return a DOM tree containing a mounted Netskrafl UI
38648
- // for the user specified in the state object
40830
+ const mountForUser = (state, container) => {
40831
+ // Mount a Netskrafl UI directly into the container
38649
40832
  const { userEmail } = state;
38650
40833
  if (!userEmail) {
38651
- // console.error("No user specified for Netskrafl UI");
38652
- throw new Error('No user specified for Netskrafl UI');
38653
- }
38654
- // Check whether we already have a mounted UI for this user
38655
- const elemId = `netskrafl-user-${userEmail}`;
38656
- const existing = document.getElementById(elemId);
38657
- if (existing) {
38658
- // console.log("Netskrafl UI already mounted for user", userEmail);
38659
- return existing;
38660
- }
38661
- // Create a new div element to hold the UI
38662
- const root = document.createElement('div');
38663
- root.id = elemId;
38664
- root.className = 'netskrafl-loading';
38665
- // Attach the partially-mounted div to the document body
38666
- // as a placeholder while the UI is being mounted
38667
- document.body.appendChild(root);
38668
- const loginResult = await main(state, root);
38669
- if (loginResult === 'success') {
38670
- // The UI was successfully mounted
38671
- root.className = 'netskrafl-user';
38672
- return root;
40834
+ return Promise.reject(new Error('No user specified for Netskrafl UI'));
38673
40835
  }
38674
- // console.error("Failed to mount Netskrafl UI for user", userEmail);
38675
- throw new Error('Failed to mount Netskrafl UI');
40836
+ return main(state, container).then((loginResult) => {
40837
+ if (loginResult !== 'success') {
40838
+ throw new Error('Failed to mount Netskrafl UI');
40839
+ }
40840
+ });
38676
40841
  };
38677
40842
  const NetskraflImpl = ({ state, tokenExpired }) => {
38678
- const ref = React.createRef();
40843
+ const ref = React.useRef(null);
40844
+ const mountedRef = React.useRef(false);
38679
40845
  const completeState = makeGlobalState({ ...state, tokenExpired });
38680
40846
  const { userEmail } = completeState;
38681
- /*
38682
- useEffect(() => {
38683
- // Check whether the stylesheet is already present
38684
- if (document.getElementById(CSS_LINK_ID)) return;
38685
- // Load and link the stylesheet into the document head
38686
- const link = document.createElement("link");
38687
- const styleUrl = `${window.location.origin}/static/css/netskrafl.css`;
38688
- link.id = CSS_LINK_ID;
38689
- link.rel = "stylesheet";
38690
- link.type = "text/css";
38691
- link.href = styleUrl;
38692
- document.head.appendChild(link);
38693
- // We don't bother to remove the stylesheet when the component is unmounted
38694
- }, []);
38695
- */
38696
40847
  // biome-ignore lint/correctness/useExhaustiveDependencies: The dependency is only on userEmail
38697
40848
  useEffect(() => {
38698
- // Load the Netskrafl (Mithril) UI for a new user
40849
+ // Mount the Netskrafl (Mithril) UI for the current user
38699
40850
  if (!userEmail)
38700
40851
  return;
38701
40852
  const container = ref.current;
@@ -38703,36 +40854,19 @@ const NetskraflImpl = ({ state, tokenExpired }) => {
38703
40854
  console.error('No container for Netskrafl UI');
38704
40855
  return;
38705
40856
  }
38706
- const elemId = `netskrafl-user-${userEmail}`;
38707
- if (container.firstElementChild?.id === elemId) {
38708
- // The Netskrafl UI is already correctly mounted
38709
- return;
38710
- }
38711
- try {
38712
- mountForUser(completeState).then((div) => {
38713
- // Attach the div as a child of the container
38714
- // instead of any previous children
38715
- const container = ref.current;
38716
- if (container) {
38717
- container.innerHTML = '';
38718
- container.appendChild(div);
38719
- }
38720
- });
38721
- }
38722
- catch (_) {
38723
- console.error('Failed to mount Netskrafl UI for user', userEmail);
38724
- const container = document.getElementById('netskrafl-container');
38725
- if (container)
38726
- container.innerHTML = '';
38727
- }
40857
+ // Mount Mithril directly into the container
40858
+ mountForUser(completeState, container)
40859
+ .then(() => {
40860
+ mountedRef.current = true;
40861
+ })
40862
+ .catch((error) => {
40863
+ console.error('Failed to mount Netskrafl UI:', error);
40864
+ });
38728
40865
  return () => {
38729
- // Move the Netskrafl UI to a hidden div under the body element
38730
- // when the component is unmounted
38731
- // console.log("Dismounting Netskrafl UI for user", userEmail);
38732
- const container = document.getElementById('netskrafl-container');
38733
- const div = container?.firstElementChild;
38734
- if (div?.id === elemId) {
38735
- document.body.appendChild(div);
40866
+ // Only unmount if mounting actually succeeded
40867
+ if (mountedRef.current) {
40868
+ unmount(container);
40869
+ mountedRef.current = false;
38736
40870
  }
38737
40871
  };
38738
40872
  }, [userEmail]);