Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix(i18n): properly format plurals with decimal numbers #32

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
66 changes: 65 additions & 1 deletion src/i18n/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import calendar from 'dayjs/plugin/calendar.js';
import LocalizedFormat from 'dayjs/plugin/localizedFormat.js';
import relativeTime from 'dayjs/plugin/relativeTime.js';
import updateLocale from 'dayjs/plugin/updateLocale.js';
import { createI18n, IntlDateTimeFormats, IntlNumberFormats } from 'vue-i18n';
import { createI18n, IntlDateTimeFormats, IntlNumberFormats, PluralizationRule } from 'vue-i18n';
import 'dayjs/locale/fr.js';
import 'dayjs/locale/en-gb.js';
dayjs.extend(updateLocale);
Expand Down Expand Up @@ -78,6 +78,65 @@ export const numberFormats: IntlNumberFormats = {
},
};

// `choiceOptions` relates to the number of options for the specified translation
// `i18n.tc()` accepts 2 or 3 options, so the code is covering both cases
// In case of translation having only 2 options, empty and single options will be the same
// `choiceOptions` value is the length of options for that translation
const getOptionsByTranslationChoices = (choiceOptions: number) =>
choiceOptions === 3
? {
returnAsEmpty: 0,
returnAsSingle: 1,
returnAsPlural: 2,
}
: {
returnAsEmpty: 0,
returnAsSingle: 0,
returnAsPlural: 1,
};

/**
* A helper function for applying decimals as plural for english speakers
* @param {number} [choice] - Number passed to `i18n.tc()` function as value to be resolved
* @param {number} [choiceOptions] - `3` or `2`: Number of options available on the translation file to that specific key
* @returns {number} 0: `empty`, 1: `single`, 2: `plural`
*/
const applyingDecimalsAsPluralForEnglishSpeakers: PluralizationRule = (count, choiceOptions) => {
// These codes are related to `vue-i18n` internals to pluralize resolution
// - 0: it should return empty value as result
// - 1: it should return single value as result
// - 2: it should return plural value as result
const { returnAsEmpty, returnAsSingle, returnAsPlural } =
getOptionsByTranslationChoices(choiceOptions);

// Returns empty if receives `0` as value
if (count === 0) {
return returnAsEmpty;
}

// NOTE: In english, decimals are also plural
// This function always receives value as `number`, so it's safe to assume
// decimal values as result of `!Number.isInteger()` and return as plural
if (!Number.isInteger(count)) {
return returnAsPlural;
}

// Otherwise, check if number is more than 1 to return as plural or single
return count > 1 ? returnAsPlural : returnAsSingle;
};

const applyingDecimalsAsPluralForFrenchSpeakers: PluralizationRule = (count, choiceOptions) => {
const { returnAsEmpty, returnAsSingle, returnAsPlural } =
getOptionsByTranslationChoices(choiceOptions);

if (count === 0) {
return returnAsEmpty;
}

// NOTE: In french, decimals are plurals only if they are 2 or more
return count >= 2 ? returnAsPlural : returnAsSingle;
};

export const DEFAULT_LOCALE = import.meta.env.VUE_APP_DEFAULT_LOCALE || 'fr-FR';
export const SUPPORTED_LOCALES = ['fr-FR', 'en-GB'] as const;
const initialLocale = localStorage.getItem(LOCALE_STORAGE_KEY) || DEFAULT_LOCALE;
Expand All @@ -89,6 +148,11 @@ export const i18nInstance = createI18n({
// messages: { [initialLocale]: messages },
datetimeFormats,
numberFormats,
pluralRules: {
// https://github.com/kazupon/vue-i18n/issues/620#issuecomment-1660638535
['en-GB']: applyingDecimalsAsPluralForEnglishSpeakers,
['fr-FR']: applyingDecimalsAsPluralForFrenchSpeakers,
},
});

// @see https://vuelidate-next.netlify.app/advanced_usage.html#i18n-support
Expand Down
6 changes: 3 additions & 3 deletions src/i18n/locales/en-GB/members.json
Original file line number Diff line number Diff line change
Expand Up @@ -39,19 +39,19 @@
"subscriptions": {
"active": "Active | Inactive",
"add": "Add a subscription",
"days": "{count} day | {count} single day | {count} days",
"days": "{count} day | {count} day | {count} days",
"more": "More",
"period": "from {startDate} to {endDate}",
"purchased": "Ordered on {date}",
"title": "Subscriptions"
},
"tickets": {
"add": "Add tickets",
"amount": "{count} ticket | {count} single ticket | {count} tickets",
"amount": "{count} ticket | {count} ticket | {count} tickets",
"more": "More",
"overconsumed": "1 overconsumed | {count} overconsumed",
"purchased": "Ordered on {date}",
"remaining": "None available | More than one | {count} remaining",
"remaining": "None available | {count} remaining | {count} remaining",
"title": "Individual Tickets"
},
"title": "Order History"
Expand Down
10 changes: 5 additions & 5 deletions src/i18n/locales/fr-FR/members.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
"last30days": {
"empty": "Absent",
"label": "Présent",
"suffix": "jour | seul jour | jours",
"suffix": "jour | jour | jours",
"value": "{amount} sur les 30 derniers jours"
},
"title": "Présence"
Expand All @@ -39,19 +39,19 @@
"subscriptions": {
"active": "Actif | Inactif",
"add": "Ajouter un abonnement",
"days": "{count} jour | {count} seul jour | {count} jours",
"days": "{count} jour | {count} jour | {count} jours",
"more": "Plus",
"period": "du {startDate} au {endDate}",
"purchased": "Commandé le {date}",
"title": "Abonnements"
},
"tickets": {
"add": "Ajouter des tickets",
"amount": "{count} ticket | {count} seul ticket | {count} tickets",
"amount": "{count} ticket | {count} ticket | {count} tickets",
"more": "Plus",
"overconsumed": "1 surconsommé | {count} surconsommés",
"overconsumed": "{count} surconsommé | {count} surconsommés",
"purchased": "Commandé le {date} | Commandé le {date} | Commandés le {date}",
"remaining": "Aucun disponible | Plus qu'un | {count} restants",
"remaining": "Aucun disponible | {count} restant | {count} restants",
"title": "Tickets à l'unité"
},
"title": "Historique des commandes"
Expand Down