From 4921188f22b21b0ba2f63027536fbbbf77848972 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc-Andr=C3=A9=20Bombeck?= Date: Sat, 7 Nov 2026 01:30:23 +0100 Subject: [PATCH] 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. --- messages/fr.json | 60 +++---- messages/pt-BR.json | 60 +++---- messages/ru.json | 64 ++++---- .../admin/invite-tokens-section.tsx | 2 +- src/components/charts/trend-hint.tsx | 2 +- .../illness/illness-insights-card.tsx | 4 +- src/components/illness/illness-view.tsx | 2 +- src/components/insights/ecg-cross-link.tsx | 2 +- .../glucose/glucose-advanced-disclosure.tsx | 1 + .../glucose/glucose-clinical-panel.tsx | 4 +- .../insights/intraday-pulse-chart.tsx | 12 +- .../medications/medication-table.tsx | 2 +- .../scheduling/schedule-history-timeline.tsx | 14 +- .../plural-one-carries-count.test.ts | 152 ++++++++++++++++++ 14 files changed, 270 insertions(+), 111 deletions(-) create mode 100644 src/lib/i18n/__tests__/plural-one-carries-count.test.ts diff --git a/messages/fr.json b/messages/fr.json index 0e6fa9cc6..24dba3492 100644 --- a/messages/fr.json +++ b/messages/fr.json @@ -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" } diff --git a/messages/pt-BR.json b/messages/pt-BR.json index aebff1a85..97989a554 100644 --- a/messages/pt-BR.json +++ b/messages/pt-BR.json @@ -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" } diff --git a/messages/ru.json b/messages/ru.json index 250c825d4..b83a16b5a 100644 --- a/messages/ru.json +++ b/messages/ru.json @@ -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} дней" } diff --git a/src/components/admin/invite-tokens-section.tsx b/src/components/admin/invite-tokens-section.tsx index 107fdca41..6092dea69 100644 --- a/src/components/admin/invite-tokens-section.tsx +++ b/src/components/admin/invite-tokens-section.tsx @@ -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 }); } diff --git a/src/components/charts/trend-hint.tsx b/src/components/charts/trend-hint.tsx index 8b767ebe7..c9f9457ad 100644 --- a/src/components/charts/trend-hint.tsx +++ b/src/components/charts/trend-hint.tsx @@ -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 ( diff --git a/src/components/illness/illness-insights-card.tsx b/src/components/illness/illness-insights-card.tsx index c74e53a1e..6434c5d7b 100644 --- a/src/components/illness/illness-insights-card.tsx +++ b/src/components/illness/illness-insights-card.tsx @@ -74,7 +74,9 @@ export function IllnessInsightsCard({

{data!.episodeCount === 1 - ? t("illness.insights.countOne") + ? t("illness.insights.countOne", { + count: data!.episodeCount, + }) : t("illness.insights.countOther", { count: data!.episodeCount, })} diff --git a/src/components/illness/illness-view.tsx b/src/components/illness/illness-view.tsx index b4ab4a057..1d57b7e19 100644 --- a/src/components/illness/illness-view.tsx +++ b/src/components/illness/illness-view.tsx @@ -77,7 +77,7 @@ function NestedEpisodes({ episodes }: { episodes?: IllnessEpisodeDTO[] }) { {allFlares ? (

{count === 1 - ? t("illness.flares.countOne") + ? t("illness.flares.countOne", { count }) : t("illness.flares.countOther", { count })}

) : null} diff --git a/src/components/insights/ecg-cross-link.tsx b/src/components/insights/ecg-cross-link.tsx index b8794420a..e588a01b2 100644 --- a/src/components/insights/ecg-cross-link.tsx +++ b/src/components/insights/ecg-cross-link.tsx @@ -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) }) diff --git a/src/components/insights/glucose/glucose-advanced-disclosure.tsx b/src/components/insights/glucose/glucose-advanced-disclosure.tsx index e28b2b68d..08ebf3f6d 100644 --- a/src/components/insights/glucose/glucose-advanced-disclosure.tsx +++ b/src/components/insights/glucose/glucose-advanced-disclosure.tsx @@ -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", diff --git a/src/components/insights/glucose/glucose-clinical-panel.tsx b/src/components/insights/glucose/glucose-clinical-panel.tsx index 56b0cd653..c88812c4e 100644 --- a/src/components/insights/glucose/glucose-clinical-panel.tsx +++ b/src/components/insights/glucose/glucose-clinical-panel.tsx @@ -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 ( diff --git a/src/components/insights/intraday-pulse-chart.tsx b/src/components/insights/intraday-pulse-chart.tsx index dd2a4307a..c45cb599c 100644 --- a/src/components/insights/intraday-pulse-chart.tsx +++ b/src/components/insights/intraday-pulse-chart.tsx @@ -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 diff --git a/src/components/medications/medication-table.tsx b/src/components/medications/medication-table.tsx index 40868142c..924222e1f 100644 --- a/src/components/medications/medication-table.tsx +++ b/src/components/medications/medication-table.tsx @@ -698,7 +698,7 @@ function MedicationTableRowItem({ )} > {stock === 1 - ? t("medications.tableStockDoseOne") + ? t("medications.tableStockDoseOne", { count: stock }) : t("medications.tableStockDoses", { count: stock })} {runwayDays !== null && ( diff --git a/src/components/medications/scheduling/schedule-history-timeline.tsx b/src/components/medications/scheduling/schedule-history-timeline.tsx index 628da2c17..844ca33b1 100644 --- a/src/components/medications/scheduling/schedule-history-timeline.tsx +++ b/src/components/medications/scheduling/schedule-history-timeline.tsx @@ -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, + })} )}