diff --git a/CHANGELOG.md b/CHANGELOG.md index 50f05eef0..f2854896a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,8 @@ - [TODO DEPLOY] `rails fablab:maintenance:rebuild_stylesheet` - [TODO DEPLOY] `rails fablab:stripe:set_product_id` - [TODO DEPLOY] `rails fablab:setup:add_schedule_reference` +- [TODO DEPLOY] add the `INTL_LOCALE` environment variable (see [doc/environment.md](doc/environment.md#INTL_LOCALE) for configuration details) +- [TODO DEPLOY] add the `INTL_CURRENCY` environment variable (see [doc/environment.md](doc/environment.md#INTL_CURRENCY) for configuration details) ## v4.6.3 2020 October 28 diff --git a/app/frontend/src/javascript/components/angular/stripe-elements.tsx b/app/frontend/src/javascript/components/angular/stripe-elements.tsx deleted file mode 100644 index 9e79201b5..000000000 --- a/app/frontend/src/javascript/components/angular/stripe-elements.tsx +++ /dev/null @@ -1,24 +0,0 @@ -/** - * This is a compatibility wrapper to allow usage of stripe.js Elements inside of the angular.js app - */ - -import React from 'react'; -import { Elements } from '@stripe/react-stripe-js'; -import { react2angular } from 'react2angular'; -import { IApplication } from '../../models/application'; -import SettingAPI from '../../api/setting'; -import { loadStripe } from "@stripe/stripe-js"; - -declare var Application: IApplication; -const stripePublicKey = SettingAPI.get('stripe_public_key'); - -const ElementsWrapper: React.FC = () => { - const publicKey = stripePublicKey.read(); - const stripePromise = loadStripe(publicKey.value); - - return ( - - ); -} - -Application.Components.component('stripeElements', react2angular(ElementsWrapper)); diff --git a/app/frontend/src/javascript/components/fab-modal.tsx b/app/frontend/src/javascript/components/fab-modal.tsx index 8aebe7dc1..0f6154017 100644 --- a/app/frontend/src/javascript/components/fab-modal.tsx +++ b/app/frontend/src/javascript/components/fab-modal.tsx @@ -1,8 +1,8 @@ /** - * This component is a modal dialog that can wraps the application style + * This component is a template for a modal dialog that wraps the application style */ -import React from 'react'; +import React, { ReactNode } from 'react'; import Modal from 'react-modal'; import { useTranslation } from 'react-i18next'; import { Loader } from './loader'; @@ -13,15 +13,23 @@ Modal.setAppElement('body'); interface FabModalProps { title: string, isOpen: boolean, - toggleModal: () => void + toggleModal: () => void, + confirmButton?: ReactNode } const blackLogoFile = CustomAssetAPI.get('logo-black-file'); -export const FabModal: React.FC = ({ title, isOpen, toggleModal, children }) => { +export const FabModal: React.FC = ({ title, isOpen, toggleModal, children, confirmButton }) => { const { t } = useTranslation('shared'); const blackLogo = blackLogoFile.read(); + /** + * Check if the confirm button should be present + */ + const hasConfirmButton = (): boolean => { + return confirmButton !== undefined; + } + return ( = ({ title, isOpen, toggleModal,
- + + {hasConfirmButton() && {confirmButton}}
diff --git a/app/frontend/src/javascript/components/payment-schedule-summary.tsx b/app/frontend/src/javascript/components/payment-schedule-summary.tsx index 32f39fdae..5a0caf7c3 100644 --- a/app/frontend/src/javascript/components/payment-schedule-summary.tsx +++ b/app/frontend/src/javascript/components/payment-schedule-summary.tsx @@ -8,19 +8,19 @@ import { react2angular } from 'react2angular'; import moment from 'moment'; import { IApplication } from '../models/application'; import '../lib/i18n'; -import { IFilterService } from 'angular'; import { PaymentSchedule } from '../models/payment-schedule'; import { Loader } from './loader'; import { FabModal } from './fab-modal'; +import { IFablab } from '../models/fablab'; declare var Application: IApplication; +declare var Fablab: IFablab; interface PaymentScheduleSummaryProps { - schedule: PaymentSchedule, - $filter: IFilterService + schedule: PaymentSchedule } -const PaymentScheduleSummary: React.FC = ({ schedule, $filter }) => { +const PaymentScheduleSummary: React.FC = ({ schedule }) => { const { t } = useTranslation('shared'); const [modal, setModal] = useState(false); @@ -34,7 +34,7 @@ const PaymentScheduleSummary: React.FC = ({ schedul * Return the formatted localized amount for the given price (eg. 20.5 => "20,50 €") */ const formatPrice = (price: number): string => { - return $filter('currency')(price); + return new Intl.NumberFormat(Fablab.intl_locale, {style: 'currency', currency: Fablab.intl_currency}).format(price); } /** * Test if all payment deadlines have the same amount @@ -90,12 +90,12 @@ const PaymentScheduleSummary: React.FC = ({ schedul ); } -const PaymentScheduleSummaryWrapper: React.FC = ({ schedule, $filter }) => { +const PaymentScheduleSummaryWrapper: React.FC = ({ schedule }) => { return ( - + ); } -Application.Components.component('paymentScheduleSummary', react2angular(PaymentScheduleSummaryWrapper, ['schedule'], ['$filter'])); +Application.Components.component('paymentScheduleSummary', react2angular(PaymentScheduleSummaryWrapper, ['schedule'])); diff --git a/app/frontend/src/javascript/components/plan-card.tsx b/app/frontend/src/javascript/components/plan-card.tsx index 9fb38cdb6..3689d1463 100644 --- a/app/frontend/src/javascript/components/plan-card.tsx +++ b/app/frontend/src/javascript/components/plan-card.tsx @@ -5,7 +5,6 @@ import React from 'react'; import { useTranslation } from 'react-i18next'; import { react2angular } from 'react2angular'; -import { IFilterService } from 'angular'; import moment from 'moment'; import _ from 'lodash' import { IApplication } from '../models/application'; @@ -13,8 +12,10 @@ import { Plan } from '../models/plan'; import { User, UserRole } from '../models/user'; import { Loader } from './loader'; import '../lib/i18n'; +import { IFablab } from '../models/fablab'; declare var Application: IApplication; +declare var Fablab: IFablab; interface PlanCardProps { plan: Plan, @@ -22,23 +23,22 @@ interface PlanCardProps { operator: User, isSelected: boolean, onSelectPlan: (plan: Plan) => void, - $filter: IFilterService } -const PlanCard: React.FC = ({ plan, user, operator, onSelectPlan, isSelected, $filter }) => { +const PlanCard: React.FC = ({ plan, user, operator, onSelectPlan, isSelected }) => { const { t } = useTranslation('public'); /** * Return the formatted localized amount of the given plan (eg. 20.5 => "20,50 €") */ const amount = () : string => { - return $filter('currency')(plan.amount); + return new Intl.NumberFormat(Fablab.intl_locale, {style: 'currency', currency: Fablab.intl_currency}).format(plan.amount); } /** * Return the formatted localized amount, divided by the number of months (eg. 120 => "10,00 € / month") */ const monthlyAmount = (): string => { const monthly = plan.amount / moment.duration(plan.interval_count, plan.interval).asMonths(); - return $filter('currency')(monthly); + return new Intl.NumberFormat(Fablab.intl_locale, {style: 'currency', currency: Fablab.intl_currency}).format(monthly); } /** * Return the formatted localized duration of te given plan (eg. Month/3 => "3 mois") @@ -122,12 +122,12 @@ const PlanCard: React.FC = ({ plan, user, operator, onSelectPlan, ); } -const PlanCardWrapper: React.FC = ({ plan, user, operator, onSelectPlan, isSelected, $filter }) => { +const PlanCardWrapper: React.FC = ({ plan, user, operator, onSelectPlan, isSelected }) => { return ( - + ); } -Application.Components.component('planCard', react2angular(PlanCardWrapper, ['plan', 'user', 'operator', 'onSelectPlan', 'isSelected'], ['$filter'])); +Application.Components.component('planCard', react2angular(PlanCardWrapper, ['plan', 'user', 'operator', 'onSelectPlan', 'isSelected'])); diff --git a/app/frontend/src/javascript/components/stripe-card.tsx b/app/frontend/src/javascript/components/stripe-card.tsx deleted file mode 100644 index 644036193..000000000 --- a/app/frontend/src/javascript/components/stripe-card.tsx +++ /dev/null @@ -1,71 +0,0 @@ -/** - * This component enables the user to type his card data. - */ - -import React from 'react'; -import { CardElement, useElements, useStripe } from '@stripe/react-stripe-js'; -import { react2angular } from 'react2angular'; -import { Loader } from './loader'; -import { IApplication } from '../models/application'; - - -declare var Application: IApplication; - -const StripeCard: React.FC = () => { - - const stripe = useStripe(); - const elements = useElements(); - - const handleSubmit = async (event) => { - event.preventDefault(); - - // Stripe.js has not loaded yet - if (!stripe || !elements) { return; } - - const cardElement = elements.getElement(CardElement); - - const { error, paymentMethod } = await stripe.createPaymentMethod({ - type: 'card', - card: cardElement, - }); - - if (error) { - console.log('[error]', error); - } else { - console.log('[PaymentMethod]', paymentMethod); - } - - } - - return ( -
-
- - -
- ); -} - -const StripeCardWrapper: React.FC = () => { - return ( - - - - ); -} - -Application.Components.component('stripeCard', react2angular(StripeCardWrapper)); diff --git a/app/frontend/src/javascript/components/stripe-elements.tsx b/app/frontend/src/javascript/components/stripe-elements.tsx new file mode 100644 index 000000000..c2e76c3d7 --- /dev/null +++ b/app/frontend/src/javascript/components/stripe-elements.tsx @@ -0,0 +1,22 @@ +/** + * This component initializes the stripe's Elements tag with the API key + */ + +import React from 'react'; +import { Elements } from '@stripe/react-stripe-js'; +import { IApplication } from '../models/application'; +import SettingAPI from '../api/setting'; +import { loadStripe } from "@stripe/stripe-js"; + +const stripePublicKey = SettingAPI.get('stripe_public_key'); + +export const StripeElements: React.FC = ({ children }) => { + const publicKey = stripePublicKey.read(); + const stripePromise = loadStripe(publicKey.value); + + return ( + + {children} + + ); +} diff --git a/app/frontend/src/javascript/components/stripe-modal.tsx b/app/frontend/src/javascript/components/stripe-modal.tsx new file mode 100644 index 000000000..fef4c9f06 --- /dev/null +++ b/app/frontend/src/javascript/components/stripe-modal.tsx @@ -0,0 +1,134 @@ +/** + * This component enables the user to type his card data. + */ + +import React, { FormEvent, ReactNode, useState } from 'react'; +import { CardElement, useElements, useStripe } from '@stripe/react-stripe-js'; +import { react2angular } from 'react2angular'; +import { Loader } from './loader'; +import { IApplication } from '../models/application'; +import { StripeElements } from './stripe-elements'; +import { useTranslation } from 'react-i18next'; +import { FabModal } from './fab-modal'; +import { PaymentMethod } from '@stripe/stripe-js'; +import { WalletInfo } from './wallet-info'; +import { Reservation } from '../models/reservation'; +import { User } from '../models/user'; +import { Wallet } from '../models/wallet'; + +declare var Application: IApplication; + +interface StripeModalProps { + isOpen: boolean, + toggleModal: () => void, + afterSuccess: (paymentMethod: PaymentMethod) => void, + reservation: Reservation, + currentUser: User, + wallet: Wallet, + price: number, + remainingPrice: number, +} + +const StripeModal: React.FC = ({ isOpen, toggleModal, afterSuccess, reservation, currentUser, wallet, price, remainingPrice }) => { + + const stripe = useStripe(); + const elements = useElements(); + const { t } = useTranslation('shared'); + + const [errors, setErrors] = useState(null); + const [submitState, setSubmitState] = useState(false); + + /** + * Handle the submission of the form. Depending on the configuration, it will create the payment method on stripe, + * or it will process a payment with the inputted card. + */ + const handleSubmit = async (event: FormEvent): Promise => { + event.preventDefault(); + + // Stripe.js has not loaded yet + if (!stripe || !elements) { return; } + + const cardElement = elements.getElement(CardElement); + const { error, paymentMethod } = await stripe.createPaymentMethod({ + type: 'card', + card: cardElement, + }); + + if (error) { + setErrors(error.message); + } else { + setErrors(null); + afterSuccess(paymentMethod); + } + } + + /** + * Check if there is currently an error to display + */ + const hasErrors = (): boolean => { + return errors !== null; + } + + /** + * Change the state of the submit button: enabled/disabled + */ + const toggleSubmitButton = (): void => { + setSubmitState(!submitState); + } + + /** + * Return the form submission button. This button will be shown into the modal footer + */ + const submitButton = (): ReactNode => { + return ( + + ); + } + + return ( +
+ + +
+ + + +
+ {hasErrors() &&
+ {errors} +
} +
+
+ ); +} + +const StripeModalWrapper: React.FC = ({ isOpen, toggleModal, afterSuccess, reservation, currentUser, wallet, price, remainingPrice }) => { + return ( + + + + ); +} + +Application.Components.component('stripeModal', react2angular(StripeModalWrapper, ['isOpen', 'toggleModal', 'afterSuccess', 'reservation', 'currentUser', 'wallet', 'price', 'remainingPrice'])); diff --git a/app/frontend/src/javascript/components/wallet-info.tsx b/app/frontend/src/javascript/components/wallet-info.tsx index f0b88768d..1baabc177 100644 --- a/app/frontend/src/javascript/components/wallet-info.tsx +++ b/app/frontend/src/javascript/components/wallet-info.tsx @@ -7,117 +7,117 @@ import { useTranslation } from 'react-i18next'; import { react2angular } from 'react2angular'; import { IApplication } from '../models/application'; import '../lib/i18n'; -import { IFilterService } from 'angular'; import { Loader } from './loader'; import { Reservation } from '../models/reservation'; import { User } from '../models/user'; import { Wallet } from '../models/wallet'; +import { IFablab } from '../models/fablab'; declare var Application: IApplication; +declare var Fablab: IFablab; interface WalletInfoProps { - reservation: Reservation, - $filter: IFilterService, - currentUser: User, - wallet: Wallet, - price: number, - remainingPrice: number, + reservation: Reservation, + currentUser: User, + wallet: Wallet, + price: number, + remainingPrice: number, } -const WalletInfo: React.FC = ({ reservation, currentUser, wallet, price, remainingPrice, $filter }) => { - const { t } = useTranslation('shared'); +export const WalletInfo: React.FC = ({reservation, currentUser, wallet, price, remainingPrice}) => { + const {t} = useTranslation('shared'); - /** - * Return the formatted localized amount for the given price (eg. 20.5 => "20,50 €") - */ - const formatPrice = (price: number): string => { - return $filter('currency')(price); - } - /** - * Check if the currently connected used is also the person making the reservation. - * If the currently connected user (ie. the operator), is an admin or a manager, he may book the reservation for someone else. - */ - const isOperatorAndClient = (): boolean => { - return currentUser.id == reservation.user_id; - } - /** - * If the client has some money in his wallet & the price is not zero, then we should display this component. - */ - const shouldBeShown = (): boolean => { - return wallet.amount > 0 && price > 0; - } - /** - * If the amount in the wallet is not enough to cover the whole price, then the user must pay the remaining price - * using another payment mean. - */ - const hasRemainingPrice = (): boolean => { - return remainingPrice > 0; - } - /** - * Does the current cart contains a payment schedule? - */ - const isPaymentSchedule = (): boolean => { - return reservation.plan_id && reservation.payment_schedule; - } - /** - * Return the human-readable name of the item currently bought with the wallet - */ - const getPriceItem = (): string => { - let item = 'other'; - if (reservation.slots_attributes.length > 0) { - item = 'reservation'; - } else if (reservation.plan_id) { - if (reservation.payment_schedule) { - item = 'first_deadline'; - } - else item = 'subscription'; - } - - return t(`app.shared.wallet.wallet_info.item_${item}`); + /** + * Return the formatted localized amount for the given price (eg. 20.5 => "20,50 €") + */ + const formatPrice = (price: number): string => { + return new Intl.NumberFormat(Fablab.intl_locale, {style: 'currency', currency: Fablab.intl_currency}).format(price); + } + /** + * Check if the currently connected used is also the person making the reservation. + * If the currently connected user (ie. the operator), is an admin or a manager, he may book the reservation for someone else. + */ + const isOperatorAndClient = (): boolean => { + return currentUser.id == reservation.user_id; + } + /** + * If the client has some money in his wallet & the price is not zero, then we should display this component. + */ + const shouldBeShown = (): boolean => { + return wallet.amount > 0 && price > 0; + } + /** + * If the amount in the wallet is not enough to cover the whole price, then the user must pay the remaining price + * using another payment mean. + */ + const hasRemainingPrice = (): boolean => { + return remainingPrice > 0; + } + /** + * Does the current cart contains a payment schedule? + */ + const isPaymentSchedule = (): boolean => { + return reservation.plan_id && reservation.payment_schedule; + } + /** + * Return the human-readable name of the item currently bought with the wallet + */ + const getPriceItem = (): string => { + let item = 'other'; + if (reservation.slots_attributes.length > 0) { + item = 'reservation'; + } else if (reservation.plan_id) { + if (reservation.payment_schedule) { + item = 'first_deadline'; + } else item = 'subscription'; } - return ( -
- {shouldBeShown() &&
- {isOperatorAndClient() &&
-

{t('app.shared.wallet.wallet_info.you_have_AMOUNT_in_wallet', {AMOUNT : formatPrice(wallet.amount)})}

- {!hasRemainingPrice() &&

- {t('app.shared.wallet.wallet_info.wallet_pay_ITEM', {ITEM: getPriceItem()})} -

} - {hasRemainingPrice() &&

- {t('app.shared.wallet.wallet_info.credit_AMOUNT_for_pay_ITEM', { - AMOUNT: formatPrice(remainingPrice), - ITEM: getPriceItem() - })} -

} -
} - {!isOperatorAndClient() &&
-

{t('app.shared.wallet.wallet_info.client_have_AMOUNT_in_wallet', {AMOUNT : formatPrice(wallet.amount)})}

- {!hasRemainingPrice() &&

- {t('app.shared.wallet.wallet_info.client_wallet_pay_ITEM', {ITEM: getPriceItem()})} -

} - {hasRemainingPrice() &&

- {t('app.shared.wallet.wallet_info.client_credit_AMOUNT_for_pay_ITEM', { - AMOUNT: formatPrice(remainingPrice), - ITEM: getPriceItem() - })} -

} -
} - {!hasRemainingPrice() && isPaymentSchedule() &&

- - {t('app.shared.wallet.wallet_info.other_deadlines_no_wallet')} -

} -
} -
- ); + return t(`app.shared.wallet.wallet_info.item_${item}`); + } + + return ( +
+ {shouldBeShown() &&
+ {isOperatorAndClient() &&
+

{t('app.shared.wallet.wallet_info.you_have_AMOUNT_in_wallet', {AMOUNT: formatPrice(wallet.amount)})}

+ {!hasRemainingPrice() &&

+ {t('app.shared.wallet.wallet_info.wallet_pay_ITEM', {ITEM: getPriceItem()})} +

} + {hasRemainingPrice() &&

+ {t('app.shared.wallet.wallet_info.credit_AMOUNT_for_pay_ITEM', { + AMOUNT: formatPrice(remainingPrice), + ITEM: getPriceItem() + })} +

} +
} + {!isOperatorAndClient() &&
+

{t('app.shared.wallet.wallet_info.client_have_AMOUNT_in_wallet', {AMOUNT: formatPrice(wallet.amount)})}

+ {!hasRemainingPrice() &&

+ {t('app.shared.wallet.wallet_info.client_wallet_pay_ITEM', {ITEM: getPriceItem()})} +

} + {hasRemainingPrice() &&

+ {t('app.shared.wallet.wallet_info.client_credit_AMOUNT_for_pay_ITEM', { + AMOUNT: formatPrice(remainingPrice), + ITEM: getPriceItem() + })} +

} +
} + {!hasRemainingPrice() && isPaymentSchedule() &&

+ + {t('app.shared.wallet.wallet_info.other_deadlines_no_wallet')} +

} +
} +
+ ); } -const WalletInfoWrapper: React.FC = ({ currentUser, reservation, $filter, price, remainingPrice, wallet }) => { - return ( - - - - ); +const WalletInfoWrapper: React.FC = ({currentUser, reservation, price, remainingPrice, wallet}) => { + return ( + + + + ); } -Application.Components.component('walletInfo', react2angular(WalletInfoWrapper, ['currentUser', 'price', 'remainingPrice', 'reservation', 'wallet'], ['$filter'])); +Application.Components.component('walletInfo', react2angular(WalletInfoWrapper, ['currentUser', 'price', 'remainingPrice', 'reservation', 'wallet'])); diff --git a/app/frontend/src/javascript/models/fablab.ts b/app/frontend/src/javascript/models/fablab.ts new file mode 100644 index 000000000..83a915b2a --- /dev/null +++ b/app/frontend/src/javascript/models/fablab.ts @@ -0,0 +1,29 @@ +export interface IFablab { + plansModule: boolean, + spacesModule: boolean, + walletModule: boolean, + statisticsModule: boolean, + defaultHost: string, + trackingId: string, + superadminId: number, + baseHostUrl: string, + locale: string, + moment_locale: string, + summernote_locale: string, + fullcalendar_locale: string, + intl_locale: string, + intl_currency: string, + timezone: string, + weekStartingDay: string, + d3DateFormat: string, + uibDateFormat: string, + sessionTours: Array, + translations: { + app: { + shared: { + buttons: Object, + messages: Object, + } + } + } +} diff --git a/app/frontend/src/stylesheets/modules/fab-modal.scss b/app/frontend/src/stylesheets/modules/fab-modal.scss index 9c11568fc..1e8d565ab 100644 --- a/app/frontend/src/stylesheets/modules/fab-modal.scss +++ b/app/frontend/src/stylesheets/modules/fab-modal.scss @@ -62,9 +62,7 @@ text-align: right; border-top: 1px solid #e5e5e5; - .close-modal-btn { - color: black; - background-color: #fbfbfb; + .modal-btn { margin-bottom: 0; margin-left: 5px; display: inline-block; @@ -75,14 +73,24 @@ touch-action: manipulation; cursor: pointer; background-image: none; - border: 1px solid #c9c9c9; padding: 6px 12px; font-size: 16px; line-height: 1.5; border-radius: 4px; - &:hover { - background-color: #f2f2f2; + &--close { + @extend .modal-btn; + color: black; + background-color: #fbfbfb; + border: 1px solid #c9c9c9; + + &:hover { + background-color: #f2f2f2; + } + } + + &--confirm { + @extend .modal-btn; } } } diff --git a/app/views/application/index.html.erb b/app/views/application/index.html.erb index bda539774..1d3a7c9f7 100644 --- a/app/views/application/index.html.erb +++ b/app/views/application/index.html.erb @@ -37,6 +37,8 @@ Fablab.moment_locale = "<%= Rails.application.secrets.moment_locale %>"; Fablab.summernote_locale = "<%= Rails.application.secrets.summernote_locale %>"; Fablab.fullcalendar_locale = "<%= Rails.application.secrets.fullcalendar_locale %>"; + Fablab.intl_locale = "<%= Rails.application.secrets.intl_locale %>"; + Fablab.intl_currency = "<%= Rails.application.secrets.intl_currency %>"; Fablab.timezone = "<%= Time.zone.tzinfo.name %>"; Fablab.translations = { app: { @@ -88,31 +90,29 @@ <%= flash_messages %> - -
+
- + -
-
- +
+
+ -
-
-
-
-
-
+
+
+
+
+
+
-
-
+
+
-
- +
diff --git a/config/secrets.yml b/config/secrets.yml index 7c544267f..992a965df 100644 --- a/config/secrets.yml +++ b/config/secrets.yml @@ -26,6 +26,8 @@ development: moment_locale: <%= ENV["MOMENT_LOCALE"] %> summernote_locale: <%= ENV["SUMMERNOTE_LOCALE"] %> angular_locale: <%= ENV["ANGULAR_LOCALE"] %> + intl_locale: <%= ENV["INTL_LOCALE"] %> + intl_currency: <%= ENV["INTL_CURRENCY"] %> fullcalendar_locale: <%= ENV["FULLCALENDAR_LOCALE"] %> postgresql_language_analyzer: <%= ENV.fetch("POSTGRESQL_LANGUAGE_ANALYZER", 'simple') %> openlab_base_uri: <%= ENV["OPENLAB_BASE_URI"] %> @@ -54,6 +56,8 @@ test: moment_locale: en summernote_locale: en-US angular_locale: en-us + intl_locale: en-US + intl_currency: USD fullcalendar_locale: en postgresql_language_analyzer: french openlab_base_uri: @@ -90,6 +94,8 @@ staging: moment_locale: <%= ENV["MOMENT_LOCALE"] %> summernote_locale: <%= ENV["SUMMERNOTE_LOCALE"] %> angular_locale: <%= ENV["ANGULAR_LOCALE"] %> + intl_locale: <%= ENV["INTL_LOCALE"] %> + intl_currency: <%= ENV["INTL_CURRENCY"] %> fullcalendar_locale: <%= ENV["FULLCALENDAR_LOCALE"] %> postgresql_language_analyzer: <%= ENV.fetch("POSTGRESQL_LANGUAGE_ANALYZER", 'simple') %> openlab_base_uri: <%= ENV["OPENLAB_BASE_URI"] %> @@ -129,6 +135,8 @@ production: moment_locale: <%= ENV["MOMENT_LOCALE"] %> summernote_locale: <%= ENV["SUMMERNOTE_LOCALE"] %> angular_locale: <%= ENV["ANGULAR_LOCALE"] %> + intl_locale: <%= ENV["INTL_LOCALE"] %> + intl_currency: <%= ENV["INTL_CURRENCY"] %> fullcalendar_locale: <%= ENV["FULLCALENDAR_LOCALE"] %> postgresql_language_analyzer: <%= ENV.fetch("POSTGRESQL_LANGUAGE_ANALYZER", 'simple') %> openlab_base_uri: <%= ENV["OPENLAB_BASE_URI"] %> diff --git a/doc/environment.md b/doc/environment.md index 9339f4c0b..adfeaea88 100644 --- a/doc/environment.md +++ b/doc/environment.md @@ -195,6 +195,21 @@ See [code.angularjs.org/i18n/angular-locale_*.js](https://code.angularjs.org/1.8 Configure the fullCalendar JS agenda library. See [github.com/fullcalendar/fullcalendar/lang/*.js](https://github.com/fullcalendar/fullcalendar/tree/v3.10.2/locale) for a list of available locales. Default is **en-us**. + + + INTL_LOCALE + +Configure the locale for the javascript Intl Object. +This locale must be a Unicode BCP 47 locale identifier. +See [Intl - Javascript | MDN](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl#Locale_identification_and_negotiation) for more info about configuring this setting. + + + INTL_CURRENCY + +Configure the currency for the javascript Intl Object. +Possible values are the ISO 4217 currency codes, such as "USD" for the US dollar, "EUR" for the euro. +See [Current currency & funds code list](http://www.currency-iso.org/en/home/tables/table-a1.html) for a list of available values. +There is no default value; this setting MUST be provided. POSTGRESQL_LANGUAGE_ANALYZER diff --git a/env.example b/env.example index 9ca5e451c..f4972153f 100644 --- a/env.example +++ b/env.example @@ -38,6 +38,8 @@ MOMENT_LOCALE=fr SUMMERNOTE_LOCALE=fr-FR ANGULAR_LOCALE=fr-fr FULLCALENDAR_LOCALE=fr +INTL_LOCALE=fr-FR +INTL_CURRENCY=EUR FORCE_VERSION_CHECK=false ALLOW_INSECURE_HTTP=false diff --git a/setup/env.example b/setup/env.example index bf6195e53..13ec812b4 100644 --- a/setup/env.example +++ b/setup/env.example @@ -26,6 +26,8 @@ MOMENT_LOCALE=fr SUMMERNOTE_LOCALE=fr-FR ANGULAR_LOCALE=fr-fr FULLCALENDAR_LOCALE=fr +INTL_LOCALE=fr-FR +INTL_CURRENCY=EUR POSTGRESQL_LANGUAGE_ANALYZER=french diff --git a/setup/setup.sh b/setup/setup.sh index 051baf5e1..414969539 100755 --- a/setup/setup.sh +++ b/setup/setup.sh @@ -236,7 +236,7 @@ configure_env_file() doc=$(\curl -sSL https://raw.githubusercontent.com/sleede/fab-manager/master/doc/environment.md) variables=(DEFAULT_HOST DEFAULT_PROTOCOL DELIVERY_METHOD SMTP_ADDRESS SMTP_PORT SMTP_USER_NAME SMTP_PASSWORD SMTP_AUTHENTICATION \ SMTP_ENABLE_STARTTLS_AUTO SMTP_OPENSSL_VERIFY_MODE SMTP_TLS LOG_LEVEL MAX_IMAGE_SIZE MAX_CAO_SIZE MAX_IMPORT_SIZE DISK_SPACE_MB_ALERT \ - SUPERADMIN_EMAIL APP_LOCALE RAILS_LOCALE MOMENT_LOCALE SUMMERNOTE_LOCALE ANGULAR_LOCALE FULLCALENDAR_LOCALE \ + SUPERADMIN_EMAIL APP_LOCALE RAILS_LOCALE MOMENT_LOCALE SUMMERNOTE_LOCALE ANGULAR_LOCALE FULLCALENDAR_LOCALE INTL_LOCALE INTL_CURRENCY\ POSTGRESQL_LANGUAGE_ANALYZER TIME_ZONE WEEK_STARTING_DAY D3_DATE_FORMAT UIB_DATE_FORMAT EXCEL_DATE_FORMAT) for variable in "${variables[@]}"; do local var_doc current