diff --git a/src/rougail/structural_directory/__init__.py b/src/rougail/structural_directory/__init__.py
new file mode 100644
index 000000000..5939dae32
--- /dev/null
+++ b/src/rougail/structural_directory/__init__.py
@@ -0,0 +1,153 @@
+"""
+Silique (https://www.silique.fr)
+Copyright (C) 2022-2024
+
+This program is free software: you can redistribute it and/or modify it
+under the terms of the GNU Lesser General Public License as published by the
+Free Software Foundation, either version 3 of the License, or (at your
+option) any later version.
+
+This program is distributed in the hope that it will be useful, but WITHOUT
+ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
+FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more
+details.
+
+You should have received a copy of the GNU Lesser General Public License
+along with this program. If not, see .
+"""
+
+from typing import Union, List, Iterator, Optional
+from itertools import chain
+from pathlib import Path
+
+from ruamel.yaml import YAML
+
+from ..utils import normalize_family
+from ..path import Paths
+
+
+class Walker:
+ def __init__(
+ self,
+ convert,
+ ) -> None:
+ """Parse directories content"""
+ self.convert = convert
+ self.yaml = YAML()
+ rougailconfig = self.convert.rougailconfig
+ self.sort_dictionaries_all = rougailconfig["sort_dictionaries_all"]
+ if rougailconfig["main_namespace"]:
+ self.convert.paths = Paths(rougailconfig["main_namespace"])
+ self.load_with_extra(
+ rougailconfig["extra_dictionaries"],
+ rougailconfig["main_namespace"],
+ rougailconfig["main_dictionaries"],
+ )
+ else:
+ self.convert.namespace = None
+ namespace_path = ""
+ if namespace_path in self.convert.parents:
+ raise Exception("pfff")
+ for filename in self.get_sorted_filename(
+ rougailconfig["main_dictionaries"]
+ ):
+ self.parse_variable_file(
+ filename,
+ namespace_path,
+ )
+
+ def load_with_extra(
+ self,
+ extra_structures: dict,
+ main_namespace: Optional[str] = None,
+ main_structures: Optional[List[str]] = None,
+ ) -> None:
+ self.convert.has_namespace = True
+ if main_namespace:
+ directory_dict = chain(
+ (
+ (
+ main_namespace,
+ main_structures,
+ ),
+ ),
+ extra_structures.items(),
+ )
+ else:
+ directory_dict = extra_structures.items()
+ for namespace, extra_dirs in directory_dict:
+ # if namespace is None:
+ # self.convert.namespace = namespace
+ # else:
+ self.convert.namespace = normalize_family(namespace)
+ namespace_path = self.convert.namespace
+ if namespace_path in self.convert.parents:
+ raise Exception("pfff")
+ for idx, filename in enumerate(self.get_sorted_filename(extra_dirs)):
+ if not idx:
+ # create only for the first file
+ self.convert.create_namespace(namespace, namespace_path)
+ self.parse_variable_file(
+ filename,
+ namespace_path,
+ )
+
+ def get_sorted_filename(
+ self,
+ directories: Union[str, List[str]],
+ ) -> Iterator[str]:
+ """Sort filename"""
+ if not isinstance(directories, list):
+ directories = [directories]
+ if self.sort_dictionaries_all:
+ filenames = {}
+ for directory_name in directories:
+ directory = Path(directory_name)
+ if not self.sort_dictionaries_all:
+ filenames = {}
+ if directory.is_file():
+ self.get_filename(directory, filenames)
+ else:
+ for file_path in directory.iterdir():
+ self.get_filename(file_path, filenames)
+ if not self.sort_dictionaries_all:
+ for filename in sorted(filenames):
+ yield filenames[filename]
+ if self.sort_dictionaries_all:
+ for filename in sorted(filenames):
+ yield filenames[filename]
+
+ def get_filename(self, file_path, filenames: List[str]) -> None:
+ if file_path.suffix not in [".yml", ".yaml"]:
+ return
+ if file_path.name in filenames:
+ raise DictConsistencyError(
+ _("duplicate dictionary file name {0}").format(file_path.name),
+ 78,
+ [filenames[file_path.name][1]],
+ )
+ filenames[file_path.name] = str(file_path)
+
+ def parse_variable_file(
+ self,
+ filename: str,
+ path: str,
+ ) -> None:
+ """Parse file"""
+ with open(filename, encoding="utf8") as file_fh:
+ objects = self.yaml.load(file_fh)
+ version = self.convert.validate_file_version(
+ objects,
+ filename,
+ )
+ if objects is None:
+ return
+ self.convert.parse_root_file(
+ filename,
+ path,
+ version,
+ objects,
+ )
+
+
+__all__ = ("Walker",)
diff --git a/src/rougail/structural_directory/config.py b/src/rougail/structural_directory/config.py
new file mode 100644
index 000000000..31839357a
--- /dev/null
+++ b/src/rougail/structural_directory/config.py
@@ -0,0 +1,99 @@
+"""
+Silique (https://www.silique.fr)
+Copyright (C) 2024
+
+This program is free software: you can redistribute it and/or modify it
+under the terms of the GNU Lesser General Public License as published by the
+Free Software Foundation, either version 3 of the License, or (at your
+option) any later version.
+
+This program is distributed in the hope that it will be useful, but WITHOUT
+ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
+FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more
+details.
+
+You should have received a copy of the GNU Lesser General Public License
+along with this program. If not, see .
+"""
+
+from ..utils import _
+
+
+def get_rougail_config(
+ *,
+ backward_compatibility=True,
+) -> dict:
+ if backward_compatibility:
+ main_namespace_default = "rougail"
+ else:
+ main_namespace_default = "null"
+ options = f"""main_dictionaries:
+ description: {_("Directories where dictionary files are placed")}
+ type: unix_filename
+ alternative_name: m
+ params:
+ allow_relative: True
+ test_existence: True
+ multi: true
+ disabled:
+ jinja: >-
+ {{% if 'directory' not in step.structural %}}
+ directory is not in step.structural
+ {{% endif %}}
+
+sort_dictionaries_all:
+ description: {_("Sort dictionaries from differents directories")}
+ negative_description: Sort dictionaries directory by directory
+ default: false
+ disabled:
+ jinja: >-
+ {{% if 'directory' not in step.structural %}}
+ directory is not in step.structural
+ {{% endif %}}
+
+main_namespace:
+ description: {_("Main namespace name")}
+ default: {main_namespace_default}
+ alternative_name: s
+ mandatory: false
+ disabled:
+ jinja: >-
+ {{% if 'directory' not in step.structural %}}
+ directory is not in step.structural
+ {{% endif %}}
+
+extra_dictionaries:
+ description: {_("Extra namespaces")}
+ type: leadership
+ disabled:
+ jinja: >-
+ {{% if 'directory' not in step.structural %}}
+ directory is not in step.structural
+ {{% endif %}}
+
+ names:
+ description: {_("Extra namespace name")}
+ alternative_name: xn
+ multi: true
+ mandatory: false
+
+ directories:
+ description: {_("Directories where extra dictionary files are placed")}
+ alternative_name: xd
+ type: unix_filename
+ params:
+ allow_relative: true
+ test_existence: true
+ types:
+ - directory
+ multi: true
+"""
+ return {
+ "name": "directory",
+ "process": "structural",
+ "options": options,
+ "level": 5,
+ }
+
+
+__all__ = "get_rougail_config"