Merge pull request #4854 from pangeachat/4853-play-test-121525

4853 play test 121525
This commit is contained in:
ggurdin 2025-12-15 14:46:20 -05:00 committed by GitHub
commit a3e70d623d
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
62 changed files with 837 additions and 224 deletions

View file

@ -1,6 +1,6 @@
{
"@@locale": "ar",
"@@last_modified": "2025-12-15 13:10:00.150906",
"@@last_modified": "2025-12-15 14:43:38.968604",
"about": "حول",
"@about": {
"type": "String",
@ -10918,5 +10918,14 @@
"@optionalRegenerateReason": {
"type": "String",
"placeholders": {}
},
"emojiSelectedSnackbar": "لقد قمت بتعيين الرموز التعبيرية لـ {lemma}! سنستخدم هذا الرمز التعبيري لتمثيل الكلمة في أنشطة الممارسة في المستقبل.",
"@emojiSelectedSnackbar": {
"type": "String",
"placeholders": {
"lemma": {
"type": "String"
}
}
}
}

View file

@ -1915,7 +1915,7 @@
"playWithAI": "Пакуль гуляйце з ШІ",
"courseStartDesc": "Pangea Bot гатовы да працы ў любы час!\n\n...але навучанне лепш з сябрамі!",
"@@locale": "be",
"@@last_modified": "2025-12-15 13:09:51.707905",
"@@last_modified": "2025-12-15 14:43:29.836415",
"@alwaysUse24HourFormat": {
"type": "String",
"placeholders": {}
@ -11800,5 +11800,14 @@
"@optionalRegenerateReason": {
"type": "String",
"placeholders": {}
},
"emojiSelectedSnackbar": "Вы ўсталявалі эмодзі для {lemma}! Мы будзем выкарыстоўваць гэтае эмодзі для прадстаўлення слова ў практычных дзейнасцях у будучыні.",
"@emojiSelectedSnackbar": {
"type": "String",
"placeholders": {
"lemma": {
"type": "String"
}
}
}
}

View file

@ -1,5 +1,5 @@
{
"@@last_modified": "2025-12-15 13:10:11.203206",
"@@last_modified": "2025-12-15 14:43:52.981237",
"about": "সম্পর্কে",
"@about": {
"type": "String",
@ -11805,5 +11805,14 @@
"@optionalRegenerateReason": {
"type": "String",
"placeholders": {}
},
"emojiSelectedSnackbar": "আপনি {lemma} এর জন্য ইমোজি সেট করেছেন! আমরা ভবিষ্যতে অনুশীলন কার্যক্রমে এই ইমোজিটি শব্দটি উপস্থাপন করতে ব্যবহার করব।",
"@emojiSelectedSnackbar": {
"type": "String",
"placeholders": {
"lemma": {
"type": "String"
}
}
}
}

View file

@ -4282,7 +4282,7 @@
"joinPublicTrip": "མི་ཚེས་ལ་ལོག་འབད།",
"startOwnTrip": "ངེད་རང་གི་ལོག་ལ་སྦྱོར་བཅོས།",
"@@locale": "bo",
"@@last_modified": "2025-12-15 13:10:08.949661",
"@@last_modified": "2025-12-15 14:43:49.837126",
"@alwaysUse24HourFormat": {
"type": "String",
"placeholders": {}
@ -10455,5 +10455,14 @@
"@optionalRegenerateReason": {
"type": "String",
"placeholders": {}
},
"emojiSelectedSnackbar": "An setti emoji për {lemma}! Ne do ta përdorim këtë emoji për të përfaqësuar fjalën në aktivitetet praktike në vazhdim.",
"@emojiSelectedSnackbar": {
"type": "String",
"placeholders": {
"lemma": {
"type": "String"
}
}
}
}

View file

@ -1,5 +1,5 @@
{
"@@last_modified": "2025-12-15 13:09:52.878217",
"@@last_modified": "2025-12-15 14:43:31.134664",
"about": "Quant a",
"@about": {
"type": "String",
@ -10725,5 +10725,14 @@
"@optionalRegenerateReason": {
"type": "String",
"placeholders": {}
},
"emojiSelectedSnackbar": "Has establert l'emoji per {lemma}! Utilitzarem aquesta emoji per representar la paraula en les activitats pràctiques d'ara endavant.",
"@emojiSelectedSnackbar": {
"type": "String",
"placeholders": {
"lemma": {
"type": "String"
}
}
}
}

View file

@ -1,6 +1,6 @@
{
"@@locale": "cs",
"@@last_modified": "2025-12-15 13:09:49.179864",
"@@last_modified": "2025-12-15 14:43:26.456306",
"about": "O aplikaci",
"@about": {
"type": "String",
@ -11308,5 +11308,14 @@
"@optionalRegenerateReason": {
"type": "String",
"placeholders": {}
},
"emojiSelectedSnackbar": "Nastavili jste emoji pro {lemma}! Toto emoji použijeme k reprezentaci slova v praktických aktivitách v budoucnu.",
"@emojiSelectedSnackbar": {
"type": "String",
"placeholders": {
"lemma": {
"type": "String"
}
}
}
}

View file

@ -1934,7 +1934,7 @@
"playWithAI": "Leg med AI for nu",
"courseStartDesc": "Pangea Bot er klar til at starte når som helst!\n\n...men læring er bedre med venner!",
"@@locale": "da",
"@@last_modified": "2025-12-15 13:09:27.476427",
"@@last_modified": "2025-12-15 14:42:53.307740",
"@aboutHomeserver": {
"type": "String",
"placeholders": {
@ -11762,5 +11762,14 @@
"@optionalRegenerateReason": {
"type": "String",
"placeholders": {}
},
"emojiSelectedSnackbar": "Du har indstillet emoji'en for {lemma}! Vi vil bruge denne emoji til at repræsentere ordet i praksisaktiviteter fremover.",
"@emojiSelectedSnackbar": {
"type": "String",
"placeholders": {
"lemma": {
"type": "String"
}
}
}
}

View file

@ -1,6 +1,6 @@
{
"@@locale": "de",
"@@last_modified": "2025-12-15 13:09:43.173559",
"@@last_modified": "2025-12-15 14:43:18.204232",
"alwaysUse24HourFormat": "true",
"@alwaysUse24HourFormat": {
"description": "Set to true to always display time of day in 24 hour format."
@ -10708,5 +10708,14 @@
"@optionalRegenerateReason": {
"type": "String",
"placeholders": {}
},
"emojiSelectedSnackbar": "Sie haben das Emoji für {lemma} festgelegt! Wir werden dieses Emoji verwenden, um das Wort in zukünftigen Übungsaktivitäten darzustellen.",
"@emojiSelectedSnackbar": {
"type": "String",
"placeholders": {
"lemma": {
"type": "String"
}
}
}
}

View file

@ -4460,7 +4460,7 @@
"playWithAI": "Παίξτε με την Τεχνητή Νοημοσύνη προς το παρόν",
"courseStartDesc": "Ο Pangea Bot είναι έτοιμος να ξεκινήσει οποιαδήποτε στιγμή!\n\n...αλλά η μάθηση είναι καλύτερη με φίλους!",
"@@locale": "el",
"@@last_modified": "2025-12-15 13:10:16.812716",
"@@last_modified": "2025-12-15 14:43:58.410031",
"@alwaysUse24HourFormat": {
"type": "String",
"placeholders": {}
@ -11759,5 +11759,14 @@
"@optionalRegenerateReason": {
"type": "String",
"placeholders": {}
},
"emojiSelectedSnackbar": "Έχετε ορίσει το emoji για {lemma}! Θα χρησιμοποιήσουμε αυτό το emoji για να εκπροσωπήσουμε τη λέξη σε πρακτικές δραστηριότητες στο εξής.",
"@emojiSelectedSnackbar": {
"type": "String",
"placeholders": {
"lemma": {
"type": "String"
}
}
}
}

View file

@ -5002,5 +5002,14 @@
"inOngoingActivity": "You have an ongoing activity!",
"vocabEmoji": "Vocab emoji",
"requestRegeneration": "Request regeneration",
"optionalRegenerateReason": "(Optional) Reason"
"optionalRegenerateReason": "(Optional) Reason",
"emojiSelectedSnackbar": "Youve set the emoji for {lemma}! Well use this emoji to represent the word in practice activities going forward.",
"@emojiSelectedSnackbar": {
"type": "String",
"placeholders": {
"lemma": {
"type": "String"
}
}
}
}

View file

@ -1,5 +1,5 @@
{
"@@last_modified": "2025-12-15 13:10:20.520198",
"@@last_modified": "2025-12-15 14:44:02.283480",
"about": "Prio",
"@about": {
"type": "String",
@ -11790,5 +11790,14 @@
"@optionalRegenerateReason": {
"type": "String",
"placeholders": {}
},
"emojiSelectedSnackbar": "Vi havas metitan la emoji por {lemma}! Ni uzos ĉi tiun emoji por reprezenti la vorton en praktikaj aktivadoj en la estonteco.",
"@emojiSelectedSnackbar": {
"type": "String",
"placeholders": {
"lemma": {
"type": "String"
}
}
}
}

View file

@ -1,6 +1,6 @@
{
"@@locale": "es",
"@@last_modified": "2025-12-15 13:09:22.556485",
"@@last_modified": "2025-12-15 14:42:48.159542",
"about": "Acerca de",
"@about": {
"type": "String",
@ -7935,5 +7935,14 @@
"@optionalRegenerateReason": {
"type": "String",
"placeholders": {}
},
"emojiSelectedSnackbar": "¡Has establecido el emoji para {lemma}! Usaremos este emoji para representar la palabra en las actividades prácticas de ahora en adelante.",
"@emojiSelectedSnackbar": {
"type": "String",
"placeholders": {
"lemma": {
"type": "String"
}
}
}
}

View file

@ -1,6 +1,6 @@
{
"@@locale": "et",
"@@last_modified": "2025-12-15 13:09:41.916204",
"@@last_modified": "2025-12-15 14:43:16.857704",
"about": "Rakenduse teave",
"@about": {
"type": "String",
@ -10972,5 +10972,14 @@
"@optionalRegenerateReason": {
"type": "String",
"placeholders": {}
},
"emojiSelectedSnackbar": "Oled seadnud emotikoni {lemma} jaoks! Kasutame seda emotikoni sõna esindamiseks praktika tegevustes edaspidi.",
"@emojiSelectedSnackbar": {
"type": "String",
"placeholders": {
"lemma": {
"type": "String"
}
}
}
}

View file

@ -1,6 +1,6 @@
{
"@@locale": "eu",
"@@last_modified": "2025-12-15 13:09:39.405643",
"@@last_modified": "2025-12-15 14:43:14.531001",
"about": "Honi buruz",
"@about": {
"type": "String",
@ -10701,5 +10701,14 @@
"@optionalRegenerateReason": {
"type": "String",
"placeholders": {}
},
"emojiSelectedSnackbar": "{lemma} hitzaren emoji-a ezarri duzu! Etorkizuneko praktiketan hitz hori irudikatzeko emoji hau erabiliko dugu.",
"@emojiSelectedSnackbar": {
"type": "String",
"placeholders": {
"lemma": {
"type": "String"
}
}
}
}

View file

@ -1,5 +1,5 @@
{
"@@last_modified": "2025-12-15 13:10:12.289964",
"@@last_modified": "2025-12-15 14:43:54.300365",
"repeatPassword": "تکرار رمزعبور",
"@repeatPassword": {},
"about": "درباره",
@ -11433,5 +11433,14 @@
"@optionalRegenerateReason": {
"type": "String",
"placeholders": {}
},
"emojiSelectedSnackbar": "شما ایموجی را برای {lemma} تنظیم کرده‌اید! ما از این ایموجی برای نمایش کلمه در فعالیت‌های عملی در آینده استفاده خواهیم کرد.",
"@emojiSelectedSnackbar": {
"type": "String",
"placeholders": {
"lemma": {
"type": "String"
}
}
}
}

View file

@ -4013,7 +4013,7 @@
"playWithAI": "Leiki tekoälyn kanssa nyt",
"courseStartDesc": "Pangea Bot on valmis milloin tahansa!\n\n...mutta oppiminen on parempaa ystävien kanssa!",
"@@locale": "fi",
"@@last_modified": "2025-12-15 13:09:25.461324",
"@@last_modified": "2025-12-15 14:42:51.625904",
"@alwaysUse24HourFormat": {
"type": "String",
"placeholders": {}
@ -11324,5 +11324,14 @@
"@optionalRegenerateReason": {
"type": "String",
"placeholders": {}
},
"emojiSelectedSnackbar": "Olet asettanut emojin {lemma} varten! Käytämme tätä emojia sanan edustamiseen käytännön aktiviteeteissa tulevaisuudessa.",
"@emojiSelectedSnackbar": {
"type": "String",
"placeholders": {
"lemma": {
"type": "String"
}
}
}
}

View file

@ -2791,7 +2791,7 @@
"selectAll": "Piliin lahat",
"deselectAll": "Huwag piliin lahat",
"@@locale": "fil",
"@@last_modified": "2025-12-15 13:09:57.098421",
"@@last_modified": "2025-12-15 14:43:36.288461",
"@setCustomPermissionLevel": {
"type": "String",
"placeholders": {}
@ -11677,5 +11677,14 @@
"@optionalRegenerateReason": {
"type": "String",
"placeholders": {}
},
"emojiSelectedSnackbar": "Naitakda mo na ang emoji para sa {lemma}! Gagamitin namin ang emoji na ito upang kumatawan sa salita sa mga aktibidad sa pagsasanay mula ngayon.",
"@emojiSelectedSnackbar": {
"type": "String",
"placeholders": {
"lemma": {
"type": "String"
}
}
}
}

View file

@ -1,6 +1,6 @@
{
"@@locale": "fr",
"@@last_modified": "2025-12-15 13:10:26.870783",
"@@last_modified": "2025-12-15 14:44:08.960073",
"about": "À propos",
"@about": {
"type": "String",
@ -11025,5 +11025,14 @@
"@optionalRegenerateReason": {
"type": "String",
"placeholders": {}
},
"emojiSelectedSnackbar": "Vous avez défini l'emoji pour {lemma} ! Nous utiliserons cet emoji pour représenter le mot dans les activités pratiques à l'avenir.",
"@emojiSelectedSnackbar": {
"type": "String",
"placeholders": {
"lemma": {
"type": "String"
}
}
}
}

View file

@ -4521,7 +4521,7 @@
"playWithAI": "Imir le AI faoi láthair",
"courseStartDesc": "Tá Bot Pangea réidh chun dul am ar bith!\n\n...ach is fearr foghlaim le cairde!",
"@@locale": "ga",
"@@last_modified": "2025-12-15 13:10:25.578909",
"@@last_modified": "2025-12-15 14:44:07.686971",
"@customReaction": {
"type": "String",
"placeholders": {}
@ -10699,5 +10699,14 @@
"@optionalRegenerateReason": {
"type": "String",
"placeholders": {}
},
"emojiSelectedSnackbar": "Tá an emoji socraithe agat do {lemma}! Úsáidfimid an emoji seo chun an focal a chur in iúl i ngníomhaíochtaí cleachtais amach anseo.",
"@emojiSelectedSnackbar": {
"type": "String",
"placeholders": {
"lemma": {
"type": "String"
}
}
}
}

View file

@ -1,6 +1,6 @@
{
"@@locale": "gl",
"@@last_modified": "2025-12-15 13:09:24.082576",
"@@last_modified": "2025-12-15 14:42:50.213816",
"about": "Acerca de",
"@about": {
"type": "String",
@ -10698,5 +10698,14 @@
"@optionalRegenerateReason": {
"type": "String",
"placeholders": {}
},
"emojiSelectedSnackbar": "Estableciches o emoji para {lemma}! Usaremos este emoji para representar a palabra nas actividades prácticas a partir de agora.",
"@emojiSelectedSnackbar": {
"type": "String",
"placeholders": {
"lemma": {
"type": "String"
}
}
}
}

View file

@ -1,5 +1,5 @@
{
"@@last_modified": "2025-12-15 13:09:36.374126",
"@@last_modified": "2025-12-15 14:43:10.892818",
"about": "אודות",
"@about": {
"type": "String",
@ -11750,5 +11750,14 @@
"@optionalRegenerateReason": {
"type": "String",
"placeholders": {}
},
"emojiSelectedSnackbar": "הגדרת את האימוג'י עבור {lemma}! נשתמש באימוג'י הזה כדי לייצג את המילה בפעילויות תרגול מעתה ואילך.",
"@emojiSelectedSnackbar": {
"type": "String",
"placeholders": {
"lemma": {
"type": "String"
}
}
}
}

View file

@ -4487,7 +4487,7 @@
"playWithAI": "अभी के लिए एआई के साथ खेलें",
"courseStartDesc": "पैंजिया बॉट कभी भी जाने के लिए तैयार है!\n\n...लेकिन दोस्तों के साथ सीखना बेहतर है!",
"@@locale": "hi",
"@@last_modified": "2025-12-15 13:10:19.180543",
"@@last_modified": "2025-12-15 14:44:00.905904",
"@alwaysUse24HourFormat": {
"type": "String",
"placeholders": {}
@ -11786,5 +11786,14 @@
"@optionalRegenerateReason": {
"type": "String",
"placeholders": {}
},
"emojiSelectedSnackbar": "आपने {lemma} के लिए इमोजी सेट किया है! हम आगे के अभ्यास गतिविधियों में इस इमोजी का उपयोग करेंगे।",
"@emojiSelectedSnackbar": {
"type": "String",
"placeholders": {
"lemma": {
"type": "String"
}
}
}
}

View file

@ -1,6 +1,6 @@
{
"@@locale": "hr",
"@@last_modified": "2025-12-15 13:09:35.275706",
"@@last_modified": "2025-12-15 14:43:09.046460",
"about": "Informacije",
"@about": {
"type": "String",
@ -11073,5 +11073,14 @@
"@optionalRegenerateReason": {
"type": "String",
"placeholders": {}
},
"emojiSelectedSnackbar": "Postavili ste emoji za {lemma}! Ovaj emoji ćemo koristiti za predstavljanje riječi u praktičnim aktivnostima ubuduće.",
"@emojiSelectedSnackbar": {
"type": "String",
"placeholders": {
"lemma": {
"type": "String"
}
}
}
}

View file

@ -1,6 +1,6 @@
{
"@@locale": "hu",
"@@last_modified": "2025-12-15 13:09:28.639253",
"@@last_modified": "2025-12-15 14:42:55.436150",
"about": "Névjegy",
"@about": {
"type": "String",
@ -10702,5 +10702,14 @@
"@optionalRegenerateReason": {
"type": "String",
"placeholders": {}
},
"emojiSelectedSnackbar": "Beállítottad a {lemma} emoji-t! Ezt az emojit fogjuk használni a szó képviseletére a gyakorlati tevékenységek során a jövőben.",
"@emojiSelectedSnackbar": {
"type": "String",
"placeholders": {
"lemma": {
"type": "String"
}
}
}
}

View file

@ -1962,7 +1962,7 @@
"playWithAI": "Joca con le IA pro ora",
"courseStartDesc": "Pangea Bot es preste a comenzar a qualunque momento!\n\n...ma apprender es melior con amicos!",
"@@locale": "ia",
"@@last_modified": "2025-12-15 13:09:37.510659",
"@@last_modified": "2025-12-15 14:43:12.499796",
"@alwaysUse24HourFormat": {
"type": "String",
"placeholders": {}
@ -11779,5 +11779,14 @@
"@optionalRegenerateReason": {
"type": "String",
"placeholders": {}
},
"emojiSelectedSnackbar": "Ați setat emoji-ul pentru {lemma}! Vom folosi acest emoji pentru a reprezenta cuvântul în activitățile practice de acum înainte.",
"@emojiSelectedSnackbar": {
"type": "String",
"placeholders": {
"lemma": {
"type": "String"
}
}
}
}

View file

@ -1,5 +1,5 @@
{
"@@last_modified": "2025-12-15 13:09:30.033862",
"@@last_modified": "2025-12-15 14:42:56.674775",
"setAsCanonicalAlias": "Atur sebagai alias utama",
"@setAsCanonicalAlias": {
"type": "String",
@ -10692,5 +10692,14 @@
"@optionalRegenerateReason": {
"type": "String",
"placeholders": {}
},
"emojiSelectedSnackbar": "Anda telah menetapkan emoji untuk {lemma}! Kami akan menggunakan emoji ini untuk mewakili kata dalam aktivitas praktik ke depan.",
"@emojiSelectedSnackbar": {
"type": "String",
"placeholders": {
"lemma": {
"type": "String"
}
}
}
}

View file

@ -4376,7 +4376,7 @@
"playWithAI": "Joca con AI pro ora",
"courseStartDesc": "Pangea Bot es preste a partir a qualunque momento!\n\n...ma apprender es melior con amicos!",
"@@locale": "ie",
"@@last_modified": "2025-12-15 13:09:34.373708",
"@@last_modified": "2025-12-15 14:43:07.923977",
"@alwaysUse24HourFormat": {
"type": "String",
"placeholders": {}
@ -11675,5 +11675,14 @@
"@optionalRegenerateReason": {
"type": "String",
"placeholders": {}
},
"emojiSelectedSnackbar": "Tú setaste an emoji pa {lemma}! Usaremos este emoji pa representar la palabra en les actividaes de práctica d'ora en adelante.",
"@emojiSelectedSnackbar": {
"type": "String",
"placeholders": {
"lemma": {
"type": "String"
}
}
}
}

View file

@ -1,5 +1,5 @@
{
"@@last_modified": "2025-12-15 13:09:46.808373",
"@@last_modified": "2025-12-15 14:43:23.466924",
"about": "Informazioni",
"@about": {
"type": "String",
@ -10704,5 +10704,14 @@
"@optionalRegenerateReason": {
"type": "String",
"placeholders": {}
},
"emojiSelectedSnackbar": "Hai impostato l'emoji per {lemma}! Useremo questa emoji per rappresentare la parola nelle attività pratiche in futuro.",
"@emojiSelectedSnackbar": {
"type": "String",
"placeholders": {
"lemma": {
"type": "String"
}
}
}
}

View file

@ -1,6 +1,6 @@
{
"@@locale": "ja",
"@@last_modified": "2025-12-15 13:10:17.963932",
"@@last_modified": "2025-12-15 14:43:59.878807",
"about": "このアプリについて",
"@about": {
"type": "String",
@ -11491,5 +11491,14 @@
"@optionalRegenerateReason": {
"type": "String",
"placeholders": {}
},
"emojiSelectedSnackbar": "{lemma}の絵文字が設定されました!今後の練習活動ではこの絵文字を使ってその単語を表現します。",
"@emojiSelectedSnackbar": {
"type": "String",
"placeholders": {
"lemma": {
"type": "String"
}
}
}
}

View file

@ -2598,7 +2598,7 @@
"playWithAI": "ამ დროისთვის ითამაშეთ AI-თან",
"courseStartDesc": "Pangea Bot მზადაა ნებისმიერ დროს გასასვლელად!\n\n...მაგრამ სწავლა უკეთესია მეგობრებთან ერთად!",
"@@locale": "ka",
"@@last_modified": "2025-12-15 13:10:22.979686",
"@@last_modified": "2025-12-15 14:44:05.262676",
"@alwaysUse24HourFormat": {
"type": "String",
"placeholders": {}
@ -11731,5 +11731,14 @@
"@optionalRegenerateReason": {
"type": "String",
"placeholders": {}
},
"emojiSelectedSnackbar": "თქვენ დააყენეთ ემოჯი {lemma}-ისთვის! ჩვენ ამ ემოჯის გამოყენებას ვაპირებთ სიტყვების პრაქტიკული აქტივობების წარმოსადგენად.",
"@emojiSelectedSnackbar": {
"type": "String",
"placeholders": {
"lemma": {
"type": "String"
}
}
}
}

View file

@ -1,5 +1,5 @@
{
"@@last_modified": "2025-12-15 13:09:21.214976",
"@@last_modified": "2025-12-15 14:42:46.121270",
"about": "소개",
"@about": {
"type": "String",
@ -10809,5 +10809,14 @@
"@optionalRegenerateReason": {
"type": "String",
"placeholders": {}
},
"emojiSelectedSnackbar": "{lemma}에 대한 이모지를 설정했습니다! 앞으로 연습 활동에서 이 이모지를 사용하여 단어를 나타낼 것입니다.",
"@emojiSelectedSnackbar": {
"type": "String",
"placeholders": {
"lemma": {
"type": "String"
}
}
}
}

View file

@ -3865,7 +3865,7 @@
"playWithAI": "Žaiskite su dirbtiniu intelektu dabar",
"courseStartDesc": "Pangea botas pasiruošęs bet kada pradėti!\n\n...bet mokymasis yra geresnis su draugais!",
"@@locale": "lt",
"@@last_modified": "2025-12-15 13:10:04.199235",
"@@last_modified": "2025-12-15 14:43:43.050040",
"@alwaysUse24HourFormat": {
"type": "String",
"placeholders": {}
@ -11506,5 +11506,14 @@
"@optionalRegenerateReason": {
"type": "String",
"placeholders": {}
},
"emojiSelectedSnackbar": "Jūs nustatėte emociją {lemma}! Mes naudosime šią emociją, kad atstovautume žodžiui praktinėse veiklose ateityje.",
"@emojiSelectedSnackbar": {
"type": "String",
"placeholders": {
"lemma": {
"type": "String"
}
}
}
}

View file

@ -4486,7 +4486,7 @@
"playWithAI": "Tagad spēlējiet ar AI",
"courseStartDesc": "Pangea bots ir gatavs jebkurā laikā!\n\n...bet mācīties ir labāk ar draugiem!",
"@@locale": "lv",
"@@last_modified": "2025-12-15 13:09:58.402497",
"@@last_modified": "2025-12-15 14:43:37.722676",
"analyticsInactiveTitle": "Pieprasījumi neaktīviem lietotājiem nevar tikt nosūtīti",
"analyticsInactiveDesc": "Neaktīvi lietotāji, kuri nav pieteikušies kopš šīs funkcijas ieviešanas, neredzēs jūsu pieprasījumu.\n\nPieprasījuma poga parādīsies, kad viņi atgriezīsies. Jūs varat atkārtoti nosūtīt pieprasījumu vēlāk, noklikšķinot uz pieprasījuma pogas viņu vārdā, kad tā būs pieejama.",
"accessRequestedTitle": "Pieprasījums piekļūt analītikai",
@ -10687,5 +10687,14 @@
"@optionalRegenerateReason": {
"type": "String",
"placeholders": {}
},
"emojiSelectedSnackbar": "Jūs esat iestatījis emocijzīmi {lemma}! Mēs izmantosim šo emocijzīmi, lai pārstāvētu vārdu praktiskajās aktivitātēs turpmāk.",
"@emojiSelectedSnackbar": {
"type": "String",
"placeholders": {
"lemma": {
"type": "String"
}
}
}
}

View file

@ -1,5 +1,5 @@
{
"@@last_modified": "2025-12-15 13:09:50.414856",
"@@last_modified": "2025-12-15 14:43:27.926249",
"about": "Om",
"@about": {
"type": "String",
@ -11794,5 +11794,14 @@
"@optionalRegenerateReason": {
"type": "String",
"placeholders": {}
},
"emojiSelectedSnackbar": "Du har satt emoji for {lemma}! Vi vil bruke denne emojien for å representere ordet i praksisaktiviteter fremover.",
"@emojiSelectedSnackbar": {
"type": "String",
"placeholders": {
"lemma": {
"type": "String"
}
}
}
}

View file

@ -1,5 +1,5 @@
{
"@@last_modified": "2025-12-15 13:10:08.027282",
"@@last_modified": "2025-12-15 14:43:48.784024",
"about": "Over ons",
"@about": {
"type": "String",
@ -10701,5 +10701,14 @@
"@optionalRegenerateReason": {
"type": "String",
"placeholders": {}
},
"emojiSelectedSnackbar": "Je hebt de emoji voor {lemma} ingesteld! We zullen deze emoji gebruiken om het woord in de praktijkactiviteiten voortaan weer te geven.",
"@emojiSelectedSnackbar": {
"type": "String",
"placeholders": {
"lemma": {
"type": "String"
}
}
}
}

View file

@ -1,6 +1,6 @@
{
"@@locale": "pl",
"@@last_modified": "2025-12-15 13:10:13.564120",
"@@last_modified": "2025-12-15 14:43:55.454694",
"about": "O aplikacji",
"@about": {
"type": "String",
@ -10699,5 +10699,14 @@
"@optionalRegenerateReason": {
"type": "String",
"placeholders": {}
},
"emojiSelectedSnackbar": "Ustawiłeś emoji dla {lemma}! Będziemy używać tego emoji do reprezentowania słowa w nadchodzących aktywnościach praktycznych.",
"@emojiSelectedSnackbar": {
"type": "String",
"placeholders": {
"lemma": {
"type": "String"
}
}
}
}

View file

@ -1,5 +1,5 @@
{
"@@last_modified": "2025-12-15 13:09:40.663100",
"@@last_modified": "2025-12-15 14:43:15.620362",
"copiedToClipboard": "Copiada para a área de transferência",
"@copiedToClipboard": {
"type": "String",
@ -11801,5 +11801,14 @@
"@optionalRegenerateReason": {
"type": "String",
"placeholders": {}
},
"emojiSelectedSnackbar": "Você definiu o emoji para {lemma}! Usaremos este emoji para representar a palavra nas atividades práticas daqui para frente.",
"@emojiSelectedSnackbar": {
"type": "String",
"placeholders": {
"lemma": {
"type": "String"
}
}
}
}

View file

@ -1,5 +1,5 @@
{
"@@last_modified": "2025-12-15 13:09:38.322859",
"@@last_modified": "2025-12-15 14:43:13.571791",
"about": "Sobre",
"@about": {
"type": "String",
@ -11059,5 +11059,14 @@
"@optionalRegenerateReason": {
"type": "String",
"placeholders": {}
},
"emojiSelectedSnackbar": "Você definiu o emoji para {lemma}! Usaremos este emoji para representar a palavra nas atividades práticas daqui para frente.",
"@emojiSelectedSnackbar": {
"type": "String",
"placeholders": {
"lemma": {
"type": "String"
}
}
}
}

View file

@ -3335,7 +3335,7 @@
"selectAll": "Selecionar tudo",
"deselectAll": "Desmarcar tudo",
"@@locale": "pt_PT",
"@@last_modified": "2025-12-15 13:09:54.999629",
"@@last_modified": "2025-12-15 14:43:33.549800",
"@alwaysUse24HourFormat": {
"type": "String",
"placeholders": {}
@ -11730,5 +11730,14 @@
"@optionalRegenerateReason": {
"type": "String",
"placeholders": {}
},
"emojiSelectedSnackbar": "Você definiu o emoji para {lemma}! Usaremos este emoji para representar a palavra nas atividades práticas daqui para frente.",
"@emojiSelectedSnackbar": {
"type": "String",
"placeholders": {
"lemma": {
"type": "String"
}
}
}
}

View file

@ -1,5 +1,5 @@
{
"@@last_modified": "2025-12-15 13:09:31.920399",
"@@last_modified": "2025-12-15 14:42:58.811266",
"about": "Despre",
"@about": {
"type": "String",
@ -11436,5 +11436,14 @@
"@optionalRegenerateReason": {
"type": "String",
"placeholders": {}
},
"emojiSelectedSnackbar": "Ai setat emoji-ul pentru {lemma}! Vom folosi acest emoji pentru a reprezenta cuvântul în activitățile practice de acum înainte.",
"@emojiSelectedSnackbar": {
"type": "String",
"placeholders": {
"lemma": {
"type": "String"
}
}
}
}

View file

@ -1,6 +1,6 @@
{
"@@locale": "ru",
"@@last_modified": "2025-12-15 13:10:22.056511",
"@@last_modified": "2025-12-15 14:44:03.535697",
"about": "О проекте",
"@about": {
"type": "String",
@ -10806,5 +10806,14 @@
"@optionalRegenerateReason": {
"type": "String",
"placeholders": {}
},
"emojiSelectedSnackbar": "Вы установили эмодзи для {lemma}! Мы будем использовать этот эмодзи для представления слова в практических заданиях в будущем.",
"@emojiSelectedSnackbar": {
"type": "String",
"placeholders": {
"lemma": {
"type": "String"
}
}
}
}

View file

@ -1,6 +1,6 @@
{
"@@locale": "sk",
"@@last_modified": "2025-12-15 13:09:33.330473",
"@@last_modified": "2025-12-15 14:43:00.627770",
"about": "O aplikácii",
"@about": {
"type": "String",
@ -11785,5 +11785,14 @@
"@optionalRegenerateReason": {
"type": "String",
"placeholders": {}
},
"emojiSelectedSnackbar": "Nastavili ste emoji pre {lemma}! Tento emoji budeme používať na reprezentáciu slova v praktických aktivitách v budúcnosti.",
"@emojiSelectedSnackbar": {
"type": "String",
"placeholders": {
"lemma": {
"type": "String"
}
}
}
}

View file

@ -2468,7 +2468,7 @@
"playWithAI": "Za zdaj igrajte z AI-jem",
"courseStartDesc": "Pangea Bot je pripravljen kadarkoli!\n\n...ampak je bolje učiti se s prijatelji!",
"@@locale": "sl",
"@@last_modified": "2025-12-15 13:09:44.475627",
"@@last_modified": "2025-12-15 14:43:19.279445",
"@alwaysUse24HourFormat": {
"type": "String",
"placeholders": {}
@ -11782,5 +11782,14 @@
"@optionalRegenerateReason": {
"type": "String",
"placeholders": {}
},
"emojiSelectedSnackbar": "Nastavili ste emoji za {lemma}! Ta emoji bomo uporabili za predstavitev besede v praktičnih dejavnostih v prihodnje.",
"@emojiSelectedSnackbar": {
"type": "String",
"placeholders": {
"lemma": {
"type": "String"
}
}
}
}

View file

@ -1,5 +1,5 @@
{
"@@last_modified": "2025-12-15 13:10:24.291700",
"@@last_modified": "2025-12-15 14:44:06.530821",
"about": "О програму",
"@about": {
"type": "String",
@ -11803,5 +11803,14 @@
"@optionalRegenerateReason": {
"type": "String",
"placeholders": {}
},
"emojiSelectedSnackbar": "Postavili ste emoji za {lemma}! Ovaj emoji ćemo koristiti da predstavimo reč u praktičnim aktivnostima ubuduće.",
"@emojiSelectedSnackbar": {
"type": "String",
"placeholders": {
"lemma": {
"type": "String"
}
}
}
}

View file

@ -1,5 +1,5 @@
{
"@@last_modified": "2025-12-15 13:10:15.788663",
"@@last_modified": "2025-12-15 14:43:56.890657",
"about": "Om",
"@about": {
"type": "String",
@ -11179,5 +11179,14 @@
"@optionalRegenerateReason": {
"type": "String",
"placeholders": {}
},
"emojiSelectedSnackbar": "Du har ställt in emojin för {lemma}! Vi kommer att använda denna emoji för att representera ordet i praktiska aktiviteter framöver.",
"@emojiSelectedSnackbar": {
"type": "String",
"placeholders": {
"lemma": {
"type": "String"
}
}
}
}

View file

@ -1,5 +1,5 @@
{
"@@last_modified": "2025-12-15 13:10:06.701558",
"@@last_modified": "2025-12-15 14:43:47.415907",
"acceptedTheInvitation": "👍 {username} அழைப்பை ஏற்றுக்கொண்டது",
"@acceptedTheInvitation": {
"type": "String",
@ -10925,5 +10925,14 @@
"@optionalRegenerateReason": {
"type": "String",
"placeholders": {}
},
"emojiSelectedSnackbar": "{lemma} க்கான எமோஜியை நீங்கள் அமைத்துள்ளீர்கள்! எதிர்காலத்தில் பயிற்சி செயல்களில் அந்த சொல்லை பிரதிநிதித்துவப்படுத்த எமோஜியை நாங்கள் பயன்படுத்துவோம்.",
"@emojiSelectedSnackbar": {
"type": "String",
"placeholders": {
"lemma": {
"type": "String"
}
}
}
}

View file

@ -1924,7 +1924,7 @@
"playWithAI": "ఇప్పుడే AI తో ఆడండి",
"courseStartDesc": "పాంజియా బాట్ ఎప్పుడైనా సిద్ధంగా ఉంటుంది!\n\n...కానీ స్నేహితులతో నేర్చుకోవడం మెరుగైనది!",
"@@locale": "te",
"@@last_modified": "2025-12-15 13:10:02.606443",
"@@last_modified": "2025-12-15 14:43:41.716893",
"@setCustomPermissionLevel": {
"type": "String",
"placeholders": {}
@ -11790,5 +11790,14 @@
"@optionalRegenerateReason": {
"type": "String",
"placeholders": {}
},
"emojiSelectedSnackbar": "{lemma} కోసం మీరు ఈ ఎమోజీని సెట్ చేసారు! మేము ఈ ఎమోజీని ప్రాక్టీస్ కార్యకలాపాలలో పదాన్ని ప్రాతినిధ్యం వహించడానికి ఉపయోగిస్తాము.",
"@emojiSelectedSnackbar": {
"type": "String",
"placeholders": {
"lemma": {
"type": "String"
}
}
}
}

View file

@ -4460,7 +4460,7 @@
"playWithAI": "เล่นกับ AI ชั่วคราว",
"courseStartDesc": "Pangea Bot พร้อมที่จะเริ่มต้นได้ทุกเมื่อ!\n\n...แต่การเรียนรู้ดีกว่ากับเพื่อน!",
"@@locale": "th",
"@@last_modified": "2025-12-15 13:09:53.958347",
"@@last_modified": "2025-12-15 14:43:32.503180",
"@alwaysUse24HourFormat": {
"type": "String",
"placeholders": {}
@ -11759,5 +11759,14 @@
"@optionalRegenerateReason": {
"type": "String",
"placeholders": {}
},
"emojiSelectedSnackbar": "คุณได้ตั้งค่าอีโมจิสำหรับ {lemma} แล้ว! เราจะใช้อีโมจินี้เพื่อแทนคำในกิจกรรมการฝึกฝนต่อไป",
"@emojiSelectedSnackbar": {
"type": "String",
"placeholders": {
"lemma": {
"type": "String"
}
}
}
}

View file

@ -1,6 +1,6 @@
{
"@@locale": "tr",
"@@last_modified": "2025-12-15 13:10:01.271793",
"@@last_modified": "2025-12-15 14:43:40.183100",
"about": "Hakkında",
"@about": {
"type": "String",
@ -10923,5 +10923,14 @@
"@optionalRegenerateReason": {
"type": "String",
"placeholders": {}
},
"emojiSelectedSnackbar": "{lemma} için emojiyi ayarladınız! Bu emojiyi, pratik aktivitelerde kelimeyi temsil etmek için kullanacağız.",
"@emojiSelectedSnackbar": {
"type": "String",
"placeholders": {
"lemma": {
"type": "String"
}
}
}
}

View file

@ -1,6 +1,6 @@
{
"@@locale": "uk",
"@@last_modified": "2025-12-15 13:09:47.980810",
"@@last_modified": "2025-12-15 14:43:25.022859",
"about": "Про застосунок",
"@about": {
"type": "String",
@ -10695,5 +10695,14 @@
"@optionalRegenerateReason": {
"type": "String",
"placeholders": {}
},
"emojiSelectedSnackbar": "Ви встановили емодзі для {lemma}! Ми будемо використовувати цей емодзі для представлення слова в практичних завданнях у майбутньому.",
"@emojiSelectedSnackbar": {
"type": "String",
"placeholders": {
"lemma": {
"type": "String"
}
}
}
}

View file

@ -1,5 +1,5 @@
{
"@@last_modified": "2025-12-15 13:10:05.261556",
"@@last_modified": "2025-12-15 14:43:45.911331",
"about": "Giới thiệu",
"@about": {
"type": "String",
@ -6271,5 +6271,14 @@
"@optionalRegenerateReason": {
"type": "String",
"placeholders": {}
},
"emojiSelectedSnackbar": "Bạn đã đặt biểu tượng cảm xúc cho {lemma}! Chúng tôi sẽ sử dụng biểu tượng cảm xúc này để đại diện cho từ trong các hoạt động thực hành trong tương lai.",
"@emojiSelectedSnackbar": {
"type": "String",
"placeholders": {
"lemma": {
"type": "String"
}
}
}
}

View file

@ -1860,7 +1860,7 @@
"selectAll": "全選",
"deselectAll": "取消全選",
"@@locale": "yue",
"@@last_modified": "2025-12-15 13:09:45.810126",
"@@last_modified": "2025-12-15 14:43:20.879510",
"@ignoreUser": {
"type": "String",
"placeholders": {}
@ -11792,5 +11792,14 @@
"@optionalRegenerateReason": {
"type": "String",
"placeholders": {}
},
"emojiSelectedSnackbar": "你已為 {lemma} 設定了表情符號!我們將在未來的練習活動中使用這個表情符號來代表這個詞。",
"@emojiSelectedSnackbar": {
"type": "String",
"placeholders": {
"lemma": {
"type": "String"
}
}
}
}

View file

@ -1,6 +1,6 @@
{
"@@locale": "zh",
"@@last_modified": "2025-12-15 13:10:10.038504",
"@@last_modified": "2025-12-15 14:43:51.241424",
"about": "关于",
"@about": {
"type": "String",
@ -10692,5 +10692,14 @@
"@optionalRegenerateReason": {
"type": "String",
"placeholders": {}
},
"emojiSelectedSnackbar": "您已为 {lemma} 设置了表情符号!我们将使用此表情符号在今后的实践活动中表示该词。",
"@emojiSelectedSnackbar": {
"type": "String",
"placeholders": {
"lemma": {
"type": "String"
}
}
}
}

View file

@ -1,5 +1,5 @@
{
"@@last_modified": "2025-12-15 13:09:56.043510",
"@@last_modified": "2025-12-15 14:43:35.064582",
"about": "關於",
"@about": {
"type": "String",
@ -10699,5 +10699,14 @@
"@optionalRegenerateReason": {
"type": "String",
"placeholders": {}
},
"emojiSelectedSnackbar": "您已為 {lemma} 設定了表情符號!我們將在未來的練習活動中使用這個表情符號來代表這個詞。",
"@emojiSelectedSnackbar": {
"type": "String",
"placeholders": {
"lemma": {
"type": "String"
}
}
}
}

View file

@ -4,11 +4,12 @@ import 'package:flutter/material.dart';
import 'package:fluffychat/l10n/l10n.dart';
import 'package:fluffychat/pangea/analytics_misc/text_loading_shimmer.dart';
import 'package:fluffychat/pangea/common/network/requests.dart';
import 'package:fluffychat/pangea/common/widgets/error_indicator.dart';
import 'package:fluffychat/pangea/languages/language_constants.dart';
import 'package:fluffychat/pangea/morphs/get_grammar_copy.dart';
import 'package:fluffychat/pangea/morphs/morph_features_enum.dart';
import 'package:fluffychat/pangea/morphs/morph_meaning/morph_info_repo.dart';
import 'package:fluffychat/pangea/morphs/morph_meaning/morph_info_request.dart';
import 'package:fluffychat/widgets/future_loading_dialog.dart';
import 'package:fluffychat/widgets/matrix.dart';
class MorphMeaningWidget extends StatefulWidget {
@ -29,16 +30,16 @@ class MorphMeaningWidget extends StatefulWidget {
class MorphMeaningWidgetState extends State<MorphMeaningWidget> {
bool _editMode = false;
late TextEditingController _controller;
static const int maxCharacters = 140;
String? _cachedResponse;
String? _definition;
bool _isLoading = true;
Object? _error;
@override
void didUpdateWidget(covariant MorphMeaningWidget oldWidget) {
if (oldWidget.tag != widget.tag || oldWidget.feature != widget.feature) {
_cachedResponse = null;
_isLoading = true;
_loadMorphMeaning();
}
@ -58,28 +59,44 @@ class MorphMeaningWidgetState extends State<MorphMeaningWidget> {
super.dispose();
}
MorphInfoRequest get _request => MorphInfoRequest(
userL1: MatrixState.pangeaController.userController.userL1?.langCode ??
LanguageKeys.defaultLanguage,
userL2: MatrixState.pangeaController.userController.userL2?.langCode ??
LanguageKeys.defaultLanguage,
);
Future<void> _loadMorphMeaning() async {
try {
final response = await _morphMeaning();
_setMeaningText(response);
} catch (e) {
_error = e;
} finally {
if (mounted) setState(() => _isLoading = false);
if (mounted) {
setState(() {
_isLoading = true;
_definition = null;
});
}
final response = await _morphMeaning();
_controller.text = response.substring(
0,
min(response.length, maxCharacters),
);
_definition = response;
if (mounted) setState(() => _isLoading = false);
}
Future<String> _morphMeaning() async {
if (_cachedResponse != null) {
return _cachedResponse!;
final result = await MorphInfoRepo.get(
MatrixState.pangeaController.userController.accessToken,
_request,
);
if (result.isError) {
return L10n.of(context).meaningNotFound;
}
final response = await MorphInfoRepo.get(
feature: widget.feature,
tag: widget.tag,
);
_cachedResponse = response;
return response ?? L10n.of(context).meaningNotFound;
final morph = result.result!.getFeatureByCode(widget.feature.name);
final data = morph?.getTagByCode(widget.tag);
return data?.l1Description ?? L10n.of(context).meaningNotFound;
}
void _toggleEditMode(bool value) => setState(() => _editMode = value);
@ -90,22 +107,15 @@ class MorphMeaningWidgetState extends State<MorphMeaningWidget> {
? userEdit.substring(0, maxCharacters)
: userEdit;
await MorphInfoRepo.setMorphDefinition(
await MorphInfoRepo.update(
_request,
feature: widget.feature,
tag: widget.tag,
defintion: truncatedEdit,
definition: truncatedEdit,
);
// Update the cached response
_cachedResponse = truncatedEdit;
_toggleEditMode(false);
}
void _setMeaningText(String initialText) {
_controller.text = initialText.substring(
0,
min(initialText.length, maxCharacters),
);
_loadMorphMeaning();
}
@override
@ -114,27 +124,11 @@ class MorphMeaningWidgetState extends State<MorphMeaningWidget> {
return const TextLoadingShimmer();
}
if (_error != null) {
return Center(
child: _error is UnsubscribedException
? ErrorIndicator(
message: L10n.of(context).subscribeToUnlockDefinitions,
onTap: () {
MatrixState.pangeaController.subscriptionController
.showPaywall(context);
},
)
: ErrorIndicator(
message: L10n.of(context).errorFetchingDefinition,
),
);
}
if (_editMode) {
return MorphEditView(
morphFeature: widget.feature,
morphTag: widget.tag,
meaning: _cachedResponse ?? "",
meaning: _definition ?? "",
controller: _controller,
toggleEditMode: _toggleEditMode,
editMorphMeaning: editMorphMeaning,
@ -149,7 +143,7 @@ class MorphMeaningWidgetState extends State<MorphMeaningWidget> {
onDoubleTap: () => _toggleEditMode(true),
child: Text(
textAlign: TextAlign.center,
_cachedResponse ?? L10n.of(context).meaningNotFound,
_definition ?? L10n.of(context).meaningNotFound,
style: widget.style,
),
),

View file

@ -1,21 +1,24 @@
import 'package:flutter/material.dart';
import 'package:fluffychat/config/app_config.dart';
import 'package:fluffychat/pangea/analytics_misc/construct_use_model.dart';
import 'package:fluffychat/pangea/common/widgets/shrinkable_text.dart';
import 'package:fluffychat/pangea/constructs/construct_level_enum.dart';
import 'package:fluffychat/pangea/constructs/construct_identifier.dart';
class VocabAnalyticsListTile extends StatefulWidget {
const VocabAnalyticsListTile({
super.key,
required this.emoji,
required this.constructUse,
required this.onTap,
required this.constructId,
required this.textColor,
required this.icon,
this.onTap,
});
final String? emoji;
final void Function() onTap;
final ConstructUses constructUse;
final void Function()? onTap;
final ConstructIdentifier constructId;
final Color textColor;
final Widget icon;
@override
VocabAnalyticsListTileState createState() => VocabAnalyticsListTileState();
@ -43,9 +46,7 @@ class VocabAnalyticsListTileState extends State<VocabAnalyticsListTile> {
padding: EdgeInsets.all(padding),
decoration: BoxDecoration(
color: _isHovered
? widget.constructUse.constructLevel
.color(context)
.withAlpha(20)
? widget.textColor.withAlpha(20)
: Colors.transparent,
borderRadius: BorderRadius.circular(AppConfig.borderRadius),
),
@ -55,28 +56,18 @@ class VocabAnalyticsListTileState extends State<VocabAnalyticsListTile> {
Container(
alignment: Alignment.center,
height: (maxWidth - padding * 2) * 0.6,
child: widget.emoji != null
? Text(
widget.emoji!,
style: const TextStyle(
fontSize: 22,
),
)
: widget.constructUse.constructLevel.icon(36.0),
child: widget.icon,
),
Container(
alignment: Alignment.topCenter,
padding: const EdgeInsets.only(top: 4),
height: (maxWidth - padding * 2) * 0.4,
child: ShrinkableText(
text: widget.constructUse.lemma,
text: widget.constructId.lemma,
maxWidth: maxWidth - padding * 2,
style: TextStyle(
fontSize: 16,
color: Theme.of(context).brightness == Brightness.light
? widget.constructUse.constructLevel
.darkColor(context)
: widget.constructUse.constructLevel.color(context),
color: widget.textColor,
),
),
),

View file

@ -177,8 +177,20 @@ class VocabAnalyticsListView extends StatelessWidget {
onTap: () => context.go(
"/rooms/analytics/${vocabItem.id.type.string}/${Uri.encodeComponent(vocabItem.id.string)}",
),
constructUse: vocabItem,
constructId: vocabItem.id,
textColor:
Theme.of(context).brightness == Brightness.light
? vocabItem.lemmaCategory.darkColor(context)
: vocabItem.lemmaCategory.color(context),
emoji: vocabItem.id.userSetEmoji.firstOrNull,
icon: vocabItem.id.userSetEmoji.isNotEmpty
? Text(
vocabItem.id.userSetEmoji.first,
style: const TextStyle(
fontSize: 22,
),
)
: vocabItem.lemmaCategory.icon(36.0),
);
},
childCount: _filteredVocab.length,

View file

@ -1,9 +1,16 @@
import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
import 'package:fluffychat/l10n/l10n.dart';
import 'package:fluffychat/pangea/analytics_details_popup/vocab_analytics_list_tile.dart';
import 'package:fluffychat/pangea/analytics_misc/construct_use_type_enum.dart';
import 'package:fluffychat/pangea/analytics_misc/constructs_model.dart';
import 'package:fluffychat/pangea/constructs/construct_identifier.dart';
import 'package:fluffychat/pangea/instructions/instructions_enum.dart';
import 'package:fluffychat/widgets/matrix.dart';
mixin LemmaEmojiSetter {
mixin LemmaEmojiSetter<T extends StatefulWidget> on State<T> {
Future<void> setLemmaEmoji(
ConstructIdentifier constructId,
String emoji,
@ -19,6 +26,50 @@ mixin LemmaEmojiSetter {
await constructId.setUserLemmaInfo(
constructId.userLemmaInfo.copyWith(emojis: [emoji]),
);
_showSnackbar(constructId, emoji);
}
void _showSnackbar(ConstructIdentifier constructId, String emoji) {
if (InstructionsEnum.setLemmaEmoji.isToggledOff) return;
InstructionsEnum.setLemmaEmoji.setToggledOff(true);
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Row(
spacing: 8.0,
children: [
VocabAnalyticsListTile(
constructId: constructId,
emoji: emoji,
textColor: Theme.of(context).colorScheme.surface,
icon: Text(
emoji,
style: const TextStyle(
fontSize: 22,
),
),
onTap: () {
ScaffoldMessenger.of(context).hideCurrentSnackBar();
context.go(
"/rooms/analytics/${constructId.type.name}/${Uri.encodeComponent(constructId.string)}",
);
},
),
Flexible(
child: Text(
L10n.of(context).emojiSelectedSnackbar(constructId.lemma),
textAlign: TextAlign.center,
style: Theme.of(context).textTheme.bodyLarge?.copyWith(
color: Theme.of(context).colorScheme.surface,
),
),
),
],
),
duration: const Duration(seconds: 30),
),
);
}
void _sendEmojiAnalytics(

View file

@ -29,6 +29,7 @@ enum InstructionsEnum {
chatParticipantTooltip,
courseParticipantTooltip,
noSavedActivitiesYet,
setLemmaEmoji,
}
extension InstructionsEnumExtension on InstructionsEnum {
@ -57,6 +58,7 @@ extension InstructionsEnumExtension on InstructionsEnum {
case InstructionsEnum.activityAnalyticsList:
case InstructionsEnum.levelAnalytics:
case InstructionsEnum.noSavedActivitiesYet:
case InstructionsEnum.setLemmaEmoji:
ErrorHandler.logError(
e: Exception("No title for this instruction"),
m: 'InstructionsEnumExtension.title',
@ -117,6 +119,8 @@ extension InstructionsEnumExtension on InstructionsEnum {
return l10n.levelInfoTooltip;
case InstructionsEnum.noSavedActivitiesYet:
return l10n.noSavedActivitiesYet;
case InstructionsEnum.setLemmaEmoji:
return "";
}
}

View file

@ -10,12 +10,14 @@ class LanguageModel {
final String langCode;
final String displayName;
final String script;
final String? localeEmoji;
final L2SupportEnum l2Support;
final TextDirection? _textDirection;
LanguageModel({
required this.langCode,
required this.displayName,
this.localeEmoji,
this.script = LanguageKeys.unknownLanguage,
this.l2Support = L2SupportEnum.na,
TextDirection? textDirection,
@ -40,6 +42,7 @@ class LanguageModel {
(e) => e.name == json['text_direction'],
)
: null,
localeEmoji: json['locale_emoji'],
);
}
@ -49,6 +52,7 @@ class LanguageModel {
'script': script,
'l2_support': l2Support.storageString,
'text_direction': textDirection.name,
'locale_emoji': localeEmoji,
};
bool get l2 => l2Support != L2SupportEnum.na;
@ -296,7 +300,13 @@ class LanguageModel {
"zuDisplayName": l10n.zuDisplayName,
};
return displayNameMap[langKey] ?? displayName;
final display = displayNameMap[langKey] ?? displayName;
if (langCode.contains('-') && localeEmoji != null) {
// use regex to replace parentheses content with the locale emoji
final regex = RegExp(r'\s*\(.*?\)\s*');
return display.replaceFirst(regex, ' $localeEmoji ');
}
return display;
}
String get langCodeShort => langCode.split('-').first;

View file

@ -98,6 +98,7 @@ class LanguageSelectionPageState extends State<LanguageSelectionPage> {
Widget build(BuildContext context) {
final theme = Theme.of(context);
final languages = MatrixState.pangeaController.pLanguageStore.targetOptions;
final isColumnMode = FluffyThemes.isColumnMode(context);
return Scaffold(
appBar: AppBar(
@ -138,8 +139,8 @@ class LanguageSelectionPageState extends State<LanguageSelectionPage> {
bottom: 60.0,
),
child: Wrap(
spacing: 8.0,
runSpacing: 8.0,
spacing: isColumnMode ? 16.0 : 8.0,
runSpacing: isColumnMode ? 16.0 : 8.0,
alignment: WrapAlignment.center,
children: languages
.where(
@ -164,7 +165,9 @@ class LanguageSelectionPageState extends State<LanguageSelectionPage> {
),
label: Text(
l.getDisplayName(context),
style: theme.textTheme.bodyMedium,
style: isColumnMode
? theme.textTheme.bodyLarge
: theme.textTheme.bodyMedium,
),
onSelected: (selected) {
_setSelectedLanguage(

View file

@ -1,130 +1,192 @@
import 'dart:convert';
import 'dart:io';
import 'package:flutter/foundation.dart';
import 'package:async/async.dart';
import 'package:get_storage/get_storage.dart';
import 'package:http/http.dart';
import 'package:fluffychat/pangea/common/config/environment.dart';
import 'package:fluffychat/pangea/common/network/requests.dart';
import 'package:fluffychat/pangea/common/network/urls.dart';
import 'package:fluffychat/pangea/languages/language_constants.dart';
import 'package:fluffychat/pangea/common/utils/error_handler.dart';
import 'package:fluffychat/pangea/morphs/morph_features_enum.dart';
import 'package:fluffychat/pangea/morphs/morph_meaning/morph_info_request.dart';
import 'package:fluffychat/pangea/morphs/morph_meaning/morph_info_response.dart';
import 'package:fluffychat/widgets/matrix.dart';
import 'package:fluffychat/widgets/future_loading_dialog.dart';
class _APICallCacheItem {
final DateTime time;
final Future<MorphInfoResponse> future;
class _MorphInfoCacheItem {
final Future<Result<MorphInfoResponse>> resultFuture;
final DateTime timestamp;
_APICallCacheItem(this.time, this.future);
const _MorphInfoCacheItem({
required this.resultFuture,
required this.timestamp,
});
}
class MorphInfoRepo {
static final GetStorage _morphMeaningStorage =
GetStorage('morph_meaning_storage');
static final shortTermCache = <String, _APICallCacheItem>{};
static const int _cacheDurationMinutes = 1;
// In-memory cache
static final Map<String, _MorphInfoCacheItem> _cache = {};
static const Duration _cacheDuration = Duration(minutes: 10);
static void set(MorphInfoRequest request, MorphInfoResponse response) {
_morphMeaningStorage.write(request.storageKey, response.toJson());
}
// Persistent storage
static final GetStorage _storage = GetStorage('morph_info_storage');
static Future<MorphInfoResponse> _fetch(MorphInfoRequest request) async {
try {
final Requests req = Requests(
choreoApiKey: Environment.choreoApiKey,
accessToken: MatrixState.pangeaController.userController.accessToken,
);
final Response res = await req.post(
url: PApiUrls.morphDictionary,
body: request.toJson(),
);
final decodedBody = jsonDecode(utf8.decode(res.bodyBytes));
final response = MorphInfoResponse.fromJson(decodedBody);
set(request, response);
return response;
} catch (e) {
debugPrint('Error fetching morph info: $e');
return Future.error(e);
}
}
static Future<MorphInfoResponse> _get(MorphInfoRequest request) async {
request.userL1 == request.userL1.split('-').first;
request.userL2 == request.userL2.split('-').first;
final cachedJson = _morphMeaningStorage.read(request.storageKey);
if (cachedJson != null) {
return MorphInfoResponse.fromJson(cachedJson);
static Future<Result<MorphInfoResponse>> get(
String accessToken,
MorphInfoRequest request,
) {
// 1. Try memory cache
final cached = _getCached(request);
if (cached != null) {
return cached;
}
final _APICallCacheItem? cachedCall = shortTermCache[request.storageKey];
if (cachedCall != null) {
if (DateTime.now().difference(cachedCall.time).inMinutes <
_cacheDurationMinutes) {
return cachedCall.future;
} else {
shortTermCache.remove(request.storageKey);
}
// 2. Try disk cache
final stored = _getStored(request);
if (stored != null) {
return Future.value(Result.value(stored));
}
final future = _fetch(request);
shortTermCache[request.storageKey] =
_APICallCacheItem(DateTime.now(), future);
// 3. Fetch from network (safe future)
final future = _safeFetch(accessToken, request);
// 4. Save to in-memory cache
_cache[request.hashCode.toString()] = _MorphInfoCacheItem(
resultFuture: future,
timestamp: DateTime.now(),
);
// 5. Write to disk *after* the fetch finishes, without rethrowing
writeToDisk(request, future);
return future;
}
static Future<String?> get({
required MorphFeaturesEnum feature,
required String tag,
}) async {
final res = await _get(
MorphInfoRequest(
userL1: MatrixState.pangeaController.userController.userL1?.langCode ??
LanguageKeys.defaultLanguage,
userL2: MatrixState.pangeaController.userController.userL2?.langCode ??
LanguageKeys.defaultLanguage,
),
);
final morph = res.getFeatureByCode(feature.name);
final data = morph?.getTagByCode(tag);
return data?.l1Description;
static Future<void> set(
MorphInfoRequest request,
MorphInfoResponse resultFuture,
) async {
final key = request.hashCode.toString();
try {
await _storage.write(key, resultFuture.toJson());
_cache.remove(key); // Invalidate in-memory cache
} catch (e, s) {
ErrorHandler.logError(
e: e,
s: s,
data: {'request': request.toJson()},
);
}
}
static Future<void> setMorphDefinition({
static Future<void> update(
MorphInfoRequest request, {
required MorphFeaturesEnum feature,
required String tag,
required String defintion,
required String definition,
}) async {
final userL1 =
MatrixState.pangeaController.userController.userL1?.langCode ??
LanguageKeys.defaultLanguage;
final userL2 =
MatrixState.pangeaController.userController.userL2?.langCode ??
LanguageKeys.defaultLanguage;
final userL1Short = userL1.split('-').first;
final userL2Short = userL2.split('-').first;
final cachedJson = _morphMeaningStorage.read(userL1Short + userL2Short);
try {
final cachedJson = await _getCached(request);
final resp = cachedJson?.result ??
MorphInfoResponse(
userL1: request.userL1,
userL2: request.userL2,
features: [],
);
MorphInfoResponse? resp = MorphInfoResponse(
userL1: userL1,
userL2: userL2,
features: [],
resp.setMorphDefinition(feature.name, tag, definition);
await set(request, resp);
} catch (e, s) {
ErrorHandler.logError(
e: e,
s: s,
data: {'request': request.toJson()},
);
}
}
static Future<Result<MorphInfoResponse>> _safeFetch(
String token,
MorphInfoRequest request,
) async {
try {
final resp = await _fetch(token, request);
return Result.value(resp);
} catch (e, s) {
// Ensure error is logged and converted to a Result
ErrorHandler.logError(e: e, s: s, data: request.toJson());
return Result.error(e);
}
}
static Future<MorphInfoResponse> _fetch(
String accessToken,
MorphInfoRequest request,
) async {
final req = Requests(
choreoApiKey: Environment.choreoApiKey,
accessToken: accessToken,
);
if (cachedJson is Map<String, dynamic>) {
resp = MorphInfoResponse.fromJson(cachedJson);
final Response res = await req.post(
url: PApiUrls.morphDictionary,
body: request.toJson(),
);
if (res.statusCode != 200) {
throw HttpException(
'Failed to fetch morph info: ${res.statusCode} ${res.reasonPhrase}',
);
}
resp.setMorphDefinition(feature.name, tag, defintion);
await _morphMeaningStorage.write(userL1Short + userL2Short, resp.toJson());
return MorphInfoResponse.fromJson(
jsonDecode(utf8.decode(res.bodyBytes)),
);
}
static Future<Result<MorphInfoResponse>>? _getCached(
MorphInfoRequest request,
) {
final now = DateTime.now();
final key = request.hashCode.toString();
// Remove stale entries first
_cache.removeWhere(
(_, item) => now.difference(item.timestamp) >= _cacheDuration,
);
final item = _cache[key];
return item?.resultFuture;
}
static Future<void> writeToDisk(
MorphInfoRequest request,
Future<Result<MorphInfoResponse>> resultFuture,
) async {
final result = await resultFuture; // SAFE: never throws
if (!result.isValue) return; // only cache successful responses
await set(request, result.asValue!.value);
}
static MorphInfoResponse? _getStored(
MorphInfoRequest request,
) {
final key = request.hashCode.toString();
try {
final entry = _storage.read(key);
if (entry == null) return null;
return MorphInfoResponse.fromJson(entry);
} catch (e, s) {
ErrorHandler.logError(
e: e,
s: s,
data: {'request': request.toJson()},
);
_storage.remove(key);
return null;
}
}
}