| ... | ... | @@ -15,7 +15,7 @@ arg_parser.add_argument( | 
| 15 | 15 |      "current_branch", metavar="<current-branch>", help="branch for the newest version"
 | 
| 16 | 16 |  )
 | 
| 17 | 17 |  arg_parser.add_argument(
 | 
| 18 |  | -    "filenames", metavar="<filenames>", help="name of the translation files"
 | 
|  | 18 | +    "files", metavar="<files>", help="JSON specifying the translation files"
 | 
| 19 | 19 |  )
 | 
| 20 | 20 |  arg_parser.add_argument("outname", metavar="<json>", help="name of the json output")
 | 
| 21 | 21 |  
 | 
| ... | ... | @@ -67,6 +67,14 @@ def git_lines(git_args: list[str]) -> list[str]: | 
| 67 | 67 |      return [line for line in git_text(git_args).split("\n") if line]
 | 
| 68 | 68 |  
 | 
| 69 | 69 |  
 | 
|  | 70 | +class TranslationFile:
 | 
|  | 71 | +    """Represents a translation file."""
 | 
|  | 72 | +
 | 
|  | 73 | +    def __init__(self, path: str, content: str) -> None:
 | 
|  | 74 | +        self.path = path
 | 
|  | 75 | +        self.content = content
 | 
|  | 76 | +
 | 
|  | 77 | +
 | 
| 70 | 78 |  class BrowserBranch:
 | 
| 71 | 79 |      """Represents a browser git branch."""
 | 
| 72 | 80 |  
 | 
| ... | ... | @@ -134,11 +142,27 @@ class BrowserBranch: | 
| 134 | 142 |      def __gt__(self, other: "BrowserBranch") -> bool:
 | 
| 135 | 143 |          return self._ordered > other._ordered
 | 
| 136 | 144 |  
 | 
| 137 |  | -    def get_file_content(self, filename: str) -> str | None:
 | 
|  | 145 | +    def _matching_dirs(self, path: str, dir_list: list[str]) -> bool:
 | 
|  | 146 | +        """Test that a path is contained in the list of dirs.
 | 
|  | 147 | +
 | 
|  | 148 | +        :param path: The path to check.
 | 
|  | 149 | +        :param dir_list: The list of directories to check against.
 | 
|  | 150 | +        :returns: Whether the path matches.
 | 
|  | 151 | +        """
 | 
|  | 152 | +        for dir_path in dir_list:
 | 
|  | 153 | +            if os.path.commonpath([dir_path, path]) == dir_path:
 | 
|  | 154 | +                return True
 | 
|  | 155 | +        return False
 | 
|  | 156 | +
 | 
|  | 157 | +    def get_file(
 | 
|  | 158 | +        self, filename: str, search_dirs: list[str] | None
 | 
|  | 159 | +    ) -> TranslationFile | None:
 | 
| 138 | 160 |          """Fetch the file content for the named file in this branch.
 | 
| 139 | 161 |  
 | 
| 140 | 162 |          :param filename: The name of the file to fetch the content for.
 | 
| 141 |  | -        :returns: The file content, or `None` if no file could be found.
 | 
|  | 163 | +        :param search_dirs: The directories to restrict the search to, or None
 | 
|  | 164 | +          to search for the file anywhere.
 | 
|  | 165 | +        :returns: The file, or `None` if no file could be found.
 | 
| 142 | 166 |          """
 | 
| 143 | 167 |          if self._file_paths is None:
 | 
| 144 | 168 |              if not self._is_head:
 | 
| ... | ... | @@ -152,7 +176,10 @@ class BrowserBranch: | 
| 152 | 176 |              )
 | 
| 153 | 177 |  
 | 
| 154 | 178 |          matching = [
 | 
| 155 |  | -            path for path in self._file_paths if os.path.basename(path) == filename
 | 
|  | 179 | +            path
 | 
|  | 180 | +            for path in self._file_paths
 | 
|  | 181 | +            if os.path.basename(path) == filename
 | 
|  | 182 | +            and (search_dirs is None or self._matching_dirs(path, search_dirs))
 | 
| 156 | 183 |          ]
 | 
| 157 | 184 |          if not matching:
 | 
| 158 | 185 |              return None
 | 
| ... | ... | @@ -161,7 +188,9 @@ class BrowserBranch: | 
| 161 | 188 |  
 | 
| 162 | 189 |          path = matching[0]
 | 
| 163 | 190 |  
 | 
| 164 |  | -        return git_text(["cat-file", "blob", f"{self._ref}:{path}"])
 | 
|  | 191 | +        return TranslationFile(
 | 
|  | 192 | +            path=path, content=git_text(["cat-file", "blob", f"{self._ref}:{path}"])
 | 
|  | 193 | +        )
 | 
| 165 | 194 |  
 | 
| 166 | 195 |  
 | 
| 167 | 196 |  def get_stable_branch(
 | 
| ... | ... | @@ -254,48 +283,63 @@ if os.environ.get("TRANSLATION_INCLUDE_LEGACY", "") != "true": | 
| 254 | 283 |  
 | 
| 255 | 284 |  files_list = []
 | 
| 256 | 285 |  
 | 
| 257 |  | -for translation_branch, name in (
 | 
| 258 |  | -    part.strip().split(":", 1) for part in args.filenames.split(" ") if part.strip()
 | 
| 259 |  | -):
 | 
| 260 |  | -    current_content = current_branch.get_file_content(name)
 | 
| 261 |  | -    stable_content = stable_branch.get_file_content(name)
 | 
|  | 286 | +for file_dict in json.loads(args.files):
 | 
|  | 287 | +    name = file_dict["name"]
 | 
|  | 288 | +    where_dirs = file_dict.get("where", None)
 | 
|  | 289 | +    current_file = current_branch.get_file(name, where_dirs)
 | 
|  | 290 | +    stable_file = stable_branch.get_file(name, where_dirs)
 | 
| 262 | 291 |  
 | 
| 263 |  | -    if current_content is None and stable_content is None:
 | 
|  | 292 | +    if current_file is None and stable_file is None:
 | 
| 264 | 293 |          # No file in either branch.
 | 
| 265 | 294 |          logger.warning(f"{name} does not exist in either the current or stable branch")
 | 
| 266 |  | -    elif current_content is None:
 | 
|  | 295 | +    elif current_file is None:
 | 
| 267 | 296 |          logger.warning(f"{name} deleted in the current branch")
 | 
| 268 |  | -    elif stable_content is None:
 | 
|  | 297 | +    elif stable_file is None:
 | 
| 269 | 298 |          logger.warning(f"{name} does not exist in the stable branch")
 | 
|  | 299 | +    elif current_file.path != stable_file.path:
 | 
|  | 300 | +        logger.warning(
 | 
|  | 301 | +            f"{name} has different paths in the current and stable branch. "
 | 
|  | 302 | +            f"{current_file.path} : {stable_file.path}"
 | 
|  | 303 | +        )
 | 
| 270 | 304 |  
 | 
| 271 | 305 |      content = combine_files(
 | 
| 272 | 306 |          name,
 | 
| 273 |  | -        current_content,
 | 
| 274 |  | -        stable_content,
 | 
|  | 307 | +        None if current_file is None else current_file.content,
 | 
|  | 308 | +        None if stable_file is None else stable_file.content,
 | 
| 275 | 309 |          f"Will be unused in Tor Browser {current_branch.browser_version}!",
 | 
| 276 | 310 |      )
 | 
| 277 | 311 |  
 | 
| 278 | 312 |      if legacy_branch:
 | 
| 279 |  | -        legacy_content = legacy_branch.get_file_content(name)
 | 
| 280 |  | -        if (
 | 
| 281 |  | -            legacy_content is not None
 | 
| 282 |  | -            and current_content is None
 | 
| 283 |  | -            and stable_content is None
 | 
| 284 |  | -        ):
 | 
|  | 313 | +        legacy_file = legacy_branch.get_file(name, where_dirs)
 | 
|  | 314 | +        if legacy_file is not None and current_file is None and stable_file is None:
 | 
| 285 | 315 |              logger.warning(f"{name} still exists in the legacy branch")
 | 
| 286 |  | -        elif legacy_content is None:
 | 
|  | 316 | +        elif legacy_file is None:
 | 
| 287 | 317 |              logger.warning(f"{name} does not exist in the legacy branch")
 | 
|  | 318 | +        elif stable_file is not None and legacy_file.path != stable_file.path:
 | 
|  | 319 | +            logger.warning(
 | 
|  | 320 | +                f"{name} has different paths in the stable and legacy branch. "
 | 
|  | 321 | +                f"{stable_file.path} : {legacy_file.path}"
 | 
|  | 322 | +            )
 | 
|  | 323 | +        elif current_file is not None and legacy_file.path != current_file.path:
 | 
|  | 324 | +            logger.warning(
 | 
|  | 325 | +                f"{name} has different paths in the current and legacy branch. "
 | 
|  | 326 | +                f"{current_file.path} : {legacy_file.path}"
 | 
|  | 327 | +            )
 | 
|  | 328 | +
 | 
| 288 | 329 |          content = combine_files(
 | 
| 289 | 330 |              name,
 | 
| 290 | 331 |              content,
 | 
| 291 |  | -            legacy_content,
 | 
|  | 332 | +            legacy_file.content,
 | 
| 292 | 333 |              f"Unused in Tor Browser {stable_branch.browser_version}!",
 | 
| 293 | 334 |          )
 | 
| 294 | 335 |  
 | 
| 295 | 336 |      files_list.append(
 | 
| 296 | 337 |          {
 | 
| 297 | 338 |              "name": name,
 | 
| 298 |  | -            "branch": translation_branch,
 | 
|  | 339 | +            # If "directory" is unspecified, we place the file directly beneath
 | 
|  | 340 | +            # en-US/ in the translation repository. i.e. "".
 | 
|  | 341 | +            "directory": file_dict.get("directory", ""),
 | 
|  | 342 | +            "branch": file_dict["branch"],
 | 
| 299 | 343 |              "content": content,
 | 
| 300 | 344 |          }
 | 
| 301 | 345 |      )
 |