mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-07 04:12:04 +00:00
fix(i18n): carry the count in One strings where one is more than 1
The plural tier comes from CLDR, and in Russian the one category also
holds 21, 31, 101, while French and Brazilian Portuguese file 0 there.
Thirty One strings in those bundles spelled out the 1, so 21 days read
as one day. They now take {count}, as do two Russian One strings that
spelled the number as a word, and every direct call of such a key passes
the count. A guard finds the locales whose one is wider than 1 from
Intl.PluralRules and holds both ends.
This commit is contained in:
1 parent
2adea7052b
commit
4921188f22
14 files changed
+270
-111
No files matched your search
+30
-30
@@ -739,10 +739,10 @@
|
||||
"emptyDescription": "Saisissez votre première mesure ou humeur. Les tuiles et graphiques apparaissent automatiquement dès qu’il y a des données.",
|
||||
"emptyAddMeasurement": "Saisir la première mesure",
|
||||
"staleHint": "Dernière valeur il y a {count} j",
|
||||
"staleHintWeeksOne": "Dernière valeur il y a 1 semaine",
|
||||
"staleHintWeeksOne": "Dernière valeur il y a {count} semaine",
|
||||
"staleHintWeeksOther": "Dernière valeur il y a {count} semaines",
|
||||
"staleHintWeeksFew": "Dernière valeur il y a {count} semaines",
|
||||
"staleHintMonthsOne": "Dernière valeur il y a 1 mois",
|
||||
"staleHintMonthsOne": "Dernière valeur il y a {count} mois",
|
||||
"staleHintMonthsOther": "Dernière valeur il y a {count} mois",
|
||||
"staleHintMonthsFew": "Dernière valeur il y a {count} mois",
|
||||
"recentWorkouts": {
|
||||
@@ -2287,7 +2287,7 @@
|
||||
"currentSince": "Plan actuel depuis {date}",
|
||||
"noTimes": "Aucune heure fixe",
|
||||
"manualChip": "Ajoutée manuellement",
|
||||
"showErasOne": "Afficher 1 période antérieure",
|
||||
"showErasOne": "Afficher {count} période antérieure",
|
||||
"showErasOther": "Afficher {count} périodes antérieures",
|
||||
"hideEras": "Masquer les périodes antérieures",
|
||||
"addEra": "Ajouter une période antérieure",
|
||||
@@ -2359,7 +2359,7 @@
|
||||
"tableColActions": "Actions",
|
||||
"tableSortBy": "Trier par {column}",
|
||||
"tableComplianceDays": "{days} j",
|
||||
"tableStockDoseOne": "1 dose",
|
||||
"tableStockDoseOne": "{count} dose",
|
||||
"tableStockDoses": "{count} doses",
|
||||
"tableStockRunway": "≈ {days} jours restants",
|
||||
"cardLowStockRunway": "Réserve : ≈ {days} jours",
|
||||
@@ -3238,7 +3238,7 @@
|
||||
"settingsClustersLabel": "Données vues par le Coach",
|
||||
"settingsClustersHint": "Choisissez les groupes de données que le Coach peut lire. Plus de données, des réponses plus riches et un prompt plus grand.",
|
||||
"settingsClusterNoData": "Pas encore de données",
|
||||
"sourcesMemberCountOne": "1 mesure",
|
||||
"sourcesMemberCountOne": "{count} mesure",
|
||||
"sourcesMemberCountFew": "{count} mesures",
|
||||
"sourcesMemberCountOther": "{count} mesures",
|
||||
"lookback": {
|
||||
@@ -3338,7 +3338,7 @@
|
||||
"indexing": "Indexation…",
|
||||
"indexingHint": "Un document est encore en cours d'indexation ; vous pourrez envoyer une fois prêt.",
|
||||
"fencedHint": "Cette conversation s'exécute sans accès à vos données de santé : le coach lit uniquement les documents joints.",
|
||||
"documentsAttachedOne": "1 document joint",
|
||||
"documentsAttachedOne": "{count} document joint",
|
||||
"documentsAttachedOther": "{count} documents joints",
|
||||
"railBadge": "Chat de document cloisonné",
|
||||
"uploadError": "Échec du téléversement. Veuillez réessayer.",
|
||||
@@ -3942,9 +3942,9 @@
|
||||
"learningBody": "Pour l’instant, cela fait {count} sur {days}. Avec un peu plus d’historique, le tableau se précise : continuez à enregistrer ou laissez un appareil connecté compléter.",
|
||||
"learningBodyEmpty": "Dès que quelques mesures arrivent (saisies par vous ou synchronisées depuis un appareil), votre temps dans la cible, votre moyenne et votre variabilité prennent forme ici.",
|
||||
"readingsCount": "{count} mesures",
|
||||
"readingsCountOne": "1 mesure",
|
||||
"readingsCountOne": "{count} mesure",
|
||||
"daysCount": "{days} jours",
|
||||
"daysCountOne": "1 jour",
|
||||
"daysCountOne": "{count} jour",
|
||||
"tir": {
|
||||
"title": "Temps dans la cible",
|
||||
"caption": "Part des mesures dans chaque plage ({low}–{high} {unit} est la cible).",
|
||||
@@ -3996,7 +3996,7 @@
|
||||
"bedtime": "Au coucher",
|
||||
"unspecified": "Non précisée",
|
||||
"readingsShort": "{count} mesures",
|
||||
"readingsShortOne": "1 mesure"
|
||||
"readingsShortOne": "{count} mesure"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -4935,7 +4935,7 @@
|
||||
},
|
||||
"crossLink": {
|
||||
"title": "Enregistrements ECG",
|
||||
"recordingsOne": "1 enregistrement disponible.",
|
||||
"recordingsOne": "{count} enregistrement disponible.",
|
||||
"recordingsMany": "{count} enregistrements disponibles.",
|
||||
"latestResult": "Dernier résultat de l’appareil : {result}.",
|
||||
"cta": "Voir les enregistrements"
|
||||
@@ -4949,7 +4949,7 @@
|
||||
"caption": "Moyenné par tranches de 10 minutes.",
|
||||
"hourlyNote": "Moyenne horaire affichée pour cette journée. Les jours récents affichent le détail complet par tranches de 10 minutes.",
|
||||
"empty": "Pas encore assez de mesures pour cette journée pour en afficher le rythme.",
|
||||
"coverageOne": "Basé sur 1 mesure sur {hours}h.",
|
||||
"coverageOne": "Basé sur {count} mesure sur {hours}h.",
|
||||
"coverageMany": "Basé sur {count} mesures sur {hours}h.",
|
||||
"baseline": "Repos",
|
||||
"today": "Aujourd'hui",
|
||||
@@ -5442,7 +5442,7 @@
|
||||
},
|
||||
"trendHints": {
|
||||
"title": "Première tendance après 5 mesures",
|
||||
"remainingOne": "Encore 1 mesure avant la tendance.",
|
||||
"remainingOne": "Encore {count} mesure avant la tendance.",
|
||||
"remainingMany": "Encore {count} mesures avant la tendance."
|
||||
},
|
||||
"settings": {
|
||||
@@ -8015,7 +8015,7 @@
|
||||
"colRedeemedBy": "Utilisée par",
|
||||
"colActions": "Actions",
|
||||
"byCreator": "par {by}",
|
||||
"expiresInDaysOne": "dans 1 jour",
|
||||
"expiresInDaysOne": "dans {count} jour",
|
||||
"expiresInDaysOther": "dans {count} jours",
|
||||
"statusActive": "Active",
|
||||
"statusExpired": "Expirée",
|
||||
@@ -11035,7 +11035,7 @@
|
||||
},
|
||||
"insights": {
|
||||
"title": "Votre historique de maladies",
|
||||
"countOne": "Vous avez enregistré 1 épisode sur cette période.",
|
||||
"countOne": "Vous avez enregistré {count} épisode sur cette période.",
|
||||
"countOther": "Vous avez enregistré {count} épisodes sur cette période.",
|
||||
"typicalGap": "Écart de récupération typique : {days} jours entre le mieux-être et la stabilisation de vos valeurs.",
|
||||
"typicalGapWithDriver": "Écart de récupération typique : {days} jours. Vous vous sentez généralement mieux avant que {vital} ne se stabilise.",
|
||||
@@ -11048,7 +11048,7 @@
|
||||
"resolved": "Résolus"
|
||||
},
|
||||
"flares": {
|
||||
"countOne": "1 poussée",
|
||||
"countOne": "{count} poussée",
|
||||
"countOther": "{count} poussées"
|
||||
},
|
||||
"timeline": {
|
||||
@@ -11180,13 +11180,13 @@
|
||||
"toggleShow": "Afficher ce que le Coach a consulté",
|
||||
"toggleHide": "Masquer ce que le Coach a consulté",
|
||||
"announceDone": "Terminé : {label}",
|
||||
"readingsOne": "1 mesure",
|
||||
"readingsOne": "{count} mesure",
|
||||
"readingsFew": "{count} mesures",
|
||||
"readingsOther": "{count} mesures",
|
||||
"rowsOne": "1 ligne",
|
||||
"rowsOne": "{count} ligne",
|
||||
"rowsFew": "{count} lignes",
|
||||
"rowsOther": "{count} lignes",
|
||||
"metricsOne": "1 indicateur",
|
||||
"metricsOne": "{count} indicateur",
|
||||
"metricsFew": "{count} indicateurs",
|
||||
"metricsOther": "{count} indicateurs",
|
||||
"reason": {
|
||||
@@ -11290,7 +11290,7 @@
|
||||
"entry": "{domain}, {window} : {detail}",
|
||||
"entryNoWindow": "{domain} : {detail}",
|
||||
"reused": "d'une réponse précédente",
|
||||
"readingsOne": "1 mesure",
|
||||
"readingsOne": "{count} mesure",
|
||||
"readingsFew": "{count} mesures",
|
||||
"readingsOther": "{count} mesures",
|
||||
"aggregation": {
|
||||
@@ -12248,7 +12248,7 @@
|
||||
},
|
||||
"ai": {
|
||||
"readDone": "L’IA a lu votre document.",
|
||||
"readStagedOne": "L’IA a lu votre document. 1 valeur de laboratoire attend votre vérification.",
|
||||
"readStagedOne": "L’IA a lu votre document. {count} valeur de laboratoire attend votre vérification.",
|
||||
"readStagedFew": "L’IA a lu votre document. {count} valeurs de laboratoire attendent votre vérification.",
|
||||
"readStagedOther": "L’IA a lu votre document. {count} valeurs de laboratoire attendent votre vérification.",
|
||||
"read": "Lire avec l’IA",
|
||||
@@ -12280,12 +12280,12 @@
|
||||
"fieldValueText": "Résultat (texte)",
|
||||
"fieldUnit": "Unité",
|
||||
"fieldDose": "Dose",
|
||||
"unverifiedNoteOne": "1 valeur reste à vérifier et demeure en attente.",
|
||||
"unverifiedNoteOne": "{count} valeur reste à vérifier et demeure en attente.",
|
||||
"unverifiedNoteFew": "{count} valeurs restent à vérifier et demeurent en attente.",
|
||||
"unverifiedNoteOther": "{count} valeurs restent à vérifier et demeurent en attente.",
|
||||
"finish": "Terminer la vérification",
|
||||
"confirmFailed": "La vérification n’a pas pu être enregistrée. Réessayez.",
|
||||
"savedToastOne": "1 valeur enregistrée",
|
||||
"savedToastOne": "{count} valeur enregistrée",
|
||||
"savedToastFew": "{count} valeurs enregistrées",
|
||||
"savedToastOther": "{count} valeurs enregistrées",
|
||||
"nothingSavedToast": "Vérification enregistrée. Aucune valeur n’a été ajoutée.",
|
||||
@@ -13605,7 +13605,7 @@
|
||||
"deleteBody": "« {symptom} » et toutes ses entrées seront supprimés. Pour garder l'historique, masquez-le plutôt.",
|
||||
"deleteEventTitle": "Supprimer l'entrée ?",
|
||||
"deleteEventBody": "Cette entrée sera supprimée.",
|
||||
"hiddenOne": "Masqués (1)",
|
||||
"hiddenOne": "Masqués ({count})",
|
||||
"hiddenFew": "Masqués ({count})",
|
||||
"hiddenOther": "Masqués ({count})",
|
||||
"recent": "30 derniers jours",
|
||||
@@ -13672,14 +13672,14 @@
|
||||
"valuesAllFew": "Les {count} valeurs",
|
||||
"valuesAllOther": "Les {count} valeurs",
|
||||
"valuesFewer": "Moins de valeurs",
|
||||
"countEntriesOne": "1 entrée",
|
||||
"countEntriesOne": "{count} entrée",
|
||||
"countEntriesFew": "{count} entrées",
|
||||
"countEntriesOther": "{count} entrées",
|
||||
"meanOfReadingsOne": "Moyenne de 1 mesure",
|
||||
"meanOfReadingsOne": "Moyenne de {count} mesure",
|
||||
"meanOfReadingsFew": "Moyenne de {count} mesures",
|
||||
"meanOfReadingsOther": "Moyenne de {count} mesures",
|
||||
"sinceLastVisit": {
|
||||
"countOne": "1 entrée",
|
||||
"countOne": "{count} entrée",
|
||||
"countFew": "{count} entrées",
|
||||
"countOther": "{count} entrées",
|
||||
"allOne": "Tout afficher",
|
||||
@@ -13868,7 +13868,7 @@
|
||||
},
|
||||
"noValue": "aucune valeur",
|
||||
"bloodPressure": "Tension artérielle",
|
||||
"readingsOne": "1 mesure",
|
||||
"readingsOne": "{count} mesure",
|
||||
"readingsFew": "moyenne de {count} mesures",
|
||||
"readingsOther": "moyenne de {count} mesures"
|
||||
},
|
||||
@@ -13901,7 +13901,7 @@
|
||||
"empty": "Aucune entrée de {from} à {to}",
|
||||
"emptySingle": "Aucune entrée en {period}",
|
||||
"ended": "Fin : {label}",
|
||||
"afterDaysOne": "après 1 jour",
|
||||
"afterDaysOne": "après {count} jour",
|
||||
"afterDaysFew": "après {count} jours",
|
||||
"afterDaysOther": "après {count} jours",
|
||||
"notable": {
|
||||
@@ -13925,7 +13925,7 @@
|
||||
"apply": "Appliquer"
|
||||
},
|
||||
"item": {
|
||||
"labValuesOne": "1 valeur de laboratoire",
|
||||
"labValuesOne": "{count} valeur de laboratoire",
|
||||
"labValuesFew": "{count} valeurs de laboratoire",
|
||||
"labValuesOther": "{count} valeurs de laboratoire"
|
||||
},
|
||||
@@ -13951,7 +13951,7 @@
|
||||
"full": "Jusqu’à {max} symptômes à la fois. Retirez-en un pour en ajouter un autre.",
|
||||
"hidden": "masqué",
|
||||
"unnamed": "Symptôme",
|
||||
"daysOne": "sur 1 jour",
|
||||
"daysOne": "sur {count} jour",
|
||||
"daysFew": "moyenne sur {count} jours",
|
||||
"daysOther": "moyenne sur {count} jours"
|
||||
}
|
||||
|
||||
+30
-30
@@ -739,10 +739,10 @@
|
||||
"emptyDescription": "Registre sua primeira medição ou entrada de humor. Blocos e gráficos aparecem automaticamente conforme os dados chegam.",
|
||||
"emptyAddMeasurement": "Adicionar sua primeira medição",
|
||||
"staleHint": "Última leitura há {count} d",
|
||||
"staleHintWeeksOne": "Última leitura há 1 semana",
|
||||
"staleHintWeeksOne": "Última leitura há {count} semana",
|
||||
"staleHintWeeksOther": "Última leitura há {count} semanas",
|
||||
"staleHintWeeksFew": "Última leitura há {count} semanas",
|
||||
"staleHintMonthsOne": "Última leitura há 1 mês",
|
||||
"staleHintMonthsOne": "Última leitura há {count} mês",
|
||||
"staleHintMonthsOther": "Última leitura há {count} meses",
|
||||
"staleHintMonthsFew": "Última leitura há {count} meses",
|
||||
"recentWorkouts": {
|
||||
@@ -2287,7 +2287,7 @@
|
||||
"currentSince": "Plano atual desde {date}",
|
||||
"noTimes": "Sem horários fixos",
|
||||
"manualChip": "Adicionado manualmente",
|
||||
"showErasOne": "Mostrar 1 era anterior",
|
||||
"showErasOne": "Mostrar {count} era anterior",
|
||||
"showErasOther": "Mostrar {count} eras anteriores",
|
||||
"hideEras": "Ocultar eras anteriores",
|
||||
"addEra": "Adicionar era anterior",
|
||||
@@ -2359,7 +2359,7 @@
|
||||
"tableColActions": "Ações",
|
||||
"tableSortBy": "Ordenar por {column}",
|
||||
"tableComplianceDays": "{days} d",
|
||||
"tableStockDoseOne": "1 dose",
|
||||
"tableStockDoseOne": "{count} dose",
|
||||
"tableStockDoses": "{count} doses",
|
||||
"tableStockRunway": "≈ {days} dias restantes",
|
||||
"cardLowStockRunway": "Estoque: ≈ {days} dias",
|
||||
@@ -3238,7 +3238,7 @@
|
||||
"settingsClustersLabel": "Dados que o Coach vê",
|
||||
"settingsClustersHint": "Escolha quais grupos dos seus dados o Coach pode ler. Mais dados significam respostas mais ricas e um prompt maior.",
|
||||
"settingsClusterNoData": "Ainda sem dados",
|
||||
"sourcesMemberCountOne": "1 métrica",
|
||||
"sourcesMemberCountOne": "{count} métrica",
|
||||
"sourcesMemberCountFew": "{count} métricas",
|
||||
"sourcesMemberCountOther": "{count} métricas",
|
||||
"lookback": {
|
||||
@@ -3338,7 +3338,7 @@
|
||||
"indexing": "Indexando…",
|
||||
"indexingHint": "Um documento ainda está sendo indexado. Você pode enviar quando estiver pronto.",
|
||||
"fencedHint": "Esta conversa roda sem acesso aos seus dados de saúde. O coach lê apenas os documentos anexados.",
|
||||
"documentsAttachedOne": "1 documento anexado",
|
||||
"documentsAttachedOne": "{count} documento anexado",
|
||||
"documentsAttachedOther": "{count} documentos anexados",
|
||||
"railBadge": "Conversa de documento isolada",
|
||||
"uploadError": "Falha no envio. Tente novamente.",
|
||||
@@ -3942,9 +3942,9 @@
|
||||
"learningBody": "Até agora são {count} em {days}. Um pouco mais de histórico e um quadro mais completo entra em foco. Continue registrando, ou deixe um dispositivo conectado preencher.",
|
||||
"learningBodyEmpty": "Assim que algumas leituras chegarem (registradas por você ou sincronizadas de um dispositivo), seu tempo na faixa, média e variabilidade tomarão forma aqui.",
|
||||
"readingsCount": "{count} leituras",
|
||||
"readingsCountOne": "1 leitura",
|
||||
"readingsCountOne": "{count} leitura",
|
||||
"daysCount": "{days} dias",
|
||||
"daysCountOne": "1 dia",
|
||||
"daysCountOne": "{count} dia",
|
||||
"tir": {
|
||||
"title": "Tempo na faixa",
|
||||
"caption": "Parcela das leituras em cada faixa ({low}–{high} {unit} é a meta).",
|
||||
@@ -3996,7 +3996,7 @@
|
||||
"bedtime": "Hora de dormir",
|
||||
"unspecified": "Sem marcação",
|
||||
"readingsShort": "{count} leituras",
|
||||
"readingsShortOne": "1 leitura"
|
||||
"readingsShortOne": "{count} leitura"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -4935,7 +4935,7 @@
|
||||
},
|
||||
"crossLink": {
|
||||
"title": "Registros de ECG",
|
||||
"recordingsOne": "1 registro em arquivo.",
|
||||
"recordingsOne": "{count} registro em arquivo.",
|
||||
"recordingsMany": "{count} registros em arquivo.",
|
||||
"latestResult": "Resultado mais recente do dispositivo: {result}.",
|
||||
"cta": "Ver registros"
|
||||
@@ -4949,7 +4949,7 @@
|
||||
"caption": "Média em intervalos de 10 minutos.",
|
||||
"hourlyNote": "Mostrando a média horária deste dia; dias recentes mostram o detalhe completo de 10 minutos.",
|
||||
"empty": "Não há leituras de frequência cardíaca suficientes para este dia mostrar sua forma ainda.",
|
||||
"coverageOne": "Com base em 1 leitura ao longo de {hours}h.",
|
||||
"coverageOne": "Com base em {count} leitura ao longo de {hours}h.",
|
||||
"coverageMany": "Com base em {count} leituras ao longo de {hours}h.",
|
||||
"baseline": "Em repouso",
|
||||
"today": "Hoje",
|
||||
@@ -5442,7 +5442,7 @@
|
||||
},
|
||||
"trendHints": {
|
||||
"title": "Primeira tendência após 5 leituras",
|
||||
"remainingOne": "Falta 1 leitura.",
|
||||
"remainingOne": "Falta {count} leitura.",
|
||||
"remainingMany": "Faltam {count} leituras."
|
||||
},
|
||||
"settings": {
|
||||
@@ -8015,7 +8015,7 @@
|
||||
"colRedeemedBy": "Resgatado por",
|
||||
"colActions": "Ações",
|
||||
"byCreator": "por {by}",
|
||||
"expiresInDaysOne": "em 1 dia",
|
||||
"expiresInDaysOne": "em {count} dia",
|
||||
"expiresInDaysOther": "em {count} dias",
|
||||
"statusActive": "Ativo",
|
||||
"statusExpired": "Expirado",
|
||||
@@ -11035,7 +11035,7 @@
|
||||
},
|
||||
"insights": {
|
||||
"title": "Seu histórico de doenças",
|
||||
"countOne": "Você registrou 1 episódio neste período.",
|
||||
"countOne": "Você registrou {count} episódio neste período.",
|
||||
"countOther": "Você registrou {count} episódios neste período.",
|
||||
"typicalGap": "Lacuna típica de recuperação: {days} dias entre se sentir melhor e seus números se estabilizarem.",
|
||||
"typicalGapWithDriver": "Lacuna típica de recuperação: {days} dias; você tende a se sentir melhor antes de seu {vital} se estabilizar.",
|
||||
@@ -11048,7 +11048,7 @@
|
||||
"resolved": "Resolvidos"
|
||||
},
|
||||
"flares": {
|
||||
"countOne": "1 surto",
|
||||
"countOne": "{count} surto",
|
||||
"countOther": "{count} surtos"
|
||||
},
|
||||
"timeline": {
|
||||
@@ -11180,13 +11180,13 @@
|
||||
"toggleShow": "Mostrar o que o Coach examinou",
|
||||
"toggleHide": "Ocultar o que o Coach examinou",
|
||||
"announceDone": "Concluído: {label}",
|
||||
"readingsOne": "1 leitura",
|
||||
"readingsOne": "{count} leitura",
|
||||
"readingsFew": "{count} leituras",
|
||||
"readingsOther": "{count} leituras",
|
||||
"rowsOne": "1 linha",
|
||||
"rowsOne": "{count} linha",
|
||||
"rowsFew": "{count} linhas",
|
||||
"rowsOther": "{count} linhas",
|
||||
"metricsOne": "1 métrica",
|
||||
"metricsOne": "{count} métrica",
|
||||
"metricsFew": "{count} métricas",
|
||||
"metricsOther": "{count} métricas",
|
||||
"reason": {
|
||||
@@ -11290,7 +11290,7 @@
|
||||
"entry": "{domain}, {window}: {detail}",
|
||||
"entryNoWindow": "{domain}: {detail}",
|
||||
"reused": "de uma resposta anterior",
|
||||
"readingsOne": "1 leitura",
|
||||
"readingsOne": "{count} leitura",
|
||||
"readingsFew": "{count} leituras",
|
||||
"readingsOther": "{count} leituras",
|
||||
"aggregation": {
|
||||
@@ -12248,7 +12248,7 @@
|
||||
},
|
||||
"ai": {
|
||||
"readDone": "A IA leu seu documento.",
|
||||
"readStagedOne": "A IA leu seu documento. 1 valor laboratorial está pronto para revisão.",
|
||||
"readStagedOne": "A IA leu seu documento. {count} valor laboratorial está pronto para revisão.",
|
||||
"readStagedFew": "A IA leu seu documento. {count} valores laboratoriais estão prontos para revisão.",
|
||||
"readStagedOther": "A IA leu seu documento. {count} valores laboratoriais estão prontos para revisão.",
|
||||
"read": "Ler com IA",
|
||||
@@ -12280,12 +12280,12 @@
|
||||
"fieldValueText": "Resultado (texto)",
|
||||
"fieldUnit": "Unidade",
|
||||
"fieldDose": "Dose",
|
||||
"unverifiedNoteOne": "1 valor ainda precisa de verificação e permanece pendente.",
|
||||
"unverifiedNoteOne": "{count} valor ainda precisa de verificação e permanece pendente.",
|
||||
"unverifiedNoteFew": "{count} valores ainda precisam de verificação e permanecem pendentes.",
|
||||
"unverifiedNoteOther": "{count} valores ainda precisam de verificação e permanecem pendentes.",
|
||||
"finish": "Concluir revisão",
|
||||
"confirmFailed": "A revisão não pôde ser salva. Tente novamente.",
|
||||
"savedToastOne": "1 valor salvo",
|
||||
"savedToastOne": "{count} valor salvo",
|
||||
"savedToastFew": "{count} valores salvos",
|
||||
"savedToastOther": "{count} valores salvos",
|
||||
"nothingSavedToast": "Revisão salva. Nenhum valor foi adicionado.",
|
||||
@@ -13605,7 +13605,7 @@
|
||||
"deleteBody": "“{symptom}” e todas as suas entradas serão excluídos. Para manter o histórico, oculte-o em vez disso.",
|
||||
"deleteEventTitle": "Excluir entrada?",
|
||||
"deleteEventBody": "Esta entrada será excluída.",
|
||||
"hiddenOne": "Ocultos (1)",
|
||||
"hiddenOne": "Ocultos ({count})",
|
||||
"hiddenFew": "Ocultos ({count})",
|
||||
"hiddenOther": "Ocultos ({count})",
|
||||
"recent": "Últimos 30 dias",
|
||||
@@ -13672,14 +13672,14 @@
|
||||
"valuesAllFew": "Todos os {count} valores",
|
||||
"valuesAllOther": "Todos os {count} valores",
|
||||
"valuesFewer": "Menos valores",
|
||||
"countEntriesOne": "1 entrada",
|
||||
"countEntriesOne": "{count} entrada",
|
||||
"countEntriesFew": "{count} entradas",
|
||||
"countEntriesOther": "{count} entradas",
|
||||
"meanOfReadingsOne": "Média de 1 leitura",
|
||||
"meanOfReadingsOne": "Média de {count} leitura",
|
||||
"meanOfReadingsFew": "Média de {count} leituras",
|
||||
"meanOfReadingsOther": "Média de {count} leituras",
|
||||
"sinceLastVisit": {
|
||||
"countOne": "1 entrada",
|
||||
"countOne": "{count} entrada",
|
||||
"countFew": "{count} entradas",
|
||||
"countOther": "{count} entradas",
|
||||
"allOne": "Mostrar tudo",
|
||||
@@ -13868,7 +13868,7 @@
|
||||
},
|
||||
"noValue": "sem valor",
|
||||
"bloodPressure": "Pressão arterial",
|
||||
"readingsOne": "1 leitura",
|
||||
"readingsOne": "{count} leitura",
|
||||
"readingsFew": "média de {count} leituras",
|
||||
"readingsOther": "média de {count} leituras"
|
||||
},
|
||||
@@ -13901,7 +13901,7 @@
|
||||
"empty": "Nenhuma entrada de {from} a {to}",
|
||||
"emptySingle": "Nenhuma entrada em {period}",
|
||||
"ended": "{label} terminou",
|
||||
"afterDaysOne": "após 1 dia",
|
||||
"afterDaysOne": "após {count} dia",
|
||||
"afterDaysFew": "após {count} dias",
|
||||
"afterDaysOther": "após {count} dias",
|
||||
"notable": {
|
||||
@@ -13925,7 +13925,7 @@
|
||||
"apply": "Aplicar"
|
||||
},
|
||||
"item": {
|
||||
"labValuesOne": "1 valor laboratorial",
|
||||
"labValuesOne": "{count} valor laboratorial",
|
||||
"labValuesFew": "{count} valores laboratoriais",
|
||||
"labValuesOther": "{count} valores laboratoriais"
|
||||
},
|
||||
@@ -13951,7 +13951,7 @@
|
||||
"full": "Até {max} sintomas ao mesmo tempo. Remova um para adicionar outro.",
|
||||
"hidden": "oculto",
|
||||
"unnamed": "Sintoma",
|
||||
"daysOne": "em 1 dia",
|
||||
"daysOne": "em {count} dia",
|
||||
"daysFew": "média de {count} dias",
|
||||
"daysOther": "média de {count} dias"
|
||||
}
|
||||
|
||||
+32
-32
@@ -739,10 +739,10 @@
|
||||
"emptyDescription": "Запишите первое измерение или настроение. Плитки и графики появятся автоматически по мере поступления данных.",
|
||||
"emptyAddMeasurement": "Добавить первое измерение",
|
||||
"staleHint": "Последнее измерение {count} дн. назад",
|
||||
"staleHintWeeksOne": "Последнее измерение 1 неделю назад",
|
||||
"staleHintWeeksOne": "Последнее измерение {count} неделю назад",
|
||||
"staleHintWeeksOther": "Последнее измерение {count} недель назад",
|
||||
"staleHintWeeksFew": "Последнее измерение {count} недели назад",
|
||||
"staleHintMonthsOne": "Последнее измерение 1 месяц назад",
|
||||
"staleHintMonthsOne": "Последнее измерение {count} месяц назад",
|
||||
"staleHintMonthsOther": "Последнее измерение {count} месяцев назад",
|
||||
"staleHintMonthsFew": "Последнее измерение {count} месяца назад",
|
||||
"recentWorkouts": {
|
||||
@@ -2287,7 +2287,7 @@
|
||||
"currentSince": "Текущий план с {date}",
|
||||
"noTimes": "Без фиксированного времени",
|
||||
"manualChip": "Добавлено вручную",
|
||||
"showErasOne": "Показать 1 прошлый период",
|
||||
"showErasOne": "Показать {count} прошлый период",
|
||||
"showErasOther": "Показать прошлые периоды ({count})",
|
||||
"hideEras": "Скрыть прошлые периоды",
|
||||
"addEra": "Добавить прошлый период",
|
||||
@@ -2359,7 +2359,7 @@
|
||||
"tableColActions": "Действия",
|
||||
"tableSortBy": "Сортировать по {column}",
|
||||
"tableComplianceDays": "{days} дн.",
|
||||
"tableStockDoseOne": "1 доза",
|
||||
"tableStockDoseOne": "{count} доза",
|
||||
"tableStockDoses": "{count} доз",
|
||||
"tableStockRunway": "≈ {days} дн. осталось",
|
||||
"cardLowStockRunway": "Запас: ≈ {days} дн.",
|
||||
@@ -3238,7 +3238,7 @@
|
||||
"settingsClustersLabel": "Данные, которые видит коуч",
|
||||
"settingsClustersHint": "Выберите, какие кластеры ваших данных может читать коуч. Больше данных — богаче ответы и больше запрос.",
|
||||
"settingsClusterNoData": "Данных пока нет",
|
||||
"sourcesMemberCountOne": "1 показатель",
|
||||
"sourcesMemberCountOne": "{count} показатель",
|
||||
"sourcesMemberCountFew": "{count} показателя",
|
||||
"sourcesMemberCountOther": "{count} показателей",
|
||||
"lookback": {
|
||||
@@ -3338,7 +3338,7 @@
|
||||
"indexing": "Индексация…",
|
||||
"indexingHint": "Документ ещё индексируется. Отправить можно, когда он будет готов.",
|
||||
"fencedHint": "Этот разговор идёт без доступа к вашим данным о здоровье. Коуч читает только прикреплённые документы.",
|
||||
"documentsAttachedOne": "Прикреплён 1 документ",
|
||||
"documentsAttachedOne": "Прикреплён {count} документ",
|
||||
"documentsAttachedOther": "Прикреплено документов: {count}",
|
||||
"railBadge": "Изолированный чат по документам",
|
||||
"uploadError": "Загрузка не удалась. Попробуйте ещё раз.",
|
||||
@@ -3942,9 +3942,9 @@
|
||||
"learningBody": "Пока это {count} за {days}. Ещё немного истории, и более полная картина прояснится. Продолжайте записывать или дайте подключённому устройству заполнить её.",
|
||||
"learningBodyEmpty": "Как только поступит несколько измерений (записанных вами или синхронизированных с устройства), здесь сформируются ваше время в диапазоне, среднее и вариабельность.",
|
||||
"readingsCount": "Измерений: {count}",
|
||||
"readingsCountOne": "1 измерение",
|
||||
"readingsCountOne": "{count} измерение",
|
||||
"daysCount": "Дней: {days}",
|
||||
"daysCountOne": "1 день",
|
||||
"daysCountOne": "{count} день",
|
||||
"tir": {
|
||||
"title": "Время в диапазоне",
|
||||
"caption": "Доля измерений в каждой полосе ({low}–{high} {unit} — цель).",
|
||||
@@ -3996,7 +3996,7 @@
|
||||
"bedtime": "Перед сном",
|
||||
"unspecified": "Без метки",
|
||||
"readingsShort": "Измерений: {count}",
|
||||
"readingsShortOne": "1 измерение"
|
||||
"readingsShortOne": "{count} измерение"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -4935,7 +4935,7 @@
|
||||
},
|
||||
"crossLink": {
|
||||
"title": "Записи ЭКГ",
|
||||
"recordingsOne": "1 запись в архиве.",
|
||||
"recordingsOne": "{count} запись в архиве.",
|
||||
"recordingsMany": "Записей в архиве: {count}.",
|
||||
"latestResult": "Последний результат устройства: {result}.",
|
||||
"cta": "Посмотреть записи"
|
||||
@@ -4949,7 +4949,7 @@
|
||||
"caption": "Усреднено шагами по 10 минут.",
|
||||
"hourlyNote": "Показано среднее по часам за этот день; для недавних дней доступна полная детализация по 10 минут.",
|
||||
"empty": "Пока недостаточно измерений ЧСС за этот день, чтобы показать его форму.",
|
||||
"coverageOne": "На основе 1 измерения за {hours} ч.",
|
||||
"coverageOne": "На основе {count} измерения за {hours} ч.",
|
||||
"coverageMany": "На основе {count} измерений за {hours} ч.",
|
||||
"baseline": "В покое",
|
||||
"today": "Сегодня",
|
||||
@@ -5442,7 +5442,7 @@
|
||||
},
|
||||
"trendHints": {
|
||||
"title": "Первая динамика после 5 измерений",
|
||||
"remainingOne": "Осталось 1 измерение.",
|
||||
"remainingOne": "Осталось {count} измерение.",
|
||||
"remainingMany": "Осталось измерений: {count}."
|
||||
},
|
||||
"settings": {
|
||||
@@ -8015,7 +8015,7 @@
|
||||
"colRedeemedBy": "Активировано",
|
||||
"colActions": "Действия",
|
||||
"byCreator": "от {by}",
|
||||
"expiresInDaysOne": "через 1 день",
|
||||
"expiresInDaysOne": "через {count} день",
|
||||
"expiresInDaysOther": "через {count} дней",
|
||||
"statusActive": "Активно",
|
||||
"statusExpired": "Истекло",
|
||||
@@ -11035,7 +11035,7 @@
|
||||
},
|
||||
"insights": {
|
||||
"title": "Ваша история заболеваний",
|
||||
"countOne": "За этот период вы записали 1 эпизод.",
|
||||
"countOne": "За этот период вы записали {count} эпизод.",
|
||||
"countOther": "За этот период вы записали эпизодов: {count}.",
|
||||
"typicalGap": "Типичный разрыв восстановления: {days} дней между улучшением самочувствия и нормализацией показателей.",
|
||||
"typicalGapWithDriver": "Типичный разрыв восстановления: {days} дней; вы обычно чувствуете себя лучше раньше, чем нормализуется {vital}.",
|
||||
@@ -11048,7 +11048,7 @@
|
||||
"resolved": "Завершённые"
|
||||
},
|
||||
"flares": {
|
||||
"countOne": "1 обострение",
|
||||
"countOne": "{count} обострение",
|
||||
"countOther": "{count} обострений"
|
||||
},
|
||||
"timeline": {
|
||||
@@ -11180,13 +11180,13 @@
|
||||
"toggleShow": "Показать, что просмотрел Коуч",
|
||||
"toggleHide": "Скрыть, что просмотрел Коуч",
|
||||
"announceDone": "Готово: {label}",
|
||||
"readingsOne": "1 измерение",
|
||||
"readingsOne": "{count} измерение",
|
||||
"readingsFew": "Измерений: {count}",
|
||||
"readingsOther": "Измерений: {count}",
|
||||
"rowsOne": "1 строка",
|
||||
"rowsOne": "{count} строка",
|
||||
"rowsFew": "{count} строки",
|
||||
"rowsOther": "{count} строк",
|
||||
"metricsOne": "1 показатель",
|
||||
"metricsOne": "{count} показатель",
|
||||
"metricsFew": "{count} показателя",
|
||||
"metricsOther": "{count} показателей",
|
||||
"reason": {
|
||||
@@ -11290,7 +11290,7 @@
|
||||
"entry": "{domain}, {window}: {detail}",
|
||||
"entryNoWindow": "{domain}: {detail}",
|
||||
"reused": "из более раннего ответа",
|
||||
"readingsOne": "1 измерение",
|
||||
"readingsOne": "{count} измерение",
|
||||
"readingsFew": "Измерений: {count}",
|
||||
"readingsOther": "Измерений: {count}",
|
||||
"aggregation": {
|
||||
@@ -12224,7 +12224,7 @@
|
||||
"errorGeneric": "Не удалось прочитать документ. Попробуйте ещё раз.",
|
||||
"errorOcr": "Не удалось прочитать текст с этого изображения.",
|
||||
"errorRateLimited": "Достигнут часовой лимит чтения с ИИ. Попробуйте позже.",
|
||||
"errorRateLimitedWaitOne": "Достигнут часовой лимит чтения с ИИ. Попробуйте примерно через минуту.",
|
||||
"errorRateLimitedWaitOne": "Достигнут часовой лимит чтения с ИИ. Попробуйте примерно через {count} минуту.",
|
||||
"errorRateLimitedWaitFew": "Достигнут часовой лимит чтения с ИИ. Попробуйте примерно через {count} минуты.",
|
||||
"errorRateLimitedWaitOther": "Достигнут часовой лимит чтения с ИИ. Попробуйте примерно через {count} минут.",
|
||||
"errorBudget": "Ваш бюджет ИИ на сегодня исчерпан.",
|
||||
@@ -12248,7 +12248,7 @@
|
||||
},
|
||||
"ai": {
|
||||
"readDone": "ИИ прочитал ваш документ.",
|
||||
"readStagedOne": "ИИ прочитал ваш документ. 1 результат анализа готов к проверке.",
|
||||
"readStagedOne": "ИИ прочитал ваш документ. {count} результат анализа готов к проверке.",
|
||||
"readStagedFew": "ИИ прочитал ваш документ. Результатов анализов готово к проверке: {count}.",
|
||||
"readStagedOther": "ИИ прочитал ваш документ. Результатов анализов готово к проверке: {count}.",
|
||||
"read": "Прочитать с ИИ",
|
||||
@@ -12280,12 +12280,12 @@
|
||||
"fieldValueText": "Результат (текст)",
|
||||
"fieldUnit": "Единица",
|
||||
"fieldDose": "Доза",
|
||||
"unverifiedNoteOne": "1 значение ещё нужно проверить, оно остаётся в ожидании.",
|
||||
"unverifiedNoteOne": "{count} значение ещё нужно проверить, оно остаётся в ожидании.",
|
||||
"unverifiedNoteFew": "Значений, которые ещё нужно проверить и которые остаются в ожидании: {count}.",
|
||||
"unverifiedNoteOther": "Значений, которые ещё нужно проверить и которые остаются в ожидании: {count}.",
|
||||
"finish": "Завершить проверку",
|
||||
"confirmFailed": "Не удалось сохранить проверку. Попробуйте ещё раз.",
|
||||
"savedToastOne": "1 значение сохранено",
|
||||
"savedToastOne": "{count} значение сохранено",
|
||||
"savedToastFew": "Сохранено значений: {count}",
|
||||
"savedToastOther": "Сохранено значений: {count}",
|
||||
"nothingSavedToast": "Проверка сохранена. Значения не добавлены.",
|
||||
@@ -13592,7 +13592,7 @@
|
||||
"title": "Ваши симптомы",
|
||||
"log": "Записать",
|
||||
"loadError": "Ваши симптомы не удалось загрузить.",
|
||||
"summaryOne": "Один раз за 30 дней, {max}/10, последний {date}",
|
||||
"summaryOne": "{count} раз за 30 дней, {max}/10, последний {date}",
|
||||
"summaryFew": "{count} раза за 30 дней, до {max}/10, последний {date}",
|
||||
"summaryOther": "{count} раз за 30 дней, до {max}/10, последний {date}",
|
||||
"summaryNoneRecent": "Не за последние 30 дней, последний {date}",
|
||||
@@ -13605,7 +13605,7 @@
|
||||
"deleteBody": "«{symptom}» и каждая его запись будут удалены. Чтобы сохранить историю, скройте его вместо этого.",
|
||||
"deleteEventTitle": "Удалить запись?",
|
||||
"deleteEventBody": "Эта запись будет удалена.",
|
||||
"hiddenOne": "Скрыто (1)",
|
||||
"hiddenOne": "Скрыто ({count})",
|
||||
"hiddenFew": "Скрыто ({count})",
|
||||
"hiddenOther": "Скрыто ({count})",
|
||||
"recent": "Последние 30 дней",
|
||||
@@ -13672,14 +13672,14 @@
|
||||
"valuesAllFew": "Все {count} значения",
|
||||
"valuesAllOther": "Все {count} значений",
|
||||
"valuesFewer": "Меньше значений",
|
||||
"countEntriesOne": "1 запись",
|
||||
"countEntriesOne": "{count} запись",
|
||||
"countEntriesFew": "Записей: {count}",
|
||||
"countEntriesOther": "Записей: {count}",
|
||||
"meanOfReadingsOne": "Среднее по 1 измерению",
|
||||
"meanOfReadingsOne": "Среднее по {count} измерению",
|
||||
"meanOfReadingsFew": "Среднее по {count} измерениям",
|
||||
"meanOfReadingsOther": "Среднее по {count} измерениям",
|
||||
"sinceLastVisit": {
|
||||
"countOne": "1 запись",
|
||||
"countOne": "{count} запись",
|
||||
"countFew": "Записей: {count}",
|
||||
"countOther": "Записей: {count}",
|
||||
"allOne": "Показать все",
|
||||
@@ -13868,7 +13868,7 @@
|
||||
},
|
||||
"noValue": "нет значения",
|
||||
"bloodPressure": "АД",
|
||||
"readingsOne": "1 измерение",
|
||||
"readingsOne": "{count} измерение",
|
||||
"readingsFew": "среднее по {count} измерениям",
|
||||
"readingsOther": "среднее по {count} измерениям"
|
||||
},
|
||||
@@ -13901,7 +13901,7 @@
|
||||
"empty": "Нет записей с {from} по {to}",
|
||||
"emptySingle": "Нет записей в {period}",
|
||||
"ended": "{label} завершено",
|
||||
"afterDaysOne": "через 1 день",
|
||||
"afterDaysOne": "через {count} день",
|
||||
"afterDaysFew": "через {count} дня",
|
||||
"afterDaysOther": "через {count} дней",
|
||||
"notable": {
|
||||
@@ -13925,7 +13925,7 @@
|
||||
"apply": "Применить"
|
||||
},
|
||||
"item": {
|
||||
"labValuesOne": "1 результат анализа",
|
||||
"labValuesOne": "{count} результат анализа",
|
||||
"labValuesFew": "{count} результата анализов",
|
||||
"labValuesOther": "{count} результатов анализов"
|
||||
},
|
||||
@@ -13951,7 +13951,7 @@
|
||||
"full": "До {max} симптомов одновременно. Уберите один, чтобы добавить другой.",
|
||||
"hidden": "скрыт",
|
||||
"unnamed": "Симптом",
|
||||
"daysOne": "за 1 день",
|
||||
"daysOne": "за {count} день",
|
||||
"daysFew": "среднее за {count} дня",
|
||||
"daysOther": "среднее за {count} дней"
|
||||
}
|
||||
|
||||
@@ -290,7 +290,7 @@ export function InviteTokensSection() {
|
||||
const days = daysUntil(invite.expiresAt, now);
|
||||
if (days <= 0) return "—";
|
||||
return days === 1
|
||||
? t("admin.invites.expiresInDaysOne")
|
||||
? t("admin.invites.expiresInDaysOne", { count: days })
|
||||
: t("admin.invites.expiresInDaysOther", { count: days });
|
||||
}
|
||||
|
||||
|
||||
@@ -26,7 +26,7 @@ export function TrendHint({ count }: TrendHintProps) {
|
||||
|
||||
const description =
|
||||
result.remaining === 1
|
||||
? t("trendHints.remainingOne")
|
||||
? t("trendHints.remainingOne", { count: result.remaining })
|
||||
: t("trendHints.remainingMany", { count: result.remaining });
|
||||
|
||||
return (
|
||||
|
||||
@@ -74,7 +74,9 @@ export function IllnessInsightsCard({
|
||||
<div className="space-y-4 text-sm">
|
||||
<p className="text-foreground">
|
||||
{data!.episodeCount === 1
|
||||
? t("illness.insights.countOne")
|
||||
? t("illness.insights.countOne", {
|
||||
count: data!.episodeCount,
|
||||
})
|
||||
: t("illness.insights.countOther", {
|
||||
count: data!.episodeCount,
|
||||
})}
|
||||
|
||||
@@ -77,7 +77,7 @@ function NestedEpisodes({ episodes }: { episodes?: IllnessEpisodeDTO[] }) {
|
||||
{allFlares ? (
|
||||
<p className="text-muted-foreground text-xs font-medium">
|
||||
{count === 1
|
||||
? t("illness.flares.countOne")
|
||||
? t("illness.flares.countOne", { count })
|
||||
: t("illness.flares.countOther", { count })}
|
||||
</p>
|
||||
) : null}
|
||||
|
||||
@@ -52,7 +52,7 @@ export function EcgCrossLink({ enabled = true, className }: EcgCrossLinkProps) {
|
||||
|
||||
const countText =
|
||||
count === 1
|
||||
? t("insights.ecg.crossLink.recordingsOne")
|
||||
? t("insights.ecg.crossLink.recordingsOne", { count })
|
||||
: t("insights.ecg.crossLink.recordingsMany", { count });
|
||||
const resultText = resultKey
|
||||
? t("insights.ecg.crossLink.latestResult", { result: t(resultKey) })
|
||||
|
||||
@@ -123,6 +123,7 @@ export function GlucoseAdvancedDisclosure({
|
||||
{summary.count === 1
|
||||
? t(
|
||||
"insights.bloodGlucose.clinical.advanced.byContext.readingsShortOne",
|
||||
{ count: summary.count },
|
||||
)
|
||||
: t(
|
||||
"insights.bloodGlucose.clinical.advanced.byContext.readingsShort",
|
||||
|
||||
@@ -90,14 +90,14 @@ export function GlucoseClinicalPanel() {
|
||||
const hasAny = clinical.readingCount > 0;
|
||||
const readingsLabel =
|
||||
clinical.readingCount === 1
|
||||
? t("insights.bloodGlucose.clinical.readingsCountOne")
|
||||
? t("insights.bloodGlucose.clinical.readingsCountOne", { count: 1 })
|
||||
: t("insights.bloodGlucose.clinical.readingsCount", {
|
||||
count: clinical.readingCount,
|
||||
});
|
||||
const spanDays = Math.max(1, Math.round(clinical.actualSpanDays));
|
||||
const daysLabel =
|
||||
spanDays === 1
|
||||
? t("insights.bloodGlucose.clinical.daysCountOne")
|
||||
? t("insights.bloodGlucose.clinical.daysCountOne", { count: spanDays })
|
||||
: t("insights.bloodGlucose.clinical.daysCount", { days: spanDays });
|
||||
|
||||
return (
|
||||
|
||||
@@ -183,12 +183,12 @@ export function IntradayPulseChart({
|
||||
: t("insights.intradayPulse.caption");
|
||||
|
||||
const coverageNote = coverage
|
||||
? coverage.readingCount === 1
|
||||
? t("insights.intradayPulse.coverageOne", { hours: coverage.hours })
|
||||
: t("insights.intradayPulse.coverageMany", {
|
||||
count: coverage.readingCount,
|
||||
hours: coverage.hours,
|
||||
})
|
||||
? t(
|
||||
coverage.readingCount === 1
|
||||
? "insights.intradayPulse.coverageOne"
|
||||
: "insights.intradayPulse.coverageMany",
|
||||
{ count: coverage.readingCount, hours: coverage.hours },
|
||||
)
|
||||
: null;
|
||||
|
||||
const dayLabel = isToday
|
||||
|
||||
@@ -698,7 +698,7 @@ function MedicationTableRowItem({
|
||||
)}
|
||||
>
|
||||
{stock === 1
|
||||
? t("medications.tableStockDoseOne")
|
||||
? t("medications.tableStockDoseOne", { count: stock })
|
||||
: t("medications.tableStockDoses", { count: stock })}
|
||||
</span>
|
||||
{runwayDays !== null && (
|
||||
|
||||
@@ -82,6 +82,12 @@ interface ScheduleRevisionsResponse {
|
||||
revisions: ScheduleRevisionRow[];
|
||||
}
|
||||
|
||||
/** The "show earlier eras" toggle, by how many there are. */
|
||||
const SHOW_ERAS_KEY = {
|
||||
one: "medications.detail.zeitplan.history.showErasOne",
|
||||
other: "medications.detail.zeitplan.history.showErasOther",
|
||||
} as const;
|
||||
|
||||
/**
|
||||
* Dose-time readout of one era: the union of its entries' times,
|
||||
* deduped + sorted ("07:00 / 19:00"). PRN-only eras have no times —
|
||||
@@ -284,11 +290,9 @@ export function ScheduleHistoryTimeline({
|
||||
/>
|
||||
{expanded
|
||||
? t("medications.detail.zeitplan.history.hideEras")
|
||||
: revisions.length === 1
|
||||
? t("medications.detail.zeitplan.history.showErasOne")
|
||||
: t("medications.detail.zeitplan.history.showErasOther", {
|
||||
count: revisions.length,
|
||||
})}
|
||||
: t(SHOW_ERAS_KEY[revisions.length === 1 ? "one" : "other"], {
|
||||
count: revisions.length,
|
||||
})}
|
||||
</Button>
|
||||
)}
|
||||
<Button
|
||||
|
||||
@@ -0,0 +1,152 @@
|
||||
/**
|
||||
* A `*One` string carries the count wherever "one" is more than 1.
|
||||
*
|
||||
* `pluralTier` takes the tier from CLDR, and in several shipped languages the
|
||||
* "one" category is not the number 1 alone: Russian files 21, 31, 101 under
|
||||
* it, French and Brazilian Portuguese file 0 there. A `*One` value that
|
||||
* spells the number out ("за 1 день") then reads "1" for 21 days. In those
|
||||
* locales the value takes the `{count}` that `tCount` passes, and a call that
|
||||
* reaches such a key directly passes the count too.
|
||||
*
|
||||
* Both checks are functions of their input, and each is shown failing once on
|
||||
* an input built to break it.
|
||||
*/
|
||||
import { describe, expect, it } from "vitest";
|
||||
|
||||
import { readSourceFolder } from "@/__tests__/helpers/source-files";
|
||||
import { resolveIntlLocale } from "@/lib/format-locale";
|
||||
|
||||
import { locales, type Locale } from "../config";
|
||||
import { pluralKey } from "../plural";
|
||||
import { getServerTranslator } from "../server-translator";
|
||||
import { allMessages } from "../shared-resolve";
|
||||
|
||||
/** Locales whose CLDR "one" category holds an integer other than 1. */
|
||||
function localesWithWideOne(): Locale[] {
|
||||
return locales.filter((locale) => {
|
||||
const rules = new Intl.PluralRules(resolveIntlLocale(locale));
|
||||
for (let n = 0; n <= 1000; n++) {
|
||||
if (n !== 1 && rules.select(n) === "one") return true;
|
||||
}
|
||||
return false;
|
||||
});
|
||||
}
|
||||
|
||||
function flatten(
|
||||
node: unknown,
|
||||
prefix = "",
|
||||
out = new Map<string, string>(),
|
||||
): Map<string, string> {
|
||||
if (typeof node === "string") {
|
||||
out.set(prefix, node);
|
||||
} else if (node && typeof node === "object") {
|
||||
for (const [key, value] of Object.entries(node)) {
|
||||
flatten(value, prefix ? `${prefix}.${key}` : key, out);
|
||||
}
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
function bundle(locale: Locale): Map<string, string> {
|
||||
return flatten(allMessages[locale]);
|
||||
}
|
||||
|
||||
/** A digit 1 standing on its own, not part of a larger number. */
|
||||
const BARE_ONE = /(?<!\d)1(?!\d)/;
|
||||
|
||||
/** `*One` values that spell out the 1 instead of carrying `{count}`. */
|
||||
function spelledOutOnes(messages: Map<string, string>): string[] {
|
||||
const offenders: string[] = [];
|
||||
for (const [key, value] of messages) {
|
||||
if (!key.endsWith("One")) continue;
|
||||
if (BARE_ONE.test(value) && !value.includes("{count}")) {
|
||||
offenders.push(key);
|
||||
}
|
||||
}
|
||||
return offenders.sort();
|
||||
}
|
||||
|
||||
/**
|
||||
* `t("<key>One")` without a `count` param, for a key whose value needs
|
||||
* `{count}`: no params at all, or an inline params object that lacks it.
|
||||
*/
|
||||
function directCallsWithoutCount(
|
||||
sources: ReadonlyArray<{ file: string; text: string }>,
|
||||
needsCount: ReadonlySet<string>,
|
||||
): string[] {
|
||||
const call = /\bt\(\s*"([\w.]+One)"\s*(?:,\s*\{([^}]*)\})?\s*,?\s*\)/g;
|
||||
const offenders: string[] = [];
|
||||
for (const { file, text } of sources) {
|
||||
for (const match of text.matchAll(call)) {
|
||||
const params = match[2];
|
||||
if (params !== undefined && /\bcount\b/.test(params)) continue;
|
||||
if (needsCount.has(match[1]!)) offenders.push(`${file}: ${match[1]}`);
|
||||
}
|
||||
}
|
||||
return offenders.sort();
|
||||
}
|
||||
|
||||
describe("plural One strings", () => {
|
||||
const wide = localesWithWideOne();
|
||||
|
||||
it("finds the locales whose one is not just 1", () => {
|
||||
expect(wide).toEqual(expect.arrayContaining(["ru", "fr", "pt-BR"]));
|
||||
expect(wide).not.toContain("en");
|
||||
expect(wide).not.toContain("pl");
|
||||
});
|
||||
|
||||
it("says 21 days in Russian, not 1", () => {
|
||||
const { t } = getServerTranslator("ru");
|
||||
expect(
|
||||
t(pluralKey("timeline.symptoms.days", 21, "ru"), { count: 21 }),
|
||||
).toBe("за 21 день");
|
||||
});
|
||||
|
||||
it("gives every One string in those locales the count", () => {
|
||||
const offenders = wide.flatMap((locale) =>
|
||||
spelledOutOnes(bundle(locale)).map((key) => `${locale}: ${key}`),
|
||||
);
|
||||
expect(offenders).toEqual([]);
|
||||
});
|
||||
|
||||
it("passes the count wherever such a key is called directly", () => {
|
||||
const needsCount = new Set<string>();
|
||||
for (const locale of wide) {
|
||||
for (const [key, value] of bundle(locale)) {
|
||||
if (key.endsWith("One") && value.includes("{count}")) {
|
||||
needsCount.add(key);
|
||||
}
|
||||
}
|
||||
}
|
||||
expect(needsCount.size).toBeGreaterThan(0);
|
||||
const sources = readSourceFolder("src", {
|
||||
floor: 1000,
|
||||
include: (rel) =>
|
||||
!rel.includes("__tests__") && !rel.includes("generated/"),
|
||||
}).map(([file, text]) => ({ file, text }));
|
||||
expect(directCallsWithoutCount(sources, needsCount)).toEqual([]);
|
||||
});
|
||||
|
||||
it("would catch a spelled-out one and a call without the count", () => {
|
||||
expect(
|
||||
spelledOutOnes(
|
||||
new Map([
|
||||
["timeline.symptoms.daysOne", "за 1 день"],
|
||||
["timeline.symptoms.daysOther", "за {count} дней"],
|
||||
["a.bOne", "за {count} день"],
|
||||
["a.cOne", "Скрыто (11)"],
|
||||
]),
|
||||
),
|
||||
).toEqual(["timeline.symptoms.daysOne"]);
|
||||
expect(
|
||||
directCallsWithoutCount(
|
||||
[
|
||||
{ file: "x.tsx", text: 'n === 1 ? t("a.bOne") : t("a.bOther")' },
|
||||
{ file: "y.tsx", text: 't("a.bOne", { hours: h })' },
|
||||
{ file: "z.tsx", text: 't("a.bOne", { count: n, hours: h })' },
|
||||
],
|
||||
new Set(["a.bOne"]),
|
||||
),
|
||||
).toEqual(["x.tsx: a.bOne", "y.tsx: a.bOne"]);
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user