Table des matières
Application de stimulation guidée
I18Suivant
一次性翻译
复数和性别
切换区域设置
在 URL 中保留区域设置
检测区域设置
结论
Maison Tutoriel CMS WordPresse Localisez les applications de relance à l'aide d'I18Next

Localisez les applications de relance à l'aide d'I18Next

Aug 28, 2023 pm 11:53 PM

使用 I18Next 本地化刺激应用程序

Dans mon article précédent, j'ai présenté Stimulus - un framework JavaScript simple créé par Basecamp. Aujourd'hui, je vais discuter de l'internationalisation des applications Stimulus, car le framework ne fournit aucun outil d'internationalisation prêt à l'emploi. L'internationalisation est une étape importante, en particulier lorsque votre application est utilisée par des personnes du monde entier. Une compréhension de base de la manière de procéder à l'internationalisation peut donc s'avérer très utile.

Bien sûr, c'est à vous de décider quelle solution d'internationalisation mettre en œuvre, que ce soit jQuery.I18n, Polyglot ou autre chose. Dans ce didacticiel, je souhaite vous montrer un framework I18n populaire appelé I18next, qui possède de nombreuses fonctionnalités intéressantes et fournit de nombreux plugins tiers supplémentaires pour simplifier davantage le processus de développement. Même avec toutes ces fonctionnalités, I18next n’est pas un outil complexe et vous n’avez pas besoin d’étudier une tonne de documentation pour commencer.

Dans cet article, vous apprendrez comment activer la prise en charge I18n dans votre application Stimulus à l'aide de la bibliothèque I18next. Plus précisément, nous aborderons :

  • Prochaine configuration I18
  • Traduisez les fichiers et chargez-les de manière asynchrone
  • Effectuez la traduction et traduisez la page entière en une seule fois
  • Gérer les informations sur le pluriel et le genre
  • Basculez entre les paramètres régionaux et conservez les paramètres régionaux sélectionnés dans les paramètres GET
  • Définissez les paramètres régionaux en fonction des préférences de l'utilisateur

Le code source est disponible dans le référentiel GitHub du didacticiel.

Application de stimulation guidée

Tout d'abord, clonons le projet Stimulus Starter et installons toutes les dépendances à l'aide du gestionnaire de packages Yarn :

git clone https://github.com/stimulusjs/stimulus-starter.git
cd stimulus-starter
yarn install
Copier après la connexion

Nous allons créer une application Web simple pour charger des informations sur les utilisateurs enregistrés. Pour chaque utilisateur, nous afficherons son nom de connexion et le nombre de photos qu'il a téléchargées jusqu'à présent (peu importe quelles sont ces photos).

De plus, nous fournirons un sélecteur de langue en haut de la page. Une fois que vous avez sélectionné une langue, l'interface doit être traduite immédiatement sans qu'il soit nécessaire de recharger la page. De plus, l'URL doit être ajoutée avec un paramètre ?locale GET spécifiant les paramètres régionaux actuellement utilisés. Bien entendu, si ce paramètre est fourni lors du chargement de la page, la langue correcte doit être définie automatiquement.

D'accord, allons-y et restituons notre utilisateur. Ajoutez les lignes de code suivantes au fichier public/index.html :

<div data-controller="users" data-users-url="/api/users/index.json"></div>
Copier après la connexion

Ici, nous utilisons le contrôleur users et fournissons une URL pour charger l'utilisateur. Dans une application réelle, nous pourrions avoir un script côté serveur qui récupère l'utilisateur de la base de données et répond avec JSON. Cependant, pour ce tutoriel nous mettrons simplement toutes les données nécessaires dans le fichier public/api/users/index.json :

[
  {
    "login": "johndoe",
    "photos_count": "15",
    "gender": "male"
  },
  {
    "login": "annsmith",
    "photos_count": "20",
    "gender": "female"
  }
]
Copier après la connexion

Créez maintenant un nouveau fichier src/controllers/users_controller.js :

import { Controller } from "stimulus"

export default class extends Controller {
  connect() {
    this.loadUsers()
  }
}
Copier après la connexion

Une fois le contrôleur connecté au DOM, on charge l'utilisateur de manière asynchrone à l'aide de la méthode loadUsers() :

  loadUsers() {
    fetch(this.data.get("url"))
    .then(response => response.text())
    .then(json => {
      this.renderUsers(json)
    })
  }
Copier après la connexion

Cette méthode envoie une requête de récupération à l'URL donnée, obtient la réponse et présente enfin à l'utilisateur :

  renderUsers(users) {
    let content = ''
    JSON.parse(users).forEach((user) => {
      content += `<div>Login: ${user.login}<br>Has uploaded ${user.photos_count} photo(s)</div><hr>`
    })
    this.element.innerHTML = content
  }
Copier après la connexion

renderUsers() 依次解析 JSON,构造一个包含所有内容的新字符串,最后将此内容显示在页面上(this.element 是将返回控制器连接到的实际 DOM 节点,在我们的例子中是 div).

I18Suivant

Nous allons maintenant passer à l'intégration d'I18next dans notre application. Ajoutez deux bibliothèques à notre projet : I18next lui-même et un plugin pour permettre le chargement asynchrone des fichiers de traduction depuis le backend :

yarn add i18next i18next-xhr-backend
Copier après la connexion

Nous stockerons tous les éléments liés à I18next dans un fichier src/i18n/config.js séparé, alors créez-le maintenant :

import i18next from 'i18next'
import I18nXHR from 'i18next-xhr-backend'

const i18n = i18next.use(I18nXHR).init({
  fallbackLng: 'en',
  whitelist: ['en', 'ru'],
  preload: ['en', 'ru'],
  ns: 'users',
  defaultNS: 'users',
  fallbackNS: false,
  debug: true,
  backend: {
    loadPath: '/i18n/{{lng}}/{{ns}}.json',
  }
}, function(err, t) {
  if (err) return console.error(err)
});
 
export { i18n as i18n }
Copier après la connexion

Voyons ce qui se passe ici de haut en bas :

  • use(I18nXHR) Activez le plugin i18next-xhr-backend.

  • fallbackLng Dites-lui d'utiliser l'anglais comme langue de secours.

  • whitelist Seuls les paramètres anglais et russe sont autorisés. Bien entendu, vous pouvez choisir n’importe quelle autre langue.

  • preload Demande de précharger les fichiers de traduction depuis le serveur au lieu de les charger lorsque la langue correspondante est sélectionnée.

  • ns 表示“命名空间”,接受字符串或数组。在此示例中,我们只有一个命名空间,但对于较大的应用程序,您可以引入其他命名空间,例如 admincartprofile Attendez. Un fichier de traduction distinct doit être créé pour chaque espace de noms.

  • defaultNS définit defaultNSusers comme espace de noms par défaut.

  • fallbackNS Désactivez le repli de l'espace de noms.

  • debug 允许在浏览器的控制台中显示调试信息。具体来说,它会说明加载哪些翻译文件、选择哪种语言等。您可能希望在将应用程序部署到生产环境之前禁用此设置。

  • backend 为 I18nXHR 插件提供配置并指定从何处加载翻译。请注意,路径应包含区域设置的标题,而文件应以命名空间命名并具有 .json 扩展名

  • function(err, t) 是当 I18next 准备好时(或当出现错误时)运行的回调。

接下来,让我们制作翻译文件。俄语翻译应放入 public/i18n/ru/users.json 文件中:

{
  "login": "Логин"
}
Copier après la connexion

login 这里是翻译键,而 Логин 是要显示的值。

英文翻译应该转到 public/i18n/en/users.json 文件:

{
  "login": "Login"
}
Copier après la connexion

为了确保 I18next 正常工作,您可以将以下代码行添加到 i18n/config.js 文件内的回调中:

// config goes here...
function(err, t) {
  if (err) return console.error(err)
  console.log(i18n.t('login'))
}
Copier après la connexion

在这里,我们使用一个名为 t 的方法,意思是“翻译”。该方法接受翻译键并返回相应的值。

但是,我们可能有很多 UI 部分需要翻译,而使用 t 方法来翻译会非常乏味。相反,我建议您使用另一个名为 loc-i18next 的插件,它允许您一次翻译多个元素。

一次性翻译

安装loc-i18next插件:

yarn add loc-i18next
Copier après la connexion

将其导入src/i18n/config.js文件的顶部:

import locI18next from 'loc-i18next'
Copier après la connexion

现在提供插件本身的配置:

// other config

const loci18n = locI18next.init(i18n, {
  selectorAttr: 'data-i18n',
  optionsAttr: 'data-i18n-options',
  useOptionsAttr: true
});

export { loci18n as loci18n, i18n as i18n }
Copier après la connexion

这里有几点需要注意:

  • locI18next.init(i18n) 基于之前定义的 I18next 实例创建一个新的插件实例。
  • selectorAttr 指定使用哪个属性来检测需要本地化的元素。基本上,loc-i18next 将搜索此类元素并使用 data-i18n 属性的值作为翻译键。

  • optionsAttr 指定哪个属性包含附加翻译选项。

  • useOptionsAttr 指示插件使用其他选项。

我们的用户正在异步加载,因此我们必须等到此操作完成,然后才执行本地化。现在,我们简单地设置一个计时器,在调用 localize() 方法之前等待两秒——当然,这是一个临时的 hack。

  import { loci18n } from '../i18n/config'
  
  // other code...
  
  loadUsers() {
    fetch(this.data.get("url"))
    .then(response => response.text())
    .then(json => {
      this.renderUsers(json)
      setTimeout(() => { // <---
        this.localize()
      }, '2000')
    })
  }
Copier après la connexion

编写 localize() 方法本身的代码:

  localize() {
    loci18n('.users')
  }
Copier après la connexion

如您所见,我们只需要将选择器传递给 loc-i18next 插件即可。内部的所有元素(设置了 data-i18n 属性)都将自动本地化。

现在调整 renderUsers 方法。现在,我们只翻译“Login”一词:

  renderUsers(users) {
    let content = ''
    JSON.parse(users).forEach((user) => {
      content += `<div class="users">ID: ${user.id}<br><span data-i18n="login"></span>: ${user.login}<br>Has uploaded ${user.photos_count} photo(s)</div><hr>`
    })
    this.element.innerHTML = content
  }
Copier après la connexion

不错!重新加载页面,等待两秒钟,并确保每个用户都显示“登录”字样。

复数和性别

我们对部分界面进行了本地化,这真的很酷。尽管如此,每个用户还有两个字段:上传的照片数量和性别。由于我们无法预测每个用户将拥有多少张照片,因此应根据给定的数量将“照片”一词正确地复数化。为此,我们需要之前配置的 data-i18n-options 属性。要提供计数,应为 data-i18n-options 分配以下对象:{ "count": YOUR_COUNT }

性别信息也应考虑在内。英语中的“uploaded”一词可以适用于男性和女性,但在俄语中它要么变成“загрузил”或“загрузила”,所以我们再次需要 data-i18n-options,其中有 { "context": "GENDER" } 作为值。顺便请注意,您可以利用此上下文来完成其他任务,而不仅仅是提供性别信息。

  renderUsers(users) {
    let content = ''
    JSON.parse(users).forEach((user) => {
      content += `<div class="users"><span data-i18n="login"></span>: ${user.login}<br><span data-i18n="uploaded" data-i18n-options="{ 'context': '${user.gender}' }"></span> <span data-i18n="photos" data-i18n-options="{ 'count': ${user.photos_count} }"></span></div><hr>`
    })
    this.element.innerHTML = content
  }
Copier après la connexion

现在更新英文翻译:

{
  "login": "Login",
  "uploaded": "Has uploaded",
  "photos": "one photo",
  "photos_plural": "{{count}} photos"
}
Copier après la connexion

这里没什么复杂的。由于对于英语,我们不关心性别信息(即上下文),因此翻译键应该只是 uploaded。为了提供正确的复数翻译,我们使用 photosphotos_plural 键。 {{count}} 部分为插值,将替换为实际数字。

至于俄语,事情就更复杂了:

{
  "login": "Логин",
  "uploaded_male": "Загрузил уже",
  "uploaded_female": "Загрузила уже",
  "photos_0": "одну фотографию",
  "photos_1": "{{count}} фотографии",
  "photos_2": "{{count}} фотографий"
}
Copier après la connexion

首先,请注意,我们有两个可能的上下文的 uploaded_maleuploaded_female 键。接下来,俄语中的复数规则也比英语中更复杂,因此我们必须提供不是两个而是三个可能的短语。 I18next 支持多种开箱即用的语言,这个小工具可以帮助您了解应该为给定语言指定哪些复数键。

切换区域设置

我们已经完成了应用程序的翻译,但用户应该能够在区域设置之间切换。因此,向 public/index.html 文件添加一个新的“语言切换器”组件:

<ul data-controller="languages" class="switcher"></ul>
Copier après la connexion

src/controllers/languages_controller.js 文件中制作相应的控制器:

import { Controller } from "stimulus"
import { i18n, loci18n } from '../i18n/config'

export default class extends Controller {
    initialize() {
      let languages = [
        {title: 'English', code: 'en'},
        {title: 'Русский', code: 'ru'}
      ]

      this.element.innerHTML = languages.map((lang) => {
        return `<li data-action="click->languages#switchLanguage"
        data-lang="${lang.code}">${lang.title}</li>`
      }).join('')
    }
}
Copier après la connexion

这里我们使用 initialize() 回调来显示支持的语言列表。每个 li 都有一个 data-action 属性,该属性指定单击元素时应触发的方法(在本例中为 switchLanguage)。

现在添加 switchLanguage() 方法:

  switchLanguage(e) {
    this.currentLang = e.target.getAttribute("data-lang")
  }
Copier après la connexion

它只是获取事件的目标并获取 data-lang 属性的值。

我还想为 currentLang 属性添加 getter 和 setter:

  get currentLang() {
    return this.data.get("currentLang")
  }

  set currentLang(lang) {
    if(i18n.language !== lang) {
      i18n.changeLanguage(lang)
    }

    if(this.currentLang !== lang) {
      this.data.set("currentLang", lang)
      loci18n('body')
      this.highlightCurrentLang()
    }
  }
Copier après la connexion

getter 非常简单——我们获取当前使用的语言的值并返回它。

setter 更复杂。首先,如果当前设置的语言与所选语言不相等,我们使用 changeLanguage 方法。此外,我们将新选择的语言环境存储在 data-current-lang 属性(在 getter 中访问)下,使用 loc-i18next 插件本地化 HTML 页面的主体,最后突出显示当前使用的区域设置。

让我们编写 highlightCurrentLang() 的代码:

  highlightCurrentLang() {
    this.switcherTargets.forEach((el, i) => {
      el.classList.toggle("current", this.currentLang === el.getAttribute("data-lang"))
    })
  }
Copier après la connexion

这里我们迭代区域设置切换器的数组,并将它们的 data-lang 属性的值与当前使用的区域设置的值进行比较。如果值匹配,则为切换器分配 current CSS 类,否则删除该类。

为了使 this.switcherTargets 构建工作,我们需要按以下方式定义刺激目标:

static targets = [ "switcher" ]
Copier après la connexion

此外,为 lis 添加值为 switcherdata-target 属性:

  initialize() {
      // ...
      this.element.innerHTML = languages.map((lang) => {
        return `<li data-action="click->languages#switchLanguage"
        data-target="languages.switcher" data-lang="${lang.code}">${lang.title}</li>`
      }).join('')
      // ...
  }
Copier après la connexion

另一个需要考虑的重要事项是翻译文件可能需要一些时间来加载,我们必须等待此操作完成才能允许切换区域设置。因此,让我们利用 loaded 回调:

  initialize() {
    i18n.on('loaded', (loaded) => { // <---
      let languages = [
        {title: 'English', code: 'en'},
        {title: 'Русский', code: 'ru'}
      ]

      this.element.innerHTML = languages.map((lang) => {
        return `<li data-action="click->languages#switchLanguage"
        data-target="languages.switcher" data-lang="${lang.code}">${lang.title}</li>`
      }).join('')

      this.currentLang = i18n.language
    })
  }
Copier après la connexion

最后,不要忘记从 loadUsers() 方法中删除 setTimeout

  loadUsers() {
    fetch(this.data.get("url"))
    .then(response => response.text())
    .then(json => {
      this.renderUsers(json)
      this.localize()
    })
  }
Copier après la connexion

在 URL 中保留区域设置

切换语言环境后,我想在包含所选语言代码的 URL 中添加 ?lang GET 参数。在 History API 的帮助下,可以轻松地添加 GET 参数而不重新加载页面:

  set currentLang(lang) {
    if(i18n.language !== lang) {
      i18n.changeLanguage(lang)
      window.history.pushState(null, null, `?lang=${lang}`) // <---
    }

    if(this.currentLang !== lang) {
      this.data.set("currentLang", lang)
      loci18n('body')
      this.highlightCurrentLang()
    }
  }
Copier après la connexion

检测区域设置

我们今天要实现的最后一件事是能够根据用户的偏好设置区域设置。一个名为 LanguageDetector 的插件可以帮助我们解决这个任务。添加新的 Yarn 包:

yarn add i18next-browser-languagedetector
Copier après la connexion

i18n/config.js 文件中导入 LanguageDetector

import LngDetector from 'i18next-browser-languagedetector'
Copier après la connexion

现在调整配置:

const i18n = i18next.use(I18nXHR).use(LngDetector).init({ // <---
  // other options go here...
  detection: {
    order: ['querystring', 'navigator', 'htmlTag'],
    lookupQuerystring: 'lang',
  }
}, function(err, t) {
  if (err) return console.error(err)
});
Copier après la connexion

order 选项列出了插件应尝试的所有技术(按重要性排序),以便“猜测”首选区域设置:

  • querystring 表示检查包含区域设置代码的 GET 参数。
  • lookupQuerystring 设置要使用的 GET 参数的名称,在我们的例子中是 lang
  • navigator 表示从用户的请求中获取语言环境数据。
  • htmlTag 涉及从 html 标记的 lang 属性获取首选区域设置。

结论

在本文中,我们介绍了 I18next——一种轻松翻译 JavaScript 应用程序的流行解决方案。您已经学习了如何将 I18next 与 Stimulus 框架集成、配置它以及以异步方式加载翻译文件。此外,您还了解了如何在区域设置之间切换以及如何根据用户的偏好设置默认语言。

I18next 有一些额外的配置选项和许多插件,因此请务必浏览其官方文档以了解更多信息。另请注意,Stimulus 不会强制您使用特定的本地化解决方案,因此您也可以尝试使用 jQuery.I18n 或 Polyglot 等解决方案。

C'est tout pour aujourd'hui ! Merci d'avoir lu et à la prochaine fois.

Ce qui précède est le contenu détaillé de. pour plus d'informations, suivez d'autres articles connexes sur le site Web de PHP en chinois!

Déclaration de ce site Web
Le contenu de cet article est volontairement contribué par les internautes et les droits d'auteur appartiennent à l'auteur original. Ce site n'assume aucune responsabilité légale correspondante. Si vous trouvez un contenu suspecté de plagiat ou de contrefaçon, veuillez contacter admin@php.cn

Outils d'IA chauds

Undresser.AI Undress

Undresser.AI Undress

Application basée sur l'IA pour créer des photos de nu réalistes

AI Clothes Remover

AI Clothes Remover

Outil d'IA en ligne pour supprimer les vêtements des photos.

Undress AI Tool

Undress AI Tool

Images de déshabillage gratuites

Clothoff.io

Clothoff.io

Dissolvant de vêtements AI

AI Hentai Generator

AI Hentai Generator

Générez AI Hentai gratuitement.

Article chaud

R.E.P.O. Crystals d'énergie expliqués et ce qu'ils font (cristal jaune)
4 Il y a quelques semaines By 尊渡假赌尊渡假赌尊渡假赌
R.E.P.O. Meilleurs paramètres graphiques
4 Il y a quelques semaines By 尊渡假赌尊渡假赌尊渡假赌
R.E.P.O. Comment réparer l'audio si vous n'entendez personne
4 Il y a quelques semaines By 尊渡假赌尊渡假赌尊渡假赌
R.E.P.O. Commandes de chat et comment les utiliser
4 Il y a quelques semaines By 尊渡假赌尊渡假赌尊渡假赌

Outils chauds

Bloc-notes++7.3.1

Bloc-notes++7.3.1

Éditeur de code facile à utiliser et gratuit

SublimeText3 version chinoise

SublimeText3 version chinoise

Version chinoise, très simple à utiliser

Envoyer Studio 13.0.1

Envoyer Studio 13.0.1

Puissant environnement de développement intégré PHP

Dreamweaver CS6

Dreamweaver CS6

Outils de développement Web visuel

SublimeText3 version Mac

SublimeText3 version Mac

Logiciel d'édition de code au niveau de Dieu (SublimeText3)

WordPress est-il facile pour les débutants? WordPress est-il facile pour les débutants? Apr 03, 2025 am 12:02 AM

WordPress est facile pour les débutants de commencer. 1. Après se connecter à l'arrière-plan, l'interface utilisateur est intuitive et le tableau de bord simple fournit tous les liens de fonction nécessaires. 2. Les opérations de base incluent la création et l'édition de contenu. L'éditeur WYSIWYG simplifie la création de contenu. 3. Les débutants peuvent étendre les fonctions du site Web via des plug-ins et des thèmes, et la courbe d'apprentissage existe mais peut être maîtrisée par la pratique.

À quoi sert WordPress? À quoi sert WordPress? Apr 07, 2025 am 12:06 AM

WordPressisGoodForvirontuallyAnyWebprojectDuetOtsSversatityAsacms.itexcelsin: 1) une convivialité, permettant à la manière

Puis-je apprendre WordPress en 3 jours? Puis-je apprendre WordPress en 3 jours? Apr 09, 2025 am 12:16 AM

Peut apprendre WordPress dans les trois jours. 1. Master les connaissances de base, telles que les thèmes, les plug-ins, etc. 2. Comprenez les fonctions principales, y compris les principes d'installation et de travail. 3. Apprenez l'utilisation de base et avancée à travers des exemples. 4. Comprendre les techniques de débogage et les suggestions d'optimisation des performances.

Dois-je utiliser Wix ou WordPress? Dois-je utiliser Wix ou WordPress? Apr 06, 2025 am 12:11 AM

Wix convient aux utilisateurs qui n'ont aucune expérience de programmation, et WordPress convient aux utilisateurs qui souhaitent plus de capacités de contrôle et d'extension. 1) Wix fournit des éditeurs de glisser-déposer et des modèles riches, ce qui facilite la création d'un site Web rapidement. 2) En tant que CMS open source, WordPress possède un énorme écosystème communautaire et plug-in, soutenant la personnalisation et l'expansion approfondies.

Combien coûte WordPress? Combien coûte WordPress? Apr 05, 2025 am 12:13 AM

WordPress lui-même est gratuit, mais il en coûte supplémentaire à utiliser: 1. WordPress.com propose un package allant du gratuit à payant, avec des prix allant de quelques dollars par mois à des dizaines de dollars; 2. WordPress.org nécessite l'achat d'un nom de domaine (10-20 dollars américains par an) et des services d'hébergement (5-50 dollars américains par mois); 3. La plupart des plug-ins sont gratuits, et le prix payant se situe entre des dizaines et des centaines de dollars; En choisissant le bon service d'hébergement, en utilisant des plug-ins et des thèmes raisonnablement, et en maintenant et en maintenant régulièrement, le coût de WordPress peut être efficacement contrôlé et optimisé.

Pourquoi quelqu'un utiliserait-il WordPress? Pourquoi quelqu'un utiliserait-il WordPress? Apr 02, 2025 pm 02:57 PM

Les gens choisissent d'utiliser WordPress en raison de son pouvoir et de sa flexibilité. 1) WordPress est un CMS open source avec une forte facilité d'utilisation et une évolutivité, adaptée à divers besoins en site Web. 2) Il a des thèmes et des plugins riches, un énorme écosystème et un fort soutien communautaire. 3) Le principe de travail de WordPress est basé sur des thèmes, des plug-ins et des fonctions de base, et utilise PHP et MySQL pour traiter les données, et prend en charge l'optimisation des performances.

WordPress est-il un CMS? WordPress est-il un CMS? Apr 08, 2025 am 12:02 AM

WordPress est un système de gestion de contenu (CMS). Il fournit une gestion de contenu, une gestion des utilisateurs, des thèmes et des capacités de plug-in pour prendre en charge la création et la gestion du contenu du site Web. Son principe de travail comprend la gestion des bases de données, les systèmes de modèles et l'architecture du plug-in, adaptés à une variété de besoins, des blogs aux sites Web d'entreprise.

WordPress est-il toujours gratuit? WordPress est-il toujours gratuit? Apr 04, 2025 am 12:06 AM

La version principale de WordPress est gratuite, mais d'autres frais peuvent être engagés pendant l'utilisation. 1. Les noms de domaine et les services d'hébergement nécessitent un paiement. 2. Des thèmes et des plug-ins avancés peuvent être facturés. 3. Les services professionnels et les fonctionnalités avancées peuvent être facturés.

See all articles