Suppression de liens morts dans la description SEO (#858)

* Personnalise la configuration ESLint pour les scripts

* 👽 Déplace la traduction de l'explication SEO

sur le simulateur salarié

* Ajout d'une fonction rule dans sitePaths

Permet de type-checker le nom de la règle pour éviter les liens morts

* 👽 Ajoute un test pour la traduction des unités

* 👽 Traduction BNC

* 👽 fix translations
pull/863/head
Maxime Quandalle 2020-01-27 12:17:26 +01:00 committed by GitHub
parent 88dcc5d76a
commit 6545281f01
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
22 changed files with 1161 additions and 1088 deletions

View File

@ -112,6 +112,7 @@
"@types/classnames": "^2.2.9",
"@types/color-convert": "^1.9.0",
"@types/iframe-resizer": "^3.5.7",
"@types/js-yaml": "^3.12.2",
"@types/ramda": "^0.26.33",
"@types/raven-for-redux": "^1.1.1",
"@types/react": "^16.9.11",

View File

@ -1,6 +1,6 @@
import { ThemeColorsContext } from 'Components/utils/colors'
import { SitePathsContext } from 'Components/utils/withSitePaths'
import { encodeRuleName, nameLeaf } from 'Engine/rules'
import { nameLeaf } from 'Engine/rules'
import React, { useContext } from 'react'
import { Link } from 'react-router-dom'
import { Rule } from 'Types/rule'
@ -21,8 +21,7 @@ export default function RuleLink({
}: RuleLinkProps) {
const sitePaths = useContext(SitePathsContext)
const { color } = useContext(ThemeColorsContext)
const newPath =
sitePaths.documentation.index + '/' + encodeRuleName(dottedName)
const newPath = sitePaths.documentation.rule(dottedName)
return (
<Link

View File

@ -10,7 +10,7 @@ import PeriodSwitch from 'Components/PeriodSwitch'
import ComparaisonConfig from 'Components/simulationConfigs/rémunération-dirigeant.yaml'
import { SitePathsContext } from 'Components/utils/withSitePaths'
import Value from 'Components/Value'
import { encodeRuleName, getRuleFromAnalysis } from 'Engine/rules.js'
import { getRuleFromAnalysis } from 'Engine/rules.js'
import revenusSVG from 'Images/revenus.svg'
import { default as React, useCallback, useContext, useState } from 'react'
import emoji from 'react-easy-emoji'
@ -639,7 +639,7 @@ function RuleValueLink({
return !rule ? null : (
<Link
onClick={() => dispatch(setSituationBranch(getBranchIndex(branch)))}
to={sitePaths.documentation.index + '/' + encodeRuleName(rule.dottedName)}
to={sitePaths.documentation.rule(rule.dottedName)}
>
<Value
maximumFractionDigits={0}

View File

@ -1,5 +1,5 @@
import { SitePathsContext } from 'Components/utils/withSitePaths'
import { encodeRuleName, parentName } from 'Engine/rules.js'
import { parentName } from 'Engine/rules.js'
import { pick, sortBy, take } from 'ramda'
import React, { useContext, useEffect, useState } from 'react'
import Highlighter from 'react-highlight-words'
@ -83,9 +83,7 @@ export default function SearchBar({
}
/>
</div>
<Link
to={sitePaths.documentation.index + '/' + encodeRuleName(dottedName)}
>
<Link to={sitePaths.documentation.rule(dottedName)}>
<Highlighter
searchWords={[input]}
textToHighlight={title || capitalise0(name) || ''}
@ -98,13 +96,7 @@ export default function SearchBar({
if (selectedOption !== null) {
finallyCallback && finallyCallback()
return (
<Redirect
to={
sitePaths.documentation.index +
'/' +
encodeRuleName(selectedOption.dottedName)
}
/>
<Redirect to={sitePaths.documentation.rule(selectedOption.dottedName)} />
)
}

View File

@ -5,7 +5,6 @@ import RuleLink from 'Components/RuleLink'
import { ThemeColorsContext } from 'Components/utils/colors'
import { SitePathsContext } from 'Components/utils/withSitePaths'
import { formatCurrency } from 'Engine/format'
import { encodeRuleName } from 'Engine/rules'
import { isEmpty, isNil } from 'ramda'
import React, { useContext, useEffect, useState } from 'react'
import emoji from 'react-easy-emoji'
@ -199,13 +198,13 @@ const Target = ({ target, initialRender }) => {
let Header = ({ target }) => {
const sitePaths = useContext(SitePathsContext)
const ruleLink =
sitePaths.documentation.index + '/' + encodeRuleName(target.dottedName)
return (
<span className="header">
<span className="texts">
<span className="optionTitle">
<Link to={ruleLink}>{target.title || target.name}</Link>
<Link to={sitePaths.documentation.rule(target.dottedName)}>
{target.title || target.name}
</Link>
</span>
<p>{target.summary}</p>
</span>

View File

@ -23,7 +23,7 @@ export default function Targets() {
<Link
title="Quel est calcul ?"
style={{ color: colors.color }}
to={sitePaths.documentation.index + '/' + dottedName}
to={sitePaths.documentation.rule(dottedName)}
className="explanation"
>
{emoji('📖')}

View File

@ -1,5 +1,5 @@
import { SitePathsContext } from 'Components/utils/withSitePaths'
import { encodeRuleName, findRuleByDottedName } from 'Engine/rules'
import { findRuleByDottedName } from 'Engine/rules'
import React, { useContext } from 'react'
import emoji from 'react-easy-emoji'
import { Link } from 'react-router-dom'
@ -33,12 +33,7 @@ export default function Namespace({ dottedName, flatRules, color }) {
return (
<li style={style} key={fragments.join()}>
<Link
style={style}
to={
sitePaths.documentation.index + '/' + encodeRuleName(ruleName)
}
>
<Link style={style} to={sitePaths.documentation.rule(ruleName)}>
{rule.icons && <span>{emoji(rule.icons)} </span>}
{ruleText}
</Link>

View File

@ -2,11 +2,7 @@ import { ThemeColorsContext } from 'Components/utils/colors'
import { SitePathsContext } from 'Components/utils/withSitePaths'
import Value from 'Components/Value'
import knownMecanisms from 'Engine/known-mecanisms.yaml'
import {
encodeRuleName,
findRuleByDottedName,
findRuleByNamespace
} from 'Engine/rules'
import { findRuleByDottedName, findRuleByNamespace } from 'Engine/rules'
import { isEmpty } from 'ramda'
import React, { Suspense, useContext, useState } from 'react'
import emoji from 'react-easy-emoji'
@ -227,11 +223,7 @@ function NamespaceRulesList({ namespaceRules }) {
color: colors.textColorOnWhite,
textDecoration: 'underline'
}}
to={
sitePaths.documentation.index +
'/' +
encodeRuleName(r.dottedName)
}
to={sitePaths.documentation.rule(r.dottedName)}
>
{r.title || r.name}
</Link>

View File

@ -134,11 +134,7 @@ export function Leaf({
<span className={classNames(classes, 'leaf')}>
{dottedName && (
<span className="nodeHead">
<Link
to={
sitePaths.documentation.index + '/' + encodeRuleName(dottedName)
}
>
<Link to={sitePaths.documentation.rule(dottedName)}>
<span className="name">
{rule.title || capitalise0(name)} {filter}
</span>

View File

@ -1,10 +0,0 @@
"Salariat ":
" CDD ":
" événements": "_"
" motif": "saisonnier"
" engagement employeur complément formation": "non"
" durée contrat": "2"
" contrat aidé": "non"
" salaire de base": "1481"
" congés non pris": "3"

View File

@ -1,2 +0,0 @@
Le script externalize.js va parcourir la base de règles, puis va traduire automatiquement (Google trad) une liste d'attributs les plus importants dans le fichier externalized.yaml.
Les traductions déjà existantes réalisées par un humain sont conservées. En effet, la traduction automatique préfixe les traductions par ~~, ce qui lui permet aussi de repérer ce qui a déjà été traduit avec soin.

File diff suppressed because it is too large Load Diff

View File

@ -1,12 +1,15 @@
fr:
heure_plural: heures
jour_plural: jours
jour ouvré_plural: jours ouvrés
semaine_plural: semaines
trimestre_plural: trimestres
trimestre validé_plural: trimestres validés
an_plural: ans
employé_plural: employés
point_plural: points
mois_plural: mois
manifestation_plural: manifestations
en:
heure: hour
heure_plural: hours
@ -14,10 +17,15 @@ en:
points_plural: points
jour: day
jour_plural: days
jour ouvré: working day
jour ouvré_plural: working days
semaine: week
semaine_plural: weeks
trimestre: quarter
trimestre_plural: quarters
trimestre validé: validated quarter
trimestre validé_plural: validated quarters
km: km
repas: meal
repas_plural: meals
employé: employee
@ -26,3 +34,5 @@ en:
an_plural: years
mois: month
mois_plural: months
manifestation: event
manifestation_plural: events

View File

@ -71,7 +71,7 @@ artiste-auteur . revenus . BNC . charges forfaitaires:
titre.en: fixed expenses
titre.fr: charges forfaitaires
artiste-auteur . revenus . BNC . frais réels:
question.en: NBC actual cost regime
question.en: BNC real cost regime
question.fr: Régime des frais réels BNC
résumé.en: >-
Amount of your expenses (business expenses, depreciation, etc.) which will
@ -84,12 +84,12 @@ artiste-auteur . revenus . BNC . frais réels:
artiste-auteur . revenus . BNC . micro-bnc:
résumé.en: 34% flat-rate tax allowance for business expenses
résumé.fr: Abattement forfaitaire fiscal de 34 % au titre des frais professionnels
titre.en: Would you like to opt for the micro-BNC diet?
titre.en: Would you like to opt-in for the micro-BNC regime?
titre.fr: Souhaitez-vous opter pour le régime micro-BNC ?
artiste-auteur . revenus . BNC . recettes:
résumé.en: The amount of your gross revenue excluding VAT
résumé.fr: Le montant de vos recettes brutes hors TVA
titre.en: Income in "NBC"
titre.en: Income in "BNC" category
titre.fr: Revenu en BNC
artiste-auteur . revenus . traitements et salaires:
résumé.en: The amount excluding VAT of your royalties (pre-deducted revenue)

View File

@ -0,0 +1,5 @@
# We overwrite the default ESLint configuration for NodeJS scripts
env:
node: true
rules:
no-console: 0

View File

View File

@ -10,9 +10,7 @@ const fs = require('fs')
const path = require('path')
const yaml = require('js-yaml')
// eslint-disable-next-line no-undef
const sourcePath = path.resolve(__dirname, '../règles/base.yaml')
// eslint-disable-next-line no-undef
const outPath = path.resolve(__dirname, '../types/dottednames.json')
function persistJsonFileFromYaml() {

View File

@ -48,7 +48,6 @@ const fakeData = [
}
]
// eslint-disable-next-line no-undef
const dataDir = path.resolve(__dirname, '../data/')
async function main() {

View File

@ -4,7 +4,6 @@ import SalaryExplanation from 'Components/SalaryExplanation'
import Simulation from 'Components/Simulation'
import salariéConfig from 'Components/simulationConfigs/salarié.yaml'
import { IsEmbeddedContext } from 'Components/utils/embeddedContext'
import { Markdown } from 'Components/utils/markdown'
import { SitePathsContext } from 'Components/utils/withSitePaths'
import urlIllustrationNetBrutEn from 'Images/illustration-net-brut-en.png'
import urlIllustrationNetBrut from 'Images/illustration-net-brut.png'
@ -15,7 +14,7 @@ import { useDispatch } from 'react-redux'
import { Link, useLocation } from 'react-router-dom'
export default function Salarié() {
const { t, i18n } = useTranslation()
const { t } = useTranslation()
const isEmbedded = React.useContext(IsEmbeddedContext)
return (
@ -42,56 +41,95 @@ export default function Salarié() {
</h1>
<div style={{ margin: '2rem' }} />
<SalarySimulation />
{!isEmbedded && (
<Markdown
source={i18n.language === 'fr' ? seoExplanations : seoExplanationsEn}
/>
)}
{!isEmbedded && <SeoExplanations />}
</>
)
}
const seoExplanations = `
## Calculer son salaire net
function SeoExplanations() {
const { t, i18n } = useTranslation()
const sitePaths = useContext(SitePathsContext)
Lors de l'entretien d'embauche l'employeur propose en général une rémunération exprimée en « brut ». Le montant annoncé inclut ainsi les cotisations salariales, qui servent à financer la protection sociale du salarié et qui sont retranchées du salaire « net » perçu par le salarié.
return (
<Trans i18nKey="simulateurs.salarié.page.explication seo">
<h2>Calculer son salaire net</h2>
Vous pouvez utiliser notre simulateur pour convertir le **salaire brut en net** : il vous suffit pour cela saisir la rémunération annoncée dans la case salaire brut. La simulation peut-être affinée en répondant aux différentes questions (sur le CDD, statut cadre, etc.).
<p>
Lors de l'entretien d'embauche l'employeur propose en général une
rémunération exprimée en « brut ». Le montant annoncé inclut ainsi les
cotisations salariales, qui servent à financer la protection sociale du
salarié et qui sont retranchées du salaire « net » perçu par le salarié.
</p>
<p>
Vous pouvez utiliser notre simulateur pour convertir le{' '}
<strong>salaire brut en net</strong> : il vous suffit pour cela saisir
la rémunération annoncée dans la case salaire brut. La simulation
peut-être affinée en répondant aux différentes questions (sur le CDD,
statut cadre, etc.).
</p>
<img
src={
i18n.language === 'fr'
? urlIllustrationNetBrut
: urlIllustrationNetBrutEn
}
css={`
width: 100%;
`}
/>
<p>
Par ailleurs depuis 2019, l'
<Link to={sitePaths.documentation.rule('impôt . impôt sur le revenu')}>
impôt sur le revenu
</Link>{' '}
est prélevé à la source. Pour ce faire, la direction générale des
finances publiques (DGFiP) transmet à l'employeur le taux d'imposition
calculé à partir de la déclaration de revenu du salarié. Si ce taux est
inconnu, par exemple lors d'une première année d'activité, l'employeur
utilise le{' '}
<Link
to={sitePaths.documentation.rule(
"impôt . taux neutre d'impôt sur le revenu"
)}
>
taux neutre
</Link>
.
</p>
![Salaire net et brut](${urlIllustrationNetBrut})
<h2>Coût d'embauche</h2>
Par ailleurs depuis 2019, l'[impôt sur le revenu](/documentation/impôt/impôt-sur-le-revenu) est prélevé à la source. Pour ce faire, la direction générale des finances publiques (DGFiP) transmet à l'employeur le taux d'imposition calculé à partir de la déclaration de revenu du salarié. Si ce taux est inconnu, par exemple lors d'une première année d'activité, l'employeur utilise le [taux neutre](/documentation/impôt/impôt-sur-le-revenu-au-taux-neutre). C'est aussi le taux neutre que nous utilisons dans le simulateur pour calculer le « net après impôt » qui est versé sur le compte bancaire du salarié.
<p>
Si vous cherchez à embaucher, vous pouvez calculer le coût total de la
rémunération de votre salarié, ainsi que les montants de cotisations
patronales et salariales correspondants. Cela vous permet de définir le
niveau de rémunération en connaissant le montant global de charge que
cela représente pour votre entreprise.
</p>
Attention : L'impôt sur le revenu est calculé à partir du [salaire net imposable](/documentation/contrat-salarié/rémunération/net-imposable) qui inclut certaines cotisations non déductibles et les avantages en nature en plus du salaire net.
<p>
En plus du salaire, notre simulateur prend en compte le calcul des
avantages en nature (téléphone, véhicule de fonction, etc.), ainsi que
la mutuelle santé obligatoire.
</p>
## Coût d'embauche
Si vous cherchez à embaucher, vous pouvez calculer le coût total de la rémunération de votre salarié, ainsi que les montants de cotisations patronales et salariales correspondants. Cela vous permet de définir le niveau de rémunération en connaissant le montant global de charge que cela représente pour votre entreprise.
En plus du salaire, notre simulateur prend en compte le calcul des avantages en nature (téléphone, véhicule de fonction, etc.), ainsi que la mutuelle santé obligatoire.
Il existe des [aides différées](/documentation/aides-employeur) à l'embauche qui ne sont pas prises en compte par notre simulateur, vous pouvez les retrouver sur [le portail officiel](http://www.aides-entreprises.fr).`
const seoExplanationsEn = `
## Calculate your net salary
During the job interview, the employer usually talks about the "gross" salary. The amount announced thus includes employee contributions, which are used to finance the employee's social protection and are deducted from the "net" salary received by the employee.
You can use our simulator to convert the gross salary into a net salary: all you need to do is enter the remuneration announced in the gross salary input. The simulation can be refined by answering the different questions (on the CDD, Cadre status, etc.).
![Gross and net salaries](${urlIllustrationNetBrutEn})
In addition, since 2019, the income tax is withholded. To do this, the Direction Générale des Finances Publiques (DGFiP) sends the employer the tax rate calculated from the employee's income tax return. If this rate is unknown, for example for the first year of employment, the employer uses the neutral rate. It is also the neutral rate that we use in the simulator to calculate the "after-tax net" that is paid into the employee's bank account.
Note: Income tax is calculated on the net taxable salary, which includes certain non-deductible contributions and benefits in kind in addition to the net salary.
## Hiring cost
If you are looking to hire, you can calculate the total cost of your employee's compensation, as well as the corresponding employer and employee contributions. This allows you to define the level of compensation by knowing the total amount of expense that this represents for your company.
In addition to the salary, our simulator takes into account the calculation of benefits in kind (telephone, company car, etc.), as well as the mandatory health insurance.
There are deferred hiring aids that are not taken into account by our simulator, you can find them on the official portal.`
<p>
Il existe des{' '}
<Link
to={sitePaths.documentation.rule('contrat salarié . aides employeur')}
>
aides différées
</Link>{' '}
à l'embauche qui ne sont pas toutes prises en compte par notre
simulateur, vous pouvez les retrouver sur{' '}
<a href="http://www.aides-entreprises.fr" target="_blank">
le portail officiel
</a>
.
</p>
</Trans>
)
}
export let SalarySimulation = () => {
const dispatch = useDispatch()

View File

@ -1,5 +1,7 @@
import { encodeRuleName } from 'Engine/rules'
import { map, reduce, toPairs, zipObj } from 'ramda'
import { LegalStatus } from 'Selectors/companyStatusSelectors'
import { DottedName } from 'Types/rule'
import i18n from '../../i18n'
export const LANDING_LEGAL_STATUS_LIST: Array<LegalStatus> = [
@ -29,7 +31,7 @@ interface HasIndex {
}
type SitePathsObject<T> = {
[key in keyof T]: string | SitePathsObject<T[key]>
[key in keyof T]: string | Function | SitePathsObject<T[key]>
}
function constructSitePaths<T extends SitePathsObject<HasIndex>>(
@ -41,6 +43,8 @@ function constructSitePaths<T extends SitePathsObject<HasIndex>>(
...map(value =>
typeof value === 'string'
? root + index + value
: typeof value === 'function'
? (...args: any) => root + index + String(value(...args))
: constructSitePaths(root + index, value as any)
)(sitePaths as any)
} as any
@ -115,7 +119,8 @@ export const constructLocalizedSitePath = (language: string) => {
nouveautés: t('path.nouveautés', '/nouveautés'),
documentation: {
exemples: t('path.documentation.exemples', '/exemples'),
index: t('path.documentation.index', '/documentation')
index: t('path.documentation.index', '/documentation'),
rule: (dottedName: DottedName) => '/' + encodeRuleName(dottedName)
},
integration: {
index: t('path.integration.index', '/intégration'),

View File

@ -0,0 +1,26 @@
import { expect } from 'chai'
import { uniq } from 'ramda'
import { rulesFr } from '../source/engine/rules'
import { parseAll } from '../source/engine/traverse'
import unitsTranslations from '../source/locales/units.yaml'
it('has translation for all base units', () => {
const rules = parseAll(rulesFr)
const units = uniq(
Object.keys(rules).reduce(
(prev, name) => [
...prev,
...(rules[name].unit?.numerators ?? []),
...(rules[name].unit?.denumerators ?? [])
],
[]
)
)
const blackList = ['€', '%']
const translatedKeys = Object.keys(unitsTranslations.en)
const missingTranslations = units.filter(
unit => ![...translatedKeys, ...blackList].includes(unit)
)
expect(missingTranslations).to.be.empty
})

View File

@ -1192,6 +1192,11 @@
"@types/istanbul-lib-coverage" "*"
"@types/istanbul-lib-report" "*"
"@types/js-yaml@^3.12.2":
version "3.12.2"
resolved "https://registry.yarnpkg.com/@types/js-yaml/-/js-yaml-3.12.2.tgz#a35a1809c33a68200fb6403d1ad708363c56470a"
integrity sha512-0CFu/g4mDSNkodVwWijdlr8jH7RoplRWNgovjFLEZeT+QEbbZXjBmCe3HwaWheAlCbHwomTwzZoSedeOycABug==
"@types/node@*":
version "13.1.1"
resolved "https://registry.yarnpkg.com/@types/node/-/node-13.1.1.tgz#6d11a8c2d58405b3db9388ab740106cbfa64c3c9"