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