Pier Angelo Vendrame pushed to branch mullvad-browser-128.6.0esr-14.5-1 at The Tor Project / Applications / Mullvad Browser

Commits:

11 changed files:

Changes:

  • .gitlab-ci.yml
    1 1
     stages:
    
    2 2
       - lint
    
    3
    +  - update-translations
    
    3 4
     
    
    4 5
     variables:
    
    5 6
       IMAGE_PATH: containers.torproject.org/tpo/applications/tor-browser/base:latest
    

  • .gitlab/ci/jobs/update-translations.yml
    1
    +.update-translation-base:
    
    2
    +  stage: update-translations
    
    3
    +  rules:
    
    4
    +    - if: ($TRANSLATION_FILES != "" && $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH && $CI_PIPELINE_SOURCE == "push")
    
    5
    +      changes:
    
    6
    +        - "**/*.ftl"
    
    7
    +        - "**/*.properties"
    
    8
    +        - "**/*.dtd"
    
    9
    +        - "**/*strings.xml"
    
    10
    +        - "**/update-translations.yml"
    
    11
    +        - "**/l10n/combine/combine.py"
    
    12
    +        - "**/l10n/combine-translation-versions.py"
    
    13
    +    - if: ($TRANSLATION_FILES != "" && $FORCE_UPDATE_TRANSLATIONS == "true")
    
    14
    +  variables:
    
    15
    +    COMBINED_FILES_JSON: "combined-translation-files.json"
    
    16
    +    TRANSLATION_FILES: '[
    
    17
    +      {
    
    18
    +        "name": "brand.ftl",
    
    19
    +        "where": ["browser/branding/mb-release"],
    
    20
    +        "branch": "mullvad-browser",
    
    21
    +        "directory": "browser/branding/mb-release"
    
    22
    +      },
    
    23
    +      {
    
    24
    +        "name": "brand.properties",
    
    25
    +        "where": ["browser/branding/mb-release"],
    
    26
    +        "branch": "mullvad-browser",
    
    27
    +        "directory": "browser/branding/mb-release"
    
    28
    +      },
    
    29
    +      {
    
    30
    +        "name": "brand.ftl",
    
    31
    +        "where": ["browser/branding/mb-alpha"],
    
    32
    +        "branch": "mullvad-browser",
    
    33
    +        "directory": "browser/branding/mb-alpha"
    
    34
    +      },
    
    35
    +      {
    
    36
    +        "name": "brand.properties",
    
    37
    +        "where": ["browser/branding/mb-alpha"],
    
    38
    +        "branch": "mullvad-browser",
    
    39
    +        "directory": "browser/branding/mb-alpha"
    
    40
    +      },
    
    41
    +      {
    
    42
    +        "name": "brand.ftl",
    
    43
    +        "where": ["browser/branding/mb-nightly"],
    
    44
    +        "branch": "mullvad-browser",
    
    45
    +        "directory": "browser/branding/mb-nightly"
    
    46
    +      },
    
    47
    +      {
    
    48
    +        "name": "brand.properties",
    
    49
    +        "where": ["browser/branding/mb-nightly"],
    
    50
    +        "branch": "mullvad-browser",
    
    51
    +        "directory": "browser/branding/mb-nightly"
    
    52
    +      },
    
    53
    +      {
    
    54
    +        "name": "mullvad-browser.ftl",
    
    55
    +        "branch": "mullvad-browser",
    
    56
    +        "directory": "toolkit/toolkit/global"
    
    57
    +      },
    
    58
    +    ]'
    
    59
    +
    
    60
    +
    
    61
    +combine-en-US-translations:
    
    62
    +  extends: .update-translation-base
    
    63
    +  needs: []
    
    64
    +  image: python
    
    65
    +  variables:
    
    66
    +    PIP_CACHE_DIR: "$CI_PROJECT_DIR/.cache/pip"
    
    67
    +  cache:
    
    68
    +    paths:
    
    69
    +      - .cache/pip
    
    70
    +  # Artifact is for translation project job
    
    71
    +  artifacts:
    
    72
    +    paths:
    
    73
    +      - "$COMBINED_FILES_JSON"
    
    74
    +    expire_in: "60 min"
    
    75
    +    reports:
    
    76
    +      dotenv: job_id.env
    
    77
    +  # Don't load artifacts for this job.
    
    78
    +  dependencies: []
    
    79
    +  script:
    
    80
    +    # Save this CI_JOB_ID to the dotenv file to be used in the variables for the
    
    81
    +    # push-en-US-translations job.
    
    82
    +    - echo 'COMBINE_TRANSLATIONS_JOB_ID='"$CI_JOB_ID" >job_id.env
    
    83
    +    - pip install compare_locales
    
    84
    +    - python ./tools/base-browser/l10n/combine-translation-versions.py "$CI_COMMIT_BRANCH" "$TRANSLATION_FILES" "$COMBINED_FILES_JSON"
    
    85
    +
    
    86
    +push-en-US-translations:
    
    87
    +  extends: .update-translation-base
    
    88
    +  needs:
    
    89
    +    - job: combine-en-US-translations
    
    90
    +  variables:
    
    91
    +    COMBINED_FILES_JSON_URL: "${CI_API_V4_URL}/projects/${CI_PROJECT_ID}/jobs/${COMBINE_TRANSLATIONS_JOB_ID}/artifacts/${COMBINED_FILES_JSON}"
    
    92
    +  trigger:
    
    93
    +    strategy: depend
    
    94
    +    project: tor-browser-translation-bot/translation
    
    95
    +    branch: tor-browser-ci

  • tools/base-browser/l10n/combine-translation-versions.py
    1
    +import argparse
    
    2
    +import json
    
    3
    +import logging
    
    4
    +import os
    
    5
    +import re
    
    6
    +import subprocess
    
    7
    +
    
    8
    +from combine import combine_files
    
    9
    +
    
    10
    +arg_parser = argparse.ArgumentParser(
    
    11
    +    description="Combine a translation file across two different versions"
    
    12
    +)
    
    13
    +
    
    14
    +arg_parser.add_argument(
    
    15
    +    "current_branch", metavar="<current-branch>", help="branch for the newest version"
    
    16
    +)
    
    17
    +arg_parser.add_argument(
    
    18
    +    "files", metavar="<files>", help="JSON specifying the translation files"
    
    19
    +)
    
    20
    +arg_parser.add_argument("outname", metavar="<json>", help="name of the json output")
    
    21
    +
    
    22
    +args = arg_parser.parse_args()
    
    23
    +
    
    24
    +logging.basicConfig()
    
    25
    +logger = logging.getLogger("combine-translation-versions")
    
    26
    +logger.setLevel(logging.INFO)
    
    27
    +
    
    28
    +
    
    29
    +def in_pink(msg: str) -> str:
    
    30
    +    """Present a message as pink in the terminal output.
    
    31
    +
    
    32
    +    :param msg: The message to wrap in pink.
    
    33
    +    :returns: The message to print to terminal.
    
    34
    +    """
    
    35
    +    # Pink and bold.
    
    36
    +    return f"\x1b[1;38;5;212m{msg}\x1b[0m"
    
    37
    +
    
    38
    +
    
    39
    +def git_run(git_args: list[str]) -> None:
    
    40
    +    """Run a git command.
    
    41
    +
    
    42
    +    :param git_args: The arguments that should follow "git".
    
    43
    +    """
    
    44
    +    # Add some text to give context to git's stderr appearing in log.
    
    45
    +    logger.info("Running: " + in_pink("git " + " ".join(git_args)))
    
    46
    +    subprocess.run(["git", *git_args], check=True)
    
    47
    +
    
    48
    +
    
    49
    +def git_text(git_args: list[str]) -> str:
    
    50
    +    """Get the text output for a git command.
    
    51
    +
    
    52
    +    :param git_args: The arguments that should follow "git".
    
    53
    +    :returns: The stdout of the command.
    
    54
    +    """
    
    55
    +    logger.info("Running: " + in_pink("git " + " ".join(git_args)))
    
    56
    +    return subprocess.run(
    
    57
    +        ["git", *git_args], text=True, check=True, stdout=subprocess.PIPE
    
    58
    +    ).stdout
    
    59
    +
    
    60
    +
    
    61
    +def git_lines(git_args: list[str]) -> list[str]:
    
    62
    +    """Get the lines from a git command.
    
    63
    +
    
    64
    +    :param git_args: The arguments that should follow "git".
    
    65
    +    :returns: The non-empty lines from stdout of the command.
    
    66
    +    """
    
    67
    +    return [line for line in git_text(git_args).split("\n") if line]
    
    68
    +
    
    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
    +
    
    78
    +class BrowserBranch:
    
    79
    +    """Represents a browser git branch."""
    
    80
    +
    
    81
    +    def __init__(self, branch_name: str, is_head: bool = False) -> None:
    
    82
    +        """Create a new instance.
    
    83
    +
    
    84
    +        :param branch_name: The branch's git name.
    
    85
    +        :param is_head: Whether the branch matches "HEAD".
    
    86
    +        """
    
    87
    +        version_match = re.match(
    
    88
    +            r"(?P<prefix>[a-z]+\-browser)\-"
    
    89
    +            r"(?P<firefox>[0-9]+(?:\.[0-9]+){1,2})esr\-"
    
    90
    +            r"(?P<browser>[0-9]+\.[05])\-"
    
    91
    +            r"(?P<number>[0-9]+)$",
    
    92
    +            branch_name,
    
    93
    +        )
    
    94
    +
    
    95
    +        if not version_match:
    
    96
    +            raise ValueError(f"Unable to parse the version from the ref {branch_name}")
    
    97
    +
    
    98
    +        self.name = branch_name
    
    99
    +        self.prefix = version_match.group("prefix")
    
    100
    +        self.browser_version = version_match.group("browser")
    
    101
    +        self._is_head = is_head
    
    102
    +        self._ref = "HEAD" if is_head else f"origin/{branch_name}"
    
    103
    +
    
    104
    +        firefox_nums = [int(n) for n in version_match.group("firefox").split(".")]
    
    105
    +        if len(firefox_nums) == 2:
    
    106
    +            firefox_nums.append(0)
    
    107
    +        browser_nums = [int(n) for n in self.browser_version.split(".")]
    
    108
    +        branch_number = int(version_match.group("number"))
    
    109
    +        # Prioritise the firefox ESR version, then the browser version then the
    
    110
    +        # branch number.
    
    111
    +        self._ordered = (
    
    112
    +            firefox_nums[0],
    
    113
    +            firefox_nums[1],
    
    114
    +            firefox_nums[2],
    
    115
    +            browser_nums[0],
    
    116
    +            browser_nums[1],
    
    117
    +            branch_number,
    
    118
    +        )
    
    119
    +
    
    120
    +        # Minor version for browser is only ever "0" or "5", so we can convert
    
    121
    +        # the version to an integer.
    
    122
    +        self._browser_int_version = int(2 * float(self.browser_version))
    
    123
    +
    
    124
    +        self._file_paths: list[str] | None = None
    
    125
    +
    
    126
    +    def release_below(self, other: "BrowserBranch", num: int) -> bool:
    
    127
    +        """Determine whether another branch is within range of a previous
    
    128
    +        browser release.
    
    129
    +
    
    130
    +        The browser versions are expected to increment by "0.5", and a previous
    
    131
    +        release branch's version is expected to be `num * 0.5` behind the
    
    132
    +        current one.
    
    133
    +
    
    134
    +        :param other: The branch to compare.
    
    135
    +        :param num: The number of "0.5" releases behind to test with.
    
    136
    +        """
    
    137
    +        return other._browser_int_version == self._browser_int_version - num
    
    138
    +
    
    139
    +    def __lt__(self, other: "BrowserBranch") -> bool:
    
    140
    +        return self._ordered < other._ordered
    
    141
    +
    
    142
    +    def __gt__(self, other: "BrowserBranch") -> bool:
    
    143
    +        return self._ordered > other._ordered
    
    144
    +
    
    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:
    
    160
    +        """Fetch the file content for the named file in this branch.
    
    161
    +
    
    162
    +        :param filename: The name of the file to fetch the content for.
    
    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.
    
    166
    +        """
    
    167
    +        if self._file_paths is None:
    
    168
    +            if not self._is_head:
    
    169
    +                # Minimal fetch of non-HEAD branch to get the file paths.
    
    170
    +                # Individual file blobs will be downloaded as needed.
    
    171
    +                git_run(
    
    172
    +                    ["fetch", "--depth=1", "--filter=blob:none", "origin", self.name]
    
    173
    +                )
    
    174
    +            self._file_paths = git_lines(
    
    175
    +                ["ls-tree", "-r", "--format=%(path)", self._ref]
    
    176
    +            )
    
    177
    +
    
    178
    +        matching = [
    
    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))
    
    183
    +        ]
    
    184
    +        if not matching:
    
    185
    +            return None
    
    186
    +        if len(matching) > 1:
    
    187
    +            raise Exception(f"Multiple occurrences of {filename}")
    
    188
    +
    
    189
    +        path = matching[0]
    
    190
    +
    
    191
    +        return TranslationFile(
    
    192
    +            path=path, content=git_text(["cat-file", "blob", f"{self._ref}:{path}"])
    
    193
    +        )
    
    194
    +
    
    195
    +
    
    196
    +def get_stable_branch(
    
    197
    +    compare_version: BrowserBranch,
    
    198
    +) -> tuple[BrowserBranch, BrowserBranch | None]:
    
    199
    +    """Find the most recent stable branch in the origin repository.
    
    200
    +
    
    201
    +    :param compare_version: The development branch to compare against.
    
    202
    +    :returns: The stable and legacy branches. If no legacy branch is found,
    
    203
    +      `None` will be returned instead.
    
    204
    +    """
    
    205
    +    # We search for build1 tags. These are added *after* the rebase of browser
    
    206
    +    # commits, so the corresponding branch should contain our strings.
    
    207
    +    # Moreover, we *assume* that the branch with the most recent ESR version
    
    208
    +    # with such a tag will be used in the *next* stable build in
    
    209
    +    # tor-browser-build.
    
    210
    +    tag_glob = f"{compare_version.prefix}-*-build1"
    
    211
    +
    
    212
    +    # To speed up, only fetch the tags without blobs.
    
    213
    +    git_run(
    
    214
    +        ["fetch", "--depth=1", "--filter=object:type=tag", "origin", "tag", tag_glob]
    
    215
    +    )
    
    216
    +    stable_branches = []
    
    217
    +    legacy_branches = []
    
    218
    +    stable_annotation_regex = re.compile(r"\bstable\b")
    
    219
    +    legacy_annotation_regex = re.compile(r"\blegacy\b")
    
    220
    +    tag_pattern = re.compile(
    
    221
    +        rf"^{re.escape(compare_version.prefix)}-[^-]+esr-[^-]+-[^-]+-build1$"
    
    222
    +    )
    
    223
    +
    
    224
    +    for build_tag, annotation in (
    
    225
    +        line.split(" ", 1) for line in git_lines(["tag", "-n1", "--list", tag_glob])
    
    226
    +    ):
    
    227
    +        if not tag_pattern.match(build_tag):
    
    228
    +            continue
    
    229
    +        is_stable = bool(stable_annotation_regex.search(annotation))
    
    230
    +        is_legacy = bool(legacy_annotation_regex.search(annotation))
    
    231
    +        if not is_stable and not is_legacy:
    
    232
    +            continue
    
    233
    +        try:
    
    234
    +            # Branch name is the same as the tag, minus "-build1".
    
    235
    +            branch = BrowserBranch(re.sub(r"-build1$", "", build_tag))
    
    236
    +        except ValueError:
    
    237
    +            logger.warning(f"Could not read the version for {build_tag}")
    
    238
    +            continue
    
    239
    +        if branch.prefix != compare_version.prefix:
    
    240
    +            continue
    
    241
    +        if is_stable:
    
    242
    +            # Stable can be one release version behind.
    
    243
    +            # NOTE: In principle, when switching between versions there may be a
    
    244
    +            # window of time where the development branch has not yet progressed
    
    245
    +            # to the next "0.5" release, so has the same browser version as the
    
    246
    +            # stable branch. So we also allow for matching browser versions.
    
    247
    +            # NOTE:
    
    248
    +            # 1. The "Will be unused in" message will not make sense, but we do
    
    249
    +            #    not expect string differences in this scenario.
    
    250
    +            # 2. We do not expect this scenario to last for long.
    
    251
    +            if not (
    
    252
    +                compare_version.release_below(branch, 1)
    
    253
    +                or compare_version.release_below(branch, 0)
    
    254
    +            ):
    
    255
    +                continue
    
    256
    +            stable_branches.append(branch)
    
    257
    +        elif is_legacy:
    
    258
    +            # Legacy can be two release versions behind.
    
    259
    +            # We also allow for being just one version behind.
    
    260
    +            if not (
    
    261
    +                compare_version.release_below(branch, 2)
    
    262
    +                or compare_version.release_below(branch, 1)
    
    263
    +            ):
    
    264
    +                continue
    
    265
    +            legacy_branches.append(branch)
    
    266
    +
    
    267
    +    if not stable_branches:
    
    268
    +        raise Exception("No stable build1 branch found")
    
    269
    +
    
    270
    +    return (
    
    271
    +        # Return the stable branch with the highest version.
    
    272
    +        max(stable_branches),
    
    273
    +        max(legacy_branches) if legacy_branches else None,
    
    274
    +    )
    
    275
    +
    
    276
    +
    
    277
    +current_branch = BrowserBranch(args.current_branch, is_head=True)
    
    278
    +
    
    279
    +stable_branch, legacy_branch = get_stable_branch(current_branch)
    
    280
    +
    
    281
    +if os.environ.get("TRANSLATION_INCLUDE_LEGACY", "") != "true":
    
    282
    +    legacy_branch = None
    
    283
    +
    
    284
    +files_list = []
    
    285
    +
    
    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)
    
    291
    +
    
    292
    +    if current_file is None and stable_file is None:
    
    293
    +        # No file in either branch.
    
    294
    +        logger.warning(f"{name} does not exist in either the current or stable branch")
    
    295
    +    elif current_file is None:
    
    296
    +        logger.warning(f"{name} deleted in the current branch")
    
    297
    +    elif stable_file is None:
    
    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
    +        )
    
    304
    +
    
    305
    +    content = combine_files(
    
    306
    +        name,
    
    307
    +        None if current_file is None else current_file.content,
    
    308
    +        None if stable_file is None else stable_file.content,
    
    309
    +        f"Will be unused in Tor Browser {current_branch.browser_version}!",
    
    310
    +    )
    
    311
    +
    
    312
    +    if legacy_branch and not file_dict.get("exclude-legacy", False):
    
    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:
    
    315
    +            logger.warning(f"{name} still exists in the legacy branch")
    
    316
    +        elif legacy_file is None:
    
    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
    +
    
    329
    +        content = combine_files(
    
    330
    +            name,
    
    331
    +            content,
    
    332
    +            legacy_file.content,
    
    333
    +            f"Unused in Tor Browser {stable_branch.browser_version}!",
    
    334
    +        )
    
    335
    +    elif legacy_branch:
    
    336
    +        logger.info(f"Excluding legacy branch for {name}")
    
    337
    +
    
    338
    +    files_list.append(
    
    339
    +        {
    
    340
    +            "name": name,
    
    341
    +            # If "directory" is unspecified, we place the file directly beneath
    
    342
    +            # en-US/ in the translation repository. i.e. "".
    
    343
    +            "directory": file_dict.get("directory", ""),
    
    344
    +            "branch": file_dict["branch"],
    
    345
    +            "content": content,
    
    346
    +        }
    
    347
    +    )
    
    348
    +
    
    349
    +
    
    350
    +ci_commit = os.environ.get("CI_COMMIT_SHA", "")
    
    351
    +ci_url_base = os.environ.get("CI_PROJECT_URL", "")
    
    352
    +
    
    353
    +json_data = {
    
    354
    +    "commit": ci_commit,
    
    355
    +    "commit-url": f"{ci_url_base}/-/commit/{ci_commit}"
    
    356
    +    if (ci_commit and ci_url_base)
    
    357
    +    else "",
    
    358
    +    "project-path": os.environ.get("CI_PROJECT_PATH", ""),
    
    359
    +    "current-branch": current_branch.name,
    
    360
    +    "stable-branch": stable_branch.name,
    
    361
    +    "files": files_list,
    
    362
    +}
    
    363
    +
    
    364
    +if legacy_branch:
    
    365
    +    json_data["legacy-branch"] = legacy_branch.name
    
    366
    +
    
    367
    +with open(args.outname, "w") as file:
    
    368
    +    json.dump(json_data, file)

  • tools/base-browser/l10n/combine/__init__.py
    1
    +# flake8: noqa
    
    2
    +
    
    3
    +from .combine import combine_files

  • tools/base-browser/l10n/combine/combine.py
    1
    +import re
    
    2
    +from typing import TYPE_CHECKING, Any
    
    3
    +
    
    4
    +from compare_locales.parser import getParser
    
    5
    +from compare_locales.parser.android import AndroidEntity, DocumentWrapper
    
    6
    +from compare_locales.parser.base import Comment, Entity, Junk, Whitespace
    
    7
    +from compare_locales.parser.dtd import DTDEntity
    
    8
    +from compare_locales.parser.fluent import FluentComment, FluentEntity
    
    9
    +from compare_locales.parser.properties import PropertiesEntity
    
    10
    +
    
    11
    +if TYPE_CHECKING:
    
    12
    +    from collections.abc import Iterable
    
    13
    +
    
    14
    +
    
    15
    +def combine_files(
    
    16
    +    filename: str,
    
    17
    +    new_content: str | None,
    
    18
    +    old_content: str | None,
    
    19
    +    comment_prefix: str,
    
    20
    +) -> str | None:
    
    21
    +    """Combine two translation files into one to include all strings from both.
    
    22
    +    The new content is presented first, and any strings only found in the old
    
    23
    +    content are placed at the end with an additional comment.
    
    24
    +
    
    25
    +    :param filename: The filename for the file, determines the format.
    
    26
    +    :param new_content: The new content for the file, or None if it has been
    
    27
    +      deleted.
    
    28
    +    :param old_content: The old content for the file, or None if it did not
    
    29
    +      exist before.
    
    30
    +    :comment_prefix: A comment to include for any strings that are only found in
    
    31
    +      the old content. This will be placed before any other comments for the
    
    32
    +      string.
    
    33
    +
    
    34
    +    :returns: The combined content, or None if both given contents are None.
    
    35
    +    """
    
    36
    +    if new_content is None and old_content is None:
    
    37
    +        return None
    
    38
    +
    
    39
    +    # getParser from compare_locale returns the same instance for the same file
    
    40
    +    # extension.
    
    41
    +    parser = getParser(filename)
    
    42
    +
    
    43
    +    is_android = filename.endswith(".xml")
    
    44
    +    if new_content is None:
    
    45
    +        if is_android:
    
    46
    +            # File was deleted, add some document parts.
    
    47
    +            content_start = (
    
    48
    +                '<?xml version="1.0" encoding="utf-8" standalone="yes"?>\n<resources>\n'
    
    49
    +            )
    
    50
    +            content_end = "</resources>\n"
    
    51
    +        else:
    
    52
    +            # Treat as an empty file.
    
    53
    +            content_start = ""
    
    54
    +            content_end = ""
    
    55
    +        existing_keys = []
    
    56
    +    else:
    
    57
    +        parser.readUnicode(new_content)
    
    58
    +
    
    59
    +        # Start with the same content as the current file.
    
    60
    +        # For android strings, we want to keep the final "</resources>" until after.
    
    61
    +        if is_android:
    
    62
    +            closing_match = re.match(
    
    63
    +                r"^(.*)(</resources>\s*)$", parser.ctx.contents, re.DOTALL
    
    64
    +            )
    
    65
    +            if not closing_match:
    
    66
    +                raise ValueError("Missing a final </resources>")
    
    67
    +            content_start = closing_match.group(1)
    
    68
    +            content_end = closing_match.group(2)
    
    69
    +        else:
    
    70
    +            content_start = parser.ctx.contents
    
    71
    +            content_end = ""
    
    72
    +        existing_keys = [entry.key for entry in parser.walk(only_localizable=True)]
    
    73
    +
    
    74
    +    # For Fluent, we want to prefix the strings using GroupComments.
    
    75
    +    # On weblate this will cause all the strings that fall under the GroupComment's
    
    76
    +    # scope to have the prefix added to their "notes".
    
    77
    +    # We set up an initial GroupComment for the first string we find. This will also
    
    78
    +    # end the scope of the last GroupComment in the new translation file.
    
    79
    +    # This will be replaced with a the next GroupComment when it is found.
    
    80
    +    fluent_group_comment_prefix = f"\n## {comment_prefix}\n"
    
    81
    +    fluent_group_comment: str | None = fluent_group_comment_prefix
    
    82
    +
    
    83
    +    # For other formats, we want to keep all the comment lines that come directly
    
    84
    +    # before the string.
    
    85
    +    # In compare_locales.parser, only the comment line directly before an Entity
    
    86
    +    # counts as the pre_comment for that Entity. I.e. only this line will be
    
    87
    +    # included in Entity.all
    
    88
    +    # However, in weblate every comment line that comes before the Entity is
    
    89
    +    # included as a comment. So we also want to keep these additional comments to
    
    90
    +    # preserve them for weblate.
    
    91
    +    # We gather these extra comments in stacked_comments, and clear them whenever we
    
    92
    +    # reach an Entity or a blank line (Whitespace is more than "\n").
    
    93
    +    stacked_comments: list[str] = []
    
    94
    +
    
    95
    +    additions: list[str] = []
    
    96
    +
    
    97
    +    entry_iter: Iterable[Any] = ()
    
    98
    +    # If the file does not exist in the old branch, don't make any additions.
    
    99
    +    if old_content is not None:
    
    100
    +        parser.readUnicode(old_content)
    
    101
    +        entry_iter = parser.walk(only_localizable=False)
    
    102
    +    for entry in entry_iter:
    
    103
    +        if isinstance(entry, Junk):
    
    104
    +            raise ValueError(f"Unexpected Junk: {entry.all}")
    
    105
    +        if isinstance(entry, Whitespace):
    
    106
    +            # Clear stacked comments if more than one empty line.
    
    107
    +            if entry.all != "\n":
    
    108
    +                stacked_comments.clear()
    
    109
    +            continue
    
    110
    +        if isinstance(entry, Comment):
    
    111
    +            if isinstance(entry, FluentComment):
    
    112
    +                # Don't stack Fluent comments.
    
    113
    +                # Only the comments included in Entity.pre_comment count towards
    
    114
    +                # that Entity's comment.
    
    115
    +                if entry.all.startswith("##"):
    
    116
    +                    # A Fluent GroupComment
    
    117
    +                    if entry.all == "##":
    
    118
    +                        # Empty GroupComment. Used to end the scope of a previous
    
    119
    +                        # GroupComment.
    
    120
    +                        # Replace this with our prefix comment.
    
    121
    +                        fluent_group_comment = fluent_group_comment_prefix
    
    122
    +                    else:
    
    123
    +                        # Prefix the group comment.
    
    124
    +                        fluent_group_comment = (
    
    125
    +                            f"{fluent_group_comment_prefix}{entry.all}\n"
    
    126
    +                        )
    
    127
    +            else:
    
    128
    +                stacked_comments.append(entry.all)
    
    129
    +            continue
    
    130
    +        if isinstance(entry, DocumentWrapper):
    
    131
    +            # Not needed.
    
    132
    +            continue
    
    133
    +
    
    134
    +        if not isinstance(entry, Entity):
    
    135
    +            raise ValueError(f"Unexpected type: {entry.__class__.__name__}")
    
    136
    +
    
    137
    +        if entry.key in existing_keys:
    
    138
    +            # Already included this string in the new translation file.
    
    139
    +            # Drop the gathered comments for this Entity.
    
    140
    +            stacked_comments.clear()
    
    141
    +            continue
    
    142
    +
    
    143
    +        if isinstance(entry, FluentEntity):
    
    144
    +            if fluent_group_comment is not None:
    
    145
    +                # We have a found GroupComment which has not been included yet.
    
    146
    +                # All following Entity's will be under its scope, until the next
    
    147
    +                # GroupComment.
    
    148
    +                additions.append(fluent_group_comment)
    
    149
    +                # Added GroupComment, so don't need to add again.
    
    150
    +                fluent_group_comment = None
    
    151
    +        elif isinstance(entry, DTDEntity):
    
    152
    +            # Include our additional comment before we print the rest for this
    
    153
    +            # Entity.
    
    154
    +            additions.append(f"<!-- LOCALIZATION NOTE: {comment_prefix} -->")
    
    155
    +        elif isinstance(entry, PropertiesEntity):
    
    156
    +            additions.append(f"# {comment_prefix}")
    
    157
    +        elif isinstance(entry, AndroidEntity):
    
    158
    +            additions.append(f"<!-- {comment_prefix} -->")
    
    159
    +        else:
    
    160
    +            raise ValueError(f"Unexpected Entity type: {entry.__class__.__name__}")
    
    161
    +
    
    162
    +        # Add any other comment lines that came directly before this Entity.
    
    163
    +        additions.extend(stacked_comments)
    
    164
    +        stacked_comments.clear()
    
    165
    +        additions.append(entry.all)
    
    166
    +
    
    167
    +    content_middle = ""
    
    168
    +
    
    169
    +    if additions:
    
    170
    +        # New line before and after the additions
    
    171
    +        additions.insert(0, "")
    
    172
    +        additions.append("")
    
    173
    +        if is_android:
    
    174
    +            content_middle = "\n    ".join(additions)
    
    175
    +        else:
    
    176
    +            content_middle = "\n".join(additions)
    
    177
    +
    
    178
    +        # Remove " " in otherwise blank lines.
    
    179
    +        content_middle = re.sub("^ +$", "", content_middle, flags=re.MULTILINE)
    
    180
    +
    
    181
    +    return content_start + content_middle + content_end

  • tools/base-browser/l10n/combine/tests/README
    1
    +python tests to be run with pytest.
    
    2
    +Requires the compare-locales package.

  • tools/base-browser/l10n/combine/tests/__init__.py

  • tools/base-browser/l10n/combine/tests/test_android.py
    1
    +import textwrap
    
    2
    +
    
    3
    +from combine import combine_files
    
    4
    +
    
    5
    +
    
    6
    +def wrap_in_xml(content):
    
    7
    +    if content is None:
    
    8
    +        return None
    
    9
    +    # Allow for indents to make the tests more readable.
    
    10
    +    content = textwrap.dedent(content)
    
    11
    +    return f"""\
    
    12
    +<?xml version="1.0" encoding="utf-8" standalone="yes"?>
    
    13
    +<resources>
    
    14
    +{textwrap.indent(content, "    ")}</resources>
    
    15
    +"""
    
    16
    +
    
    17
    +
    
    18
    +def assert_result(new_content, old_content, expect):
    
    19
    +    new_content = wrap_in_xml(new_content)
    
    20
    +    old_content = wrap_in_xml(old_content)
    
    21
    +    expect = wrap_in_xml(expect)
    
    22
    +    assert expect == combine_files(
    
    23
    +        "test_strings.xml", new_content, old_content, "REMOVED STRING"
    
    24
    +    )
    
    25
    +
    
    26
    +
    
    27
    +def test_combine_empty():
    
    28
    +    assert_result(None, None, None)
    
    29
    +
    
    30
    +
    
    31
    +def test_combine_new_file():
    
    32
    +    # New file with no old content.
    
    33
    +    assert_result(
    
    34
    +        """\
    
    35
    +        <string name="string_1">First</string>
    
    36
    +        <string name="string_2">Second</string>
    
    37
    +        """,
    
    38
    +        None,
    
    39
    +        """\
    
    40
    +        <string name="string_1">First</string>
    
    41
    +        <string name="string_2">Second</string>
    
    42
    +        """,
    
    43
    +    )
    
    44
    +
    
    45
    +
    
    46
    +def test_combine_removed_file():
    
    47
    +    # Entire file was removed.
    
    48
    +    assert_result(
    
    49
    +        None,
    
    50
    +        """\
    
    51
    +        <string name="string_1">First</string>
    
    52
    +        <string name="string_2">Second</string>
    
    53
    +        """,
    
    54
    +        """\
    
    55
    +
    
    56
    +        <!-- REMOVED STRING -->
    
    57
    +        <string name="string_1">First</string>
    
    58
    +        <!-- REMOVED STRING -->
    
    59
    +        <string name="string_2">Second</string>
    
    60
    +        """,
    
    61
    +    )
    
    62
    +
    
    63
    +
    
    64
    +def test_no_change():
    
    65
    +    content = """\
    
    66
    +        <string name="string_1">First</string>
    
    67
    +        <string name="string_2">Second</string>
    
    68
    +        """
    
    69
    +    assert_result(content, content, content)
    
    70
    +
    
    71
    +
    
    72
    +def test_added_string():
    
    73
    +    assert_result(
    
    74
    +        """\
    
    75
    +        <string name="string_1">First</string>
    
    76
    +        <string name="string_new">NEW</string>
    
    77
    +        <string name="string_2">Second</string>
    
    78
    +        """,
    
    79
    +        """\
    
    80
    +        <string name="string_1">First</string>
    
    81
    +        <string name="string_2">Second</string>
    
    82
    +        """,
    
    83
    +        """\
    
    84
    +        <string name="string_1">First</string>
    
    85
    +        <string name="string_new">NEW</string>
    
    86
    +        <string name="string_2">Second</string>
    
    87
    +        """,
    
    88
    +    )
    
    89
    +
    
    90
    +
    
    91
    +def test_removed_string():
    
    92
    +    assert_result(
    
    93
    +        """\
    
    94
    +        <string name="string_1">First</string>
    
    95
    +        <string name="string_2">Second</string>
    
    96
    +        """,
    
    97
    +        """\
    
    98
    +        <string name="string_1">First</string>
    
    99
    +        <string name="removed">REMOVED</string>
    
    100
    +        <string name="string_2">Second</string>
    
    101
    +        """,
    
    102
    +        """\
    
    103
    +        <string name="string_1">First</string>
    
    104
    +        <string name="string_2">Second</string>
    
    105
    +
    
    106
    +        <!-- REMOVED STRING -->
    
    107
    +        <string name="removed">REMOVED</string>
    
    108
    +        """,
    
    109
    +    )
    
    110
    +
    
    111
    +
    
    112
    +def test_removed_and_added():
    
    113
    +    assert_result(
    
    114
    +        """\
    
    115
    +        <string name="new_1">New string</string>
    
    116
    +        <string name="string_1">First</string>
    
    117
    +        <string name="string_2">Second</string>
    
    118
    +        <string name="new_2">New string 2</string>
    
    119
    +        """,
    
    120
    +        """\
    
    121
    +        <string name="string_1">First</string>
    
    122
    +        <string name="removed_1">First removed</string>
    
    123
    +        <string name="removed_2">Second removed</string>
    
    124
    +        <string name="string_2">Second</string>
    
    125
    +        <string name="removed_3">Third removed</string>
    
    126
    +        """,
    
    127
    +        """\
    
    128
    +        <string name="new_1">New string</string>
    
    129
    +        <string name="string_1">First</string>
    
    130
    +        <string name="string_2">Second</string>
    
    131
    +        <string name="new_2">New string 2</string>
    
    132
    +
    
    133
    +        <!-- REMOVED STRING -->
    
    134
    +        <string name="removed_1">First removed</string>
    
    135
    +        <!-- REMOVED STRING -->
    
    136
    +        <string name="removed_2">Second removed</string>
    
    137
    +        <!-- REMOVED STRING -->
    
    138
    +        <string name="removed_3">Third removed</string>
    
    139
    +        """,
    
    140
    +    )
    
    141
    +
    
    142
    +
    
    143
    +def test_updated():
    
    144
    +    # String content was updated.
    
    145
    +    assert_result(
    
    146
    +        """\
    
    147
    +        <string name="changed_string">NEW</string>
    
    148
    +        """,
    
    149
    +        """\
    
    150
    +        <string name="changed_string">OLD</string>
    
    151
    +        """,
    
    152
    +        """\
    
    153
    +        <string name="changed_string">NEW</string>
    
    154
    +        """,
    
    155
    +    )
    
    156
    +
    
    157
    +
    
    158
    +def test_updated_comment():
    
    159
    +    # String comment was updated.
    
    160
    +    assert_result(
    
    161
    +        """\
    
    162
    +        <!-- NEW -->
    
    163
    +        <string name="changed_string">string</string>
    
    164
    +        """,
    
    165
    +        """\
    
    166
    +        <!-- OLD -->
    
    167
    +        <string name="changed_string">string</string>
    
    168
    +        """,
    
    169
    +        """\
    
    170
    +        <!-- NEW -->
    
    171
    +        <string name="changed_string">string</string>
    
    172
    +        """,
    
    173
    +    )
    
    174
    +    # Comment added.
    
    175
    +    assert_result(
    
    176
    +        """\
    
    177
    +        <!-- NEW -->
    
    178
    +        <string name="changed_string">string</string>
    
    179
    +        """,
    
    180
    +        """\
    
    181
    +        <string name="changed_string">string</string>
    
    182
    +        """,
    
    183
    +        """\
    
    184
    +        <!-- NEW -->
    
    185
    +        <string name="changed_string">string</string>
    
    186
    +        """,
    
    187
    +    )
    
    188
    +    # Comment removed.
    
    189
    +    assert_result(
    
    190
    +        """\
    
    191
    +        <string name="changed_string">string</string>
    
    192
    +        """,
    
    193
    +        """\
    
    194
    +        <!-- OLD -->
    
    195
    +        <string name="changed_string">string</string>
    
    196
    +        """,
    
    197
    +        """\
    
    198
    +        <string name="changed_string">string</string>
    
    199
    +        """,
    
    200
    +    )
    
    201
    +
    
    202
    +    # With file comments
    
    203
    +    assert_result(
    
    204
    +        """\
    
    205
    +        <!-- NEW file comment -->
    
    206
    +
    
    207
    +        <!-- NEW -->
    
    208
    +        <string name="changed_string">string</string>
    
    209
    +        """,
    
    210
    +        """\
    
    211
    +        <!-- OLD file comment -->
    
    212
    +
    
    213
    +        <!-- OLD -->
    
    214
    +        <string name="changed_string">string</string>
    
    215
    +        """,
    
    216
    +        """\
    
    217
    +        <!-- NEW file comment -->
    
    218
    +
    
    219
    +        <!-- NEW -->
    
    220
    +        <string name="changed_string">string</string>
    
    221
    +        """,
    
    222
    +    )
    
    223
    +
    
    224
    +
    
    225
    +def test_reordered():
    
    226
    +    # String was re_ordered.
    
    227
    +    assert_result(
    
    228
    +        """\
    
    229
    +        <string name="string_1">value</string>
    
    230
    +        <string name="moved_string">move</string>
    
    231
    +        """,
    
    232
    +        """\
    
    233
    +        <string name="moved_string">move</string>
    
    234
    +        <string name="string_1">value</string>
    
    235
    +        """,
    
    236
    +        """\
    
    237
    +        <string name="string_1">value</string>
    
    238
    +        <string name="moved_string">move</string>
    
    239
    +        """,
    
    240
    +    )
    
    241
    +
    
    242
    +
    
    243
    +def test_removed_string_with_comment():
    
    244
    +    assert_result(
    
    245
    +        """\
    
    246
    +        <!-- Comment for first. -->
    
    247
    +        <string name="string_1">First</string>
    
    248
    +        <string name="string_2">Second</string>
    
    249
    +        """,
    
    250
    +        """\
    
    251
    +        <!-- Comment for first. -->
    
    252
    +        <string name="string_1">First</string>
    
    253
    +        <!-- Comment for removed. -->
    
    254
    +        <string name="removed">REMOVED</string>
    
    255
    +        <string name="string_2">Second</string>
    
    256
    +        """,
    
    257
    +        """\
    
    258
    +        <!-- Comment for first. -->
    
    259
    +        <string name="string_1">First</string>
    
    260
    +        <string name="string_2">Second</string>
    
    261
    +
    
    262
    +        <!-- REMOVED STRING -->
    
    263
    +        <!-- Comment for removed. -->
    
    264
    +        <string name="removed">REMOVED</string>
    
    265
    +        """,
    
    266
    +    )
    
    267
    +
    
    268
    +    # With file comments and multi-line.
    
    269
    +    # All comments prior to a removed string are moved with it, until another
    
    270
    +    # entity or blank line is reached.
    
    271
    +    assert_result(
    
    272
    +        """\
    
    273
    +        <!-- First File comment -->
    
    274
    +
    
    275
    +        <!-- Comment for first. -->
    
    276
    +        <!-- Comment 2 for first. -->
    
    277
    +        <string name="string_1">First</string>
    
    278
    +
    
    279
    +        <!-- Second -->
    
    280
    +        <!-- File comment -->
    
    281
    +
    
    282
    +        <string name="string_2">Second</string>
    
    283
    +        """,
    
    284
    +        """\
    
    285
    +        <!-- First File comment -->
    
    286
    +
    
    287
    +        <!-- Comment for first. -->
    
    288
    +        <!-- Comment 2 for first. -->
    
    289
    +        <string name="string_1">First</string>
    
    290
    +        <string name="removed_1">First removed</string>
    
    291
    +        <!-- Comment for second removed. -->
    
    292
    +        <string name="removed_2">Second removed</string>
    
    293
    +
    
    294
    +        <!-- Removed file comment -->
    
    295
    +
    
    296
    +        <!-- Comment 1 for third removed -->
    
    297
    +        <!-- Comment 2 for third removed -->
    
    298
    +        <string name="removed_3">Third removed</string>
    
    299
    +
    
    300
    +        <!-- Second -->
    
    301
    +        <!-- File comment -->
    
    302
    +
    
    303
    +        <string name="removed_4">Fourth removed</string>
    
    304
    +        <string name="string_2">Second</string>
    
    305
    +        """,
    
    306
    +        """\
    
    307
    +        <!-- First File comment -->
    
    308
    +
    
    309
    +        <!-- Comment for first. -->
    
    310
    +        <!-- Comment 2 for first. -->
    
    311
    +        <string name="string_1">First</string>
    
    312
    +
    
    313
    +        <!-- Second -->
    
    314
    +        <!-- File comment -->
    
    315
    +
    
    316
    +        <string name="string_2">Second</string>
    
    317
    +
    
    318
    +        <!-- REMOVED STRING -->
    
    319
    +        <string name="removed_1">First removed</string>
    
    320
    +        <!-- REMOVED STRING -->
    
    321
    +        <!-- Comment for second removed. -->
    
    322
    +        <string name="removed_2">Second removed</string>
    
    323
    +        <!-- REMOVED STRING -->
    
    324
    +        <!-- Comment 1 for third removed -->
    
    325
    +        <!-- Comment 2 for third removed -->
    
    326
    +        <string name="removed_3">Third removed</string>
    
    327
    +        <!-- REMOVED STRING -->
    
    328
    +        <string name="removed_4">Fourth removed</string>
    
    329
    +        """,
    
    330
    +    )

  • tools/base-browser/l10n/combine/tests/test_dtd.py
    1
    +import textwrap
    
    2
    +
    
    3
    +from combine import combine_files
    
    4
    +
    
    5
    +
    
    6
    +def assert_result(new_content, old_content, expect):
    
    7
    +    # Allow for indents to make the tests more readable.
    
    8
    +    if new_content is not None:
    
    9
    +        new_content = textwrap.dedent(new_content)
    
    10
    +    if old_content is not None:
    
    11
    +        old_content = textwrap.dedent(old_content)
    
    12
    +    if expect is not None:
    
    13
    +        expect = textwrap.dedent(expect)
    
    14
    +    assert expect == combine_files(
    
    15
    +        "test.dtd", new_content, old_content, "REMOVED STRING"
    
    16
    +    )
    
    17
    +
    
    18
    +
    
    19
    +def test_combine_empty():
    
    20
    +    assert_result(None, None, None)
    
    21
    +
    
    22
    +
    
    23
    +def test_combine_new_file():
    
    24
    +    # New file with no old content.
    
    25
    +    assert_result(
    
    26
    +        """\
    
    27
    +        <!ENTITY string.1 "First">
    
    28
    +        <!ENTITY string.2 "Second">
    
    29
    +        """,
    
    30
    +        None,
    
    31
    +        """\
    
    32
    +        <!ENTITY string.1 "First">
    
    33
    +        <!ENTITY string.2 "Second">
    
    34
    +        """,
    
    35
    +    )
    
    36
    +
    
    37
    +
    
    38
    +def test_combine_removed_file():
    
    39
    +    # Entire file was removed.
    
    40
    +    assert_result(
    
    41
    +        None,
    
    42
    +        """\
    
    43
    +        <!ENTITY string.1 "First">
    
    44
    +        <!ENTITY string.2 "Second">
    
    45
    +        """,
    
    46
    +        """\
    
    47
    +
    
    48
    +        <!-- LOCALIZATION NOTE: REMOVED STRING -->
    
    49
    +        <!ENTITY string.1 "First">
    
    50
    +        <!-- LOCALIZATION NOTE: REMOVED STRING -->
    
    51
    +        <!ENTITY string.2 "Second">
    
    52
    +        """,
    
    53
    +    )
    
    54
    +
    
    55
    +
    
    56
    +def test_no_change():
    
    57
    +    content = """\
    
    58
    +        <!ENTITY string.1 "First">
    
    59
    +        <!ENTITY string.2 "Second">
    
    60
    +        """
    
    61
    +    assert_result(content, content, content)
    
    62
    +
    
    63
    +
    
    64
    +def test_added_string():
    
    65
    +    assert_result(
    
    66
    +        """\
    
    67
    +        <!ENTITY string.1 "First">
    
    68
    +        <!ENTITY string.new "NEW">
    
    69
    +        <!ENTITY string.2 "Second">
    
    70
    +        """,
    
    71
    +        """\
    
    72
    +        <!ENTITY string.1 "First">
    
    73
    +        <!ENTITY string.2 "Second">
    
    74
    +        """,
    
    75
    +        """\
    
    76
    +        <!ENTITY string.1 "First">
    
    77
    +        <!ENTITY string.new "NEW">
    
    78
    +        <!ENTITY string.2 "Second">
    
    79
    +        """,
    
    80
    +    )
    
    81
    +
    
    82
    +
    
    83
    +def test_removed_string():
    
    84
    +    assert_result(
    
    85
    +        """\
    
    86
    +        <!ENTITY string.1 "First">
    
    87
    +        <!ENTITY string.2 "Second">
    
    88
    +        """,
    
    89
    +        """\
    
    90
    +        <!ENTITY string.1 "First">
    
    91
    +        <!ENTITY removed "REMOVED">
    
    92
    +        <!ENTITY string.2 "Second">
    
    93
    +        """,
    
    94
    +        """\
    
    95
    +        <!ENTITY string.1 "First">
    
    96
    +        <!ENTITY string.2 "Second">
    
    97
    +
    
    98
    +        <!-- LOCALIZATION NOTE: REMOVED STRING -->
    
    99
    +        <!ENTITY removed "REMOVED">
    
    100
    +        """,
    
    101
    +    )
    
    102
    +
    
    103
    +
    
    104
    +def test_removed_and_added():
    
    105
    +    assert_result(
    
    106
    +        """\
    
    107
    +        <!ENTITY new.1 "New string">
    
    108
    +        <!ENTITY string.1 "First">
    
    109
    +        <!ENTITY string.2 "Second">
    
    110
    +        <!ENTITY new.2 "New string 2">
    
    111
    +        """,
    
    112
    +        """\
    
    113
    +        <!ENTITY string.1 "First">
    
    114
    +        <!ENTITY removed.1 "First removed">
    
    115
    +        <!ENTITY removed.2 "Second removed">
    
    116
    +        <!ENTITY string.2 "Second">
    
    117
    +        <!ENTITY removed.3 "Third removed">
    
    118
    +        """,
    
    119
    +        """\
    
    120
    +        <!ENTITY new.1 "New string">
    
    121
    +        <!ENTITY string.1 "First">
    
    122
    +        <!ENTITY string.2 "Second">
    
    123
    +        <!ENTITY new.2 "New string 2">
    
    124
    +
    
    125
    +        <!-- LOCALIZATION NOTE: REMOVED STRING -->
    
    126
    +        <!ENTITY removed.1 "First removed">
    
    127
    +        <!-- LOCALIZATION NOTE: REMOVED STRING -->
    
    128
    +        <!ENTITY removed.2 "Second removed">
    
    129
    +        <!-- LOCALIZATION NOTE: REMOVED STRING -->
    
    130
    +        <!ENTITY removed.3 "Third removed">
    
    131
    +        """,
    
    132
    +    )
    
    133
    +
    
    134
    +
    
    135
    +def test_updated():
    
    136
    +    # String content was updated.
    
    137
    +    assert_result(
    
    138
    +        """\
    
    139
    +        <!ENTITY changed.string "NEW">
    
    140
    +        """,
    
    141
    +        """\
    
    142
    +        <!ENTITY changed.string "OLD">
    
    143
    +        """,
    
    144
    +        """\
    
    145
    +        <!ENTITY changed.string "NEW">
    
    146
    +        """,
    
    147
    +    )
    
    148
    +
    
    149
    +
    
    150
    +def test_updated_comment():
    
    151
    +    # String comment was updated.
    
    152
    +    assert_result(
    
    153
    +        """\
    
    154
    +        <!-- LOCALIZATION NOTE: NEW -->
    
    155
    +        <!ENTITY changed.string "string">
    
    156
    +        """,
    
    157
    +        """\
    
    158
    +        <!-- LOCALIZATION NOTE: OLD -->
    
    159
    +        <!ENTITY changed.string "string">
    
    160
    +        """,
    
    161
    +        """\
    
    162
    +        <!-- LOCALIZATION NOTE: NEW -->
    
    163
    +        <!ENTITY changed.string "string">
    
    164
    +        """,
    
    165
    +    )
    
    166
    +    # Comment added.
    
    167
    +    assert_result(
    
    168
    +        """\
    
    169
    +        <!-- LOCALIZATION NOTE: NEW -->
    
    170
    +        <!ENTITY changed.string "string">
    
    171
    +        """,
    
    172
    +        """\
    
    173
    +        <!ENTITY changed.string "string">
    
    174
    +        """,
    
    175
    +        """\
    
    176
    +        <!-- LOCALIZATION NOTE: NEW -->
    
    177
    +        <!ENTITY changed.string "string">
    
    178
    +        """,
    
    179
    +    )
    
    180
    +    # Comment removed.
    
    181
    +    assert_result(
    
    182
    +        """\
    
    183
    +        <!ENTITY changed.string "string">
    
    184
    +        """,
    
    185
    +        """\
    
    186
    +        <!-- LOCALIZATION NOTE: OLD -->
    
    187
    +        <!ENTITY changed.string "string">
    
    188
    +        """,
    
    189
    +        """\
    
    190
    +        <!ENTITY changed.string "string">
    
    191
    +        """,
    
    192
    +    )
    
    193
    +
    
    194
    +    # With multiple comments
    
    195
    +    assert_result(
    
    196
    +        """\
    
    197
    +        <!-- NEW FILE COMMENT -->
    
    198
    +
    
    199
    +        <!-- LOCALIZATION NOTE: NEW -->
    
    200
    +        <!ENTITY changed.string "string">
    
    201
    +        """,
    
    202
    +        """\
    
    203
    +        <!-- OLD -->
    
    204
    +
    
    205
    +        <!-- LOCALIZATION NOTE: OLD -->
    
    206
    +        <!ENTITY changed.string "string">
    
    207
    +        """,
    
    208
    +        """\
    
    209
    +        <!-- NEW FILE COMMENT -->
    
    210
    +
    
    211
    +        <!-- LOCALIZATION NOTE: NEW -->
    
    212
    +        <!ENTITY changed.string "string">
    
    213
    +        """,
    
    214
    +    )
    
    215
    +
    
    216
    +
    
    217
    +def test_reordered():
    
    218
    +    # String was re.ordered.
    
    219
    +    assert_result(
    
    220
    +        """\
    
    221
    +        <!ENTITY string.1 "value">
    
    222
    +        <!ENTITY moved.string "move">
    
    223
    +        """,
    
    224
    +        """\
    
    225
    +        <!ENTITY moved.string "move">
    
    226
    +        <!ENTITY string.1 "value">
    
    227
    +        """,
    
    228
    +        """\
    
    229
    +        <!ENTITY string.1 "value">
    
    230
    +        <!ENTITY moved.string "move">
    
    231
    +        """,
    
    232
    +    )
    
    233
    +
    
    234
    +
    
    235
    +def test_removed_string_with_comment():
    
    236
    +    assert_result(
    
    237
    +        """\
    
    238
    +        <!-- LOCALIZATION NOTE: Comment for first. -->
    
    239
    +        <!ENTITY string.1 "First">
    
    240
    +        <!ENTITY string.2 "Second">
    
    241
    +        """,
    
    242
    +        """\
    
    243
    +        <!-- LOCALIZATION NOTE: Comment for first. -->
    
    244
    +        <!ENTITY string.1 "First">
    
    245
    +        <!-- LOCALIZATION NOTE: Comment for removed. -->
    
    246
    +        <!ENTITY removed "REMOVED">
    
    247
    +        <!ENTITY string.2 "Second">
    
    248
    +        """,
    
    249
    +        """\
    
    250
    +        <!-- LOCALIZATION NOTE: Comment for first. -->
    
    251
    +        <!ENTITY string.1 "First">
    
    252
    +        <!ENTITY string.2 "Second">
    
    253
    +
    
    254
    +        <!-- LOCALIZATION NOTE: REMOVED STRING -->
    
    255
    +        <!-- LOCALIZATION NOTE: Comment for removed. -->
    
    256
    +        <!ENTITY removed "REMOVED">
    
    257
    +        """,
    
    258
    +    )
    
    259
    +
    
    260
    +    # With multiple lines of comments.
    
    261
    +
    
    262
    +    assert_result(
    
    263
    +        """\
    
    264
    +        <!-- First file comment -->
    
    265
    +
    
    266
    +        <!-- LOCALIZATION NOTE: Comment for first. -->
    
    267
    +        <!-- LOCALIZATION NOTE: Comment 2 for first. -->
    
    268
    +        <!ENTITY string.1 "First">
    
    269
    +
    
    270
    +        <!-- Second
    
    271
    +           - file
    
    272
    +           - comment -->
    
    273
    +
    
    274
    +        <!ENTITY string.2 "Second">
    
    275
    +        """,
    
    276
    +        """\
    
    277
    +        <!-- First file comment -->
    
    278
    +
    
    279
    +        <!-- LOCALIZATION NOTE: Comment for first. -->
    
    280
    +        <!ENTITY string.1 "First">
    
    281
    +        <!ENTITY removed.1 "First removed">
    
    282
    +        <!-- LOCALIZATION NOTE: Comment for second removed. -->
    
    283
    +        <!ENTITY removed.2 "Second removed">
    
    284
    +
    
    285
    +        <!-- Removed file comment -->
    
    286
    +
    
    287
    +        <!-- LOCALIZATION NOTE: Comment for third removed. -->
    
    288
    +        <!-- LOCALIZATION NOTE: Comment 2 for
    
    289
    +        third removed. -->
    
    290
    +        <!ENTITY removed.3 "Third removed">
    
    291
    +
    
    292
    +        <!-- Second
    
    293
    +           - file
    
    294
    +           - comment -->
    
    295
    +
    
    296
    +        <!ENTITY removed.4 "Fourth removed">
    
    297
    +        <!ENTITY string.2 "Second">
    
    298
    +        """,
    
    299
    +        """\
    
    300
    +        <!-- First file comment -->
    
    301
    +
    
    302
    +        <!-- LOCALIZATION NOTE: Comment for first. -->
    
    303
    +        <!-- LOCALIZATION NOTE: Comment 2 for first. -->
    
    304
    +        <!ENTITY string.1 "First">
    
    305
    +
    
    306
    +        <!-- Second
    
    307
    +           - file
    
    308
    +           - comment -->
    
    309
    +
    
    310
    +        <!ENTITY string.2 "Second">
    
    311
    +
    
    312
    +        <!-- LOCALIZATION NOTE: REMOVED STRING -->
    
    313
    +        <!ENTITY removed.1 "First removed">
    
    314
    +        <!-- LOCALIZATION NOTE: REMOVED STRING -->
    
    315
    +        <!-- LOCALIZATION NOTE: Comment for second removed. -->
    
    316
    +        <!ENTITY removed.2 "Second removed">
    
    317
    +        <!-- LOCALIZATION NOTE: REMOVED STRING -->
    
    318
    +        <!-- LOCALIZATION NOTE: Comment for third removed. -->
    
    319
    +        <!-- LOCALIZATION NOTE: Comment 2 for
    
    320
    +        third removed. -->
    
    321
    +        <!ENTITY removed.3 "Third removed">
    
    322
    +        <!-- LOCALIZATION NOTE: REMOVED STRING -->
    
    323
    +        <!ENTITY removed.4 "Fourth removed">
    
    324
    +        """,
    
    325
    +    )

  • tools/base-browser/l10n/combine/tests/test_fluent.py
    1
    +import textwrap
    
    2
    +
    
    3
    +from combine import combine_files
    
    4
    +
    
    5
    +
    
    6
    +def assert_result(new_content, old_content, expect):
    
    7
    +    # Allow for indents to make the tests more readable.
    
    8
    +    if new_content is not None:
    
    9
    +        new_content = textwrap.dedent(new_content)
    
    10
    +    if old_content is not None:
    
    11
    +        old_content = textwrap.dedent(old_content)
    
    12
    +    if expect is not None:
    
    13
    +        expect = textwrap.dedent(expect)
    
    14
    +    assert expect == combine_files(
    
    15
    +        "test.ftl", new_content, old_content, "REMOVED STRING"
    
    16
    +    )
    
    17
    +
    
    18
    +
    
    19
    +def test_combine_empty():
    
    20
    +    assert_result(None, None, None)
    
    21
    +
    
    22
    +
    
    23
    +def test_combine_new_file():
    
    24
    +    # New file with no old content.
    
    25
    +    assert_result(
    
    26
    +        """\
    
    27
    +        string-1 = First
    
    28
    +        string-2 = Second
    
    29
    +        """,
    
    30
    +        None,
    
    31
    +        """\
    
    32
    +        string-1 = First
    
    33
    +        string-2 = Second
    
    34
    +        """,
    
    35
    +    )
    
    36
    +
    
    37
    +
    
    38
    +def test_combine_removed_file():
    
    39
    +    # Entire file was removed.
    
    40
    +    assert_result(
    
    41
    +        None,
    
    42
    +        """\
    
    43
    +        string-1 = First
    
    44
    +        string-2 = Second
    
    45
    +        """,
    
    46
    +        """\
    
    47
    +
    
    48
    +
    
    49
    +        ## REMOVED STRING
    
    50
    +
    
    51
    +        string-1 = First
    
    52
    +        string-2 = Second
    
    53
    +        """,
    
    54
    +    )
    
    55
    +
    
    56
    +
    
    57
    +def test_no_change():
    
    58
    +    content = """\
    
    59
    +        string-1 = First
    
    60
    +        string-2 = Second
    
    61
    +        """
    
    62
    +    assert_result(content, content, content)
    
    63
    +
    
    64
    +
    
    65
    +def test_added_string():
    
    66
    +    assert_result(
    
    67
    +        """\
    
    68
    +        string-1 = First
    
    69
    +        string-new = NEW
    
    70
    +        string-2 = Second
    
    71
    +        """,
    
    72
    +        """\
    
    73
    +        string-1 = First
    
    74
    +        string-2 = Second
    
    75
    +        """,
    
    76
    +        """\
    
    77
    +        string-1 = First
    
    78
    +        string-new = NEW
    
    79
    +        string-2 = Second
    
    80
    +        """,
    
    81
    +    )
    
    82
    +
    
    83
    +
    
    84
    +def test_removed_string():
    
    85
    +    assert_result(
    
    86
    +        """\
    
    87
    +        string-1 = First
    
    88
    +        string-2 = Second
    
    89
    +        """,
    
    90
    +        """\
    
    91
    +        string-1 = First
    
    92
    +        removed = REMOVED
    
    93
    +        string-2 = Second
    
    94
    +        """,
    
    95
    +        """\
    
    96
    +        string-1 = First
    
    97
    +        string-2 = Second
    
    98
    +
    
    99
    +
    
    100
    +        ## REMOVED STRING
    
    101
    +
    
    102
    +        removed = REMOVED
    
    103
    +        """,
    
    104
    +    )
    
    105
    +
    
    106
    +
    
    107
    +def test_removed_and_added():
    
    108
    +    assert_result(
    
    109
    +        """\
    
    110
    +        new-1 = New string
    
    111
    +        string-1 =
    
    112
    +            .attr = First
    
    113
    +        string-2 = Second
    
    114
    +        new-2 =
    
    115
    +            .title = New string 2
    
    116
    +        """,
    
    117
    +        """\
    
    118
    +        string-1 =
    
    119
    +            .attr = First
    
    120
    +        removed-1 = First removed
    
    121
    +        removed-2 =
    
    122
    +            .attr = Second removed
    
    123
    +        string-2 = Second
    
    124
    +        removed-3 = Third removed
    
    125
    +        """,
    
    126
    +        """\
    
    127
    +        new-1 = New string
    
    128
    +        string-1 =
    
    129
    +            .attr = First
    
    130
    +        string-2 = Second
    
    131
    +        new-2 =
    
    132
    +            .title = New string 2
    
    133
    +
    
    134
    +
    
    135
    +        ## REMOVED STRING
    
    136
    +
    
    137
    +        removed-1 = First removed
    
    138
    +        removed-2 =
    
    139
    +            .attr = Second removed
    
    140
    +        removed-3 = Third removed
    
    141
    +        """,
    
    142
    +    )
    
    143
    +
    
    144
    +
    
    145
    +def test_updated():
    
    146
    +    # String content was updated.
    
    147
    +    assert_result(
    
    148
    +        """\
    
    149
    +        changed-string = NEW
    
    150
    +        """,
    
    151
    +        """\
    
    152
    +        changed-string = OLD
    
    153
    +        """,
    
    154
    +        """\
    
    155
    +        changed-string = NEW
    
    156
    +        """,
    
    157
    +    )
    
    158
    +
    
    159
    +
    
    160
    +def test_updated_comment():
    
    161
    +    # String comment was updated.
    
    162
    +    assert_result(
    
    163
    +        """\
    
    164
    +        # NEW
    
    165
    +        changed-string = string
    
    166
    +        """,
    
    167
    +        """\
    
    168
    +        # OLD
    
    169
    +        changed-string = string
    
    170
    +        """,
    
    171
    +        """\
    
    172
    +        # NEW
    
    173
    +        changed-string = string
    
    174
    +        """,
    
    175
    +    )
    
    176
    +    # Comment added.
    
    177
    +    assert_result(
    
    178
    +        """\
    
    179
    +        # NEW
    
    180
    +        changed-string = string
    
    181
    +        """,
    
    182
    +        """\
    
    183
    +        changed-string = string
    
    184
    +        """,
    
    185
    +        """\
    
    186
    +        # NEW
    
    187
    +        changed-string = string
    
    188
    +        """,
    
    189
    +    )
    
    190
    +    # Comment removed.
    
    191
    +    assert_result(
    
    192
    +        """\
    
    193
    +        changed-string = string
    
    194
    +        """,
    
    195
    +        """\
    
    196
    +        # OLD
    
    197
    +        changed-string = string
    
    198
    +        """,
    
    199
    +        """\
    
    200
    +        changed-string = string
    
    201
    +        """,
    
    202
    +    )
    
    203
    +
    
    204
    +    # With group comments.
    
    205
    +    assert_result(
    
    206
    +        """\
    
    207
    +        ## GROUP NEW
    
    208
    +
    
    209
    +        # NEW
    
    210
    +        changed-string = string
    
    211
    +        """,
    
    212
    +        """\
    
    213
    +        ## GROUP OLD
    
    214
    +
    
    215
    +        # OLD
    
    216
    +        changed-string = string
    
    217
    +        """,
    
    218
    +        """\
    
    219
    +        ## GROUP NEW
    
    220
    +
    
    221
    +        # NEW
    
    222
    +        changed-string = string
    
    223
    +        """,
    
    224
    +    )
    
    225
    +
    
    226
    +
    
    227
    +def test_reordered():
    
    228
    +    # String was re-ordered.
    
    229
    +    assert_result(
    
    230
    +        """\
    
    231
    +        string-1 = value
    
    232
    +        moved-string = move
    
    233
    +        """,
    
    234
    +        """\
    
    235
    +        moved-string = move
    
    236
    +        string-1 = value
    
    237
    +        """,
    
    238
    +        """\
    
    239
    +        string-1 = value
    
    240
    +        moved-string = move
    
    241
    +        """,
    
    242
    +    )
    
    243
    +
    
    244
    +
    
    245
    +def test_removed_string_with_comment():
    
    246
    +    assert_result(
    
    247
    +        """\
    
    248
    +        # Comment for first.
    
    249
    +        string-1 = First
    
    250
    +        string-2 = Second
    
    251
    +        """,
    
    252
    +        """\
    
    253
    +        # Comment for first.
    
    254
    +        string-1 = First
    
    255
    +        # Comment for removed.
    
    256
    +        removed = REMOVED
    
    257
    +        string-2 = Second
    
    258
    +        """,
    
    259
    +        """\
    
    260
    +        # Comment for first.
    
    261
    +        string-1 = First
    
    262
    +        string-2 = Second
    
    263
    +
    
    264
    +
    
    265
    +        ## REMOVED STRING
    
    266
    +
    
    267
    +        # Comment for removed.
    
    268
    +        removed = REMOVED
    
    269
    +        """,
    
    270
    +    )
    
    271
    +
    
    272
    +    # Group comments are combined with the "REMOVED STRING" comments.
    
    273
    +    # If strings have no group comment, then a single "REMOVED STRING" is
    
    274
    +    # included for them.
    
    275
    +    assert_result(
    
    276
    +        """\
    
    277
    +        ## First Group comment
    
    278
    +
    
    279
    +        # Comment for first.
    
    280
    +        string-1 = First
    
    281
    +
    
    282
    +        ##
    
    283
    +
    
    284
    +        no-group = No group comment
    
    285
    +
    
    286
    +        ## Second
    
    287
    +        ## Group comment
    
    288
    +
    
    289
    +        string-2 = Second
    
    290
    +        """,
    
    291
    +        """\
    
    292
    +        ## First Group comment
    
    293
    +
    
    294
    +        # Comment for first.
    
    295
    +        string-1 = First
    
    296
    +        removed-1 = First removed
    
    297
    +        # Comment for second removed.
    
    298
    +        removed-2 = Second removed
    
    299
    +
    
    300
    +        ##
    
    301
    +
    
    302
    +        no-group = No group comment
    
    303
    +        removed-3 = Third removed
    
    304
    +
    
    305
    +        ## Second
    
    306
    +        ## Group comment
    
    307
    +
    
    308
    +        removed-4 = Fourth removed
    
    309
    +        string-2 = Second
    
    310
    +        """,
    
    311
    +        """\
    
    312
    +        ## First Group comment
    
    313
    +
    
    314
    +        # Comment for first.
    
    315
    +        string-1 = First
    
    316
    +
    
    317
    +        ##
    
    318
    +
    
    319
    +        no-group = No group comment
    
    320
    +
    
    321
    +        ## Second
    
    322
    +        ## Group comment
    
    323
    +
    
    324
    +        string-2 = Second
    
    325
    +
    
    326
    +
    
    327
    +        ## REMOVED STRING
    
    328
    +        ## First Group comment
    
    329
    +
    
    330
    +        removed-1 = First removed
    
    331
    +        # Comment for second removed.
    
    332
    +        removed-2 = Second removed
    
    333
    +
    
    334
    +        ## REMOVED STRING
    
    335
    +
    
    336
    +        removed-3 = Third removed
    
    337
    +
    
    338
    +        ## REMOVED STRING
    
    339
    +        ## Second
    
    340
    +        ## Group comment
    
    341
    +
    
    342
    +        removed-4 = Fourth removed
    
    343
    +        """,
    
    344
    +    )

  • tools/base-browser/l10n/combine/tests/test_properties.py
    1
    +import textwrap
    
    2
    +
    
    3
    +from combine import combine_files
    
    4
    +
    
    5
    +
    
    6
    +def assert_result(new_content, old_content, expect):
    
    7
    +    # Allow for indents to make the tests more readable.
    
    8
    +    if new_content is not None:
    
    9
    +        new_content = textwrap.dedent(new_content)
    
    10
    +    if old_content is not None:
    
    11
    +        old_content = textwrap.dedent(old_content)
    
    12
    +    if expect is not None:
    
    13
    +        expect = textwrap.dedent(expect)
    
    14
    +    assert expect == combine_files(
    
    15
    +        "test.properties", new_content, old_content, "REMOVED STRING"
    
    16
    +    )
    
    17
    +
    
    18
    +
    
    19
    +def test_combine_empty():
    
    20
    +    assert_result(None, None, None)
    
    21
    +
    
    22
    +
    
    23
    +def test_combine_new_file():
    
    24
    +    # New file with no old content.
    
    25
    +    assert_result(
    
    26
    +        """\
    
    27
    +        string.1 = First
    
    28
    +        string.2 = Second
    
    29
    +        """,
    
    30
    +        None,
    
    31
    +        """\
    
    32
    +        string.1 = First
    
    33
    +        string.2 = Second
    
    34
    +        """,
    
    35
    +    )
    
    36
    +
    
    37
    +
    
    38
    +def test_combine_removed_file():
    
    39
    +    # Entire file was removed.
    
    40
    +    assert_result(
    
    41
    +        None,
    
    42
    +        """\
    
    43
    +        string.1 = First
    
    44
    +        string.2 = Second
    
    45
    +        """,
    
    46
    +        """\
    
    47
    +
    
    48
    +        # REMOVED STRING
    
    49
    +        string.1 = First
    
    50
    +        # REMOVED STRING
    
    51
    +        string.2 = Second
    
    52
    +        """,
    
    53
    +    )
    
    54
    +
    
    55
    +
    
    56
    +def test_no_change():
    
    57
    +    content = """\
    
    58
    +        string.1 = First
    
    59
    +        string.2 = Second
    
    60
    +        """
    
    61
    +    assert_result(content, content, content)
    
    62
    +
    
    63
    +
    
    64
    +def test_added_string():
    
    65
    +    assert_result(
    
    66
    +        """\
    
    67
    +        string.1 = First
    
    68
    +        string.new = NEW
    
    69
    +        string.2 = Second
    
    70
    +        """,
    
    71
    +        """\
    
    72
    +        string.1 = First
    
    73
    +        string.2 = Second
    
    74
    +        """,
    
    75
    +        """\
    
    76
    +        string.1 = First
    
    77
    +        string.new = NEW
    
    78
    +        string.2 = Second
    
    79
    +        """,
    
    80
    +    )
    
    81
    +
    
    82
    +
    
    83
    +def test_removed_string():
    
    84
    +    assert_result(
    
    85
    +        """\
    
    86
    +        string.1 = First
    
    87
    +        string.2 = Second
    
    88
    +        """,
    
    89
    +        """\
    
    90
    +        string.1 = First
    
    91
    +        removed = REMOVED
    
    92
    +        string.2 = Second
    
    93
    +        """,
    
    94
    +        """\
    
    95
    +        string.1 = First
    
    96
    +        string.2 = Second
    
    97
    +
    
    98
    +        # REMOVED STRING
    
    99
    +        removed = REMOVED
    
    100
    +        """,
    
    101
    +    )
    
    102
    +
    
    103
    +
    
    104
    +def test_removed_and_added():
    
    105
    +    assert_result(
    
    106
    +        """\
    
    107
    +        new.1 = New string
    
    108
    +        string.1 = First
    
    109
    +        string.2 = Second
    
    110
    +        new.2 = New string 2
    
    111
    +        """,
    
    112
    +        """\
    
    113
    +        string.1 = First
    
    114
    +        removed.1 = First removed
    
    115
    +        removed.2 = Second removed
    
    116
    +        string.2 = Second
    
    117
    +        removed.3 = Third removed
    
    118
    +        """,
    
    119
    +        """\
    
    120
    +        new.1 = New string
    
    121
    +        string.1 = First
    
    122
    +        string.2 = Second
    
    123
    +        new.2 = New string 2
    
    124
    +
    
    125
    +        # REMOVED STRING
    
    126
    +        removed.1 = First removed
    
    127
    +        # REMOVED STRING
    
    128
    +        removed.2 = Second removed
    
    129
    +        # REMOVED STRING
    
    130
    +        removed.3 = Third removed
    
    131
    +        """,
    
    132
    +    )
    
    133
    +
    
    134
    +
    
    135
    +def test_updated():
    
    136
    +    # String content was updated.
    
    137
    +    assert_result(
    
    138
    +        """\
    
    139
    +        changed.string = NEW
    
    140
    +        """,
    
    141
    +        """\
    
    142
    +        changed.string = OLD
    
    143
    +        """,
    
    144
    +        """\
    
    145
    +        changed.string = NEW
    
    146
    +        """,
    
    147
    +    )
    
    148
    +
    
    149
    +
    
    150
    +def test_updated_comment():
    
    151
    +    # String comment was updated.
    
    152
    +    assert_result(
    
    153
    +        """\
    
    154
    +        # NEW
    
    155
    +        changed.string = string
    
    156
    +        """,
    
    157
    +        """\
    
    158
    +        # OLD
    
    159
    +        changed.string = string
    
    160
    +        """,
    
    161
    +        """\
    
    162
    +        # NEW
    
    163
    +        changed.string = string
    
    164
    +        """,
    
    165
    +    )
    
    166
    +    # Comment added.
    
    167
    +    assert_result(
    
    168
    +        """\
    
    169
    +        # NEW
    
    170
    +        changed.string = string
    
    171
    +        """,
    
    172
    +        """\
    
    173
    +        changed.string = string
    
    174
    +        """,
    
    175
    +        """\
    
    176
    +        # NEW
    
    177
    +        changed.string = string
    
    178
    +        """,
    
    179
    +    )
    
    180
    +    # Comment removed.
    
    181
    +    assert_result(
    
    182
    +        """\
    
    183
    +        changed.string = string
    
    184
    +        """,
    
    185
    +        """\
    
    186
    +        # OLD
    
    187
    +        changed.string = string
    
    188
    +        """,
    
    189
    +        """\
    
    190
    +        changed.string = string
    
    191
    +        """,
    
    192
    +    )
    
    193
    +
    
    194
    +    # With file comments
    
    195
    +    assert_result(
    
    196
    +        """\
    
    197
    +        # NEW file comment
    
    198
    +
    
    199
    +        # NEW
    
    200
    +        changed.string = string
    
    201
    +        """,
    
    202
    +        """\
    
    203
    +        # OLD file comment
    
    204
    +
    
    205
    +        # OLD
    
    206
    +        changed.string = string
    
    207
    +        """,
    
    208
    +        """\
    
    209
    +        # NEW file comment
    
    210
    +
    
    211
    +        # NEW
    
    212
    +        changed.string = string
    
    213
    +        """,
    
    214
    +    )
    
    215
    +
    
    216
    +
    
    217
    +def test_reordered():
    
    218
    +    # String was re.ordered.
    
    219
    +    assert_result(
    
    220
    +        """\
    
    221
    +        string.1 = value
    
    222
    +        moved.string = move
    
    223
    +        """,
    
    224
    +        """\
    
    225
    +        moved.string = move
    
    226
    +        string.1 = value
    
    227
    +        """,
    
    228
    +        """\
    
    229
    +        string.1 = value
    
    230
    +        moved.string = move
    
    231
    +        """,
    
    232
    +    )
    
    233
    +
    
    234
    +
    
    235
    +def test_removed_string_with_comment():
    
    236
    +    assert_result(
    
    237
    +        """\
    
    238
    +        # Comment for first.
    
    239
    +        string.1 = First
    
    240
    +        string.2 = Second
    
    241
    +        """,
    
    242
    +        """\
    
    243
    +        # Comment for first.
    
    244
    +        string.1 = First
    
    245
    +        # Comment for removed.
    
    246
    +        removed = REMOVED
    
    247
    +        string.2 = Second
    
    248
    +        """,
    
    249
    +        """\
    
    250
    +        # Comment for first.
    
    251
    +        string.1 = First
    
    252
    +        string.2 = Second
    
    253
    +
    
    254
    +        # REMOVED STRING
    
    255
    +        # Comment for removed.
    
    256
    +        removed = REMOVED
    
    257
    +        """,
    
    258
    +    )
    
    259
    +
    
    260
    +    # With file comments and multi-line.
    
    261
    +    # All comments prior to a removed string are moved with it, until another
    
    262
    +    # entity or blank line is reached.
    
    263
    +    assert_result(
    
    264
    +        """\
    
    265
    +        # First File comment
    
    266
    +
    
    267
    +        # Comment for first.
    
    268
    +        # Comment 2 for first.
    
    269
    +        string.1 = First
    
    270
    +
    
    271
    +        # Second
    
    272
    +        # File comment
    
    273
    +
    
    274
    +        string.2 = Second
    
    275
    +        """,
    
    276
    +        """\
    
    277
    +        # First File comment
    
    278
    +
    
    279
    +        # Comment for first.
    
    280
    +        # Comment 2 for first.
    
    281
    +        string.1 = First
    
    282
    +        removed.1 = First removed
    
    283
    +        # Comment for second removed.
    
    284
    +        removed.2 = Second removed
    
    285
    +
    
    286
    +        # Removed file comment
    
    287
    +
    
    288
    +        # Comment 1 for third removed
    
    289
    +        # Comment 2 for third removed
    
    290
    +        removed.3 = Third removed
    
    291
    +
    
    292
    +        # Second
    
    293
    +        # File comment
    
    294
    +
    
    295
    +        removed.4 = Fourth removed
    
    296
    +        string.2 = Second
    
    297
    +        """,
    
    298
    +        """\
    
    299
    +        # First File comment
    
    300
    +
    
    301
    +        # Comment for first.
    
    302
    +        # Comment 2 for first.
    
    303
    +        string.1 = First
    
    304
    +
    
    305
    +        # Second
    
    306
    +        # File comment
    
    307
    +
    
    308
    +        string.2 = Second
    
    309
    +
    
    310
    +        # REMOVED STRING
    
    311
    +        removed.1 = First removed
    
    312
    +        # REMOVED STRING
    
    313
    +        # Comment for second removed.
    
    314
    +        removed.2 = Second removed
    
    315
    +        # REMOVED STRING
    
    316
    +        # Comment 1 for third removed
    
    317
    +        # Comment 2 for third removed
    
    318
    +        removed.3 = Third removed
    
    319
    +        # REMOVED STRING
    
    320
    +        removed.4 = Fourth removed
    
    321
    +        """,
    
    322
    +    )