[
  {
    "path": ".editorconfig",
    "content": "root = true\n\n[*]\nindent_size = 2\ncharset = utf-8\ntrim_trailing_whitespace = true\ninsert_final_newline = true\n\n[*.{kt, kts}]\nij_kotlin_allow_trailing_comma = true\nij_kotlin_allow_trailing_comma_on_call_site = true\nij_kotlin_imports_layout = *\n"
  },
  {
    "path": ".gitattributes",
    "content": "* text=auto eol=lf\n\n*.bat text eol=crlf\n*.jar binary\n"
  },
  {
    "path": ".github/renovate.json5",
    "content": "{\n  $schema: 'https://docs.renovatebot.com/renovate-schema.json',\n  extends: [\n    'config:recommended',\n  ],\n  packageRules: [\n    // Compiler plugins are tightly coupled to Kotlin version.\n    {\n      groupName: 'Kotlin',\n      matchPackageNames: [\n        'androidx.compose.compiler{/,}**',\n        'dev.drewhamilton.poko{/,}**',\n        'org.jetbrains.kotlin{/,}**',\n      ],\n    },\n  ],\n  ignorePresets: [\n    // Ensure we get the latest version and are not pinned to old versions.\n    'workarounds:javaLTSVersions',\n  ],\n  customManagers: [\n    // Update .java-version file with the latest JDK version.\n    {\n      customType: 'regex',\n      fileMatch: [\n        '\\\\.java-version$',\n      ],\n      matchStrings: [\n        '(?<currentValue>.*)\\\\n',\n      ],\n      datasourceTemplate: 'java-version',\n      depNameTemplate: 'java',\n      // Only write the major version.\n      extractVersionTemplate: '^(?<version>\\\\d+)',\n    },\n  ]\n}\n"
  },
  {
    "path": ".github/workflows/.java-version",
    "content": "25\n"
  },
  {
    "path": ".github/workflows/build.yaml",
    "content": "name: build\n\non:\n  pull_request: {}\n  workflow_dispatch: {}\n  push:\n    branches:\n      - 'main'\n    tags-ignore:\n      - '**'\n\nenv:\n  GRADLE_OPTS: \"-Dorg.gradle.jvmargs=-Xmx6g -Dorg.gradle.daemon=false -Dkotlin.incremental=false\"\n\njobs:\n  build:\n    runs-on: ubuntu-latest\n    steps:\n      - uses: actions/checkout@v6\n      - uses: actions/setup-java@v5\n        with:\n          distribution: 'zulu'\n          java-version-file: .github/workflows/.java-version\n\n      - uses: gradle/actions/setup-gradle@v6\n\n      - run: ./gradlew build\n\n  emulator:\n    runs-on: ubuntu-latest\n    steps:\n      - name: Enable KVM\n        run: |\n          echo 'KERNEL==\"kvm\", GROUP=\"kvm\", MODE=\"0666\", OPTIONS+=\"static_node=kvm\"' | sudo tee /etc/udev/rules.d/99-kvm4all.rules\n          sudo udevadm control --reload-rules\n          sudo udevadm trigger --name-match=kvm\n\n      - uses: actions/checkout@v6\n      - uses: actions/setup-java@v5\n        with:\n          distribution: 'zulu'\n          java-version-file: .github/workflows/.java-version\n\n      - uses: gradle/actions/setup-gradle@v6\n\n      - name: Run integration tests\n        uses: reactivecircus/android-emulator-runner@v2\n        with:\n          api-level: 24\n          script: ./gradlew :tests:connectedCheck\n\n  publish:\n    runs-on: ubuntu-latest\n    if: ${{ github.ref == 'refs/heads/main' && github.repository == 'cashapp/paraphrase' }}\n    needs:\n      - build\n      - emulator\n    steps:\n      - uses: actions/checkout@v6\n      - uses: actions/setup-java@v5\n        with:\n          distribution: 'zulu'\n          java-version-file: .github/workflows/.java-version\n\n      - uses: gradle/actions/setup-gradle@v6\n\n      - run: ./gradlew dokkaGenerate publish\n        env:\n          ORG_GRADLE_PROJECT_mavenCentralUsername: ${{ secrets.SONATYPE_CENTRAL_USERNAME }}\n          ORG_GRADLE_PROJECT_mavenCentralPassword: ${{ secrets.SONATYPE_CENTRAL_PASSWORD }}\n          ORG_GRADLE_PROJECT_signingInMemoryKey: ${{ secrets.GPG_SECRET_KEY }}\n          ORG_GRADLE_PROJECT_signingInMemoryKeyPassword: ${{ secrets.GPG_SECRET_PASSPHRASE }}\n\n      - name: Deploy docs to website\n        uses: JamesIves/github-pages-deploy-action@releases/v3\n        with:\n          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}\n          BRANCH: site\n          FOLDER: build/dokka/html/\n          TARGET_FOLDER: docs/latest/\n          CLEAN: true\n"
  },
  {
    "path": ".github/workflows/release.yaml",
    "content": "name: release\n\non:\n  push:\n    tags:\n      - '**'\n\nenv:\n  GRADLE_OPTS: \"-Dorg.gradle.jvmargs=-Xmx4g -Dorg.gradle.daemon=false -Dkotlin.incremental=false\"\n\njobs:\n  publish:\n    runs-on: ubuntu-latest\n\n    steps:\n      - uses: actions/checkout@v6\n      - uses: actions/setup-java@v5\n        with:\n          distribution: 'zulu'\n          java-version-file: .github/workflows/.java-version\n\n      - name: Build and publish artifacts\n        env:\n          ORG_GRADLE_PROJECT_mavenCentralUsername: ${{ secrets.SONATYPE_CENTRAL_USERNAME }}\n          ORG_GRADLE_PROJECT_mavenCentralPassword: ${{ secrets.SONATYPE_CENTRAL_PASSWORD }}\n          ORG_GRADLE_PROJECT_signingInMemoryKey: ${{ secrets.GPG_SECRET_KEY }}\n          ORG_GRADLE_PROJECT_signingInMemoryKeyPassword: ${{ secrets.GPG_SECRET_PASSPHRASE }}\n        run: ./gradlew publish\n\n      - name: Extract release notes\n        id: release_notes\n        uses: ffurrer2/extract-release-notes@v3\n\n      - name: Create release\n        uses: ncipollo/release-action@v1\n        with:\n          body: ${{ steps.release_notes.outputs.release_notes }}\n          discussionCategory: Announcements\n\n      - run: ./gradlew dokkaGenerate\n\n      - name: Deploy docs to website\n        uses: JamesIves/github-pages-deploy-action@releases/v3\n        with:\n          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}\n          BRANCH: site\n          FOLDER: build/dokka/html/\n          TARGET_FOLDER: 0.x/docs/\n          CLEAN: true\n"
  },
  {
    "path": ".gitignore",
    "content": "# IntelliJ IDEA\n.idea/*\n!.idea/copyright\n\n# Gradle\n.gradle\nbuild\n/reports\n\n# Android\nlocal.properties\n\n# Kotlin\n.kotlin\n"
  },
  {
    "path": ".idea/copyright/Square.xml",
    "content": "<component name=\"CopyrightManager\">\n  <copyright>\n    <option name=\"notice\" value=\"Copyright (C) &amp;#36;{today.year} Cash App&#10;&#10;Licensed under the Apache License, Version 2.0 (the &quot;License&quot;);&#10;you may not use this file except in compliance with the License.&#10;You may obtain a copy of the License at&#10;&#10;     http://www.apache.org/licenses/LICENSE-2.0&#10;&#10;Unless required by applicable law or agreed to in writing, software&#10;distributed under the License is distributed on an &quot;AS IS&quot; BASIS,&#10;WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.&#10;See the License for the specific language governing permissions and&#10;limitations under the License.\" />\n    <option name=\"myName\" value=\"Square\" />\n  </copyright>\n</component>"
  },
  {
    "path": ".idea/copyright/profiles_settings.xml",
    "content": "<component name=\"CopyrightManager\">\n  <settings default=\"Square\">\n    <LanguageOptions name=\"__TEMPLATE__\">\n      <option name=\"addBlankAfter\" value=\"false\" />\n    </LanguageOptions>\n  </settings>\n</component>\n"
  },
  {
    "path": "CHANGELOG.md",
    "content": "# Change Log\n\n## [Unreleased]\n[Unreleased]: https://github.com/cashapp/paraphrase/compare/0.6.1...HEAD\n\n## [0.6.1] - 2026-03-04\n[0.6.1]: https://github.com/cashapp/paraphrase/releases/tag/0.6.1\n\nFixed:\n\n- Fix crash when generating KDocs that contain URL encoded characters.\n\n## [0.6.0] - 2026-03-03\n[0.6.0]: https://github.com/cashapp/paraphrase/releases/tag/0.6.0\n\nNew:\n\n- Paraphrase now generates `FormattedResources` properties for no-arg resources.\n\n## [0.5.0] - 2025-11-10\n[0.5.0]: https://github.com/cashapp/paraphrase/releases/tag/0.5.0\n\nNew:\n\n- `FormattedResource` is now `Parcelable`.\n\nChanged:\n\n- The minimum-supported Gradle version is now 9.0.\n\n\n## [0.4.1] - 2025-09-12\n[0.4.1]: https://github.com/cashapp/paraphrase/releases/tag/0.4.1\n\nNew:\n\n- Support for AGP 9.0.0\n\nChanged:\n\n- In-development snapshots are now published to the Central Portal Snapshots repository at https://central.sonatype.com/repository/maven-snapshots/.\n\n\n## [0.4.0] - 2024-06-11\n[0.4.0]: https://github.com/cashapp/paraphrase/releases/tag/0.4.0\n\nNew:\n\n- Support for Kotlin 2.0.0\n\nChanged:\n\n- Detect usage of `org.jetbrains.kotlin.plugin.compose` plugin and automatically add\n  the `runtime-compose-ui` dependency.\n\n\n## [0.3.1] - 2023-12-14\n[0.3.1]: https://github.com/cashapp/paraphrase/releases/tag/0.3.1\n\nFixed:\n\n- Include file names in read/parse failures to help with debugging\n- Fix crash when processing non-XML resource files\n\n## [0.3.0] - 2023-10-12\n[0.3.0]: https://github.com/cashapp/paraphrase/releases/tag/0.3.0\n\nChanged:\n\n- Generate `Number` parameter instead of `Int` for plural and choice arguments\n- Generate `Long` parameter along with `Int` overload for ordinal and selectordinal arguments\n- Deprecate generated function using choice arguments, which are discouraged in ICU documentation in\n  favor of plural and select arguments.\n\n## [0.2.2] - 2023-07-20\n[0.2.2]: https://github.com/cashapp/paraphrase/releases/tag/0.2.2\n\nFixed:\n\n- Fix build cache issue by deleting the generated class when all string resources are removed\n\n## [0.2.1] - 2023-07-19\n[0.2.1]: https://github.com/cashapp/paraphrase/releases/tag/0.2.1\n\nChanged:\n\n- Automatically add the `runtime-compose-ui` dependency if buildFeatures.compose is true\n- Add missing `)` to `FormattedResource.toString`\n- Optimize insertion performance of map arguments\n\n## [0.2.0] - 2023-04-25\n[0.2.0]: https://github.com/cashapp/paraphrase/releases/tag/0.2.0\n\nNew:\n\n- Add runtime support for Compose UI\n\n## [0.1.2] - 2023-04-17\n[0.1.2]: https://github.com/cashapp/paraphrase/releases/tag/0.1.2\n\nChanged:\n\n- Use `androidx.collection.ArrayMap` instead of `android.util.ArrayMap` to hold named arguments.\n\n## [0.1.1] - 2023-04-07\n[0.1.1]: https://github.com/cashapp/paraphrase/releases/tag/0.1.1\n\nFixed:\n\n- Fix crash when processing modules with no merged resources.\n\n## [0.1.0] - 2023-04-06\n[0.1.0]: https://github.com/cashapp/paraphrase/releases/tag/0.1.0\n\nInitial release.\n"
  },
  {
    "path": "LICENSE.txt",
    "content": "\n                                 Apache License\n                           Version 2.0, January 2004\n                        http://www.apache.org/licenses/\n\n   TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION\n\n   1. Definitions.\n\n      \"License\" shall mean the terms and conditions for use, reproduction,\n      and distribution as defined by Sections 1 through 9 of this document.\n\n      \"Licensor\" shall mean the copyright owner or entity authorized by\n      the copyright owner that is granting the License.\n\n      \"Legal Entity\" shall mean the union of the acting entity and all\n      other entities that control, are controlled by, or are under common\n      control with that entity. For the purposes of this definition,\n      \"control\" means (i) the power, direct or indirect, to cause the\n      direction or management of such entity, whether by contract or\n      otherwise, or (ii) ownership of fifty percent (50%) or more of the\n      outstanding shares, or (iii) beneficial ownership of such entity.\n\n      \"You\" (or \"Your\") shall mean an individual or Legal Entity\n      exercising permissions granted by this License.\n\n      \"Source\" form shall mean the preferred form for making modifications,\n      including but not limited to software source code, documentation\n      source, and configuration files.\n\n      \"Object\" form shall mean any form resulting from mechanical\n      transformation or translation of a Source form, including but\n      not limited to compiled object code, generated documentation,\n      and conversions to other media types.\n\n      \"Work\" shall mean the work of authorship, whether in Source or\n      Object form, made available under the License, as indicated by a\n      copyright notice that is included in or attached to the work\n      (an example is provided in the Appendix below).\n\n      \"Derivative Works\" shall mean any work, whether in Source or Object\n      form, that is based on (or derived from) the Work and for which the\n      editorial revisions, annotations, elaborations, or other modifications\n      represent, as a whole, an original work of authorship. For the purposes\n      of this License, Derivative Works shall not include works that remain\n      separable from, or merely link (or bind by name) to the interfaces of,\n      the Work and Derivative Works thereof.\n\n      \"Contribution\" shall mean any work of authorship, including\n      the original version of the Work and any modifications or additions\n      to that Work or Derivative Works thereof, that is intentionally\n      submitted to Licensor for inclusion in the Work by the copyright owner\n      or by an individual or Legal Entity authorized to submit on behalf of\n      the copyright owner. For the purposes of this definition, \"submitted\"\n      means any form of electronic, verbal, or written communication sent\n      to the Licensor or its representatives, including but not limited to\n      communication on electronic mailing lists, source code control systems,\n      and issue tracking systems that are managed by, or on behalf of, the\n      Licensor for the purpose of discussing and improving the Work, but\n      excluding communication that is conspicuously marked or otherwise\n      designated in writing by the copyright owner as \"Not a Contribution.\"\n\n      \"Contributor\" shall mean Licensor and any individual or Legal Entity\n      on behalf of whom a Contribution has been received by Licensor and\n      subsequently incorporated within the Work.\n\n   2. Grant of Copyright License. Subject to the terms and conditions of\n      this License, each Contributor hereby grants to You a perpetual,\n      worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n      copyright license to reproduce, prepare Derivative Works of,\n      publicly display, publicly perform, sublicense, and distribute the\n      Work and such Derivative Works in Source or Object form.\n\n   3. Grant of Patent License. Subject to the terms and conditions of\n      this License, each Contributor hereby grants to You a perpetual,\n      worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n      (except as stated in this section) patent license to make, have made,\n      use, offer to sell, sell, import, and otherwise transfer the Work,\n      where such license applies only to those patent claims licensable\n      by such Contributor that are necessarily infringed by their\n      Contribution(s) alone or by combination of their Contribution(s)\n      with the Work to which such Contribution(s) was submitted. If You\n      institute patent litigation against any entity (including a\n      cross-claim or counterclaim in a lawsuit) alleging that the Work\n      or a Contribution incorporated within the Work constitutes direct\n      or contributory patent infringement, then any patent licenses\n      granted to You under this License for that Work shall terminate\n      as of the date such litigation is filed.\n\n   4. Redistribution. You may reproduce and distribute copies of the\n      Work or Derivative Works thereof in any medium, with or without\n      modifications, and in Source or Object form, provided that You\n      meet the following conditions:\n\n      (a) You must give any other recipients of the Work or\n          Derivative Works a copy of this License; and\n\n      (b) You must cause any modified files to carry prominent notices\n          stating that You changed the files; and\n\n      (c) You must retain, in the Source form of any Derivative Works\n          that You distribute, all copyright, patent, trademark, and\n          attribution notices from the Source form of the Work,\n          excluding those notices that do not pertain to any part of\n          the Derivative Works; and\n\n      (d) If the Work includes a \"NOTICE\" text file as part of its\n          distribution, then any Derivative Works that You distribute must\n          include a readable copy of the attribution notices contained\n          within such NOTICE file, excluding those notices that do not\n          pertain to any part of the Derivative Works, in at least one\n          of the following places: within a NOTICE text file distributed\n          as part of the Derivative Works; within the Source form or\n          documentation, if provided along with the Derivative Works; or,\n          within a display generated by the Derivative Works, if and\n          wherever such third-party notices normally appear. The contents\n          of the NOTICE file are for informational purposes only and\n          do not modify the License. You may add Your own attribution\n          notices within Derivative Works that You distribute, alongside\n          or as an addendum to the NOTICE text from the Work, provided\n          that such additional attribution notices cannot be construed\n          as modifying the License.\n\n      You may add Your own copyright statement to Your modifications and\n      may provide additional or different license terms and conditions\n      for use, reproduction, or distribution of Your modifications, or\n      for any such Derivative Works as a whole, provided Your use,\n      reproduction, and distribution of the Work otherwise complies with\n      the conditions stated in this License.\n\n   5. Submission of Contributions. Unless You explicitly state otherwise,\n      any Contribution intentionally submitted for inclusion in the Work\n      by You to the Licensor shall be under the terms and conditions of\n      this License, without any additional terms or conditions.\n      Notwithstanding the above, nothing herein shall supersede or modify\n      the terms of any separate license agreement you may have executed\n      with Licensor regarding such Contributions.\n\n   6. Trademarks. This License does not grant permission to use the trade\n      names, trademarks, service marks, or product names of the Licensor,\n      except as required for reasonable and customary use in describing the\n      origin of the Work and reproducing the content of the NOTICE file.\n\n   7. Disclaimer of Warranty. Unless required by applicable law or\n      agreed to in writing, Licensor provides the Work (and each\n      Contributor provides its Contributions) on an \"AS IS\" BASIS,\n      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or\n      implied, including, without limitation, any warranties or conditions\n      of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A\n      PARTICULAR PURPOSE. You are solely responsible for determining the\n      appropriateness of using or redistributing the Work and assume any\n      risks associated with Your exercise of permissions under this License.\n\n   8. Limitation of Liability. In no event and under no legal theory,\n      whether in tort (including negligence), contract, or otherwise,\n      unless required by applicable law (such as deliberate and grossly\n      negligent acts) or agreed to in writing, shall any Contributor be\n      liable to You for damages, including any direct, indirect, special,\n      incidental, or consequential damages of any character arising as a\n      result of this License or out of the use or inability to use the\n      Work (including but not limited to damages for loss of goodwill,\n      work stoppage, computer failure or malfunction, or any and all\n      other commercial damages or losses), even if such Contributor\n      has been advised of the possibility of such damages.\n\n   9. Accepting Warranty or Additional Liability. While redistributing\n      the Work or Derivative Works thereof, You may choose to offer,\n      and charge a fee for, acceptance of support, warranty, indemnity,\n      or other liability obligations and/or rights consistent with this\n      License. However, in accepting such obligations, You may act only\n      on Your own behalf and on Your sole responsibility, not on behalf\n      of any other Contributor, and only if You agree to indemnify,\n      defend, and hold each Contributor harmless for any liability\n      incurred by, or claims asserted against, such Contributor by reason\n      of your accepting any such warranty or additional liability.\n\n   END OF TERMS AND CONDITIONS\n\n   APPENDIX: How to apply the Apache License to your work.\n\n      To apply the Apache License to your work, attach the following\n      boilerplate notice, with the fields enclosed by brackets \"[]\"\n      replaced with your own identifying information. (Don't include\n      the brackets!)  The text should be enclosed in the appropriate\n      comment syntax for the file format. We also recommend that a\n      file or class name and description of purpose be included on the\n      same \"printed page\" as the copyright notice for easier\n      identification within third-party archives.\n\n   Copyright [yyyy] [name of copyright owner]\n\n   Licensed under the Apache License, Version 2.0 (the \"License\");\n   you may not use this file except in compliance with the License.\n   You may obtain a copy of the License at\n\n       http://www.apache.org/licenses/LICENSE-2.0\n\n   Unless required by applicable law or agreed to in writing, software\n   distributed under the License is distributed on an \"AS IS\" BASIS,\n   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n   See the License for the specific language governing permissions and\n   limitations under the License.\n"
  },
  {
    "path": "README.md",
    "content": "# Paraphrase\n\nA Gradle plugin that generates type-safe formatters for Android string resources in the ICU message format. It integrates easily with Android Views and Compose UI.\n\n## Usage\n\n### Step 1: Add the Paraphrase Plugin\n\nIn the `build.gradle.kts` file of an Android application or library module:\n\n```kotlin\nplugins {\n  id(\"app.cash.paraphrase\") version \"0.6.1\"\n}\n```\n\n### Step 2: Add an ICU String Resource\n\nIn the `strings.xml` file within the module:\n\n```xml\n<resources>\n  <!-- Describes an order placed at the deli. -->\n  <string name=\"order_description\">\n    {count, plural,\n      =0 {{name} does not order any bagels}\n      =1 {{name} orders an everything bagel}\n      other {{name} orders # everything bagels}\n    }\n  </string>\n</resources>\n```\n\nFor more information on the ICU message format, see the [ICU docs](https://unicode-org.github.io/icu/userguide/format_parse/messages).\n\n### Step 3: Generate the Formatted Resources\n\nBuild the module:\n\n```shell\n./gradlew my-module:build\n```\n\nOr run the Paraphrase gradle task for the relevant variant:\n\n```shell\n./gradlew my-module:generateFormattedResourcesDebug\n./gradlew my-module:generateFormattedResourcesRelease\n```\n\nThat generates a formatted resource function that looks something like this:\n```kotlin\n/**\n * Describes an order placed at the deli.\n */\npublic fun order_description(count: Int, name: Any): FormattedResource {\n  val arguments = mapOf(\"count\" to count, \"name\" to name)\n  return FormattedResource(\n    id = R.string.order_description,\n    arguments = arguments,\n  )\n}\n```\n\n\n### Step 4: Use the Formatted Resources\n\nIn an Android View:\n\n```kotlin\nimport app.cash.paraphrase.getString\n\nval orderDescription = resources.getString(\n  FormattedResources.order_description(\n    count = 12,\n    name = \"Jobu Tupaki\",\n  )\n)\n\n// Jobu Tupaki orders 12 everything bagels\n```\n\nIn Compose UI:\n\n```kotlin\nimport app.cash.paraphrase.compose.formattedResource\n\nval orderDescription = formattedResource(\n  FormattedResources.order_description(\n    count = 12,\n    name = \"Jobu Tupaki\",\n  ),\n)\n\n// Jobu Tupaki orders 12 everything bagels\n```\n\n## Modules\n\n* [plugin](plugin): The Gradle plugin, with logic to parse string resources and generate formatter methods.\n* [runtime](runtime): The data types and Android extensions that Paraphrase requires to work at runtime.\n* [runtime-compose-ui](runtime-compose-ui): The extensions that Paraphrase requires to work with Compose UI at runtime.\n* [sample](sample): A sample Android project that demonstrates usage of Paraphrase.\n\n## License\n\n    Copyright 2023 Cash App\n\n    Licensed under the Apache License, Version 2.0 (the \"License\");\n    you may not use this file except in compliance with the License.\n    You may obtain a copy of the License at\n\n       http://www.apache.org/licenses/LICENSE-2.0\n\n    Unless required by applicable law or agreed to in writing, software\n    distributed under the License is distributed on an \"AS IS\" BASIS,\n    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n    See the License for the specific language governing permissions and\n    limitations under the License.\n"
  },
  {
    "path": "RELEASING.md",
    "content": "# Releasing\n\n1. Update the `VERSION_NAME` in `gradle.properties` to the release version\n   (i.e., no \"-SNAPSHOT\" suffix).\n\n2. Update the `CHANGELOG.md`:\n   1. Change the `Unreleased` header to the release version.\n   2. Add a link URL to the bottom of the page, to ensure the header link works.\n   3. Add a new `Unreleased` section to the top.\n\n3. Update the `README.md` so the \"Download\" section reflects the new release version and the\n   snapshot section reflects the next \"SNAPSHOT\" version.\n\n4. Commit\n\n   ```\n   $ git commit -am \"Prepare version X.Y.Z\"\n   ```\n\n5. Tag\n\n   ```\n   $ git tag -am \"Version X.Y.Z\" X.Y.Z\n   ```\n\n6. Update the `VERSION_NAME` in `gradle.properties` to what is likely the next version and\n   re-append the \"-SNAPSHOT\" suffix'\n\n7. Commit\n\n   ```\n   $ git commit -am \"Prepare next development version\"\n   ```\n\n8. Push!\n\n   ```\n   $ git push && git push --tags\n   ```\n\n   This will trigger a GitHub Action workflow which will create a GitHub release, upload the\n   release artifacts to Sonatype Nexus, and trigger synchronization to Maven Central.\n\n   You're done!\n"
  },
  {
    "path": "build-logic/build.gradle.kts",
    "content": ""
  },
  {
    "path": "build-logic/settings.gradle.kts",
    "content": "pluginManagement {\n  repositories {\n    google()\n    mavenCentral()\n    gradlePluginPortal()\n  }\n}\n\n@Suppress(\"UnstableApiUsage\")\ndependencyResolutionManagement {\n  versionCatalogs {\n    create(\"libs\") {\n      from(files(\"../gradle/libs.versions.toml\"))\n    }\n  }\n\n  repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)\n  repositories {\n    google()\n    mavenCentral()\n  }\n}\n\nrootProject.name = \"build-logic\"\ninclude(\n  \":\",\n  \":plugin\"\n)\nproject(\":plugin\").projectDir = File(\"../plugin\")\n"
  },
  {
    "path": "build.gradle.kts",
    "content": "import com.android.build.api.dsl.CommonExtension\nimport com.diffplug.gradle.spotless.SpotlessExtension\nimport com.vanniktech.maven.publish.JavadocJar\nimport com.vanniktech.maven.publish.MavenPublishBaseExtension\nimport org.jetbrains.kotlin.gradle.dsl.JvmTarget\nimport org.jetbrains.kotlin.gradle.dsl.KotlinBaseExtension\nimport org.jetbrains.kotlin.gradle.tasks.KotlinJvmCompile\n\nbuildscript { dependencies { classpath(\"app.cash.paraphrase:plugin\") } }\n\nplugins {\n  alias(libs.plugins.androidApplication) apply false\n  alias(libs.plugins.androidLibrary) apply false\n  alias(libs.plugins.androidTest) apply false\n  alias(libs.plugins.kotlinJvm) apply false\n  alias(libs.plugins.kotlinParcelize) apply false\n  alias(libs.plugins.poko) apply false\n  alias(libs.plugins.kotlinApiDump) apply false\n  alias(libs.plugins.dokka)\n  alias(libs.plugins.mavenPublish)\n  alias(libs.plugins.spotless)\n}\n\ndependencies {\n  dokka(projects.runtime)\n  dokka(projects.runtimeComposeUi)\n}\n\nconfigure<SpotlessExtension> {\n  kotlin {\n    target(\"**/*.kt\")\n    ktfmt(libs.ktfmt.get().version).googleStyle()\n    licenseHeaderFile(file(\"gradle/license-header.txt\"))\n  }\n  kotlinGradle { ktfmt(libs.ktfmt.get().version).googleStyle() }\n}\n\nsubprojects {\n  version = extra[\"VERSION_NAME\"]!!\n\n  plugins.withId(\"com.vanniktech.maven.publish\") {\n    // Disable Javadoc jars. They're basically useless relics, but enabling this will also cause\n    // AGP to use an old version of Dokka which fails to run on the latest Java versions.\n    extensions\n      .getByType(MavenPublishBaseExtension::class)\n      .configureBasedOnAppliedPlugins(javadocJar = JavadocJar.Empty())\n\n    // All published libraries must use API tracking to help maintain compatibility.\n    plugins.apply(libs.plugins.kotlinApiDump.get().pluginId)\n\n    val kotlin = extensions.getByName(\"kotlin\") as KotlinBaseExtension\n    kotlin.explicitApi()\n\n    publishing {\n      repositories {\n        /**\n         * Want to push to an internal repository for testing? Set the following properties in\n         * `~/.gradle/gradle.properties`.\n         *\n         * ```\n         * internalUrl=YOUR_INTERNAL_URL\n         * internalUsername=YOUR_USERNAME\n         * internalPassword=YOUR_PASSWORD\n         * ```\n         */\n        val internalUrl = providers.gradleProperty(\"internalUrl\")\n        if (internalUrl.isPresent()) {\n          maven {\n            name = \"internal\"\n            setUrl(internalUrl)\n            credentials(PasswordCredentials::class)\n          }\n        }\n      }\n    }\n  }\n\n  val javaVersion = JavaVersion.VERSION_1_8\n  tasks.withType<KotlinJvmCompile> {\n    compilerOptions { jvmTarget = JvmTarget.fromTarget(javaVersion.toString()) }\n  }\n  val configureAndroid =\n    Action<Plugin<Any>> {\n      with(extensions.getByType<CommonExtension>()) {\n        compileSdk = 36\n        defaultConfig.minSdk = 24\n\n        compileOptions.apply {\n          sourceCompatibility = javaVersion\n          targetCompatibility = javaVersion\n        }\n      }\n    }\n  plugins.withId(\"com.android.application\", configureAndroid)\n  plugins.withId(\"com.android.library\", configureAndroid)\n  plugins.withId(\"com.android.test\", configureAndroid)\n}\n"
  },
  {
    "path": "gradle/libs.versions.toml",
    "content": "[versions]\nagp = \"9.2.1\"\nkotlin = \"2.3.21\"\n\n[libraries]\nagp = { module = \"com.android.tools.build:gradle\", version.ref = \"agp\" }\nandroidActivityCompose = \"androidx.activity:activity-compose:1.13.0\"\nandroidAnnotation = \"androidx.annotation:annotation:1.10.0\"\nandroidCollection = \"androidx.collection:collection:1.6.0\"\nandroidTestRunner = \"androidx.test:runner:1.7.0\"\ncomposeMaterial = \"androidx.compose.material:material:1.11.1\"\ncomposeUi = \"androidx.compose.ui:ui:1.11.1\"\ngoogleMaterial = \"com.google.android.material:material:1.13.0\"\nicu4j = \"com.ibm.icu:icu4j:78.3\"\njunit = \"junit:junit:4.13.2\"\nkotlinPoet = \"com.squareup:kotlinpoet:2.3.0\"\nrobolectric = \"org.robolectric:robolectric:4.16.1\"\ntestParameterInjector = \"com.google.testparameterinjector:test-parameter-injector:1.22\"\nassertk = \"com.willowtreeapps.assertk:assertk:0.28.1\"\ncoreLibraryDesugaring = \"com.android.tools:desugar_jdk_libs:2.1.5\"\nktfmt = \"com.facebook:ktfmt:0.62\"\n\n[plugins]\nandroidApplication = { id = \"com.android.application\", version.ref = \"agp\" }\nandroidLibrary = { id = \"com.android.library\", version.ref = \"agp\" }\nandroidTest = { id = \"com.android.test\", version.ref = \"agp\" }\nbuildConfig = { id = \"com.github.gmazzo.buildconfig\", version = \"6.0.9\" }\nkotlinCompose = { id = \"org.jetbrains.kotlin.plugin.compose\", version.ref = \"kotlin\" }\nkotlinJvm = { id = \"org.jetbrains.kotlin.jvm\", version.ref = \"kotlin\" }\nkotlinParcelize = { id = \"org.jetbrains.kotlin.plugin.parcelize\", version.ref = \"kotlin\" }\ndokka = { id = \"org.jetbrains.dokka\", version = \"2.2.0\" }\nmavenPublish = { id = \"com.vanniktech.maven.publish\", version = \"0.36.0\" }\npoko = { id = \"dev.drewhamilton.poko\", version = \"0.22.1\" }\nspotless = { id = \"com.diffplug.spotless\", version = \"8.4.0\" }\nkotlinApiDump = { id = \"org.jetbrains.kotlinx.binary-compatibility-validator\", version = \"0.18.1\" }\n"
  },
  {
    "path": "gradle/license-header.txt",
    "content": "/*\n * Copyright (C) $YEAR Cash App\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n"
  },
  {
    "path": "gradle/wrapper/gradle-wrapper.properties",
    "content": "distributionBase=GRADLE_USER_HOME\ndistributionPath=wrapper/dists\ndistributionUrl=https\\://services.gradle.org/distributions/gradle-9.5.0-bin.zip\nnetworkTimeout=10000\nretries=0\nretryBackOffMs=500\nvalidateDistributionUrl=true\nzipStoreBase=GRADLE_USER_HOME\nzipStorePath=wrapper/dists\n"
  },
  {
    "path": "gradle.properties",
    "content": "# Maven\nGROUP=app.cash.paraphrase\n# HEY! If you change major version update release.yaml doc folder.\nVERSION_NAME=0.7.0-SNAPSHOT\n\nPOM_DESCRIPTION=Type-checked formatters for patterned Android string resources\n\nPOM_URL=https://github.com/cashapp/paraphrase/\nPOM_SCM_URL=https://github.com/cashapp/paraphrase/\nPOM_SCM_CONNECTION=scm:git:git://github.com/cashapp/paraphrase.git\nPOM_SCM_DEV_CONNECTION=scm:git:ssh://git@github.com/cashapp/paraphrase.git\n\nPOM_LICENCE_NAME=Apache-2.0\nPOM_LICENCE_URL=https://www.apache.org/licenses/LICENSE-2.0\nPOM_LICENCE_DIST=repo\n\nPOM_DEVELOPER_ID=cashapp\nPOM_DEVELOPER_NAME=CashApp\nPOM_DEVELOPER_URL=https://github.com/cashapp\n\nmavenCentralPublishing=true\nmavenCentralAutomaticPublishing=true\nsignAllPublications=true\n\n# Signals to our own plugin that we are building within the repo.\napp.cash.paraphrase.internal=true\n\nandroid.defaults.buildfeatures.resvalues=false\nandroid.defaults.buildfeatures.shaders=false\n\nkotlin.code.style=official\norg.gradle.jvmargs=-Xmx2048m -Dfile.encoding\\=UTF-8\n"
  },
  {
    "path": "gradlew",
    "content": "#!/bin/sh\n\n#\n# Copyright © 2015 the original authors.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#      https://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n# SPDX-License-Identifier: Apache-2.0\n#\n\n##############################################################################\n#\n#   Gradle start up script for POSIX generated by Gradle.\n#\n#   Important for running:\n#\n#   (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is\n#       noncompliant, but you have some other compliant shell such as ksh or\n#       bash, then to run this script, type that shell name before the whole\n#       command line, like:\n#\n#           ksh Gradle\n#\n#       Busybox and similar reduced shells will NOT work, because this script\n#       requires all of these POSIX shell features:\n#         * functions;\n#         * expansions «$var», «${var}», «${var:-default}», «${var+SET}»,\n#           «${var#prefix}», «${var%suffix}», and «$( cmd )»;\n#         * compound commands having a testable exit status, especially «case»;\n#         * various built-in commands including «command», «set», and «ulimit».\n#\n#   Important for patching:\n#\n#   (2) This script targets any POSIX shell, so it avoids extensions provided\n#       by Bash, Ksh, etc; in particular arrays are avoided.\n#\n#       The \"traditional\" practice of packing multiple parameters into a\n#       space-separated string is a well documented source of bugs and security\n#       problems, so this is (mostly) avoided, by progressively accumulating\n#       options in \"$@\", and eventually passing that to Java.\n#\n#       Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS,\n#       and GRADLE_OPTS) rely on word-splitting, this is performed explicitly;\n#       see the in-line comments for details.\n#\n#       There are tweaks for specific operating systems such as AIX, CygWin,\n#       Darwin, MinGW, and NonStop.\n#\n#   (3) This script is generated from the Groovy template\n#       https://github.com/gradle/gradle/blob/3d91ce3b8caaf77ad09f381f43615b715b53f72c/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt\n#       within the Gradle project.\n#\n#       You can find Gradle at https://github.com/gradle/gradle/.\n#\n##############################################################################\n\n# Attempt to set APP_HOME\n\n# Resolve links: $0 may be a link\napp_path=$0\n\n# Need this for daisy-chained symlinks.\nwhile\n    APP_HOME=${app_path%\"${app_path##*/}\"}  # leaves a trailing /; empty if no leading path\n    [ -h \"$app_path\" ]\ndo\n    ls=$( ls -ld \"$app_path\" )\n    link=${ls#*' -> '}\n    case $link in             #(\n      /*)   app_path=$link ;; #(\n      *)    app_path=$APP_HOME$link ;;\n    esac\ndone\n\n# This is normally unused\n# shellcheck disable=SC2034\nAPP_BASE_NAME=${0##*/}\n# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036)\nAPP_HOME=$( cd -P \"${APP_HOME:-./}\" > /dev/null && printf '%s\\n' \"$PWD\" ) || exit\n\n# Use the maximum available, or set MAX_FD != -1 to use that value.\nMAX_FD=maximum\n\nwarn () {\n    echo \"$*\"\n} >&2\n\ndie () {\n    echo\n    echo \"$*\"\n    echo\n    exit 1\n} >&2\n\n# OS specific support (must be 'true' or 'false').\ncygwin=false\nmsys=false\ndarwin=false\nnonstop=false\ncase \"$( uname )\" in                #(\n  CYGWIN* )         cygwin=true  ;; #(\n  Darwin* )         darwin=true  ;; #(\n  MSYS* | MINGW* )  msys=true    ;; #(\n  NONSTOP* )        nonstop=true ;;\nesac\n\n\n\n# Determine the Java command to use to start the JVM.\nif [ -n \"$JAVA_HOME\" ] ; then\n    if [ -x \"$JAVA_HOME/jre/sh/java\" ] ; then\n        # IBM's JDK on AIX uses strange locations for the executables\n        JAVACMD=$JAVA_HOME/jre/sh/java\n    else\n        JAVACMD=$JAVA_HOME/bin/java\n    fi\n    if [ ! -x \"$JAVACMD\" ] ; then\n        die \"ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME\n\nPlease set the JAVA_HOME variable in your environment to match the\nlocation of your Java installation.\"\n    fi\nelse\n    JAVACMD=java\n    if ! command -v java >/dev/null 2>&1\n    then\n        die \"ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.\n\nPlease set the JAVA_HOME variable in your environment to match the\nlocation of your Java installation.\"\n    fi\nfi\n\n# Increase the maximum file descriptors if we can.\nif ! \"$cygwin\" && ! \"$darwin\" && ! \"$nonstop\" ; then\n    case $MAX_FD in #(\n      max*)\n        # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked.\n        # shellcheck disable=SC2039,SC3045\n        MAX_FD=$( ulimit -H -n ) ||\n            warn \"Could not query maximum file descriptor limit\"\n    esac\n    case $MAX_FD in  #(\n      '' | soft) :;; #(\n      *)\n        # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked.\n        # shellcheck disable=SC2039,SC3045\n        ulimit -n \"$MAX_FD\" ||\n            warn \"Could not set maximum file descriptor limit to $MAX_FD\"\n    esac\nfi\n\n# Collect all arguments for the java command, stacking in reverse order:\n#   * args from the command line\n#   * the main class name\n#   * -classpath\n#   * -D...appname settings\n#   * --module-path (only if needed)\n#   * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables.\n\n# For Cygwin or MSYS, switch paths to Windows format before running java\nif \"$cygwin\" || \"$msys\" ; then\n    APP_HOME=$( cygpath --path --mixed \"$APP_HOME\" )\n\n    JAVACMD=$( cygpath --unix \"$JAVACMD\" )\n\n    # Now convert the arguments - kludge to limit ourselves to /bin/sh\n    for arg do\n        if\n            case $arg in                                #(\n              -*)   false ;;                            # don't mess with options #(\n              /?*)  t=${arg#/} t=/${t%%/*}              # looks like a POSIX filepath\n                    [ -e \"$t\" ] ;;                      #(\n              *)    false ;;\n            esac\n        then\n            arg=$( cygpath --path --ignore --mixed \"$arg\" )\n        fi\n        # Roll the args list around exactly as many times as the number of\n        # args, so each arg winds up back in the position where it started, but\n        # possibly modified.\n        #\n        # NB: a `for` loop captures its iteration list before it begins, so\n        # changing the positional parameters here affects neither the number of\n        # iterations, nor the values presented in `arg`.\n        shift                   # remove old arg\n        set -- \"$@\" \"$arg\"      # push replacement arg\n    done\nfi\n\n\n# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.\nDEFAULT_JVM_OPTS='\"-Xmx64m\" \"-Xms64m\"'\n\n# Collect all arguments for the java command:\n#   * DEFAULT_JVM_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments,\n#     and any embedded shellness will be escaped.\n#   * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be\n#     treated as '${Hostname}' itself on the command line.\n\nset -- \\\n        \"-Dorg.gradle.appname=$APP_BASE_NAME\" \\\n        -jar \"$APP_HOME/gradle/wrapper/gradle-wrapper.jar\" \\\n        \"$@\"\n\n# Stop when \"xargs\" is not available.\nif ! command -v xargs >/dev/null 2>&1\nthen\n    die \"xargs is not available\"\nfi\n\n# Use \"xargs\" to parse quoted args.\n#\n# With -n1 it outputs one arg per line, with the quotes and backslashes removed.\n#\n# In Bash we could simply go:\n#\n#   readarray ARGS < <( xargs -n1 <<<\"$var\" ) &&\n#   set -- \"${ARGS[@]}\" \"$@\"\n#\n# but POSIX shell has neither arrays nor command substitution, so instead we\n# post-process each arg (as a line of input to sed) to backslash-escape any\n# character that might be a shell metacharacter, then use eval to reverse\n# that process (while maintaining the separation between arguments), and wrap\n# the whole thing up as a single \"set\" statement.\n#\n# This will of course break if any of these variables contains a newline or\n# an unmatched quote.\n#\n\neval \"set -- $(\n        printf '%s\\n' \"$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS\" |\n        xargs -n1 |\n        sed ' s~[^-[:alnum:]+,./:=@_]~\\\\&~g; ' |\n        tr '\\n' ' '\n    )\" '\"$@\"'\n\nexec \"$JAVACMD\" \"$@\"\n"
  },
  {
    "path": "gradlew.bat",
    "content": "@rem\r\n@rem Copyright 2015 the original author or authors.\r\n@rem\r\n@rem Licensed under the Apache License, Version 2.0 (the \"License\");\r\n@rem you may not use this file except in compliance with the License.\r\n@rem You may obtain a copy of the License at\r\n@rem\r\n@rem      https://www.apache.org/licenses/LICENSE-2.0\r\n@rem\r\n@rem Unless required by applicable law or agreed to in writing, software\r\n@rem distributed under the License is distributed on an \"AS IS\" BASIS,\r\n@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\r\n@rem See the License for the specific language governing permissions and\r\n@rem limitations under the License.\r\n@rem\r\n@rem SPDX-License-Identifier: Apache-2.0\r\n@rem\r\n\r\n@if \"%DEBUG%\"==\"\" @echo off\r\n@rem ##########################################################################\r\n@rem\r\n@rem  Gradle startup script for Windows\r\n@rem\r\n@rem ##########################################################################\r\n\r\n@rem Set local scope for the variables, and ensure extensions are enabled\r\nsetlocal EnableExtensions\r\n\r\nset DIRNAME=%~dp0\r\nif \"%DIRNAME%\"==\"\" set DIRNAME=.\r\n@rem This is normally unused\r\nset APP_BASE_NAME=%~n0\r\nset APP_HOME=%DIRNAME%\r\n\r\n@rem Resolve any \".\" and \"..\" in APP_HOME to make it shorter.\r\nfor %%i in (\"%APP_HOME%\") do set APP_HOME=%%~fi\r\n\r\n@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.\r\nset DEFAULT_JVM_OPTS=\"-Xmx64m\" \"-Xms64m\"\r\n\r\n@rem Find java.exe\r\nif defined JAVA_HOME goto findJavaFromJavaHome\r\n\r\nset JAVA_EXE=java.exe\r\n%JAVA_EXE% -version >NUL 2>&1\r\nif %ERRORLEVEL% equ 0 goto execute\r\n\r\necho. 1>&2\r\necho ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2\r\necho. 1>&2\r\necho Please set the JAVA_HOME variable in your environment to match the 1>&2\r\necho location of your Java installation. 1>&2\r\n\r\n\"%COMSPEC%\" /c exit 1\r\n\r\n:findJavaFromJavaHome\r\nset JAVA_HOME=%JAVA_HOME:\"=%\r\nset JAVA_EXE=%JAVA_HOME%/bin/java.exe\r\n\r\nif exist \"%JAVA_EXE%\" goto execute\r\n\r\necho. 1>&2\r\necho ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2\r\necho. 1>&2\r\necho Please set the JAVA_HOME variable in your environment to match the 1>&2\r\necho location of your Java installation. 1>&2\r\n\r\n\"%COMSPEC%\" /c exit 1\r\n\r\n:execute\r\n@rem Setup the command line\r\n\r\n\r\n\r\n@rem Execute Gradle\r\n@rem endlocal doesn't take effect until after the line is parsed and variables are expanded\r\n@rem which allows us to clear the local environment before executing the java command\r\nendlocal & \"%JAVA_EXE%\" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% \"-Dorg.gradle.appname=%APP_BASE_NAME%\" -jar \"%APP_HOME%\\gradle\\wrapper\\gradle-wrapper.jar\" %* & call :exitWithErrorLevel\r\n\r\n:exitWithErrorLevel\r\n@rem Use \"%COMSPEC%\" /c exit to allow operators to work properly in scripts\r\n\"%COMSPEC%\" /c exit %ERRORLEVEL%\r\n"
  },
  {
    "path": "plugin/api/plugin.api",
    "content": "public final class app/cash/paraphrase/plugin/ParaphrasePlugin : org/gradle/api/Plugin {\n\tpublic fun <init> ()V\n\tpublic synthetic fun apply (Ljava/lang/Object;)V\n\tpublic fun apply (Lorg/gradle/api/Project;)V\n}\n\n"
  },
  {
    "path": "plugin/build.gradle.kts",
    "content": "import org.jetbrains.kotlin.gradle.dsl.JvmTarget\nimport org.jetbrains.kotlin.gradle.dsl.KotlinVersion\nimport org.jetbrains.kotlin.gradle.tasks.KotlinJvmCompile\n\n@Suppress(\"DSL_SCOPE_VIOLATION\")\nplugins {\n  `java-gradle-plugin`\n  alias(libs.plugins.kotlinJvm)\n  alias(libs.plugins.buildConfig)\n  alias(libs.plugins.mavenPublish)\n}\n\nbuildConfig {\n  useKotlinOutput {\n    internalVisibility = true\n  }\n  packageName(\"app.cash.paraphrase.plugin\")\n  buildConfigField(\"String\", \"VERSION\", \"\\\"${project.version}\\\"\")\n  buildConfigField(\"String\", \"LIB_ANDROID_COLLECTION\", \"\\\"${libs.androidCollection.get()}\\\"\")\n}\n\ngradlePlugin {\n  plugins {\n    create(\"paraphrase\") {\n      id = \"app.cash.paraphrase\"\n      implementationClass = \"app.cash.paraphrase.plugin.ParaphrasePlugin\"\n    }\n  }\n}\n\ntasks.named(\"validatePlugins\", ValidatePlugins::class) {\n  enableStricterValidation = true\n}\n\ntasks.withType<KotlinJvmCompile> {\n  compilerOptions {\n    jvmTarget = JvmTarget.JVM_17\n    // Ensure compatibility with older Gradle versions. Keep in sync with ParaphrasePlugin.kt.\n    apiVersion = KotlinVersion.KOTLIN_2_2\n    languageVersion = KotlinVersion.KOTLIN_2_2\n  }\n}\n\njava {\n  sourceCompatibility = JavaVersion.VERSION_17\n  targetCompatibility = JavaVersion.VERSION_17\n}\n\ndependencies {\n  compileOnly(libs.agp)\n\n  implementation(libs.icu4j)\n  implementation(libs.kotlinPoet)\n\n  testImplementation(libs.junit)\n  testImplementation(libs.assertk)\n}\n"
  },
  {
    "path": "plugin/gradle.properties",
    "content": "# Maven\nPOM_ARTIFACT_ID=paraphrase-plugin\nPOM_NAME=Paraphrase Gradle plugin\n\n# Omit automatic compile dependency on kotlin-stdlib. We inherit what Gradle provides.\nkotlin.stdlib.default.dependency=false\n"
  },
  {
    "path": "plugin/src/main/java/app/cash/paraphrase/plugin/ArgumentTypeResolver.kt",
    "content": "/*\n * Copyright (C) 2023 Cash App\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage app.cash.paraphrase.plugin\n\nimport app.cash.paraphrase.plugin.TokenType.Choice\nimport app.cash.paraphrase.plugin.TokenType.Date\nimport app.cash.paraphrase.plugin.TokenType.DateTime\nimport app.cash.paraphrase.plugin.TokenType.DateTimeWithOffset\nimport app.cash.paraphrase.plugin.TokenType.DateTimeWithZone\nimport app.cash.paraphrase.plugin.TokenType.Duration\nimport app.cash.paraphrase.plugin.TokenType.NoArg\nimport app.cash.paraphrase.plugin.TokenType.None\nimport app.cash.paraphrase.plugin.TokenType.Number\nimport app.cash.paraphrase.plugin.TokenType.Offset\nimport app.cash.paraphrase.plugin.TokenType.Ordinal\nimport app.cash.paraphrase.plugin.TokenType.Plural\nimport app.cash.paraphrase.plugin.TokenType.Select\nimport app.cash.paraphrase.plugin.TokenType.SelectOrdinal\nimport app.cash.paraphrase.plugin.TokenType.SpellOut\nimport app.cash.paraphrase.plugin.TokenType.Time\nimport app.cash.paraphrase.plugin.TokenType.TimeWithOffset\nimport java.time.LocalDate\nimport java.time.LocalDateTime\nimport java.time.LocalTime\nimport java.time.OffsetDateTime\nimport java.time.OffsetTime\nimport java.time.ZoneOffset\nimport java.time.ZonedDateTime\nimport kotlin.Number as KotlinNumber\nimport kotlin.reflect.KClass\nimport kotlin.time.Duration as KotlinDuration\n\n/**\n * Returns the final argument type for the given list of token types, or null if there is no\n * suitable argument type for the given combination of token types.\n *\n * For example:\n * - [Date] -> [LocalDate]\n * - [Date] + [Time] -> [LocalDateTime]\n * - [Date] + [Plural] -> null\n */\ninternal fun resolveArgumentType(tokenTypes: List<TokenType>): KClass<*>? =\n  when (resolveCompatibleTokenType(tokenTypes)) {\n    null -> null\n    None -> Any::class\n    Choice,\n    Number,\n    Plural,\n    SpellOut -> KotlinNumber::class\n    Date -> LocalDate::class\n    Time -> LocalTime::class\n    TimeWithOffset -> OffsetTime::class\n    DateTime -> LocalDateTime::class\n    DateTimeWithOffset -> OffsetDateTime::class\n    DateTimeWithZone -> ZonedDateTime::class\n    Offset -> ZoneOffset::class\n    Duration -> KotlinDuration::class\n    Ordinal,\n    SelectOrdinal -> Long::class\n    Select -> String::class\n    NoArg -> Nothing::class\n  }\n\nprivate fun resolveCompatibleTokenType(tokens: List<TokenType>): TokenType? =\n  tokens.reduceOrNull(::resolveCompatibleTokenType)\n\nprivate fun resolveCompatibleTokenType(first: TokenType?, second: TokenType?): TokenType? =\n  when {\n    first == null || second == null -> null\n    first == second || second.compatibleTypes.contains(first) -> first\n    first.compatibleTypes.contains(second) -> second\n    else -> first.compatibleTypes.firstOrNull { second.compatibleTypes.contains(it) }\n  }\n\nprivate val TokenType.compatibleTypes: List<TokenType>\n  get() = compatibleTokenTypes[this]!!\n\n/**\n * For a token of type A, tokens of type B are considered compatible if the argument type that\n * satisfies B also satisfies A.\n *\n * For example:\n * - [DateTime] is compatible with [Date], because [LocalDateTime] contains the date information\n *   required by [Date] tokens.\n * - [Date] is not compatible with [DateTime], because [LocalDate] does not contain the time\n *   information required by [DateTime] tokens.\n *\n * Compatible types are ordered from least restrictive to most restrictive. [ZonedDateTime] contains\n * a superset of the information in [DateTime], so it comes later in the list of types compatible\n * with [Date].\n */\nprivate val compatibleTokenTypes: Map<TokenType, List<TokenType>> =\n  mapOf(\n    None to TokenType.values().asList(),\n    Number to listOf(Choice, Ordinal, Plural, SelectOrdinal, SpellOut),\n    Date to listOf(DateTime, DateTimeWithOffset, DateTimeWithZone),\n    Time to listOf(DateTime, TimeWithOffset, DateTimeWithOffset, DateTimeWithZone),\n    TimeWithOffset to listOf(DateTimeWithOffset, DateTimeWithZone),\n    DateTime to listOf(DateTimeWithOffset, DateTimeWithZone),\n    DateTimeWithOffset to listOf(DateTimeWithZone),\n    DateTimeWithZone to emptyList(),\n    Offset to listOf(TimeWithOffset, DateTimeWithOffset, DateTimeWithZone),\n    SpellOut to listOf(Choice, Number, Ordinal, Plural, SelectOrdinal),\n    Ordinal to listOf(SelectOrdinal),\n    Duration to emptyList(),\n    Choice to listOf(Number, Ordinal, Plural, SelectOrdinal, SpellOut),\n    Plural to listOf(Choice, Number, Ordinal, SelectOrdinal, SpellOut),\n    Select to emptyList(),\n    SelectOrdinal to listOf(Ordinal),\n    NoArg to emptyList(),\n  )\n"
  },
  {
    "path": "plugin/src/main/java/app/cash/paraphrase/plugin/GenerateFormattedResources.kt",
    "content": "/*\n * Copyright (C) 2023 Cash App\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage app.cash.paraphrase.plugin\n\nimport app.cash.paraphrase.plugin.model.ResourceFolder\nimport java.io.File\nimport java.io.InputStream\nimport javax.inject.Inject\nimport org.gradle.api.DefaultTask\nimport org.gradle.api.file.ConfigurableFileCollection\nimport org.gradle.api.file.DirectoryProperty\nimport org.gradle.api.provider.Property\nimport org.gradle.api.tasks.CacheableTask\nimport org.gradle.api.tasks.Input\nimport org.gradle.api.tasks.InputFiles\nimport org.gradle.api.tasks.OutputDirectory\nimport org.gradle.api.tasks.PathSensitive\nimport org.gradle.api.tasks.PathSensitivity.RELATIVE\nimport org.gradle.api.tasks.TaskAction\n\n/**\n * A Gradle task that reads all of the Android string resources in a module and then generates\n * formatted resource methods for any that contain ICU arguments.\n */\n@CacheableTask\ninternal abstract class GenerateFormattedResources @Inject constructor() : DefaultTask() {\n  @get:Input abstract val namespace: Property<String>\n\n  @get:InputFiles\n  @get:PathSensitive(RELATIVE)\n  abstract val resourceDirectories: ConfigurableFileCollection\n\n  @get:OutputDirectory abstract val outputDirectory: DirectoryProperty\n\n  @TaskAction\n  fun generateFormattedStringResources() {\n    outputDirectory.get().asFile.deleteRecursively()\n\n    // Extract the 'values'-style directories from each resource directory.\n    val valuesFolders =\n      resourceDirectories.files\n        .flatMap { it.listFiles().orEmpty().toList() }\n        .filter { it.name == \"values\" || it.name.startsWith(\"values-\") }\n\n    // Turn each resource folder into a map of its name to its files.\n    //\n    // Example:\n    //   values -> [strings.xml, dimens.xml]\n    //   values-es -> [strings.xml]\n    val filesByConfiguration = valuesFolders.associate { folder ->\n      ResourceFolder(folder.name) to\n        folder.listFiles().orEmpty().filter { it.extension.equals(\"xml\", ignoreCase = true) }\n    }\n\n    // Parse the files in each folder into the tokenized resources.\n    //\n    // Example:\n    //   values -> [TokenizedResource(name=hi, ..), TokenizedResource(name=hello, ..)]\n    //   values-es -> [TokenizedResource(name=hello, ..)]\n    val resourcesByConfiguration = filesByConfiguration.mapValues { (_, files) ->\n      files.flatMap { it.checkedRead(::parseResources) }.map(::tokenizeResource)\n    }\n\n    // Split the folder map into individual maps keyed on resource name.\n    //\n    // Example:\n    //   hello -> { values -> TokenizedResource(..)\n    //              values-es -> TokenizedResource(..) }\n    //   hi -> { values -> TokenizedResource(..) }\n    val resourceConfigurationsByName =\n      resourcesByConfiguration\n        .flatMap { (key, resources) -> resources.map { resource -> key to resource } }\n        .groupBy { (_, resource) -> resource.name }\n        .mapValues { (_, value) -> value.toMap() }\n\n    // Parse the files in each folder into a set of public resource declarations.\n    // TODO: Can limit parsing to only public.xml? The wording used at\n    //  https://developer.android.com/studio/projects/android-library#PrivateResources suggests this\n    //  is the case. Check AGP source.\n    val publicResources =\n      (filesByConfiguration[ResourceFolder.Default] ?: emptyList())\n        .flatMap { it.checkedRead(::parsePublicResources) }\n        .toSet()\n\n    // Merge each resource's configuration map into final, canonical versions.\n    val mergedResources =\n      resourceConfigurationsByName.mapNotNull { (name, resourceByConfiguration) ->\n        mergeResources(name, resourceByConfiguration, publicResources)\n      }\n\n    if (mergedResources.isNotEmpty()) {\n      writeResources(namespace.get(), mergedResources).writeTo(outputDirectory.get().asFile)\n    }\n\n    // TODO Fail on errors which make it this far.\n  }\n\n  private fun <T> File.checkedRead(parser: (InputStream) -> T): T {\n    return try {\n      inputStream().buffered().use(parser)\n    } catch (e: Exception) {\n      throw IllegalArgumentException(\"Unable to parse $this\", e)\n    }\n  }\n}\n"
  },
  {
    "path": "plugin/src/main/java/app/cash/paraphrase/plugin/NodeListIterator.kt",
    "content": "/*\n * Copyright (C) 2023 Cash App\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage app.cash.paraphrase.plugin\n\nimport org.w3c.dom.Node\nimport org.w3c.dom.NodeList\n\ninternal fun NodeList.asIterator(): Iterator<Node> =\n  object : Iterator<Node> {\n    private var index = 0\n\n    override fun hasNext(): Boolean = index < length\n\n    override fun next(): Node = item(index++)\n  }\n"
  },
  {
    "path": "plugin/src/main/java/app/cash/paraphrase/plugin/ParaphrasePlugin.kt",
    "content": "/*\n * Copyright (C) 2023 Cash App\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage app.cash.paraphrase.plugin\n\nimport com.android.build.api.dsl.CommonExtension\nimport com.android.build.api.variant.AndroidComponentsExtension\nimport com.android.build.api.variant.HasAndroidTest\nimport com.android.build.api.variant.Sources\nimport org.gradle.api.Plugin\nimport org.gradle.api.Project\nimport org.gradle.api.provider.Provider\nimport org.gradle.util.GradleVersion\n\n/**\n * A Gradle plugin that generates type checked formatters for patterned Android string resources.\n */\npublic class ParaphrasePlugin : Plugin<Project> {\n  override fun apply(target: Project): Unit = target.run {\n    // If you update the minimum-supported Gradle version, check if the Kotlin api/language\n    // version\n    // can be bumped. See https://docs.gradle.org/current/userguide/compatibility.html#kotlin.\n    val gradleMinimum = GradleVersion.version(\"9.0\")\n    val gradleCurrent = GradleVersion.current()\n    require(gradleCurrent >= gradleMinimum) {\n      \"Plugin requires $gradleMinimum or newer. Found $gradleCurrent\"\n    }\n\n    addDependencies()\n    extensions.getByType(AndroidComponentsExtension::class.java).onVariants { variant ->\n      registerGenerateFormattedResourcesTask(\n        sources = variant.sources,\n        name = variant.name,\n        namespace = variant.namespace,\n      )\n\n      (variant as? HasAndroidTest)?.androidTest?.let { androidTest ->\n        registerGenerateFormattedResourcesTask(\n          sources = androidTest.sources,\n          name = androidTest.name,\n          namespace = androidTest.namespace,\n        )\n      }\n    }\n  }\n\n  private fun Project.addDependencies() {\n    val isInternal = properties[\"app.cash.paraphrase.internal\"].toString() == \"true\"\n\n    // Automatically add the runtime dependency.\n    val runtimeDependency: Any =\n      if (isInternal) {\n        dependencies.project(mapOf(\"path\" to \":runtime\"))\n      } else {\n        \"app.cash.paraphrase:paraphrase-runtime:${BuildConfig.VERSION}\"\n      }\n    dependencies.add(\"api\", runtimeDependency)\n\n    // Automatically add the runtime Compose UI dependency if Compose is being used.\n    afterEvaluate {\n      val hasComposeFeature =\n        extensions.getByType(CommonExtension::class.java).buildFeatures.compose == true\n      val hasComposePlugin = pluginManager.hasPlugin(\"org.jetbrains.kotlin.plugin.compose\")\n      if (hasComposeFeature || hasComposePlugin) {\n        val runtimeComposeUiDependency: Any =\n          if (isInternal) {\n            dependencies.project(mapOf(\"path\" to \":runtime-compose-ui\"))\n          } else {\n            \"app.cash.paraphrase:paraphrase-runtime-compose-ui:${BuildConfig.VERSION}\"\n          }\n        dependencies.add(\"implementation\", runtimeComposeUiDependency)\n      }\n    }\n\n    // Automatically add the AndroidX Collection dependency for ArrayMap.\n    dependencies.add(\"implementation\", BuildConfig.LIB_ANDROID_COLLECTION)\n  }\n\n  private fun Project.registerGenerateFormattedResourcesTask(\n    sources: Sources,\n    name: String,\n    namespace: Provider<String>,\n  ) {\n    val javaSources = sources.java ?: return\n    val resSources = sources.res ?: return\n    tasks\n      .register(\n        \"generateFormattedResources${name.replaceFirstChar { it.uppercase() }}\",\n        GenerateFormattedResources::class.java,\n      )\n      .apply {\n        javaSources.addGeneratedSourceDirectory(this, GenerateFormattedResources::outputDirectory)\n        configure { task ->\n          task.description = \"Generates type-safe formatters for $name string resources\"\n          task.namespace.set(namespace)\n          task.resourceDirectories.from(resSources.all)\n          task.outputDirectory.set(layout.buildDirectory.dir(\"generated/source/paraphrase/$name\"))\n        }\n      }\n  }\n}\n"
  },
  {
    "path": "plugin/src/main/java/app/cash/paraphrase/plugin/PublicResourceParser.kt",
    "content": "/*\n * Copyright (C) 2023 Cash App\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage app.cash.paraphrase.plugin\n\nimport app.cash.paraphrase.plugin.model.PublicResource\nimport app.cash.paraphrase.plugin.model.ResourceName\nimport java.io.InputStream\nimport javax.xml.parsers.DocumentBuilderFactory\nimport org.w3c.dom.Node\n\n/**\n * Parses and returns all of the Android <public> resources declared in the given stream, regardless\n * of type.\n */\ninternal fun parsePublicResources(inputStream: InputStream): List<PublicResource> =\n  DocumentBuilderFactory.newInstance()\n    .newDocumentBuilder()\n    .parse(inputStream)\n    .getElementsByTagName(\"public\")\n    .asIterator()\n    .asSequence()\n    .filter { it.nodeType == Node.ELEMENT_NODE }\n    .map {\n      val name = it.attributes.getNamedItem(\"name\")?.childNodes?.item(0)?.textContent\n      val type = it.attributes.getNamedItem(\"type\")?.childNodes?.item(0)?.textContent\n\n      when {\n        name != null && type != null -> PublicResource.Named(name = ResourceName(name), type = type)\n        name == null && type == null -> PublicResource.EmptyDeclaration\n        name == null ->\n          throw IllegalArgumentException(\"<public> resource with type $type must have a name\")\n        else -> throw IllegalArgumentException(\"<public> resource named $name has no type\")\n      }\n    }\n    .toList()\n"
  },
  {
    "path": "plugin/src/main/java/app/cash/paraphrase/plugin/ResourceMerger.kt",
    "content": "/*\n * Copyright (C) 2023 Cash App\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage app.cash.paraphrase.plugin\n\nimport app.cash.paraphrase.plugin.model.MergedResource\nimport app.cash.paraphrase.plugin.model.PublicResource\nimport app.cash.paraphrase.plugin.model.ResourceFolder\nimport app.cash.paraphrase.plugin.model.ResourceName\nimport app.cash.paraphrase.plugin.model.TokenizedResource\nimport app.cash.paraphrase.plugin.model.TokenizedResource.Token\nimport app.cash.paraphrase.plugin.model.TokenizedResource.Token.NamedToken\nimport app.cash.paraphrase.plugin.model.TokenizedResource.Token.NumberedToken\n\ninternal fun mergeResources(\n  name: ResourceName,\n  tokenizedResources: Map<ResourceFolder, TokenizedResource>,\n  publicResources: Collection<PublicResource>,\n): MergedResource? {\n  // TODO For now, we only process strings in the default \"values\" folder.\n  val defaultResource = tokenizedResources[ResourceFolder.Default] ?: return null\n\n  val hasContiguousNumberedTokens = run {\n    val argumentCount =\n      defaultResource.tokens\n        .mapTo(mutableSetOf()) {\n          when (it) {\n            is NamedToken -> it.name\n            is NumberedToken -> it.number.toString()\n          }\n        }\n        .size\n\n    val tokenNumbers =\n      defaultResource.tokens.filterIsInstance<NumberedToken>().mapTo(mutableSetOf()) { it.number }\n\n    (0 until argumentCount).toSet() == tokenNumbers\n  }\n\n  val deprecation =\n    if (defaultResource.tokens.any { it.type == TokenType.Choice }) {\n      MergedResource.Deprecation.WithMessage(\n        message =\n          \"\"\"\n          Use of the old 'choice' argument type is discouraged. Use a 'plural' argument to select\n          sub-messages based on a numeric value, together with the plural rules for the specified\n          language. Use a 'select' argument to select sub-messages via a fixed set of keywords.\n          \"\"\"\n            .trimIndent()\n            .replace(\"\\n\", \" \")\n      )\n    } else {\n      MergedResource.Deprecation.None\n    }\n  val arguments =\n    defaultResource.tokens\n      .groupBy { it.argumentKey }\n      .mapValues { (argumentKey, tokens) ->\n        resolveArgumentType(tokens.map { it.type })?.let { argumentType ->\n          MergedResource.Argument(\n            key = argumentKey,\n            name = tokens.first().argumentName,\n            type = argumentType,\n          )\n        }\n      }\n\n  return MergedResource(\n    name = name,\n    description = defaultResource.description,\n    visibility = publicResources.resolveVisibility(name = name, type = \"string\"),\n    arguments = arguments.values.filterNotNull(),\n    deprecation = deprecation,\n    hasContiguousNumberedTokens = hasContiguousNumberedTokens,\n    parsingErrors =\n      arguments.filterValues { it == null }.keys.map { \"Incompatible argument types for: $it\" },\n  )\n}\n\nprivate val Token.argumentKey: String\n  get() =\n    when (this) {\n      is NamedToken -> name\n      is NumberedToken -> number.toString()\n    }\n\nprivate val Token.argumentName: String\n  get() =\n    when (this) {\n      is NamedToken -> name\n      is NumberedToken -> \"arg$number\"\n    }\n\n/**\n * If no public resource declarations exist, then all resources are public. Otherwise, only those\n * declared public are public.\n */\nprivate fun Collection<PublicResource>.resolveVisibility(\n  name: ResourceName,\n  type: String,\n): MergedResource.Visibility {\n  val public = isEmpty() || any { it is PublicResource.Named && it.type == type && it.name == name }\n  return if (public) MergedResource.Visibility.Public else MergedResource.Visibility.Private\n}\n"
  },
  {
    "path": "plugin/src/main/java/app/cash/paraphrase/plugin/ResourceParser.kt",
    "content": "/*\n * Copyright (C) 2023 Cash App\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage app.cash.paraphrase.plugin\n\nimport app.cash.paraphrase.plugin.model.ResourceName\nimport app.cash.paraphrase.plugin.model.StringResource\nimport java.io.InputStream\nimport javax.xml.parsers.DocumentBuilderFactory\nimport org.w3c.dom.Node\n\n/**\n * Parses and returns all of the Android <string> resources declared in the given stream.\n *\n * Ignores all other resources, including <plurals> and <string-array>.\n */\ninternal fun parseResources(inputStream: InputStream): List<StringResource> =\n  DocumentBuilderFactory.newInstance()\n    .newDocumentBuilder()\n    .parse(inputStream)\n    .getElementsByTagName(\"string\")\n    .asIterator()\n    .asSequence()\n    .filter { it.nodeType == Node.ELEMENT_NODE }\n    .map {\n      val name = it.attributes.getNamedItem(\"name\").childNodes.item(0).textContent\n      StringResource(\n        name = ResourceName(name),\n        description = it.precedingComment()?.textContent?.trim(),\n        text = it.textContent,\n      )\n    }\n    .toList()\n\nprivate fun Node.precedingComment(): Node? {\n  val candidate = previousSibling ?: return null\n  return when {\n    candidate.nodeType == Node.COMMENT_NODE -> {\n      candidate\n    }\n    candidate.nodeType == Node.TEXT_NODE && candidate.textContent.isBlank() -> {\n      candidate.precedingComment()\n    }\n    else -> {\n      null\n    }\n  }\n}\n"
  },
  {
    "path": "plugin/src/main/java/app/cash/paraphrase/plugin/ResourceTokenizer.kt",
    "content": "/*\n * Copyright (C) 2023 Cash App\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage app.cash.paraphrase.plugin\n\nimport app.cash.paraphrase.plugin.model.StringResource\nimport app.cash.paraphrase.plugin.model.TokenizedResource\nimport app.cash.paraphrase.plugin.model.TokenizedResource.Token\nimport app.cash.paraphrase.plugin.model.TokenizedResource.Token.NamedToken\nimport app.cash.paraphrase.plugin.model.TokenizedResource.Token.NumberedToken\nimport com.ibm.icu.text.MessagePattern\nimport com.ibm.icu.text.MessagePattern.ArgType.CHOICE\nimport com.ibm.icu.text.MessagePattern.ArgType.NONE\nimport com.ibm.icu.text.MessagePattern.ArgType.PLURAL\nimport com.ibm.icu.text.MessagePattern.ArgType.SELECT\nimport com.ibm.icu.text.MessagePattern.ArgType.SELECTORDINAL\nimport com.ibm.icu.text.MessagePattern.ArgType.SIMPLE\nimport com.ibm.icu.text.MessagePattern.Part\nimport com.ibm.icu.text.MessagePattern.Part.Type.ARG_NAME\nimport com.ibm.icu.text.MessagePattern.Part.Type.ARG_NUMBER\nimport com.ibm.icu.text.MessagePattern.Part.Type.ARG_START\nimport com.ibm.icu.text.MessagePattern.Part.Type.ARG_STYLE\n\n/** Parses the given resource and extracts the ICU argument tokens. */\ninternal fun tokenizeResource(stringResource: StringResource): TokenizedResource {\n  val pattern =\n    try {\n      MessagePattern(stringResource.text)\n    } catch (throwable: Throwable) {\n      return stringResource.toTokenizedResource(\n        tokens = emptyList(),\n        parsingError = throwable.message,\n      )\n    }\n\n  if (!pattern.hasNamedArguments() && !pattern.hasNumberedArguments()) {\n    return stringResource.toTokenizedResource(tokens = emptyList())\n  }\n\n  val tokens =\n    pattern\n      .partsIterator()\n      .asSequence()\n      .withIndex()\n      .filter { (_, part) -> part.type == ARG_START }\n      .map { (index, part) ->\n        pattern.getToken(\n          identifier = pattern.getPart(index + 1),\n          type =\n            when (part.argType) {\n              NONE -> TokenType.None\n              SIMPLE ->\n                when (\n                  val simpleType = pattern.getSubstring(pattern.getPart(index + 2)).lowercase()\n                ) {\n                  \"date\" -> {\n                    val stylePart = pattern.getPart(index + 3)\n                    if (stylePart.type == ARG_STYLE) {\n                      val style = pattern.getSubstring(stylePart).trim()\n                      when (style.lowercase()) {\n                        \"short\",\n                        \"medium\",\n                        \"long\",\n                        \"full\" -> TokenType.Date\n                        else -> getTokenType(dateTimeFormatPattern = style)\n                      }\n                    } else {\n                      TokenType.Date\n                    }\n                  }\n                  \"duration\" -> TokenType.Duration\n                  \"ordinal\" -> TokenType.Ordinal\n                  \"number\" -> TokenType.Number\n                  \"spellout\" -> TokenType.SpellOut\n                  \"time\" -> {\n                    val stylePart = pattern.getPart(index + 3)\n                    if (stylePart.type == ARG_STYLE) {\n                      val style = pattern.getSubstring(stylePart).trim()\n                      when (style.lowercase()) {\n                        \"short\",\n                        \"medium\" -> TokenType.Time\n                        \"long\",\n                        \"full\" -> TokenType.DateTimeWithZone\n                        else -> getTokenType(dateTimeFormatPattern = style)\n                      }\n                    } else {\n                      TokenType.Time\n                    }\n                  }\n                  else -> error(\"Unexpected simple argument type: $simpleType\")\n                }\n              CHOICE -> TokenType.Choice\n              PLURAL -> TokenType.Plural\n              SELECT -> TokenType.Select\n              SELECTORDINAL -> TokenType.SelectOrdinal\n              else -> error(\"Unexpected argument type: ${part.argType.name}\")\n            },\n        )\n      }\n\n  return stringResource.toTokenizedResource(tokens = tokens.toList())\n}\n\nprivate fun StringResource.toTokenizedResource(\n  tokens: List<Token>,\n  parsingError: String? = null,\n): TokenizedResource =\n  TokenizedResource(\n    name = name,\n    description = description,\n    tokens = tokens,\n    parsingError = parsingError,\n  )\n\nprivate fun MessagePattern.getToken(identifier: Part, type: TokenType): Token =\n  when (identifier.type) {\n    ARG_NAME -> NamedToken(name = getSubstring(identifier), type = type)\n    ARG_NUMBER -> NumberedToken(number = identifier.value, type = type)\n    else -> error(\"Unexpected identifier type: ${identifier.type.name}\")\n  }\n\nprivate fun MessagePattern.partsIterator(): Iterator<Part> =\n  object : Iterator<Part> {\n    private var index = 0\n\n    override fun hasNext(): Boolean = index < countParts()\n\n    override fun next(): Part = getPart(index++)\n  }\n\ninternal enum class TokenType {\n  None,\n  Number,\n  Date,\n  Time,\n  TimeWithOffset,\n  DateTime,\n  DateTimeWithOffset,\n  DateTimeWithZone,\n  Offset,\n  SpellOut,\n  Ordinal,\n  Duration,\n  Choice,\n  Plural,\n  Select,\n  SelectOrdinal,\n  NoArg,\n}\n\nprivate fun getTokenType(dateTimeFormatPattern: String): TokenType {\n  var hasDate = false\n  var hasTime = false\n  var hasOffset = false\n  var hasZone = false\n  for (patternItem in dateTimeFormatPattern.getDateTimeSymbols()) {\n    if (patternItem in DateSymbols) hasDate = true\n    if (patternItem in TimeSymbols) hasTime = true\n    if (patternItem in OffsetSymbols) hasOffset = true\n    if (patternItem in ZoneSymbols) hasZone = true\n\n    // Break if we already satisfy the highest-priority condition:\n    if (hasDate && hasTime && hasZone) break\n  }\n\n  return when {\n    hasDate && hasTime && hasZone -> TokenType.DateTimeWithZone\n    hasDate && hasTime && hasOffset -> TokenType.DateTimeWithOffset\n    hasDate && hasTime -> TokenType.DateTime\n    hasDate && hasZone -> TokenType.DateTimeWithZone\n    hasDate && hasOffset -> TokenType.DateTimeWithOffset\n    hasDate -> TokenType.Date\n    hasTime && hasZone -> TokenType.DateTimeWithZone\n    hasTime && hasOffset -> TokenType.TimeWithOffset\n    hasTime -> TokenType.Time\n    hasZone -> TokenType.DateTimeWithZone\n    hasOffset -> TokenType.Offset\n    else -> TokenType.NoArg\n  }\n}\n\n// region Date/time format symbols\n// https://unicode-org.github.io/icu/userguide/format_parse/datetime/#date-field-symbol-table\n\n// Adapted from android.icu.text.SimpleDateFormat.getPatternItems\n//\n// https://cs.android.com/android/platform/superproject/+/master:external/icu/android_icu4j/src/main/java/android/icu/text/SimpleDateFormat.java;l=2146\nprivate fun String.getDateTimeSymbols(): List<Char> {\n  var isPrevQuote = false\n  var inQuote = false\n  val text = StringBuilder()\n  var itemType = Char(0)\n  var itemLength = 1\n\n  val items = mutableListOf<Char>()\n\n  forEach { ch ->\n    if (ch == '\\'') {\n      if (isPrevQuote) {\n        text.append(ch)\n        isPrevQuote = false\n      } else {\n        isPrevQuote = true\n        if (itemType != Char(0)) {\n          items.add(itemType)\n          itemType = Char(0)\n        }\n      }\n      inQuote = !inQuote\n    } else {\n      isPrevQuote = false\n      if (inQuote) {\n        text.append(ch)\n      } else {\n        if (ch.isDateTimeFormatSymbol) {\n          // a date/time pattern character\n          if (ch == itemType) {\n            itemLength++\n          } else {\n            if (itemType == Char(0)) {\n              if (text.isNotEmpty()) {\n                // Skip adding string literals to the pattern items list\n                text.setLength(0)\n              }\n            } else {\n              items.add(itemType)\n            }\n            itemType = ch\n            itemLength = 1\n          }\n        } else {\n          // a string literal\n          if (itemType != Char(0)) {\n            items.add(itemType)\n            itemType = Char(0)\n          }\n          text.append(ch)\n        }\n      }\n    }\n  }\n  // handle last item\n  if (itemType == Char(0)) {\n    if (text.isNotEmpty()) {\n      // Skip adding string literals to the pattern items list\n      text.setLength(0)\n    }\n  } else {\n    items.add(itemType)\n  }\n\n  return items.filter { it != Char(0) }\n}\n\nprivate val DateSymbols =\n  setOf(\n    'G', // era designator\n    'y', // year\n    'Y', // year of \"Week of Year\"\n    'u', // extended year\n    'U', // cyclic year name, as in Chinese lunar calendar\n    'r', // related Gregorian year\n    'Q', // quarter\n    'q', // stand-alone quarter\n    'M', // month in year\n    'L', // stand-alone month in year\n    'w', // week of year\n    'W', // week of month\n    'd', // day in month\n    'D', // day of year\n    'F', // day of week in month\n    'g', // modified julian day\n    'E', // day of week\n    'e', // local day of week (example: if Monday is 1st day, Tuesday is 2nd)\n    'c', // stand-alone local day of week\n  )\n\nprivate val TimeSymbols =\n  setOf(\n    'a', // AM or PM\n    'b', // am, pm, noon, midnight\n    'B', // flexible day periods\n    'h', // hour in am/pm (1~12)\n    'H', // hour in day (0~23)\n    'k', // hour in day (1~24)\n    'K', // hour in am/pm (0~11)\n    'm', // minute in hour\n    's', // second in minute\n    'S', // fractional second - truncates/appends zeros to the count of letters when formatting\n    'A', // milliseconds in day\n  )\n\n/**\n * Time zone formats that only depict an offset from GMT, and thus require only a\n * [java.time.ZoneOffset].\n */\nprivate val OffsetSymbols =\n  setOf(\n    'Z', // ISO8601 basic/extended hms? / long localized GMT\n    'O', // short/long localized GMT\n    'X', // ISO8601 variants, with Z for 0\n    'x', // ISO8601 variants, without Z for 0\n  )\n\n/** Time zone formats that depict a named time zone, and thus require a [java.time.ZoneId]. */\nprivate val ZoneSymbols =\n  setOf(\n    'z', // specific non-location\n    'v', // generic non-location (falls back first to VVVV)\n    'V', // short/long time zone ID / exemplar city / generic location (falls back to OOOO)\n  )\n\nprivate val Char.isDateTimeFormatSymbol: Boolean\n  get() = this in DateSymbols || this in TimeSymbols || this in OffsetSymbols || this in ZoneSymbols\n// endregion\n"
  },
  {
    "path": "plugin/src/main/java/app/cash/paraphrase/plugin/ResourceWriter.kt",
    "content": "/*\n * Copyright (C) 2023 Cash App\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage app.cash.paraphrase.plugin\n\nimport app.cash.paraphrase.plugin.model.MergedResource\nimport app.cash.paraphrase.plugin.model.MergedResource.Argument\nimport app.cash.paraphrase.plugin.model.MergedResource.Deprecation\nimport com.squareup.kotlinpoet.ANY\nimport com.squareup.kotlinpoet.AnnotationSpec\nimport com.squareup.kotlinpoet.ClassName\nimport com.squareup.kotlinpoet.CodeBlock\nimport com.squareup.kotlinpoet.FileSpec\nimport com.squareup.kotlinpoet.FunSpec\nimport com.squareup.kotlinpoet.KModifier\nimport com.squareup.kotlinpoet.NOTHING\nimport com.squareup.kotlinpoet.ParameterSpec\nimport com.squareup.kotlinpoet.ParameterizedTypeName.Companion.parameterizedBy\nimport com.squareup.kotlinpoet.PropertySpec\nimport com.squareup.kotlinpoet.STRING\nimport com.squareup.kotlinpoet.TypeName\nimport com.squareup.kotlinpoet.TypeSpec\nimport com.squareup.kotlinpoet.asClassName\nimport com.squareup.kotlinpoet.buildCodeBlock\nimport java.time.LocalDate\nimport java.time.LocalDateTime\nimport java.time.LocalTime\nimport java.time.OffsetDateTime\nimport java.time.OffsetTime\nimport java.time.ZoneOffset\nimport java.time.ZonedDateTime\nimport kotlin.time.Duration\n\n/** Writes the given tokenized resources to a Kotlin source file. */\ninternal fun writeResources(packageName: String, mergedResources: List<MergedResource>): FileSpec {\n  val packageStringsType = ClassName(packageName = packageName, \"R\", \"string\")\n  val maxVisibility = mergedResources.maxOf { it.visibility }\n  return FileSpec.builder(packageName = packageName, fileName = \"FormattedResources\")\n    .addFileComment(\n      \"\"\"\n      This code was generated by the Paraphrase Gradle plugin.\n      Do not edit this file directly. Instead, edit the string resources in the source file.\n      \"\"\"\n        .trimIndent()\n    )\n    .addImport(packageName = packageName, \"R\")\n    .addType(\n      TypeSpec.objectBuilder(\"FormattedResources\")\n        .apply {\n          mergedResources.forEach { mergedResource ->\n            if (mergedResource.arguments.isNotEmpty()) {\n              val funSpec = mergedResource.toFunSpec(packageStringsType)\n              addFunction(funSpec)\n\n              if (mergedResource.arguments.any { it.type == Long::class }) {\n                // Since Ints are used more commonly than Longs, provide an overload to accept\n                // Ints for Long arguments:\n                addFunction(mergedResource.toIntOverloadFunSpec(funSpec))\n              }\n            } else {\n              addProperty(mergedResource.toPropertySpec(packageStringsType))\n            }\n          }\n        }\n        .addModifiers(maxVisibility.toKModifier())\n        .build()\n    )\n    .build()\n}\n\nprivate fun MergedResource.toFunSpec(packageStringsType: TypeName): FunSpec {\n  return FunSpec.builder(name.value)\n    .apply { if (description != null) addKdoc(\"%L\", description) }\n    .apply { arguments.forEach { addParameter(it.toParameterSpec()) } }\n    .returns(Types.FormattedResource)\n    .apply {\n      if (deprecation is Deprecation.WithMessage) {\n        addAnnotation(annotationSpec = deprecatedAnnotationSpec(deprecation))\n      }\n\n      if (hasContiguousNumberedTokens) {\n        addCode(\n          buildCodeBlock {\n            add(\"val arguments = arrayOf(⇥\\n\")\n            for (argument in arguments) {\n              addStatement(\"%L,\", argument.toParameterCodeBlock())\n            }\n            add(\"⇤)\\n\")\n          }\n        )\n      } else {\n        addStatement(\n          \"val arguments = %T(%L)\",\n          Types.ArrayMap.parameterizedBy(STRING, ANY),\n          arguments.size,\n        )\n\n        // The `ArrayMap` into which we place the key-argument pairs uses a backing storage of an\n        // array representing a binary tree of the keys ordered by their `hashCode()`. To maximize\n        // storage density and minimize runtime complexity, the ideal insertion order sorted, as\n        // it results in linear insertion into the array with no shifts. Thankfully\n        // `String.hashCode()` is specified by the documentation and thus safe to rely on across the\n        // JVM and Android.\n        for (argument in arguments.sortedBy { it.key.hashCode() }) {\n          addCode(\"arguments.put(\\n⇥\")\n          addCode(\"%S,\\n\", argument.key)\n          addCode(\"%L,\\n\", argument.toParameterCodeBlock())\n          addCode(\"⇤)\\n\")\n        }\n      }\n    }\n    .addCode(\n      buildCodeBlock {\n        add(\"return %T(⇥\\n\", Types.FormattedResource)\n        addStatement(\"id = %T.%L,\", packageStringsType, name.value)\n        addStatement(\"arguments = arguments,\")\n        add(\"⇤)\\n\")\n      }\n    )\n    .addModifiers(visibility.toKModifier())\n    .build()\n}\n\nprivate fun deprecatedAnnotationSpec(deprecation: Deprecation.WithMessage): AnnotationSpec {\n  return AnnotationSpec.builder(Deprecated::class).addMember(\"%S\", deprecation.message).build()\n}\n\nprivate fun Argument.toParameterSpec(): ParameterSpec =\n  ParameterSpec(\n    name = name,\n    type =\n      when (type) {\n        Nothing::class -> NOTHING.copy(nullable = true)\n        else -> type.asClassName()\n      },\n  )\n\nprivate fun Argument.toParameterCodeBlock(): CodeBlock =\n  when (type) {\n    Duration::class -> CodeBlock.of(\"%L.inWholeSeconds\", name)\n    LocalDate::class ->\n      buildCodeBlock {\n        addCalendarInstance {\n          addStatement(\"set(%1L.year, %1L.monthValue·-·1, %1L.dayOfMonth)\", name)\n        }\n      }\n\n    LocalTime::class ->\n      buildCodeBlock {\n        addCalendarInstance {\n          addStatement(\"set(%T.HOUR_OF_DAY, %L.hour)\", Types.Calendar, name)\n          addStatement(\"set(%T.MINUTE, %L.minute)\", Types.Calendar, name)\n          addStatement(\"set(%T.SECOND, %L.second)\", Types.Calendar, name)\n          addStatement(\"set(%T.MILLISECOND, %L.nano·/·1_000_000)\", Types.Calendar, name)\n        }\n      }\n\n    LocalDateTime::class ->\n      buildCodeBlock { addCalendarInstance { addDateTimeSetStatements(name) } }\n\n    // `Nothing` arg must be null, but passing null to the formatter replaces the whole format\n    // with\n    //  \"null\". Passing an `Int` allows the formatter to function as expected.\n    Nothing::class -> CodeBlock.of(\"-1\")\n\n    OffsetTime::class ->\n      buildCodeBlock {\n        addCalendarInstance(timeZoneId = \"\\\"GMT\\${%L.offset.id}\\\"\", name) {\n          addStatement(\"set(%T.HOUR_OF_DAY, %L.hour)\", Types.Calendar, name)\n          addStatement(\"set(%T.MINUTE, %L.minute)\", Types.Calendar, name)\n          addStatement(\"set(%T.SECOND, %L.second)\", Types.Calendar, name)\n          addStatement(\"set(%T.MILLISECOND, %L.nano·/·1_000_000)\", Types.Calendar, name)\n        }\n      }\n\n    OffsetDateTime::class ->\n      buildCodeBlock {\n        addCalendarInstance(timeZoneId = \"\\\"GMT\\${%L.offset.id}\\\"\", name) {\n          addDateTimeSetStatements(name)\n        }\n      }\n\n    ZonedDateTime::class ->\n      buildCodeBlock {\n        addCalendarInstance(timeZoneId = \"%L.zone.id\", name) { addDateTimeSetStatements(name) }\n      }\n\n    ZoneOffset::class ->\n      buildCodeBlock { addCalendarInstance(timeZoneId = \"\\\"GMT\\${%L.id}\\\"\", name) }\n\n    else -> CodeBlock.of(\"%L\", name)\n  }\n\nprivate fun CodeBlock.Builder.addCalendarInstance(\n  timeZoneId: String? = null,\n  vararg timeZoneIdArgs: Any? = emptyArray(),\n  applyBlock: (() -> Unit)? = null,\n) {\n  val timeZoneReference = if (timeZoneId == null) \"GMT_ZONE\" else \"getTimeZone($timeZoneId)\"\n  add(\"%T.getInstance(\\n⇥\", Types.Calendar)\n  addStatement(\"%T.$timeZoneReference,\", Types.TimeZone, *timeZoneIdArgs)\n  addStatement(\"%T.Builder().setExtension('u', \\\"ca-iso8601\\\").build(),\", Types.ULocale)\n  add(\"⇤)\")\n\n  if (applyBlock != null) {\n    add(\".apply·{\\n⇥\")\n    applyBlock.invoke()\n    add(\"⇤}\")\n  }\n}\n\nprivate fun CodeBlock.Builder.addDateTimeSetStatements(dateTimeArgName: String) {\n  add(\"set(\\n⇥\")\n  addStatement(\"%L.year,\", dateTimeArgName)\n  addStatement(\"%L.monthValue·-·1,\", dateTimeArgName)\n  addStatement(\"%L.dayOfMonth,\", dateTimeArgName)\n  addStatement(\"%L.hour,\", dateTimeArgName)\n  addStatement(\"%L.minute,\", dateTimeArgName)\n  addStatement(\"%L.second,\", dateTimeArgName)\n  add(\"⇤)\\n\")\n  addStatement(\"set(%T.MILLISECOND, %L.nano·/·1_000_000)\", Types.Calendar, dateTimeArgName)\n}\n\nprivate fun MergedResource.Visibility.toKModifier(): KModifier {\n  return when (this) {\n    MergedResource.Visibility.Public -> KModifier.PUBLIC\n    MergedResource.Visibility.Private -> KModifier.INTERNAL\n  }\n}\n\nprivate fun MergedResource.toIntOverloadFunSpec(overloaded: FunSpec): FunSpec {\n  return FunSpec.builder(name.value)\n    .apply {\n      if (description != null) addKdoc(\"%L\", description)\n      addAnnotation(\n        annotationSpec =\n          AnnotationSpec.builder(Suppress::class).addMember(\"%S\", \"NOTHING_TO_INLINE\").build()\n      )\n      arguments.forEach { argument ->\n        val parameterSpec =\n          if (argument.type == Long::class) {\n            ParameterSpec(name = argument.name, type = Int::class.asClassName())\n          } else {\n            argument.toParameterSpec()\n          }\n        addParameter(parameterSpec)\n      }\n    }\n    .returns(Types.FormattedResource)\n    .apply {\n      addCode(\n        buildCodeBlock {\n          add(\"return %N(⇥\\n\", overloaded)\n          arguments.forEach { argument ->\n            val argumentInvocation =\n              if (argument.type == Long::class) {\n                \"%L.toLong(),\\n\"\n              } else {\n                \"%L,\\n\"\n              }\n            add(argumentInvocation, argument.name)\n          }\n          add(\"⇤)\\n\")\n        }\n      )\n    }\n    .addModifiers(visibility.toKModifier(), KModifier.INLINE)\n    .build()\n}\n\nprivate fun MergedResource.toPropertySpec(packageStringsType: TypeName): PropertySpec =\n  PropertySpec.builder(name.value, Int::class)\n    .getter(FunSpec.getterBuilder().addCode(\"return %T.%L\", packageStringsType, name.value).build())\n    .apply { if (description != null) addKdoc(\"%L\", description) }\n    .apply {\n      if (deprecation is Deprecation.WithMessage) {\n        val spec =\n          deprecatedAnnotationSpec(deprecation)\n            .toBuilder()\n            .useSiteTarget(AnnotationSpec.UseSiteTarget.GET)\n            .build()\n        addAnnotation(annotationSpec = spec)\n      }\n    }\n    .addAnnotation(\n      AnnotationSpec.builder(Types.StringRes)\n        .useSiteTarget(AnnotationSpec.UseSiteTarget.GET)\n        .build()\n    )\n    .addModifiers(visibility.toKModifier())\n    .build()\n\nprivate object Types {\n  val ArrayMap = ClassName(\"androidx.collection\", \"ArrayMap\")\n  val Calendar = ClassName(\"android.icu.util\", \"Calendar\")\n  val FormattedResource = ClassName(\"app.cash.paraphrase\", \"FormattedResource\")\n  val TimeZone = ClassName(\"android.icu.util\", \"TimeZone\")\n  val ULocale = ClassName(\"android.icu.util\", \"ULocale\")\n  val StringRes = ClassName(\"androidx.annotation\", \"StringRes\")\n}\n"
  },
  {
    "path": "plugin/src/main/java/app/cash/paraphrase/plugin/model/MergedResource.kt",
    "content": "/*\n * Copyright (C) 2023 Cash App\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage app.cash.paraphrase.plugin.model\n\nimport kotlin.reflect.KClass\n\n/** A string resource parsed from a strings.xml file with its associated argument tokens. */\ninternal data class MergedResource(\n  val name: ResourceName,\n  val description: String?,\n  val visibility: Visibility,\n  val arguments: List<Argument>,\n  val deprecation: Deprecation,\n  /* True when the arguments bind to a contiguous set of integer tokens counting from 0. */\n  val hasContiguousNumberedTokens: Boolean,\n  val parsingErrors: List<String>,\n) {\n  data class Argument(\n    /** The key into the format argument map. */\n    val key: String,\n    /** The public name used as a function parameter. */\n    val name: String,\n    val type: KClass<*>,\n  )\n\n  enum class Visibility {\n    Private,\n    Public,\n  }\n\n  sealed interface Deprecation {\n    object None : Deprecation {\n      override fun toString() = \"None\"\n    }\n\n    data class WithMessage(val message: String) : Deprecation\n  }\n}\n"
  },
  {
    "path": "plugin/src/main/java/app/cash/paraphrase/plugin/model/PublicResource.kt",
    "content": "/*\n * Copyright (C) 2023 Cash App\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage app.cash.paraphrase.plugin.model\n\n/** A raw public resource parsed from e.g. a public.xml file. */\ninternal sealed interface PublicResource {\n  /** An actual public resource, referencing another declared resource by [name] and [type]. */\n  data class Named(val name: ResourceName, val type: String) : PublicResource\n\n  /**\n   * An empty <public /> declaration, typically used to ensure all of a library's resources are\n   * private.\n   */\n  object EmptyDeclaration : PublicResource\n}\n"
  },
  {
    "path": "plugin/src/main/java/app/cash/paraphrase/plugin/model/ResourceFolder.kt",
    "content": "/*\n * Copyright (C) 2023 Cash App\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage app.cash.paraphrase.plugin.model\n\n/** Represents \"values-es\" in `res/values-es/strings.xml` */\n@JvmInline\ninternal value class ResourceFolder(val name: String) {\n  companion object {\n    val Default = ResourceFolder(\"values\")\n  }\n}\n"
  },
  {
    "path": "plugin/src/main/java/app/cash/paraphrase/plugin/model/ResourceName.kt",
    "content": "/*\n * Copyright (C) 2023 Cash App\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage app.cash.paraphrase.plugin.model\n\n/**\n * Represents \"hello\" in\n *\n * ```\n * <string name=\"hello\">Sup</string>\n * ```\n */\n@JvmInline internal value class ResourceName(val value: String)\n"
  },
  {
    "path": "plugin/src/main/java/app/cash/paraphrase/plugin/model/StringResource.kt",
    "content": "/*\n * Copyright (C) 2023 Cash App\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage app.cash.paraphrase.plugin.model\n\n/** A raw string resource parsed from a strings.xml file. */\ninternal data class StringResource(\n  val name: ResourceName,\n  val description: String?,\n  val text: String,\n)\n"
  },
  {
    "path": "plugin/src/main/java/app/cash/paraphrase/plugin/model/TokenizedResource.kt",
    "content": "/*\n * Copyright (C) 2023 Cash App\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage app.cash.paraphrase.plugin.model\n\nimport app.cash.paraphrase.plugin.TokenType\n\n/** A string resource parsed from a strings.xml file with its associated argument tokens. */\ninternal data class TokenizedResource(\n  val name: ResourceName,\n  val description: String?,\n  val tokens: List<Token>,\n  val parsingError: String?,\n) {\n  sealed interface Token {\n    val type: TokenType\n\n    data class NamedToken(val name: String, override val type: TokenType) : Token\n\n    data class NumberedToken(val number: Int, override val type: TokenType) : Token\n  }\n}\n"
  },
  {
    "path": "plugin/src/test/java/app/cash/paraphrase/plugin/ArgumentTypeResolverTest.kt",
    "content": "/*\n * Copyright (C) 2023 Cash App\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage app.cash.paraphrase.plugin\n\nimport app.cash.paraphrase.plugin.TokenType.Choice\nimport app.cash.paraphrase.plugin.TokenType.Date\nimport app.cash.paraphrase.plugin.TokenType.DateTime\nimport app.cash.paraphrase.plugin.TokenType.DateTimeWithOffset\nimport app.cash.paraphrase.plugin.TokenType.DateTimeWithZone\nimport app.cash.paraphrase.plugin.TokenType.Duration\nimport app.cash.paraphrase.plugin.TokenType.NoArg\nimport app.cash.paraphrase.plugin.TokenType.None\nimport app.cash.paraphrase.plugin.TokenType.Number\nimport app.cash.paraphrase.plugin.TokenType.Offset\nimport app.cash.paraphrase.plugin.TokenType.Ordinal\nimport app.cash.paraphrase.plugin.TokenType.Plural\nimport app.cash.paraphrase.plugin.TokenType.Select\nimport app.cash.paraphrase.plugin.TokenType.SelectOrdinal\nimport app.cash.paraphrase.plugin.TokenType.SpellOut\nimport app.cash.paraphrase.plugin.TokenType.Time\nimport app.cash.paraphrase.plugin.TokenType.TimeWithOffset\nimport assertk.assertThat\nimport assertk.assertions.isEqualTo\nimport java.time.LocalDate\nimport java.time.LocalDateTime\nimport java.time.LocalTime\nimport java.time.OffsetDateTime\nimport java.time.OffsetTime\nimport java.time.ZoneOffset\nimport java.time.ZonedDateTime\nimport kotlin.Number as KotlinNumber\nimport kotlin.reflect.KClass\nimport kotlin.time.Duration as KotlinDuration\nimport org.junit.Test\n\nclass ArgumentTypeResolverTest {\n  @Test\n  fun resolveEmpty() {\n    emptyList<TokenType>().assertArgumentType(null)\n  }\n\n  @Test\n  fun resolveNone() {\n    None.assertArgumentTypes { other -> resolveArgumentType(listOf(other)) }\n  }\n\n  @Test\n  fun resolveNumber() {\n    Number.assertArgumentTypes { other ->\n      when (other) {\n        None,\n        Choice,\n        Number,\n        Plural,\n        SpellOut -> KotlinNumber::class\n        Ordinal,\n        SelectOrdinal -> Long::class\n        else -> null\n      }\n    }\n  }\n\n  @Test\n  fun resolveDate() {\n    Date.assertArgumentTypes { other ->\n      when (other) {\n        None,\n        Date -> LocalDate::class\n        Time,\n        DateTime -> LocalDateTime::class\n        Offset,\n        TimeWithOffset,\n        DateTimeWithOffset -> OffsetDateTime::class\n        DateTimeWithZone -> ZonedDateTime::class\n        else -> null\n      }\n    }\n  }\n\n  @Test\n  fun resolveTime() {\n    Time.assertArgumentTypes { other ->\n      when (other) {\n        None,\n        Time -> LocalTime::class\n        Date,\n        DateTime -> LocalDateTime::class\n        Offset,\n        TimeWithOffset -> OffsetTime::class\n        DateTimeWithOffset -> OffsetDateTime::class\n        DateTimeWithZone -> ZonedDateTime::class\n        else -> null\n      }\n    }\n  }\n\n  @Test\n  fun resolveOffset() {\n    Offset.assertArgumentTypes { other ->\n      when (other) {\n        None,\n        Offset -> ZoneOffset::class\n        Date,\n        DateTime,\n        DateTimeWithOffset -> OffsetDateTime::class\n        Time,\n        TimeWithOffset -> OffsetTime::class\n        DateTimeWithZone -> ZonedDateTime::class\n        else -> null\n      }\n    }\n  }\n\n  @Test\n  fun resolveDateTime() {\n    DateTime.assertArgumentTypes { other ->\n      when (other) {\n        None,\n        Date,\n        Time,\n        DateTime -> LocalDateTime::class\n        Offset,\n        TimeWithOffset,\n        DateTimeWithOffset -> OffsetDateTime::class\n        DateTimeWithZone -> ZonedDateTime::class\n        else -> null\n      }\n    }\n  }\n\n  @Test\n  fun resolveTimeWithOffset() {\n    TimeWithOffset.assertArgumentTypes { other ->\n      when (other) {\n        None,\n        Time,\n        Offset,\n        TimeWithOffset -> OffsetTime::class\n        Date,\n        DateTime,\n        DateTimeWithOffset -> OffsetDateTime::class\n        DateTimeWithZone -> ZonedDateTime::class\n        else -> null\n      }\n    }\n  }\n\n  @Test\n  fun resolveDateTimeWithOffset() {\n    DateTimeWithOffset.assertArgumentTypes { other ->\n      when (other) {\n        None,\n        Date,\n        Time,\n        Offset,\n        DateTime,\n        TimeWithOffset,\n        DateTimeWithOffset -> OffsetDateTime::class\n        DateTimeWithZone -> ZonedDateTime::class\n        else -> null\n      }\n    }\n  }\n\n  @Test\n  fun resolveDateTimeWithZone() {\n    DateTimeWithZone.assertArgumentTypes { other ->\n      when (other) {\n        None,\n        Date,\n        Time,\n        Offset,\n        DateTime,\n        TimeWithOffset,\n        DateTimeWithOffset,\n        DateTimeWithZone -> ZonedDateTime::class\n        else -> null\n      }\n    }\n  }\n\n  @Test\n  fun resolveSpellOut() {\n    SpellOut.assertArgumentTypes { other ->\n      when (other) {\n        None,\n        Choice,\n        Number,\n        Plural,\n        SpellOut -> KotlinNumber::class\n        Ordinal,\n        SelectOrdinal -> Long::class\n        else -> null\n      }\n    }\n  }\n\n  @Test\n  fun resolveOrdinal() {\n    Ordinal.assertArgumentTypes { other ->\n      when (other) {\n        None,\n        Choice,\n        Number,\n        Ordinal,\n        Plural,\n        SelectOrdinal,\n        SpellOut -> Long::class\n        else -> null\n      }\n    }\n  }\n\n  @Test\n  fun resolveDuration() {\n    Duration.assertArgumentTypes { other ->\n      when (other) {\n        None,\n        Duration -> KotlinDuration::class\n        else -> null\n      }\n    }\n  }\n\n  @Test\n  fun resolveChoice() {\n    Choice.assertArgumentTypes { other ->\n      when (other) {\n        None,\n        Choice,\n        Number,\n        Plural,\n        SpellOut -> KotlinNumber::class\n        Ordinal,\n        SelectOrdinal -> Long::class\n        else -> null\n      }\n    }\n  }\n\n  @Test\n  fun resolvePlural() {\n    Plural.assertArgumentTypes { other ->\n      when (other) {\n        None,\n        Choice,\n        Number,\n        Plural,\n        SpellOut -> KotlinNumber::class\n        Ordinal,\n        SelectOrdinal -> Long::class\n        else -> null\n      }\n    }\n  }\n\n  @Test\n  fun resolveSelect() {\n    Select.assertArgumentTypes { other ->\n      when (other) {\n        None,\n        Select -> String::class\n        else -> null\n      }\n    }\n  }\n\n  @Test\n  fun resolveSelectOrdinal() {\n    SelectOrdinal.assertArgumentTypes { other ->\n      when (other) {\n        None,\n        Choice,\n        Number,\n        Ordinal,\n        Plural,\n        SelectOrdinal,\n        SpellOut -> Long::class\n        else -> null\n      }\n    }\n  }\n\n  @Test\n  fun resolveNoArg() {\n    NoArg.assertArgumentTypes { other ->\n      when (other) {\n        None,\n        NoArg -> Nothing::class\n        else -> null\n      }\n    }\n  }\n\n  private fun TokenType.assertArgumentTypes(expected: (TokenType) -> KClass<*>?) {\n    listOf(this).assertArgumentType(expected(this))\n    TokenType.values().forEach { other -> listOf(this, other).assertArgumentType(expected(other)) }\n  }\n\n  private fun List<TokenType>.assertArgumentType(expected: KClass<*>?) =\n    assertThat(resolveArgumentType(tokenTypes = this)).isEqualTo(expected)\n}\n"
  },
  {
    "path": "plugin/src/test/java/app/cash/paraphrase/plugin/PublicResourceParserTest.kt",
    "content": "/*\n * Copyright (C) 2023 Cash App\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage app.cash.paraphrase.plugin\n\nimport app.cash.paraphrase.plugin.model.PublicResource\nimport app.cash.paraphrase.plugin.model.ResourceName\nimport assertk.assertThat\nimport assertk.assertions.containsExactly\nimport org.junit.Assert.assertThrows\nimport org.junit.Test\n\nclass PublicResourceParserTest {\n  @Test\n  fun parseSinglePublicResource() {\n    \"\"\"\n    <?xml version=\"1.0\" encoding=\"utf-8\"?>\n    <resources>\n      <public name=\"test\" type=\"string\" />\n    </resources>\n    \"\"\"\n      .trimIndent()\n      .assertParse(PublicResource.Named(name = ResourceName(\"test\"), type = \"string\"))\n  }\n\n  @Test\n  fun parseMultiplePublicResources() {\n    \"\"\"\n    <?xml version=\"1.0\" encoding=\"utf-8\"?>\n    <resources>\n      <public name=\"test_1\" type=\"string\" />\n      <public name=\"test_2\" type=\"anim\" />\n      <public name=\"test_3\" type=\"color\" />\n    </resources>\n    \"\"\"\n      .trimIndent()\n      .assertParse(\n        PublicResource.Named(name = ResourceName(\"test_1\"), type = \"string\"),\n        PublicResource.Named(name = ResourceName(\"test_2\"), type = \"anim\"),\n        PublicResource.Named(name = ResourceName(\"test_3\"), type = \"color\"),\n      )\n  }\n\n  @Test\n  fun parseEmptyPublicResource() {\n    \"\"\"\n    <?xml version=\"1.0\" encoding=\"utf-8\"?>\n    <resources>\n      <public />\n    </resources>\n    \"\"\"\n      .trimIndent()\n      .assertParse(PublicResource.EmptyDeclaration)\n  }\n\n  @Test\n  fun ignoreOtherResources() {\n    \"\"\"\n    <?xml version=\"1.0\" encoding=\"utf-8\"?>\n    <resources>\n      <public name=\"test1\" type=\"bool\" />\n      <bool name=\"test1\">true</bool>\n      <string name=\"test2\">String test2</string>\n    </resources>\n    \"\"\"\n      .trimIndent()\n      .assertParse(PublicResource.Named(name = ResourceName(\"test1\"), type = \"bool\"))\n  }\n\n  @Test\n  fun throwOnNamedResourceWithNoName() {\n    assertThrows(IllegalArgumentException::class.java) {\n      \"\"\"\n      <?xml version=\"1.0\" encoding=\"utf-8\"?>\n      <resources>\n        <public type=\"string\" />\n      </resources>\n      \"\"\"\n        .trimIndent()\n        .assertParse()\n    }\n  }\n\n  @Test\n  fun throwOnNamedResourceWithNoType() {\n    assertThrows(IllegalArgumentException::class.java) {\n      \"\"\"\n      <?xml version=\"1.0\" encoding=\"utf-8\"?>\n      <resources>\n        <public name=\"test\" />\n      </resources>\n      \"\"\"\n        .trimIndent()\n        .assertParse()\n    }\n  }\n\n  private fun String.assertParse(vararg expectedResources: PublicResource) {\n    assertThat(parsePublicResources(byteInputStream())).containsExactly(*expectedResources)\n  }\n}\n"
  },
  {
    "path": "plugin/src/test/java/app/cash/paraphrase/plugin/ResourceMergerTest.kt",
    "content": "/*\n * Copyright (C) 2023 Cash App\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage app.cash.paraphrase.plugin\n\nimport app.cash.paraphrase.plugin.TokenType.Choice\nimport app.cash.paraphrase.plugin.TokenType.Date\nimport app.cash.paraphrase.plugin.TokenType.Plural\nimport app.cash.paraphrase.plugin.TokenType.Time\nimport app.cash.paraphrase.plugin.model.MergedResource\nimport app.cash.paraphrase.plugin.model.MergedResource.Argument\nimport app.cash.paraphrase.plugin.model.MergedResource.Deprecation\nimport app.cash.paraphrase.plugin.model.PublicResource\nimport app.cash.paraphrase.plugin.model.ResourceFolder\nimport app.cash.paraphrase.plugin.model.ResourceName\nimport app.cash.paraphrase.plugin.model.TokenizedResource\nimport app.cash.paraphrase.plugin.model.TokenizedResource.Token.NamedToken\nimport app.cash.paraphrase.plugin.model.TokenizedResource.Token.NumberedToken\nimport assertk.assertThat\nimport assertk.assertions.contains\nimport assertk.assertions.containsExactly\nimport assertk.assertions.isEmpty\nimport assertk.assertions.isEqualTo\nimport java.time.LocalDateTime\nimport org.junit.Test\n\nclass ResourceMergerTest {\n\n  @Test\n  fun emptyPublicResourcesProducesPublicResource() {\n    val result =\n      mergeResources(\n        name = ResourceName(\"test\"),\n        tokenizedResources =\n          mapOf(\n            ResourceFolder.Default to\n              TokenizedResource(\n                name = ResourceName(\"test\"),\n                description = null,\n                tokens = emptyList(),\n                parsingError = null,\n              )\n          ),\n        publicResources = emptyList(),\n      )\n    assertThat(result!!.visibility).isEqualTo(MergedResource.Visibility.Public)\n    assertThat(result.deprecation).isEqualTo(Deprecation.None)\n  }\n\n  @Test\n  fun inclusionInPublicResourcesProducesPublicResource() {\n    val result =\n      mergeResources(\n        name = ResourceName(\"test\"),\n        tokenizedResources =\n          mapOf(\n            ResourceFolder.Default to\n              TokenizedResource(\n                name = ResourceName(\"test\"),\n                description = null,\n                tokens = emptyList(),\n                parsingError = null,\n              )\n          ),\n        publicResources = listOf(PublicResource.Named(name = ResourceName(\"test\"), type = \"string\")),\n      )\n    assertThat(result!!.visibility).isEqualTo(MergedResource.Visibility.Public)\n    assertThat(result.deprecation).isEqualTo(Deprecation.None)\n  }\n\n  @Test\n  fun inclusionInPublicResourcesWithWrongTypeProducesPrivateResource() {\n    val result =\n      mergeResources(\n        name = ResourceName(\"test\"),\n        tokenizedResources =\n          mapOf(\n            ResourceFolder.Default to\n              TokenizedResource(\n                name = ResourceName(\"test\"),\n                description = null,\n                tokens = emptyList(),\n                parsingError = null,\n              )\n          ),\n        publicResources = listOf(PublicResource.Named(name = ResourceName(\"test\"), type = \"color\")),\n      )\n    assertThat(result!!.visibility).isEqualTo(MergedResource.Visibility.Private)\n    assertThat(result.deprecation).isEqualTo(Deprecation.None)\n  }\n\n  @Test\n  fun exclusionFromPublicResourcesProducesPrivateResource() {\n    val result =\n      mergeResources(\n        name = ResourceName(\"test\"),\n        tokenizedResources =\n          mapOf(\n            ResourceFolder.Default to\n              TokenizedResource(\n                name = ResourceName(\"test\"),\n                description = null,\n                tokens = emptyList(),\n                parsingError = null,\n              )\n          ),\n        publicResources =\n          listOf(\n            PublicResource.EmptyDeclaration,\n            PublicResource.Named(name = ResourceName(\"different\"), type = \"string\"),\n          ),\n      )\n    assertThat(result!!.visibility).isEqualTo(MergedResource.Visibility.Private)\n    assertThat(result.deprecation).isEqualTo(Deprecation.None)\n  }\n\n  @Test\n  fun tokensWithCompatibleTypesAreCombined() {\n    val result =\n      mergeResources(\n        name = ResourceName(\"test\"),\n        tokenizedResources =\n          mapOf(\n            ResourceFolder.Default to\n              TokenizedResource(\n                name = ResourceName(\"test\"),\n                description = null,\n                tokens =\n                  listOf(\n                    NumberedToken(number = 0, type = Date),\n                    NumberedToken(number = 0, type = Time),\n                  ),\n                parsingError = null,\n              )\n          ),\n        publicResources = emptyList(),\n      )\n    assertThat(result!!.arguments)\n      .containsExactly(Argument(key = \"0\", name = \"arg0\", type = LocalDateTime::class))\n    assertThat(result.parsingErrors).isEmpty()\n    assertThat(result.deprecation).isEqualTo(Deprecation.None)\n  }\n\n  @Test\n  fun tokensWithIncompatibleTypesReportsParsingError() {\n    val result =\n      mergeResources(\n        name = ResourceName(\"test\"),\n        tokenizedResources =\n          mapOf(\n            ResourceFolder.Default to\n              TokenizedResource(\n                name = ResourceName(\"test\"),\n                description = null,\n                tokens =\n                  listOf(\n                    NumberedToken(number = 0, type = Date),\n                    NumberedToken(number = 0, type = Plural),\n                  ),\n                parsingError = null,\n              )\n          ),\n        publicResources = emptyList(),\n      )\n    assertThat(result!!.arguments).isEmpty()\n    assertThat(result.parsingErrors).containsExactly(\"Incompatible argument types for: 0\")\n    assertThat(result.deprecation).isEqualTo(Deprecation.None)\n  }\n\n  @Test\n  fun choiceArgumentProducesDeprecatedFunction() {\n    val result =\n      mergeResources(\n        name = ResourceName(\"test\"),\n        tokenizedResources =\n          mapOf(\n            ResourceFolder.Default to\n              TokenizedResource(\n                name = ResourceName(\"test\"),\n                description = null,\n                tokens =\n                  listOf(\n                    NamedToken(name = \"choice\", type = Choice),\n                    NamedToken(name = \"other\", type = Date),\n                  ),\n                parsingError = null,\n              )\n          ),\n        publicResources = emptyList(),\n      )\n\n    val deprecation = result!!.deprecation as Deprecation.WithMessage\n    assertThat(deprecation.message).contains(\"Use of the old 'choice' argument type is discouraged\")\n  }\n}\n"
  },
  {
    "path": "plugin/src/test/java/app/cash/paraphrase/plugin/ResourceParserTest.kt",
    "content": "/*\n * Copyright (C) 2023 Cash App\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage app.cash.paraphrase.plugin\n\nimport app.cash.paraphrase.plugin.model.ResourceName\nimport app.cash.paraphrase.plugin.model.StringResource\nimport assertk.assertThat\nimport assertk.assertions.containsExactly\nimport org.junit.Test\n\nclass ResourceParserTest {\n  @Test\n  fun parseSingleStringResource() {\n    \"\"\"\n    <?xml version=\"1.0\" encoding=\"utf-8\"?>\n    <resources>\n      <string name=\"test\">Test</string>\n    </resources>\n    \"\"\"\n      .trimIndent()\n      .assertParse(StringResource(name = ResourceName(\"test\"), description = null, text = \"Test\"))\n  }\n\n  @Test\n  fun parseMultipleStringResources() {\n    \"\"\"\n    <?xml version=\"1.0\" encoding=\"utf-8\"?>\n    <resources>\n      <string name=\"test_1\">Test 1</string>\n      <string name=\"test_2\">Test 2</string>\n      <string name=\"test_3\">Test 3</string>\n    </resources>\n    \"\"\"\n      .trimIndent()\n      .assertParse(\n        StringResource(name = ResourceName(\"test_1\"), description = null, text = \"Test 1\"),\n        StringResource(name = ResourceName(\"test_2\"), description = null, text = \"Test 2\"),\n        StringResource(name = ResourceName(\"test_3\"), description = null, text = \"Test 3\"),\n      )\n  }\n\n  @Test\n  fun parseStringResourceWithDescription() {\n    \"\"\"\n    <?xml version=\"1.0\" encoding=\"utf-8\"?>\n    <resources>\n      <!-- Test Description -->\n      <string name=\"test\">Test</string>\n    </resources>\n    \"\"\"\n      .trimIndent()\n      .assertParse(\n        StringResource(name = ResourceName(\"test\"), description = \"Test Description\", text = \"Test\")\n      )\n  }\n\n  @Test\n  fun parseUntranslatableStringResource() {\n    \"\"\"\n    <?xml version=\"1.0\" encoding=\"utf-8\"?>\n    <resources>\n      <string name=\"test\" translatable=\"false\">Test</string>\n    </resources>\n    \"\"\"\n      .trimIndent()\n      .assertParse(StringResource(name = ResourceName(\"test\"), description = null, text = \"Test\"))\n  }\n\n  @Test\n  fun ignorePluralResource() {\n    \"\"\"\n    <?xml version=\"1.0\" encoding=\"utf-8\"?>\n    <resources>\n      <plurals name=\"test\">\n        <item quantity=\"zero\">Test 0</item>\n        <item quantity=\"one\">Test 1</item>\n        <item quantity=\"other\">Test</item>\n      </plurals>\n    </resources>\n    \"\"\"\n      .trimIndent()\n      .assertParse()\n  }\n\n  @Test\n  fun ignoreStringArrayResource() {\n    \"\"\"\n    <?xml version=\"1.0\" encoding=\"utf-8\"?>\n    <resources>\n      <string-array name=\"test\">\n        <item>Test 1</item>\n        <item>Test 2</item>\n        <item>Test 3</item>\n      </string-array>\n    </resources>\n    \"\"\"\n      .trimIndent()\n      .assertParse()\n  }\n\n  private fun String.assertParse(vararg expectedResources: StringResource) {\n    assertThat(parseResources(byteInputStream())).containsExactly(*expectedResources)\n  }\n}\n"
  },
  {
    "path": "plugin/src/test/java/app/cash/paraphrase/plugin/ResourceTokenizerTest.kt",
    "content": "/*\n * Copyright (C) 2023 Cash App\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage app.cash.paraphrase.plugin\n\nimport app.cash.paraphrase.plugin.TokenType.Date\nimport app.cash.paraphrase.plugin.TokenType.DateTime\nimport app.cash.paraphrase.plugin.TokenType.DateTimeWithOffset\nimport app.cash.paraphrase.plugin.TokenType.DateTimeWithZone\nimport app.cash.paraphrase.plugin.TokenType.NoArg\nimport app.cash.paraphrase.plugin.TokenType.None\nimport app.cash.paraphrase.plugin.TokenType.Number\nimport app.cash.paraphrase.plugin.TokenType.Offset\nimport app.cash.paraphrase.plugin.TokenType.Plural\nimport app.cash.paraphrase.plugin.TokenType.Select\nimport app.cash.paraphrase.plugin.TokenType.SelectOrdinal\nimport app.cash.paraphrase.plugin.TokenType.Time\nimport app.cash.paraphrase.plugin.TokenType.TimeWithOffset\nimport app.cash.paraphrase.plugin.model.ResourceName\nimport app.cash.paraphrase.plugin.model.StringResource\nimport app.cash.paraphrase.plugin.model.TokenizedResource\nimport app.cash.paraphrase.plugin.model.TokenizedResource.Token\nimport app.cash.paraphrase.plugin.model.TokenizedResource.Token.NamedToken\nimport app.cash.paraphrase.plugin.model.TokenizedResource.Token.NumberedToken\nimport assertk.assertThat\nimport assertk.assertions.isEqualTo\nimport org.junit.Test\n\nclass ResourceTokenizerTest {\n  @Test\n  fun tokenizeResourceWithNoArguments() {\n    \"Test\".assertTokens()\n  }\n\n  @Test\n  fun tokenizeResourceWithNamedSimpleTokens() {\n    \"Test {test} {test_number, number} {test_date, date} {test_time, time}\"\n      .assertTokens(\n        NamedToken(name = \"test\", type = None),\n        NamedToken(name = \"test_number\", type = Number),\n        NamedToken(name = \"test_date\", type = Date),\n        NamedToken(name = \"test_time\", type = Time),\n      )\n  }\n\n  @Test\n  fun tokenizeResourceWithNumberedSimpleTokens() {\n    \"Test {0} {1, number} {2, date} {3, time}\"\n      .assertTokens(\n        NumberedToken(number = 0, type = None),\n        NumberedToken(number = 1, type = Number),\n        NumberedToken(number = 2, type = Date),\n        NumberedToken(number = 3, type = Time),\n      )\n  }\n\n  @Test\n  fun tokenizeResourceWithNamedPluralArgument() {\n    \"\"\"\n    {test, plural,\n      zero {Test 0 {test_nested}}\n      one {Test 1 {test_nested}}\n      other {Test # {test_nested}}\n    }\n    \"\"\"\n      .trimIndent()\n      .assertTokens(\n        NamedToken(name = \"test\", type = Plural),\n        NamedToken(name = \"test_nested\", type = None),\n        NamedToken(name = \"test_nested\", type = None),\n        NamedToken(name = \"test_nested\", type = None),\n      )\n  }\n\n  @Test\n  fun tokenizeResourceWithNumberedPluralArgument() {\n    \"\"\"\n    {0, plural,\n      zero {Test 0 {1}}\n      one {Test 1 {1}}\n      other {Test # {1}}\n    }\n    \"\"\"\n      .trimIndent()\n      .assertTokens(\n        NumberedToken(number = 0, type = Plural),\n        NumberedToken(number = 1, type = None),\n        NumberedToken(number = 1, type = None),\n        NumberedToken(number = 1, type = None),\n      )\n  }\n\n  @Test\n  fun tokenizeResourceWithNamedSelectArgument() {\n    \"\"\"\n    {test, select,\n      red {Test red {test_nested}}\n      green {Test green {test_nested}}\n      blue {Test blue {test_nested}}\n      other {Test other {test_nested}}\n    }\n    \"\"\"\n      .trimIndent()\n      .assertTokens(\n        NamedToken(name = \"test\", type = Select),\n        NamedToken(name = \"test_nested\", type = None),\n        NamedToken(name = \"test_nested\", type = None),\n        NamedToken(name = \"test_nested\", type = None),\n        NamedToken(name = \"test_nested\", type = None),\n      )\n  }\n\n  @Test\n  fun tokenizeResourceWithNumberedSelectArgument() {\n    \"\"\"\n    {0, select,\n      red {Test red {1}}\n      green {Test green {1}}\n      blue {Test blue {1}}\n      other {Test other {1}}\n    }\n    \"\"\"\n      .trimIndent()\n      .assertTokens(\n        NumberedToken(number = 0, type = Select),\n        NumberedToken(number = 1, type = None),\n        NumberedToken(number = 1, type = None),\n        NumberedToken(number = 1, type = None),\n        NumberedToken(number = 1, type = None),\n      )\n  }\n\n  @Test\n  fun tokenizeResourceWithNamedSelectOrdinalArgument() {\n    \"\"\"\n    {test, selectordinal,\n      zero {Test 0 {test_nested}}\n      one {Test 1 {test_nested}}\n      other {Test # {test_nested}}\n    }\n    \"\"\"\n      .trimIndent()\n      .assertTokens(\n        NamedToken(name = \"test\", type = SelectOrdinal),\n        NamedToken(name = \"test_nested\", type = None),\n        NamedToken(name = \"test_nested\", type = None),\n        NamedToken(name = \"test_nested\", type = None),\n      )\n  }\n\n  @Test\n  fun tokenizeResourceWithNumberedSelectOrdinalArgument() {\n    \"\"\"\n    {0, selectordinal,\n      zero {Test 0 {1}}\n      one {Test 1 {1}}\n      other {Test # {1}}\n    }\n    \"\"\"\n      .trimIndent()\n      .assertTokens(\n        NumberedToken(number = 0, type = SelectOrdinal),\n        NumberedToken(number = 1, type = None),\n        NumberedToken(number = 1, type = None),\n        NumberedToken(number = 1, type = None),\n      )\n  }\n\n  @Test\n  fun tokenizeResourceWithReusedNamedChoiceArgument() {\n    \"\"\"\n    {count, plural,\n      zero {Test 0 {count}}\n      one {Test 1 {count}}\n      other {Test # {count}}\n    }\n    \"\"\"\n      .trimIndent()\n      .assertTokens(\n        NamedToken(name = \"count\", type = Plural),\n        NamedToken(name = \"count\", type = None),\n        NamedToken(name = \"count\", type = None),\n        NamedToken(name = \"count\", type = None),\n      )\n  }\n\n  @Test\n  fun tokenizeResourceWithReusedNumberedChoiceArgument() {\n    \"\"\"\n    {0, plural,\n      zero {Test 0 {0}}\n      one {Test 1 {0}}\n      other {Test # {0}}\n    }\n    \"\"\"\n      .trimIndent()\n      .assertTokens(\n        NumberedToken(number = 0, type = Plural),\n        NumberedToken(number = 0, type = None),\n        NumberedToken(number = 0, type = None),\n        NumberedToken(number = 0, type = None),\n      )\n  }\n\n  @Test\n  fun tokenizeResourceWithDateFormat() {\n    \"\"\"\n    Test\n    {short, date, short}\n    {medium, date, medium}\n    {long, date, long}\n    {full, date, full}\n    \"\"\"\n      .trimIndent()\n      .assertTokens(\n        NamedToken(name = \"short\", type = Date),\n        NamedToken(name = \"medium\", type = Date),\n        NamedToken(name = \"long\", type = Date),\n        NamedToken(name = \"full\", type = Date),\n      )\n  }\n\n  @Test\n  fun tokenizeResourceWithTimeFormat() {\n    \"\"\"\n    Test\n    {short, time, short}\n    {medium, time, medium}\n    {long, time, long}\n    {full, time, full}\n    \"\"\"\n      .trimIndent()\n      .assertTokens(\n        NamedToken(name = \"short\", type = Time),\n        NamedToken(name = \"medium\", type = Time),\n        NamedToken(name = \"long\", type = DateTimeWithZone),\n        NamedToken(name = \"full\", type = DateTimeWithZone),\n      )\n  }\n\n  @Test\n  fun tokenizeResourceWithDateTimeFormatPattern() {\n    for (type in setOf(\"date\", \"time\")) {\n      \"\"\"\n        Test\n        {date_time_id, $type, yaz}\n        {date_time_offset, $type, MbZ}\n        {date_time, $type, Lh}\n        {date_id, $type, wz}\n        {date_offset, $type, WO}\n        {date, $type, d}\n        {time_id, $type, Hv}\n        {time_offset, $type, mX}\n        {time, $type, s}\n        {id, $type, V}\n        {offset, $type, x}\n        {no_arg, $type, 'yaz'}\n      \"\"\"\n        .trimIndent()\n        .assertTokens(\n          NamedToken(name = \"date_time_id\", type = DateTimeWithZone),\n          NamedToken(name = \"date_time_offset\", type = DateTimeWithOffset),\n          NamedToken(name = \"date_time\", type = DateTime),\n          NamedToken(name = \"date_id\", type = DateTimeWithZone),\n          NamedToken(name = \"date_offset\", type = DateTimeWithOffset),\n          NamedToken(name = \"date\", type = Date),\n          NamedToken(name = \"time_id\", type = DateTimeWithZone),\n          NamedToken(name = \"time_offset\", type = TimeWithOffset),\n          NamedToken(name = \"time\", type = Time),\n          NamedToken(name = \"id\", type = DateTimeWithZone),\n          NamedToken(name = \"offset\", type = Offset),\n          NamedToken(name = \"no_arg\", type = NoArg),\n        )\n    }\n  }\n\n  @Test\n  fun tokenizeResourceWithInvalidIcuFormat() {\n    \"Test {{test}}\"\n      .assertNoTokensWithError(\"\"\"Bad argument syntax: [at pattern index 6] \"{test}}\"\"\"\")\n  }\n\n  private fun String.assertTokens(vararg tokens: Token) {\n    assertThat(\n        tokenizeResource(\n          StringResource(name = ResourceName(\"test\"), description = \"Test Description\", text = this)\n        )\n      )\n      .isEqualTo(\n        TokenizedResource(\n          name = ResourceName(\"test\"),\n          description = \"Test Description\",\n          tokens = tokens.toList(),\n          parsingError = null,\n        )\n      )\n  }\n\n  private fun String.assertNoTokensWithError(message: String) {\n    assertThat(\n        tokenizeResource(\n          StringResource(name = ResourceName(\"test\"), description = \"Test Description\", text = this)\n        )\n      )\n      .isEqualTo(\n        TokenizedResource(\n          name = ResourceName(\"test\"),\n          description = \"Test Description\",\n          tokens = emptyList(),\n          parsingError = message,\n        )\n      )\n  }\n}\n"
  },
  {
    "path": "plugin/src/test/java/app/cash/paraphrase/plugin/ResourceWriterTest.kt",
    "content": "/*\n * Copyright (C) 2023 Cash App\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage app.cash.paraphrase.plugin\n\nimport app.cash.paraphrase.plugin.model.MergedResource\nimport app.cash.paraphrase.plugin.model.MergedResource.Deprecation\nimport app.cash.paraphrase.plugin.model.ResourceName\nimport assertk.assertThat\nimport assertk.assertions.contains\nimport com.squareup.kotlinpoet.AnnotationSpec\nimport com.squareup.kotlinpoet.FileSpec\nimport com.squareup.kotlinpoet.KModifier\nimport com.squareup.kotlinpoet.TypeSpec\nimport org.junit.Assert.fail\nimport org.junit.Test\n\nclass ResourceWriterTest {\n\n  @Test\n  fun publicResourceGetsPublicFunction() {\n    val result =\n      writeResources(\n        packageName = \"com.example\",\n        mergedResources =\n          listOf(\n            MergedResource(\n              name = ResourceName(\"test\"),\n              description = \"See https://example.com/foo%3Abar for details\",\n              visibility = MergedResource.Visibility.Public,\n              arguments = listOf(MergedResource.Argument(\"key\", \"name\", String::class)),\n              deprecation = Deprecation.None,\n              hasContiguousNumberedTokens = false,\n              parsingErrors = emptyList(),\n            ),\n            MergedResource(\n              name = ResourceName(\"test_no_args\"),\n              description = \"See https://example.com/foo%3Abar for details\",\n              visibility = MergedResource.Visibility.Public,\n              arguments = emptyList(),\n              deprecation = Deprecation.None,\n              hasContiguousNumberedTokens = false,\n              parsingErrors = emptyList(),\n            ),\n          ),\n      )\n\n    result.assertFunctionVisibility(\n      expectedClassVisibility = KModifier.PUBLIC,\n      \"test\" to KModifier.PUBLIC,\n    )\n    result.assertPropertyVisibility(\n      expectedClassVisibility = KModifier.PUBLIC,\n      \"test_no_args\" to KModifier.PUBLIC,\n    )\n  }\n\n  @Test\n  fun privateResourceGetsInternalFunction() {\n    val result =\n      writeResources(\n        packageName = \"com.example\",\n        mergedResources =\n          listOf(\n            MergedResource(\n              name = ResourceName(\"test1\"),\n              description = null,\n              visibility = MergedResource.Visibility.Public,\n              arguments = listOf(MergedResource.Argument(\"key\", \"name\", String::class)),\n              deprecation = Deprecation.None,\n              hasContiguousNumberedTokens = false,\n              parsingErrors = emptyList(),\n            ),\n            MergedResource(\n              name = ResourceName(\"test2\"),\n              description = null,\n              visibility = MergedResource.Visibility.Private,\n              arguments = listOf(MergedResource.Argument(\"key\", \"name\", String::class)),\n              deprecation = Deprecation.None,\n              hasContiguousNumberedTokens = false,\n              parsingErrors = emptyList(),\n            ),\n            MergedResource(\n              name = ResourceName(\"test_no_args1\"),\n              description = null,\n              visibility = MergedResource.Visibility.Public,\n              arguments = emptyList(),\n              deprecation = Deprecation.None,\n              hasContiguousNumberedTokens = false,\n              parsingErrors = emptyList(),\n            ),\n            MergedResource(\n              name = ResourceName(\"test_no_args2\"),\n              description = null,\n              visibility = MergedResource.Visibility.Private,\n              arguments = emptyList(),\n              deprecation = Deprecation.None,\n              hasContiguousNumberedTokens = false,\n              parsingErrors = emptyList(),\n            ),\n          ),\n      )\n\n    result.assertFunctionVisibility(\n      expectedClassVisibility = KModifier.PUBLIC,\n      \"test1\" to KModifier.PUBLIC,\n      \"test2\" to KModifier.INTERNAL,\n    )\n    result.assertPropertyVisibility(\n      expectedClassVisibility = KModifier.PUBLIC,\n      \"test_no_args1\" to KModifier.PUBLIC,\n      \"test_no_args2\" to KModifier.INTERNAL,\n    )\n  }\n\n  @Test\n  fun onlyPrivateResourcesProduceInternalObject() {\n    val result =\n      writeResources(\n        packageName = \"com.example\",\n        mergedResources =\n          listOf(\n            MergedResource(\n              name = ResourceName(\"test2\"),\n              description = null,\n              visibility = MergedResource.Visibility.Private,\n              arguments = listOf(MergedResource.Argument(\"key\", \"name\", String::class)),\n              deprecation = Deprecation.None,\n              hasContiguousNumberedTokens = false,\n              parsingErrors = emptyList(),\n            ),\n            MergedResource(\n              name = ResourceName(\"test3\"),\n              description = null,\n              visibility = MergedResource.Visibility.Private,\n              arguments = listOf(MergedResource.Argument(\"key\", \"name\", String::class)),\n              deprecation = Deprecation.None,\n              hasContiguousNumberedTokens = false,\n              parsingErrors = emptyList(),\n            ),\n            MergedResource(\n              name = ResourceName(\"test_no_args2\"),\n              description = null,\n              visibility = MergedResource.Visibility.Private,\n              arguments = emptyList(),\n              deprecation = Deprecation.None,\n              hasContiguousNumberedTokens = false,\n              parsingErrors = emptyList(),\n            ),\n            MergedResource(\n              name = ResourceName(\"test_no_args3\"),\n              description = null,\n              visibility = MergedResource.Visibility.Private,\n              arguments = emptyList(),\n              deprecation = Deprecation.None,\n              hasContiguousNumberedTokens = false,\n              parsingErrors = emptyList(),\n            ),\n          ),\n      )\n\n    result.assertFunctionVisibility(\n      expectedClassVisibility = KModifier.INTERNAL,\n      \"test2\" to KModifier.INTERNAL,\n      \"test3\" to KModifier.INTERNAL,\n    )\n    result.assertPropertyVisibility(\n      expectedClassVisibility = KModifier.INTERNAL,\n      \"test_no_args2\" to KModifier.INTERNAL,\n      \"test_no_args3\" to KModifier.INTERNAL,\n    )\n  }\n\n  private fun FileSpec.assertFunctionVisibility(\n    expectedClassVisibility: KModifier,\n    vararg expectedFunctionVisibility: Pair<String, KModifier>,\n  ) {\n    assertOnFormattedResourcesObject { formattedResourcesObject ->\n      assertThat(formattedResourcesObject.modifiers).contains(expectedClassVisibility)\n\n      expectedFunctionVisibility.forEach { (name, expectedVisibility) ->\n        val function = formattedResourcesObject.funSpecs.find { it.name == name }\n        if (function == null) {\n          fail(\"Function with name <$name> not found\")\n        } else {\n          assertThat(function.modifiers).contains(expectedVisibility)\n        }\n      }\n    }\n  }\n\n  private fun FileSpec.assertPropertyVisibility(\n    expectedClassVisibility: KModifier,\n    vararg expectedPropertyVisibility: Pair<String, KModifier>,\n  ) {\n    assertOnFormattedResourcesObject { formattedResourcesObject ->\n      assertThat(formattedResourcesObject.modifiers).contains(expectedClassVisibility)\n\n      expectedPropertyVisibility.forEach { (name, expectedVisibility) ->\n        val property = formattedResourcesObject.propertySpecs.find { it.name == name }\n        if (property == null) {\n          fail(\"Property with name <$name> not found\")\n        } else {\n          assertThat(property.modifiers).contains(expectedVisibility)\n        }\n      }\n    }\n  }\n\n  @Test\n  fun deprecationWithMessageProducesDeprecationWithMessage() {\n    val result =\n      writeResources(\n        packageName = \"com.example\",\n        mergedResources =\n          listOf(\n            MergedResource(\n              name = ResourceName(\"testFun\"),\n              description = null,\n              visibility = MergedResource.Visibility.Public,\n              arguments = listOf(MergedResource.Argument(\"key\", \"name\", String::class)),\n              deprecation = Deprecation.WithMessage(\"Test message 1\"),\n              hasContiguousNumberedTokens = false,\n              parsingErrors = emptyList(),\n            ),\n            MergedResource(\n              name = ResourceName(\"testProp\"),\n              description = null,\n              visibility = MergedResource.Visibility.Public,\n              arguments = emptyList(),\n              deprecation = Deprecation.WithMessage(\"Test message 2\"),\n              hasContiguousNumberedTokens = false,\n              parsingErrors = emptyList(),\n            ),\n          ),\n      )\n\n    result.assertOnFormattedResourcesObject { formattedResourcesObject ->\n      val testFun = formattedResourcesObject.funSpecs.single { it.name == \"testFun\" }\n      assertThat(testFun.annotations)\n        .contains(\n          AnnotationSpec.builder(Deprecated::class).addMember(\"%S\", \"Test message 1\").build()\n        )\n\n      val testProp = formattedResourcesObject.propertySpecs.single { it.name == \"testProp\" }\n      assertThat(testProp.annotations)\n        .contains(\n          AnnotationSpec.builder(Deprecated::class)\n            .useSiteTarget(AnnotationSpec.UseSiteTarget.GET)\n            .addMember(\"%S\", \"Test message 2\")\n            .build()\n        )\n    }\n  }\n\n  private inline fun FileSpec.assertOnFormattedResourcesObject(\n    block: (formattedResourcesObject: TypeSpec) -> Unit\n  ) {\n    val formattedResourcesObject =\n      members.filterIsInstance<TypeSpec>().find { it.name == \"FormattedResources\" }\n    if (formattedResourcesObject == null) {\n      fail(\"FormattedResources object not found\")\n    } else {\n      block(formattedResourcesObject)\n    }\n  }\n}\n"
  },
  {
    "path": "runtime/api/runtime.api",
    "content": "public final class app/cash/paraphrase/FormattedResource {\n\tpublic fun <init> (ILjava/lang/Object;)V\n\tpublic fun equals (Ljava/lang/Object;)Z\n\tpublic final fun getArguments ()Ljava/lang/Object;\n\tpublic final fun getId ()I\n\tpublic fun hashCode ()I\n\tpublic fun toString ()Ljava/lang/String;\n}\n\npublic final class app/cash/paraphrase/FormattedResourcesKt {\n\tpublic static final fun getString (Landroid/content/Context;Lapp/cash/paraphrase/FormattedResource;)Ljava/lang/String;\n\tpublic static final fun getString (Landroid/content/Context;Lapp/cash/paraphrase/FormattedResource;Landroid/icu/util/ULocale;)Ljava/lang/String;\n\tpublic static final fun getString (Landroid/content/Context;Lapp/cash/paraphrase/FormattedResource;Ljava/util/Locale;)Ljava/lang/String;\n\tpublic static final fun getString (Landroid/content/res/Resources;Lapp/cash/paraphrase/FormattedResource;)Ljava/lang/String;\n\tpublic static final fun getString (Landroid/content/res/Resources;Lapp/cash/paraphrase/FormattedResource;Landroid/icu/util/ULocale;)Ljava/lang/String;\n\tpublic static final fun getString (Landroid/content/res/Resources;Lapp/cash/paraphrase/FormattedResource;Ljava/util/Locale;)Ljava/lang/String;\n}\n\n"
  },
  {
    "path": "runtime/build.gradle.kts",
    "content": "@Suppress(\"DSL_SCOPE_VIOLATION\")\nplugins {\n  alias(libs.plugins.androidLibrary)\n  alias(libs.plugins.kotlinParcelize)\n  alias(libs.plugins.mavenPublish)\n  alias(libs.plugins.dokka)\n  alias(libs.plugins.poko)\n}\n\nandroid {\n  namespace = \"app.cash.paraphrase\"\n}\n\ndependencies {\n  api(libs.androidAnnotation)\n\n  testImplementation(libs.junit)\n  testImplementation(libs.assertk)\n  testImplementation(libs.robolectric)\n}\n"
  },
  {
    "path": "runtime/gradle.properties",
    "content": "# Maven\nPOM_ARTIFACT_ID=paraphrase-runtime\nPOM_NAME=Paraphrase runtime\n"
  },
  {
    "path": "runtime/src/main/java/app/cash/paraphrase/FormattedResource.kt",
    "content": "/*\n * Copyright (C) 2023 Cash App\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage app.cash.paraphrase\n\nimport android.icu.text.MessageFormat\nimport android.os.Parcelable\nimport androidx.annotation.StringRes\nimport dev.drewhamilton.poko.Poko\nimport kotlinx.parcelize.Parcelize\nimport kotlinx.parcelize.RawValue\n\n/**\n * A [FormattedResource] consists of:\n * 1. An Android string resource ID\n * 2. The arguments required to resolve it\n *\n * For example, if the following string was declared in the strings.xml resource file:\n * ```xml\n * <string name=\"detective_has_suspects\">\n *   {suspects, plural,\n *     =0 {{detective} has no suspects}\n *     =1 {{detective} has one suspect}\n *     other {{detective} has # suspects}\n *   }\n * </string>\n * ```\n *\n * The [FormattedResource] would contain:\n * - The R.string.detective_has_suspects resource ID\n * - An integer value for the suspects argument\n * - A string value for the detective argument\n *\n * @property arguments Arguments passed directly to [MessageFormat.format].\n */\n@Parcelize\n@Poko\npublic class FormattedResource(\n  @get:StringRes @param:StringRes public val id: Int,\n  @Poko.ReadArrayContent public val arguments: @RawValue Any,\n) : Parcelable\n"
  },
  {
    "path": "runtime/src/main/java/app/cash/paraphrase/FormattedResources.kt",
    "content": "/*\n * Copyright (C) 2023 Cash App\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage app.cash.paraphrase\n\nimport android.content.Context\nimport android.content.res.Resources\nimport android.icu.text.MessageFormat\nimport android.icu.util.ULocale\nimport java.util.Locale\n\n/** Resolves and returns the final formatted version of the given resource in the default locale. */\npublic fun Context.getString(formattedResource: FormattedResource): String =\n  resources.getString(formattedResource)\n\n/** Resolves and returns the final formatted version of the given resource in the given locale. */\npublic fun Context.getString(formattedResource: FormattedResource, locale: Locale): String =\n  resources.getString(formattedResource, locale)\n\n/** Resolves and returns the final formatted version of the given resource in the given locale. */\npublic fun Context.getString(formattedResource: FormattedResource, locale: ULocale): String =\n  resources.getString(formattedResource, locale)\n\n/** Resolves and returns the final formatted version of the given resource in the default locale. */\npublic fun Resources.getString(formattedResource: FormattedResource): String =\n  MessageFormat(getString(formattedResource.id)).format(formattedResource.arguments)\n\n/** Resolves and returns the final formatted version of the given resource in the given locale. */\npublic fun Resources.getString(formattedResource: FormattedResource, locale: Locale): String =\n  MessageFormat(getString(formattedResource.id), locale).format(formattedResource.arguments)\n\n/** Resolves and returns the final formatted version of the given resource in the given locale. */\npublic fun Resources.getString(formattedResource: FormattedResource, locale: ULocale): String =\n  MessageFormat(getString(formattedResource.id), locale).format(formattedResource.arguments)\n"
  },
  {
    "path": "runtime/src/test/java/app/cash/paraphrase/FormattedResourceTest.kt",
    "content": "/*\n * Copyright (C) 2023 Cash App\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage app.cash.paraphrase\n\nimport android.os.Parcel\nimport assertk.assertThat\nimport assertk.assertions.isEqualTo\nimport assertk.assertions.isFalse\nimport assertk.assertions.isNotEqualTo\nimport assertk.assertions.isTrue\nimport org.junit.Test\nimport org.junit.runner.RunWith\nimport org.robolectric.RobolectricTestRunner\n\n@RunWith(RobolectricTestRunner::class)\nclass FormattedResourceTest {\n  // region equals\n  @Test\n  fun `equals same instance returns true`() {\n    val instance = FormattedResource(id = 123, arguments = intArrayOf(1, 2))\n    @Suppress(\"KotlinConstantConditions\") assertThat(instance == instance).isTrue()\n  }\n\n  @Test\n  fun `equals instance of different class returns false`() {\n    val instance = FormattedResource(id = 123, arguments = 1)\n    assertThat(instance.equals(listOf(\"a\"))).isFalse()\n  }\n\n  @Test\n  fun `equals with different ids returns false`() {\n    val a = FormattedResource(id = 123, arguments = mapOf(\"a\" to 1, \"b\" to 2))\n    val b = FormattedResource(id = 321, arguments = mapOf(\"a\" to 1, \"b\" to 2))\n    assertThat(a == b).isFalse()\n    assertThat(b == a).isFalse()\n  }\n\n  @Test\n  fun `equals with different argument maps returns false`() {\n    val a = FormattedResource(id = 123, arguments = mapOf(\"a\" to 1, \"b\" to 2))\n    val b = FormattedResource(id = 123, arguments = mapOf(\"a\" to 3, \"b\" to 4))\n    assertThat(a == b).isFalse()\n    assertThat(b == a).isFalse()\n  }\n\n  @Test\n  fun `equals with same ids and argument maps returns true`() {\n    val a = FormattedResource(id = 123, arguments = mapOf(\"a\" to 1, \"b\" to 2))\n    val b = FormattedResource(id = 123, arguments = mapOf(\"a\" to 1, \"b\" to 2))\n    assertThat(a == b).isTrue()\n    assertThat(b == a).isTrue()\n  }\n\n  @Test\n  fun `equals with different argument arrays returns false`() {\n    val a = FormattedResource(id = 123, arguments = arrayOf(\"a\", \"b\"))\n    val b = FormattedResource(id = 123, arguments = arrayOf(\"c\", \"d\"))\n    assertThat(a == b).isFalse()\n    assertThat(b == a).isFalse()\n  }\n\n  @Test\n  fun `equals with only one argument array returns false`() {\n    val a = FormattedResource(id = 123, arguments = arrayOf(\"a\", \"b\"))\n    val b = FormattedResource(id = 123, arguments = \"cd\")\n    assertThat(a == b).isFalse()\n    assertThat(b == a).isFalse()\n  }\n\n  @Test\n  fun `equals with same ids and argument arrays returns true`() {\n    val a = FormattedResource(id = 123, arguments = arrayOf(\"a\", \"b\"))\n    val b = FormattedResource(id = 123, arguments = arrayOf(\"a\", \"b\"))\n    assertThat(a == b).isTrue()\n    assertThat(b == a).isTrue()\n  }\n\n  // endregion\n\n  // region hashCode\n  @Test\n  fun `hashCode with different ids returns different values`() {\n    val a = FormattedResource(id = 123, arguments = mapOf(\"a\" to 1, \"b\" to 2))\n    val b = FormattedResource(id = 321, arguments = mapOf(\"a\" to 1, \"b\" to 2))\n    assertThat(a.hashCode()).isNotEqualTo(b.hashCode())\n  }\n\n  @Test\n  fun `hashCode with different argument maps returns different values`() {\n    val a = FormattedResource(id = 123, arguments = mapOf(\"a\" to 1, \"b\" to 2))\n    val b = FormattedResource(id = 123, arguments = mapOf(\"a\" to 3, \"b\" to 4))\n    assertThat(a.hashCode()).isNotEqualTo(b.hashCode())\n  }\n\n  @Test\n  fun `hashCode with same ids and argument maps returns same values`() {\n    val a = FormattedResource(id = 123, arguments = mapOf(\"a\" to 1, \"b\" to 2))\n    val b = FormattedResource(id = 123, arguments = mapOf(\"a\" to 1, \"b\" to 2))\n    assertThat(a.hashCode()).isEqualTo(b.hashCode())\n  }\n\n  @Test\n  fun `hashCode with different argument arrays returns different values`() {\n    val a = FormattedResource(id = 123, arguments = arrayOf(\"a\", \"b\"))\n    val b = FormattedResource(id = 123, arguments = arrayOf(\"c\", \"d\"))\n    assertThat(a.hashCode()).isNotEqualTo(b.hashCode())\n  }\n\n  @Test\n  fun `hashCode with same ids and argument arrays returns same values`() {\n    val a = FormattedResource(id = 123, arguments = arrayOf(\"a\", \"b\"))\n    val b = FormattedResource(id = 123, arguments = arrayOf(\"a\", \"b\"))\n    assertThat(a.hashCode()).isEqualTo(b.hashCode())\n  }\n\n  // endregion\n\n  // region toString\n  @Test\n  fun `toString with map includes contents`() {\n    val instance = FormattedResource(id = 123, arguments = mapOf(\"a\" to 1, \"b\" to 2))\n    assertThat(instance.toString()).isEqualTo(\"FormattedResource(id=123, arguments={a=1, b=2})\")\n  }\n\n  @Test\n  fun `toString with array includes contents`() {\n    val instance = FormattedResource(id = 123, arguments = arrayOf(\"a\", \"b\"))\n    assertThat(instance.toString()).isEqualTo(\"FormattedResource(id=123, arguments=[a, b])\")\n  }\n\n  // endregion\n\n  // region Parcelable\n  @Test\n  fun `parcelable with map`() {\n    val instance = FormattedResource(id = 123, arguments = mapOf(\"a\" to 1, \"b\" to 2))\n    assertThat(instance.roundTripThroughParcel()).isEqualTo(instance)\n  }\n\n  @Test\n  fun `parcelable with array`() {\n    val instance = FormattedResource(id = 123, arguments = arrayOf(\"a\", \"b\"))\n    assertThat(instance.roundTripThroughParcel()).isEqualTo(instance)\n  }\n\n  private fun FormattedResource.roundTripThroughParcel(): FormattedResource {\n    val bytes =\n      Parcel.obtain().run {\n        writeParcelable(this@roundTripThroughParcel, 0)\n        marshall()\n      }\n    return Parcel.obtain().run {\n      unmarshall(bytes, 0, bytes.size)\n      setDataPosition(0)\n      readParcelable(FormattedResource::class.java.classLoader)!!\n    }\n  }\n  // endregion\n}\n"
  },
  {
    "path": "runtime-compose-ui/api/runtime-compose-ui.api",
    "content": "public final class app/cash/paraphrase/compose/ComposableFormattedResourcesKt {\n\tpublic static final fun formattedResource (Lapp/cash/paraphrase/FormattedResource;Landroid/icu/util/ULocale;Landroidx/compose/runtime/Composer;I)Ljava/lang/String;\n\tpublic static final fun formattedResource (Lapp/cash/paraphrase/FormattedResource;Landroidx/compose/runtime/Composer;I)Ljava/lang/String;\n\tpublic static final fun formattedResource (Lapp/cash/paraphrase/FormattedResource;Ljava/util/Locale;Landroidx/compose/runtime/Composer;I)Ljava/lang/String;\n}\n\n"
  },
  {
    "path": "runtime-compose-ui/build.gradle.kts",
    "content": "plugins {\n  alias(libs.plugins.androidLibrary)\n  alias(libs.plugins.kotlinCompose)\n  alias(libs.plugins.mavenPublish)\n  alias(libs.plugins.dokka)\n}\n\nandroid {\n  namespace = \"app.cash.paraphrase.compose\"\n}\n\ndependencies {\n  api(libs.composeUi)\n  api(projects.runtime)\n}\n"
  },
  {
    "path": "runtime-compose-ui/gradle.properties",
    "content": "# Maven\nPOM_ARTIFACT_ID=paraphrase-runtime-compose-ui\nPOM_NAME=Paraphrase runtime for Compose UI\n"
  },
  {
    "path": "runtime-compose-ui/src/main/java/app/cash/paraphrase/compose/ComposableFormattedResources.kt",
    "content": "/*\n * Copyright (C) 2023 Cash App\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage app.cash.paraphrase.compose\n\nimport android.icu.text.MessageFormat\nimport android.icu.util.ULocale\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.ReadOnlyComposable\nimport androidx.compose.ui.res.stringResource\nimport app.cash.paraphrase.FormattedResource\nimport java.util.Locale\n\n/** Resolves and returns the final formatted version of the given resource in the default locale. */\n@Composable\n@ReadOnlyComposable\npublic fun formattedResource(formattedResource: FormattedResource): String =\n  MessageFormat(stringResource(formattedResource.id)).format(formattedResource.arguments)\n\n/** Resolves and returns the final formatted version of the given resource in the given locale. */\n@Composable\n@ReadOnlyComposable\npublic fun formattedResource(formattedResource: FormattedResource, locale: Locale): String =\n  MessageFormat(stringResource(formattedResource.id), locale).format(formattedResource.arguments)\n\n/** Resolves and returns the final formatted version of the given resource in the given locale. */\n@Composable\n@ReadOnlyComposable\npublic fun formattedResource(formattedResource: FormattedResource, locale: ULocale): String =\n  MessageFormat(stringResource(formattedResource.id), locale).format(formattedResource.arguments)\n"
  },
  {
    "path": "sample/app/build.gradle.kts",
    "content": "@Suppress(\"DSL_SCOPE_VIOLATION\")\nplugins {\n  alias(libs.plugins.androidApplication)\n  alias(libs.plugins.kotlinCompose)\n  id(\"app.cash.paraphrase\")\n}\n\nandroid {\n  namespace = \"app.cash.paraphrase.sample.app\"\n\n  defaultConfig {\n    targetSdk = 34\n    versionCode = 1\n    versionName = \"1.0\"\n  }\n\n  compileOptions {\n    isCoreLibraryDesugaringEnabled = true\n  }\n}\n\nkotlin {\n  compilerOptions {\n    allWarningsAsErrors = true\n  }\n}\n\ndependencies {\n  implementation(projects.sample.library)\n  implementation(libs.androidActivityCompose)\n  implementation(libs.googleMaterial)\n  implementation(libs.composeMaterial)\n  implementation(libs.composeUi)\n\n  coreLibraryDesugaring(libs.coreLibraryDesugaring)\n}\n"
  },
  {
    "path": "sample/app/src/androidTest/kotlin/app/cash/paraphrase/sample/app/ParaphraseTest.kt",
    "content": "/*\n * Copyright (C) 2023 Cash App\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage app.cash.paraphrase.sample.app\n\nimport app.cash.paraphrase.sample.app.test.FormattedResources\n\n// Note: This is a compilation test, not a runtime test, so no assertions are needed.\nclass ParaphraseTest {\n  fun testFormattedResources() {\n    FormattedResources.app_test_text_argument(\"Jobu Tupaki\")\n  }\n}\n"
  },
  {
    "path": "sample/app/src/androidTest/res/values/strings.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n  <string name=\"app_test_text_argument\">{name}</string>\n</resources>\n"
  },
  {
    "path": "sample/app/src/debug/res/values/strings.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n  <string name=\"variant_specific\">Debug: {arg}</string>\n</resources>\n"
  },
  {
    "path": "sample/app/src/main/AndroidManifest.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<manifest xmlns:android=\"http://schemas.android.com/apk/res/android\">\n\n  <application\n      android:allowBackup=\"true\"\n      android:icon=\"@mipmap/ic_launcher\"\n      android:label=\"@string/app_name\"\n      android:roundIcon=\"@mipmap/ic_launcher_round\"\n      android:supportsRtl=\"true\"\n      android:theme=\"@style/Theme.Paraphrase\">\n    <activity\n        android:name=\"app.cash.paraphrase.sample.app.MainActivity\"\n        android:exported=\"true\">\n      <intent-filter>\n        <action android:name=\"android.intent.action.MAIN\" />\n\n        <category android:name=\"android.intent.category.LAUNCHER\" />\n      </intent-filter>\n\n      <meta-data\n          android:name=\"android.app.lib_name\"\n          android:value=\"\" />\n    </activity>\n  </application>\n\n</manifest>\n"
  },
  {
    "path": "sample/app/src/main/java/app/cash/paraphrase/sample/app/MainActivity.kt",
    "content": "/*\n * Copyright (C) 2023 Cash App\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage app.cash.paraphrase.sample.app\n\nimport android.os.Bundle\nimport androidx.activity.ComponentActivity\nimport androidx.activity.compose.setContent\nimport androidx.compose.foundation.layout.Column\nimport androidx.compose.foundation.layout.padding\nimport androidx.compose.foundation.lazy.LazyColumn\nimport androidx.compose.foundation.lazy.items\nimport androidx.compose.material.Text\nimport androidx.compose.runtime.Composable\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.graphics.Color\nimport androidx.compose.ui.res.stringResource\nimport androidx.compose.ui.text.font.FontWeight\nimport androidx.compose.ui.unit.dp\nimport androidx.compose.ui.unit.sp\nimport app.cash.paraphrase.FormattedResource\nimport app.cash.paraphrase.compose.formattedResource\nimport app.cash.paraphrase.sample.app.FormattedResources as AppFormattedResources\nimport app.cash.paraphrase.sample.library.FormattedResources as LibraryFormattedResources\nimport java.time.LocalDate\nimport java.time.LocalTime\nimport java.time.ZonedDateTime\n\nclass MainActivity : ComponentActivity() {\n  override fun onCreate(savedInstanceState: Bundle?) {\n    super.onCreate(savedInstanceState)\n    setContent {\n      LazyColumn {\n        item { Header(text = \"App Strings\") }\n        item { SampleRow(\"Text No Arguments\", stringResource(AppFormattedResources.app_name)) }\n        items(APP_SAMPLES) { SampleRow(it) }\n\n        item { Header(text = \"Library Strings\") }\n        item {\n          SampleRow(\"Text No Arguments\", stringResource(LibraryFormattedResources.library_name))\n        }\n        items(LIBRARY_SAMPLES) { SampleRow(it) }\n      }\n    }\n  }\n\n  @Composable\n  private fun Header(text: String) {\n    Text(\n      modifier = Modifier.padding(horizontal = 16.dp, vertical = 8.dp),\n      text = text.uppercase(),\n      fontSize = 14.sp,\n      fontWeight = FontWeight.Black,\n    )\n  }\n\n  @Composable\n  private fun SampleRow(sample: Sample) {\n    SampleRow(label = sample.label, text = formattedResource(sample.resource))\n  }\n\n  @Composable\n  private fun SampleRow(label: String, text: String) {\n    Column(modifier = Modifier.padding(horizontal = 16.dp, vertical = 8.dp)) {\n      Text(\n        modifier = Modifier.padding(bottom = 4.dp),\n        color = Color.DarkGray,\n        text = label,\n        fontSize = 12.sp,\n      )\n\n      Text(text = text, fontSize = 16.sp)\n    }\n  }\n\n  companion object {\n    data class Sample(val label: String, val resource: FormattedResource)\n\n    private val APP_SAMPLES =\n      listOf(\n        Sample(\n          label = \"Text Argument\",\n          resource = AppFormattedResources.app_text_argument(name = \"Jobu Tupaki\"),\n        ),\n        Sample(\n          label = \"Date Argument\",\n          resource = AppFormattedResources.app_date_argument(release_date = LocalDate.now()),\n        ),\n        Sample(\n          label = \"Number Argument\",\n          resource = AppFormattedResources.app_number_argument(budget = 10_000_000),\n        ),\n        Sample(\n          label = \"Time Argument\",\n          resource = AppFormattedResources.app_time_argument(showtime = ZonedDateTime.now()),\n        ),\n        Sample(\n          label = \"Plural Argument\",\n          resource = AppFormattedResources.app_plural_argument(count = 5),\n        ),\n        Sample(\n          label = \"Select Argument\",\n          resource = AppFormattedResources.app_select_argument(verse = \"alpha\"),\n        ),\n        Sample(\n          label = \"Select Ordinal Argument\",\n          resource = AppFormattedResources.app_select_ordinal_argument(count = 5),\n        ),\n      )\n\n    private val LIBRARY_SAMPLES =\n      listOf(\n        Sample(\n          label = \"Text Argument\",\n          resource = LibraryFormattedResources.library_text_argument(name = \"Jobu Tupaki\"),\n        ),\n        Sample(\n          label = \"Date Argument\",\n          resource = LibraryFormattedResources.library_date_argument(release_date = LocalDate.now()),\n        ),\n        Sample(\n          label = \"Number Argument\",\n          resource = LibraryFormattedResources.library_number_argument(budget = 10_000_000),\n        ),\n        Sample(\n          label = \"Time Argument\",\n          resource = LibraryFormattedResources.library_time_argument(showtime = LocalTime.now()),\n        ),\n        Sample(\n          label = \"Plural Argument\",\n          resource = LibraryFormattedResources.library_plural_argument(count = 5),\n        ),\n        Sample(\n          label = \"Select Argument\",\n          resource = LibraryFormattedResources.library_select_argument(verse = \"alpha\"),\n        ),\n        Sample(\n          label = \"Select Ordinal Argument\",\n          resource = LibraryFormattedResources.library_select_ordinal_argument(count = 5),\n        ),\n        @Suppress(\"DEPRECATION\")\n        Sample(\n          label = \"Choice argument\",\n          resource = LibraryFormattedResources.library_choice_argument(outlook = 100),\n        ),\n      )\n  }\n}\n"
  },
  {
    "path": "sample/app/src/main/res/drawable/ic_launcher_background.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:height=\"108dp\"\n    android:viewportHeight=\"108\"\n    android:viewportWidth=\"108\"\n    android:width=\"108dp\">\n  <path\n      android:fillColor=\"#3DDC84\"\n      android:pathData=\"M0,0h108v108h-108z\" />\n  <path\n      android:fillColor=\"#00000000\"\n      android:pathData=\"M9,0L9,108\"\n      android:strokeColor=\"#33FFFFFF\"\n      android:strokeWidth=\"0.8\" />\n  <path\n      android:fillColor=\"#00000000\"\n      android:pathData=\"M19,0L19,108\"\n      android:strokeColor=\"#33FFFFFF\"\n      android:strokeWidth=\"0.8\" />\n  <path\n      android:fillColor=\"#00000000\"\n      android:pathData=\"M29,0L29,108\"\n      android:strokeColor=\"#33FFFFFF\"\n      android:strokeWidth=\"0.8\" />\n  <path\n      android:fillColor=\"#00000000\"\n      android:pathData=\"M39,0L39,108\"\n      android:strokeColor=\"#33FFFFFF\"\n      android:strokeWidth=\"0.8\" />\n  <path\n      android:fillColor=\"#00000000\"\n      android:pathData=\"M49,0L49,108\"\n      android:strokeColor=\"#33FFFFFF\"\n      android:strokeWidth=\"0.8\" />\n  <path\n      android:fillColor=\"#00000000\"\n      android:pathData=\"M59,0L59,108\"\n      android:strokeColor=\"#33FFFFFF\"\n      android:strokeWidth=\"0.8\" />\n  <path\n      android:fillColor=\"#00000000\"\n      android:pathData=\"M69,0L69,108\"\n      android:strokeColor=\"#33FFFFFF\"\n      android:strokeWidth=\"0.8\" />\n  <path\n      android:fillColor=\"#00000000\"\n      android:pathData=\"M79,0L79,108\"\n      android:strokeColor=\"#33FFFFFF\"\n      android:strokeWidth=\"0.8\" />\n  <path\n      android:fillColor=\"#00000000\"\n      android:pathData=\"M89,0L89,108\"\n      android:strokeColor=\"#33FFFFFF\"\n      android:strokeWidth=\"0.8\" />\n  <path\n      android:fillColor=\"#00000000\"\n      android:pathData=\"M99,0L99,108\"\n      android:strokeColor=\"#33FFFFFF\"\n      android:strokeWidth=\"0.8\" />\n  <path\n      android:fillColor=\"#00000000\"\n      android:pathData=\"M0,9L108,9\"\n      android:strokeColor=\"#33FFFFFF\"\n      android:strokeWidth=\"0.8\" />\n  <path\n      android:fillColor=\"#00000000\"\n      android:pathData=\"M0,19L108,19\"\n      android:strokeColor=\"#33FFFFFF\"\n      android:strokeWidth=\"0.8\" />\n  <path\n      android:fillColor=\"#00000000\"\n      android:pathData=\"M0,29L108,29\"\n      android:strokeColor=\"#33FFFFFF\"\n      android:strokeWidth=\"0.8\" />\n  <path\n      android:fillColor=\"#00000000\"\n      android:pathData=\"M0,39L108,39\"\n      android:strokeColor=\"#33FFFFFF\"\n      android:strokeWidth=\"0.8\" />\n  <path\n      android:fillColor=\"#00000000\"\n      android:pathData=\"M0,49L108,49\"\n      android:strokeColor=\"#33FFFFFF\"\n      android:strokeWidth=\"0.8\" />\n  <path\n      android:fillColor=\"#00000000\"\n      android:pathData=\"M0,59L108,59\"\n      android:strokeColor=\"#33FFFFFF\"\n      android:strokeWidth=\"0.8\" />\n  <path\n      android:fillColor=\"#00000000\"\n      android:pathData=\"M0,69L108,69\"\n      android:strokeColor=\"#33FFFFFF\"\n      android:strokeWidth=\"0.8\" />\n  <path\n      android:fillColor=\"#00000000\"\n      android:pathData=\"M0,79L108,79\"\n      android:strokeColor=\"#33FFFFFF\"\n      android:strokeWidth=\"0.8\" />\n  <path\n      android:fillColor=\"#00000000\"\n      android:pathData=\"M0,89L108,89\"\n      android:strokeColor=\"#33FFFFFF\"\n      android:strokeWidth=\"0.8\" />\n  <path\n      android:fillColor=\"#00000000\"\n      android:pathData=\"M0,99L108,99\"\n      android:strokeColor=\"#33FFFFFF\"\n      android:strokeWidth=\"0.8\" />\n  <path\n      android:fillColor=\"#00000000\"\n      android:pathData=\"M19,29L89,29\"\n      android:strokeColor=\"#33FFFFFF\"\n      android:strokeWidth=\"0.8\" />\n  <path\n      android:fillColor=\"#00000000\"\n      android:pathData=\"M19,39L89,39\"\n      android:strokeColor=\"#33FFFFFF\"\n      android:strokeWidth=\"0.8\" />\n  <path\n      android:fillColor=\"#00000000\"\n      android:pathData=\"M19,49L89,49\"\n      android:strokeColor=\"#33FFFFFF\"\n      android:strokeWidth=\"0.8\" />\n  <path\n      android:fillColor=\"#00000000\"\n      android:pathData=\"M19,59L89,59\"\n      android:strokeColor=\"#33FFFFFF\"\n      android:strokeWidth=\"0.8\" />\n  <path\n      android:fillColor=\"#00000000\"\n      android:pathData=\"M19,69L89,69\"\n      android:strokeColor=\"#33FFFFFF\"\n      android:strokeWidth=\"0.8\" />\n  <path\n      android:fillColor=\"#00000000\"\n      android:pathData=\"M19,79L89,79\"\n      android:strokeColor=\"#33FFFFFF\"\n      android:strokeWidth=\"0.8\" />\n  <path\n      android:fillColor=\"#00000000\"\n      android:pathData=\"M29,19L29,89\"\n      android:strokeColor=\"#33FFFFFF\"\n      android:strokeWidth=\"0.8\" />\n  <path\n      android:fillColor=\"#00000000\"\n      android:pathData=\"M39,19L39,89\"\n      android:strokeColor=\"#33FFFFFF\"\n      android:strokeWidth=\"0.8\" />\n  <path\n      android:fillColor=\"#00000000\"\n      android:pathData=\"M49,19L49,89\"\n      android:strokeColor=\"#33FFFFFF\"\n      android:strokeWidth=\"0.8\" />\n  <path\n      android:fillColor=\"#00000000\"\n      android:pathData=\"M59,19L59,89\"\n      android:strokeColor=\"#33FFFFFF\"\n      android:strokeWidth=\"0.8\" />\n  <path\n      android:fillColor=\"#00000000\"\n      android:pathData=\"M69,19L69,89\"\n      android:strokeColor=\"#33FFFFFF\"\n      android:strokeWidth=\"0.8\" />\n  <path\n      android:fillColor=\"#00000000\"\n      android:pathData=\"M79,19L79,89\"\n      android:strokeColor=\"#33FFFFFF\"\n      android:strokeWidth=\"0.8\" />\n</vector>\n"
  },
  {
    "path": "sample/app/src/main/res/drawable-v24/ic_launcher_foreground.xml",
    "content": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    xmlns:aapt=\"http://schemas.android.com/aapt\"\n    android:height=\"108dp\"\n    android:viewportHeight=\"108\"\n    android:viewportWidth=\"108\"\n    android:width=\"108dp\">\n  <path android:pathData=\"M31,63.928c0,0 6.4,-11 12.1,-13.1c7.2,-2.6 26,-1.4 26,-1.4l38.1,38.1L107,108.928l-32,-1L31,63.928z\">\n    <aapt:attr name=\"android:fillColor\">\n      <gradient\n          android:endX=\"85.84757\"\n          android:endY=\"92.4963\"\n          android:startX=\"42.9492\"\n          android:startY=\"49.59793\"\n          android:type=\"linear\">\n        <item\n            android:color=\"#44000000\"\n            android:offset=\"0.0\" />\n        <item\n            android:color=\"#00000000\"\n            android:offset=\"1.0\" />\n      </gradient>\n    </aapt:attr>\n  </path>\n  <path\n      android:fillColor=\"#FFFFFF\"\n      android:fillType=\"nonZero\"\n      android:pathData=\"M65.3,45.828l3.8,-6.6c0.2,-0.4 0.1,-0.9 -0.3,-1.1c-0.4,-0.2 -0.9,-0.1 -1.1,0.3l-3.9,6.7c-6.3,-2.8 -13.4,-2.8 -19.7,0l-3.9,-6.7c-0.2,-0.4 -0.7,-0.5 -1.1,-0.3C38.8,38.328 38.7,38.828 38.9,39.228l3.8,6.6C36.2,49.428 31.7,56.028 31,63.928h46C76.3,56.028 71.8,49.428 65.3,45.828zM43.4,57.328c-0.8,0 -1.5,-0.5 -1.8,-1.2c-0.3,-0.7 -0.1,-1.5 0.4,-2.1c0.5,-0.5 1.4,-0.7 2.1,-0.4c0.7,0.3 1.2,1 1.2,1.8C45.3,56.528 44.5,57.328 43.4,57.328L43.4,57.328zM64.6,57.328c-0.8,0 -1.5,-0.5 -1.8,-1.2s-0.1,-1.5 0.4,-2.1c0.5,-0.5 1.4,-0.7 2.1,-0.4c0.7,0.3 1.2,1 1.2,1.8C66.5,56.528 65.6,57.328 64.6,57.328L64.6,57.328z\"\n      android:strokeColor=\"#00000000\"\n      android:strokeWidth=\"1\" />\n</vector>\n"
  },
  {
    "path": "sample/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<adaptive-icon xmlns:android=\"http://schemas.android.com/apk/res/android\">\n  <background android:drawable=\"@drawable/ic_launcher_background\" />\n  <foreground android:drawable=\"@drawable/ic_launcher_foreground\" />\n  <monochrome android:drawable=\"@drawable/ic_launcher_foreground\" />\n</adaptive-icon>\n"
  },
  {
    "path": "sample/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<adaptive-icon xmlns:android=\"http://schemas.android.com/apk/res/android\">\n  <background android:drawable=\"@drawable/ic_launcher_background\" />\n  <foreground android:drawable=\"@drawable/ic_launcher_foreground\" />\n  <monochrome android:drawable=\"@drawable/ic_launcher_foreground\" />\n</adaptive-icon>\n"
  },
  {
    "path": "sample/app/src/main/res/values/colors.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n  <color name=\"purple_200\">#FFBB86FC</color>\n  <color name=\"purple_500\">#FF6200EE</color>\n  <color name=\"purple_700\">#FF3700B3</color>\n  <color name=\"teal_200\">#FF03DAC5</color>\n  <color name=\"teal_700\">#FF018786</color>\n  <color name=\"black\">#FF000000</color>\n  <color name=\"white\">#FFFFFFFF</color>\n</resources>\n"
  },
  {
    "path": "sample/app/src/main/res/values/strings.xml",
    "content": "<resources>\n  <string name=\"app_name\">Paraphrase Sample App</string>\n  <string name=\"app_text_argument\">{name}</string>\n  <string name=\"app_date_argument\">{release_date, date, full}</string>\n  <string name=\"app_number_argument\">{budget, number}</string>\n  <string name=\"app_time_argument\">{showtime, time, long}</string>\n  <string name=\"app_plural_argument\">\n    {count, plural,\n      =0 {Jobu does not order any bagels}\n      =1 {Jobu orders an everything bagel}\n      other {Jobu orders # everything bagels}\n    }\n  </string>\n  <string name=\"app_select_argument\">\n    {verse, select,\n      alpha {Evelyn talks to Alpha-Waymond}\n      other {Evelyn talks to Waymond}\n    }\n  </string>\n  <string name=\"app_select_ordinal_argument\">\n    {count, selectordinal,\n      one {Evelyn verse jumps for the #st time}\n      two {Evelyn verse jumps for the #nd time}\n      few {Evelyn verse jumps for the #rd time}\n      other {Evelyn verse jumps for the #th time}\n    }\n  </string>\n</resources>\n"
  },
  {
    "path": "sample/app/src/main/res/values/themes.xml",
    "content": "<resources xmlns:tools=\"http://schemas.android.com/tools\">\n  <!-- Base application theme. -->\n  <style name=\"Theme.Paraphrase\" parent=\"Theme.MaterialComponents.DayNight.DarkActionBar\">\n    <!-- Primary brand color. -->\n    <item name=\"colorPrimary\">@color/purple_500</item>\n    <item name=\"colorPrimaryVariant\">@color/purple_700</item>\n    <item name=\"colorOnPrimary\">@color/white</item>\n    <!-- Secondary brand color. -->\n    <item name=\"colorSecondary\">@color/teal_200</item>\n    <item name=\"colorSecondaryVariant\">@color/teal_700</item>\n    <item name=\"colorOnSecondary\">@color/black</item>\n    <!-- Status bar color. -->\n    <item name=\"android:statusBarColor\">?attr/colorPrimaryVariant</item>\n    <!-- Customize your theme here. -->\n  </style>\n</resources>\n"
  },
  {
    "path": "sample/app/src/main/res/values-night/themes.xml",
    "content": "<resources xmlns:tools=\"http://schemas.android.com/tools\">\n  <!-- Base application theme. -->\n  <style name=\"Theme.Paraphrase\" parent=\"Theme.MaterialComponents.DayNight.DarkActionBar\">\n    <!-- Primary brand color. -->\n    <item name=\"colorPrimary\">@color/purple_200</item>\n    <item name=\"colorPrimaryVariant\">@color/purple_700</item>\n    <item name=\"colorOnPrimary\">@color/black</item>\n    <!-- Secondary brand color. -->\n    <item name=\"colorSecondary\">@color/teal_200</item>\n    <item name=\"colorSecondaryVariant\">@color/teal_200</item>\n    <item name=\"colorOnSecondary\">@color/black</item>\n    <!-- Status bar color. -->\n    <item name=\"android:statusBarColor\">?attr/colorPrimaryVariant</item>\n    <!-- Customize your theme here. -->\n  </style>\n</resources>\n"
  },
  {
    "path": "sample/app/src/release/res/values/strings.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n  <string name=\"variant_specific\">Release: {arg}</string>\n</resources>\n"
  },
  {
    "path": "sample/library/build.gradle.kts",
    "content": "@Suppress(\"DSL_SCOPE_VIOLATION\")\nplugins {\n  alias(libs.plugins.androidLibrary)\n  id(\"app.cash.paraphrase\")\n}\n\nandroid {\n  namespace = \"app.cash.paraphrase.sample.library\"\n}\n"
  },
  {
    "path": "sample/library/src/androidTest/kotlin/app/cash/paraphrase/sample/library/ParaphraseTest.kt",
    "content": "/*\n * Copyright (C) 2023 Cash App\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage app.cash.paraphrase.sample.library\n\nimport app.cash.paraphrase.sample.library.test.FormattedResources\n\n// Note: This is a compilation test, not a runtime test, so no assertions are needed.\nclass ParaphraseTest {\n  fun testFormattedResources() {\n    FormattedResources.library_test_text_argument(\"Jobu Tupaki\")\n  }\n}\n"
  },
  {
    "path": "sample/library/src/androidTest/res/values/strings.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n  <string name=\"library_test_text_argument\">{name}</string>\n</resources>\n"
  },
  {
    "path": "sample/library/src/main/res/values/strings.xml",
    "content": "<resources>\n  <string name=\"library_name\">Paraphrase Sample Library</string>\n  <string name=\"library_text_argument\">{name}</string>\n  <string name=\"library_date_argument\">{release_date, date, short}</string>\n  <string name=\"library_number_argument\">{budget, number}</string>\n  <string name=\"library_time_argument\">{showtime, time, short}</string>\n  <string name=\"library_plural_argument\">\n    {count, plural,\n      =0 {Jobu does not order any bagels}\n      =1 {Jobu orders an everything bagel}\n      other {Jobu orders # everything bagels}\n    }\n  </string>\n  <string name=\"library_select_argument\">\n    {verse, select,\n      alpha {Evelyn talks to Alpha-Waymond}\n      other {Evelyn talks to Waymond}\n    }\n  </string>\n  <string name=\"library_select_ordinal_argument\">\n    {count, selectordinal,\n      one {Evelyn verse jumps for the #st time}\n      two {Evelyn verse jumps for the #nd time}\n      few {Evelyn verse jumps for the #rd time}\n      other {Evelyn verse jumps for the #th time}\n    }\n  </string>\n  <string name=\"library_choice_argument\">\n    Jobu\\'s outlook is {outlook, choice,\n      -1#negative|\n      0#neutral|\n      1#positive\n    }\n  </string>\n</resources>\n"
  },
  {
    "path": "settings.gradle.kts",
    "content": "enableFeaturePreview(\"TYPESAFE_PROJECT_ACCESSORS\")\n\npluginManagement {\n  repositories {\n    google()\n    mavenCentral()\n    gradlePluginPortal()\n  }\n}\n\n@Suppress(\"UnstableApiUsage\")\ndependencyResolutionManagement {\n  repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)\n  repositories {\n    google()\n    mavenCentral()\n  }\n}\n\nrootProject.name = \"paraphrase\"\n\ninclude(\":plugin\", \":runtime\", \":runtime-compose-ui\", \":sample:app\", \":sample:library\", \":tests\")\n\nincludeBuild(\"build-logic\") {\n  dependencySubstitution {\n    substitute(module(\"app.cash.paraphrase:plugin\")).using(project(\":plugin\"))\n  }\n}\n"
  },
  {
    "path": "tests/build.gradle.kts",
    "content": "@Suppress(\"DSL_SCOPE_VIOLATION\")\nplugins {\n  alias(libs.plugins.androidTest)\n  id(\"app.cash.paraphrase\")\n}\n\nandroid {\n  namespace = \"app.cash.paraphrase.tests\"\n\n  // This must point at an application module, although we won't use it.\n  targetProjectPath = \":sample:app\"\n  // Our test APK runs independently of the target project APK.\n  experimentalProperties[\"android.experimental.self-instrumenting\"] = true\n\n  defaultConfig {\n    targetSdk = 34\n\n    testInstrumentationRunner = \"androidx.test.runner.AndroidJUnitRunner\"\n  }\n\n  compileOptions {\n    isCoreLibraryDesugaringEnabled = true\n  }\n}\n\ndependencies {\n  implementation(libs.junit)\n  implementation(libs.assertk)\n  implementation(libs.androidTestRunner)\n  implementation(libs.testParameterInjector)\n\n  coreLibraryDesugaring(libs.coreLibraryDesugaring)\n}\n"
  },
  {
    "path": "tests/src/main/kotlin/app/cash/paraphrase/tests/LocaleAndTimeZoneRule.kt",
    "content": "/*\n * Copyright (C) 2023 Cash App\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage app.cash.paraphrase.tests\n\nimport java.util.Locale\nimport java.util.TimeZone\nimport org.junit.rules.TestRule\nimport org.junit.runner.Description\nimport org.junit.runners.model.Statement\n\nclass LocaleAndTimeZoneRule(\n  private val locale: Locale = Locale.getDefault(),\n  private val timeZone: TimeZone = TimeZone.getDefault(),\n) : TestRule {\n  override fun apply(base: Statement, description: Description): Statement {\n    return object : Statement() {\n      override fun evaluate() {\n        val oldLocale = Locale.getDefault()\n        Locale.setDefault(locale)\n\n        val oldTimeZone = TimeZone.getDefault()\n        TimeZone.setDefault(timeZone)\n\n        try {\n          base.evaluate()\n        } finally {\n          Locale.setDefault(oldLocale)\n          TimeZone.setDefault(oldTimeZone)\n        }\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "tests/src/main/kotlin/app/cash/paraphrase/tests/LocalesTest.kt",
    "content": "/*\n * Copyright (C) 2023 Cash App\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage app.cash.paraphrase.tests\n\nimport android.icu.util.ULocale\nimport androidx.test.platform.app.InstrumentationRegistry\nimport app.cash.paraphrase.FormattedResource\nimport app.cash.paraphrase.getString\nimport app.cash.paraphrase.tests.LocalesTest.TestLocale.en_IL_ca_hebrew\nimport app.cash.paraphrase.tests.LocalesTest.TestLocale.en_US\nimport assertk.assertThat\nimport assertk.assertions.isEqualTo\nimport com.google.testing.junit.testparameterinjector.TestParameter\nimport com.google.testing.junit.testparameterinjector.TestParameterInjector\nimport java.time.LocalDate\nimport java.time.Month\nimport java.util.Locale\nimport org.junit.Before\nimport org.junit.Rule\nimport org.junit.Test\nimport org.junit.runner.RunWith\n\n@RunWith(TestParameterInjector::class)\nclass LocalesTest(@TestParameter private val testLocale: TestLocale) {\n  @get:Rule val localeRule = LocaleAndTimeZoneRule(locale = testLocale.value)\n\n  private val context = InstrumentationRegistry.getInstrumentation().context\n  private val releaseDate = LocalDate.of(2022, Month.MARCH, 24)\n\n  /**\n   * Must be instantiated after [localeRule] has taken effect, to be sure we're testing with a\n   * Calendar that was created under the [testLocale].\n   */\n  private lateinit var resource: FormattedResource\n\n  @Before\n  fun instantiateResource() {\n    resource = FormattedResources.locale_date(releaseDate)\n  }\n\n  @Test\n  fun defaultLocale() {\n    val expected =\n      when (testLocale) {\n        en_US -> \"Mar 24, 2022\"\n        en_IL_ca_hebrew -> \"21 Adar II 5782\"\n      }\n    assertThat(context.getString(resource)).isEqualTo(\"A $expected B\")\n  }\n\n  @Test\n  fun franceLocale() {\n    assertThat(context.getString(resource, Locale.FRANCE)).isEqualTo(\"A 24 mars 2022 B\")\n  }\n\n  @Test\n  fun germanyULocale() {\n    assertThat(context.getString(resource, ULocale.GERMANY)).isEqualTo(\"A 24.03.2022 B\")\n  }\n\n  @Test\n  fun hebrewCalendarLocale() {\n    assertThat(context.getString(resource, hebrewCalendarLocale)).isEqualTo(\"A 21 Adar II 5782 B\")\n  }\n\n  @Suppress(\"EnumEntryName\", \"unused\")\n  enum class TestLocale(val value: Locale) {\n    en_US(value = Locale(\"en\", \"US\")),\n    en_IL_ca_hebrew(value = hebrewCalendarLocale),\n  }\n\n  private companion object {\n    val hebrewCalendarLocale: Locale =\n      Locale.Builder().setLocale(Locale(\"en\", \"IL\")).setExtension('u', \"ca-hebrew\").build()\n  }\n}\n"
  },
  {
    "path": "tests/src/main/kotlin/app/cash/paraphrase/tests/NamedTest.kt",
    "content": "/*\n * Copyright (C) 2023 Cash App\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage app.cash.paraphrase.tests\n\nimport androidx.test.platform.app.InstrumentationRegistry\nimport app.cash.paraphrase.getString\nimport assertk.assertThat\nimport assertk.assertions.isEqualTo\nimport org.junit.Test\n\nclass NamedTest {\n  private val context = InstrumentationRegistry.getInstrumentation().context\n\n  @Test\n  fun numberedSparseOne() {\n    val formattedResource = FormattedResources.named_one(\"Z\")\n    assertThat(formattedResource.arguments as Map<String, Any>).isEqualTo(mapOf(\"one\" to \"Z\"))\n\n    val formatted = context.getString(formattedResource)\n    assertThat(formatted).isEqualTo(\"A Z B\")\n  }\n\n  @Test\n  fun numberedSparseThree() {\n    val formattedResource = FormattedResources.named_three(\"Z\", \"Y\", \"X\")\n    assertThat(formattedResource.arguments as Map<String, Any>)\n      .isEqualTo(mapOf(\"one\" to \"Z\", \"two\" to \"Y\", \"three\" to \"X\"))\n\n    val formatted = context.getString(formattedResource)\n    assertThat(formatted).isEqualTo(\"A Z B Y C X D\")\n  }\n}\n"
  },
  {
    "path": "tests/src/main/kotlin/app/cash/paraphrase/tests/NumberedTest.kt",
    "content": "/*\n * Copyright (C) 2023 Cash App\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage app.cash.paraphrase.tests\n\nimport androidx.test.platform.app.InstrumentationRegistry\nimport app.cash.paraphrase.getString\nimport assertk.assertThat\nimport assertk.assertions.containsExactly\nimport assertk.assertions.isEqualTo\nimport org.junit.Test\n\nclass NumberedTest {\n  private val context = InstrumentationRegistry.getInstrumentation().context\n\n  @Test\n  fun numberedContiguousOne() {\n    val formattedResource = FormattedResources.numbered_contiguous_one(\"Z\")\n    assertThat(formattedResource.arguments as Array<Any>).containsExactly(\"Z\")\n\n    val formatted = context.getString(formattedResource)\n    assertThat(formatted).isEqualTo(\"A Z B\")\n  }\n\n  @Test\n  fun numberedContiguousThree() {\n    val formattedResource = FormattedResources.numbered_contiguous_three(\"Z\", \"Y\", \"X\")\n    assertThat(formattedResource.arguments as Array<Any>).containsExactly(\"Z\", \"Y\", \"X\")\n\n    val formatted = context.getString(formattedResource)\n    assertThat(formatted).isEqualTo(\"A Z B Y C X D\")\n  }\n\n  @Test\n  fun numberedSparseOne() {\n    val formattedResource = FormattedResources.numbered_sparse_one(\"Z\")\n    assertThat(formattedResource.arguments as Map<String, Any>).isEqualTo(mapOf(\"1\" to \"Z\"))\n\n    val formatted = context.getString(formattedResource)\n    assertThat(formatted).isEqualTo(\"A Z B\")\n  }\n\n  @Test\n  fun numberedSparseThree() {\n    val formattedResource = FormattedResources.numbered_sparse_three(\"Z\", \"Y\", \"X\")\n    assertThat(formattedResource.arguments as Map<String, Any>)\n      .isEqualTo(mapOf(\"1\" to \"Z\", \"3\" to \"Y\", \"5\" to \"X\"))\n\n    val formatted = context.getString(formattedResource)\n    assertThat(formatted).isEqualTo(\"A Z B Y C X D\")\n  }\n}\n"
  },
  {
    "path": "tests/src/main/kotlin/app/cash/paraphrase/tests/TypesTest.kt",
    "content": "/*\n * Copyright (C) 2023 Cash App\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage app.cash.paraphrase.tests\n\nimport android.os.Build\nimport androidx.test.platform.app.InstrumentationRegistry\nimport app.cash.paraphrase.getString\nimport assertk.assertThat\nimport assertk.assertions.isEqualTo\nimport java.time.LocalDate\nimport java.time.LocalTime\nimport java.time.Month\nimport java.time.OffsetTime\nimport java.time.ZoneId\nimport java.time.ZoneOffset\nimport java.time.ZonedDateTime\nimport java.util.Locale\nimport kotlin.time.Duration.Companion.hours\nimport kotlin.time.Duration.Companion.minutes\nimport kotlin.time.Duration.Companion.seconds\nimport org.junit.Rule\nimport org.junit.Test\n\nclass TypesTest {\n  @get:Rule val localeRule = LocaleAndTimeZoneRule(locale = Locale(\"en\", \"US\"))\n\n  private val context = InstrumentationRegistry.getInstrumentation().context\n  private val releaseDate = LocalDate.of(2022, Month.MARCH, 24)\n  private val releaseTime = LocalTime.of(19, 23, 45)\n  private val releaseDateTime =\n    ZonedDateTime.of(releaseDate, releaseTime, ZoneId.of(\"Pacific/Honolulu\"))\n\n  @Test\n  fun typeNone() {\n    val formattedString = context.getString(FormattedResources.type_none(\"Z\"))\n    assertThat(formattedString).isEqualTo(\"A Z B\")\n    val formattedInteger = context.getString(FormattedResources.type_none(2))\n    assertThat(formattedInteger).isEqualTo(\"A 2 B\")\n    val formattedDouble = context.getString(FormattedResources.type_none(2.345))\n    assertThat(formattedDouble).isEqualTo(\"A 2.345 B\")\n    val formattedInstant =\n      context.getString(FormattedResources.type_none(releaseDateTime.toInstant()))\n    assertThat(formattedInstant).isEqualTo(\"A 2022-03-25T05:23:45Z B\")\n  }\n\n  @Test\n  fun typeNumber() {\n    val formattedInteger = context.getString(FormattedResources.type_number(2))\n    assertThat(formattedInteger).isEqualTo(\"A 2 B\")\n    val formattedDouble = context.getString(FormattedResources.type_number(2.345))\n    assertThat(formattedDouble).isEqualTo(\"A 2.345 B\")\n  }\n\n  @Test\n  fun typeNumberInteger() {\n    val formatted = context.getString(FormattedResources.type_number_integer(2))\n    assertThat(formatted).isEqualTo(\"A 2 B\")\n  }\n\n  @Test\n  fun typeNumberCurrency() {\n    val formatted = context.getString(FormattedResources.type_number_currency(2))\n    assertThat(formatted).isEqualTo(\"A $2.00 B\")\n  }\n\n  @Test\n  fun typeNumberPercent() {\n    val formatted = context.getString(FormattedResources.type_number_percent(.2))\n    assertThat(formatted).isEqualTo(\"A 20% B\")\n  }\n\n  @Test\n  fun typeNumberCustom() {\n    val formatted = context.getString(FormattedResources.type_number_custom(1234567))\n    assertThat(formatted).isEqualTo(\"A 12,34,567 B\")\n  }\n\n  @Test\n  fun typeDate() {\n    val formatted = context.getString(FormattedResources.type_date(releaseDate))\n    assertThat(formatted).isEqualTo(\"A Mar 24, 2022 B\")\n  }\n\n  @Test\n  fun typeDateShort() {\n    val formatted = context.getString(FormattedResources.type_date_short(releaseDate))\n    assertThat(formatted).isEqualTo(\"A 3/24/22 B\")\n  }\n\n  @Test\n  fun typeDateMedium() {\n    val formatted = context.getString(FormattedResources.type_date_medium(releaseDate))\n    assertThat(formatted).isEqualTo(\"A Mar 24, 2022 B\")\n  }\n\n  @Test\n  fun typeDateLong() {\n    val formatted = context.getString(FormattedResources.type_date_long(releaseDate))\n    assertThat(formatted).isEqualTo(\"A March 24, 2022 B\")\n  }\n\n  @Test\n  fun typeDateFull() {\n    val formatted = context.getString(FormattedResources.type_date_full(releaseDate))\n    assertThat(formatted).isEqualTo(\"A Thursday, March 24, 2022 B\")\n  }\n\n  @Test\n  fun typeDatePatternDateTimeZone() {\n    val formatted =\n      context.getString(FormattedResources.type_date_pattern_date_time_zone(releaseDateTime))\n    assertThat(formatted).isEqualTo(\"A 3-24, 7PM HST B\")\n  }\n\n  @Test\n  fun typeDatePatternDateTimeOffset() {\n    val formatted =\n      context.getString(\n        FormattedResources.type_date_pattern_date_time_offset(releaseDateTime.toOffsetDateTime())\n      )\n    assertThat(formatted).isEqualTo(\"A 3-24, 7PM -10:00 B\")\n  }\n\n  @Test\n  fun typeDatePatternDateTime() {\n    val localDateTime = releaseDateTime.toLocalDateTime()\n    val formatted = context.getString(FormattedResources.type_date_pattern_date_time(localDateTime))\n    assertThat(formatted).isEqualTo(\"A 3-24 7PM B\")\n  }\n\n  @Test\n  fun typeDatePatternDateZone() {\n    val formatted =\n      context.getString(FormattedResources.type_date_pattern_date_zone(releaseDateTime))\n    assertThat(formatted).isEqualTo(\"A March (HST) B\")\n  }\n\n  @Test\n  fun typeDatePatternDateOffset() {\n    val formatted =\n      context.getString(\n        FormattedResources.type_date_pattern_date_offset(releaseDateTime.toOffsetDateTime())\n      )\n    assertThat(formatted).isEqualTo(\"A March (-10:00) B\")\n  }\n\n  @Test\n  fun typeDatePatternDate() {\n    val formatted = context.getString(FormattedResources.type_date_pattern_date(releaseDate))\n    assertThat(formatted).isEqualTo(\"A 2022-03-24 B\")\n  }\n\n  @Test\n  fun typeDatePatternTimeZone() {\n    val formatted =\n      context.getString(FormattedResources.type_date_pattern_time_zone(releaseDateTime))\n    assertThat(formatted).isEqualTo(\"A 19:23 HST B\")\n  }\n\n  @Test\n  fun typeDatePatternTimeOffset() {\n    val formatted =\n      context.getString(\n        FormattedResources.type_date_pattern_time_offset(\n          // Ensures the UTC/GMT case works:\n          releaseDateTime.withZoneSameLocal(ZoneOffset.UTC).toOffsetDateTime().toOffsetTime()\n        )\n      )\n    assertThat(formatted).isEqualTo(\"A 19:23+0000 B\")\n  }\n\n  @Test\n  fun typeDatePatternTime() {\n    val formatted = context.getString(FormattedResources.type_date_pattern_time(releaseTime))\n    assertThat(formatted).isEqualTo(\"A 23 past 7 B\")\n  }\n\n  @Test\n  fun typeDatePatternZone() {\n    val formatted = context.getString(FormattedResources.type_date_pattern_zone(releaseDateTime))\n    assertThat(formatted).isEqualTo(\"A Hawaii-Aleutian Standard Time B\")\n  }\n\n  @Test\n  fun typeDatePatternOffset() {\n    val formatted =\n      context.getString(FormattedResources.type_date_pattern_offset(releaseDateTime.offset))\n    assertThat(formatted).isEqualTo(\"A GMT-10:00 B\")\n  }\n\n  @Test\n  fun typeDatePatternNone() {\n    val formatted = context.getString(FormattedResources.type_date_pattern_none(null))\n    assertThat(formatted).isEqualTo(\"A What is this for? B\")\n  }\n\n  @Test\n  fun typeTime() {\n    val formatted = context.getString(FormattedResources.type_time(releaseTime))\n    assertThat(formatted).isEqualTo(\"A 7:23:45 PM B\")\n  }\n\n  @Test\n  fun typeTimeShort() {\n    val formatted = context.getString(FormattedResources.type_time_short(releaseTime))\n    assertThat(formatted).isEqualTo(\"A 7:23 PM B\")\n  }\n\n  @Test\n  fun typeTimeMedium() {\n    val formatted = context.getString(FormattedResources.type_time_medium(releaseTime))\n    assertThat(formatted).isEqualTo(\"A 7:23:45 PM B\")\n  }\n\n  @Test\n  fun typeTimeLong() {\n    val formatted = context.getString(FormattedResources.type_time_long(releaseDateTime))\n    assertThat(formatted).isEqualTo(\"A 7:23:45 PM HST B\")\n  }\n\n  @Test\n  fun typeTimeFull() {\n    val formatted = context.getString(FormattedResources.type_time_full(releaseDateTime))\n    assertThat(formatted).isEqualTo(\"A 7:23:45 PM Hawaii-Aleutian Standard Time B\")\n  }\n\n  @Test\n  fun typeTimePatternDateTimeZone() {\n    val formatted =\n      context.getString(FormattedResources.type_time_pattern_date_time_zone(releaseDateTime))\n    assertThat(formatted).isEqualTo(\"A 3-24, 7PM HST B\")\n  }\n\n  @Test\n  fun typeTimePatternDateTimeOffset() {\n    val formatted =\n      context.getString(\n        FormattedResources.type_time_pattern_date_time_offset(releaseDateTime.toOffsetDateTime())\n      )\n    assertThat(formatted).isEqualTo(\"A 3-24, 7PM -10 B\")\n  }\n\n  @Test\n  fun typeTimePatternDateTime() {\n    val localDateTime = releaseDateTime.toLocalDateTime()\n    val formatted = context.getString(FormattedResources.type_time_pattern_date_time(localDateTime))\n    assertThat(formatted).isEqualTo(\"A 3-24 7PM B\")\n  }\n\n  @Test\n  fun typeTimePatternDateZone() {\n    val formatted =\n      context.getString(FormattedResources.type_time_pattern_date_zone(releaseDateTime))\n    assertThat(formatted).isEqualTo(\"A March (HST) B\")\n  }\n\n  @Test\n  fun typeTimePatternDateOffset() {\n    val formatted =\n      context.getString(\n        FormattedResources.type_time_pattern_date_offset(releaseDateTime.toOffsetDateTime())\n      )\n    assertThat(formatted).isEqualTo(\"A March (-10) B\")\n  }\n\n  @Test\n  fun typeTimePatternDate() {\n    val formatted = context.getString(FormattedResources.type_time_pattern_date(releaseDate))\n    assertThat(formatted).isEqualTo(\"A 2022-03-24 B\")\n  }\n\n  @Test\n  fun typeTimePatternTimeZone() {\n    val formatted =\n      context.getString(FormattedResources.type_time_pattern_time_zone(releaseDateTime))\n    assertThat(formatted).isEqualTo(\"A 19:23 HST B\")\n  }\n\n  @Test\n  fun typeTimePatternTimeOffset() {\n    val formatted =\n      context.getString(\n        FormattedResources.type_time_pattern_time_offset(\n          OffsetTime.of(releaseDateTime.toLocalTime(), releaseDateTime.offset)\n        )\n      )\n    assertThat(formatted).isEqualTo(\"A 19:23-1000 B\")\n  }\n\n  @Test\n  fun typeTimePatternTime() {\n    val formatted = context.getString(FormattedResources.type_time_pattern_time(releaseTime))\n    assertThat(formatted).isEqualTo(\"A 19-23-45 B\")\n  }\n\n  @Test\n  fun typeTimePatternZone() {\n    val formatted = context.getString(FormattedResources.type_time_pattern_zone(releaseDateTime))\n    assertThat(formatted).isEqualTo(\"A Hawaii-Aleutian Standard Time B\")\n  }\n\n  @Test\n  fun typeTimePatternOffset() {\n    val formatted =\n      context.getString(FormattedResources.type_time_pattern_offset(releaseDateTime.offset))\n    assertThat(formatted).isEqualTo(\"A GMT-10:00 B\")\n  }\n\n  @Test\n  fun typeTimePatternNone() {\n    val formatted = context.getString(FormattedResources.type_time_pattern_none(null))\n    assertThat(formatted).isEqualTo(\"A What is this for? B\")\n  }\n\n  @Test\n  fun typeTimeWithWinterTimeZone() {\n    val winterDateTime =\n      ZonedDateTime.of(\n        LocalDate.of(2023, Month.FEBRUARY, 17),\n        LocalTime.NOON,\n        ZoneId.of(\"America/Chicago\"),\n      )\n    val formatted = context.getString(FormattedResources.type_time_long(winterDateTime))\n    assertThat(formatted).isEqualTo(\"A 12:00:00 PM CST B\")\n  }\n\n  @Test\n  fun typeTimeWithSummerTimeZone() {\n    val summerDateTime =\n      ZonedDateTime.of(\n        LocalDate.of(2023, Month.JULY, 17),\n        LocalTime.NOON,\n        ZoneId.of(\"America/Chicago\"),\n      )\n    val formatted = context.getString(FormattedResources.type_time_long(summerDateTime))\n    assertThat(formatted).isEqualTo(\"A 12:00:00 PM CDT B\")\n  }\n\n  @Test\n  fun typeDuration() {\n    val formattedSeconds = context.getString(FormattedResources.type_duration(3.seconds))\n    assertThat(formattedSeconds).isEqualTo(\"A 3 sec. B\")\n    val formattedMinutes =\n      context.getString(FormattedResources.type_duration(3.minutes + 2.seconds))\n    assertThat(formattedMinutes).isEqualTo(\"A 3:02 B\")\n    val formattedHours =\n      context.getString(FormattedResources.type_duration(3.hours + 2.minutes + 1.seconds))\n    assertThat(formattedHours).isEqualTo(\"A 3:02:01 B\")\n  }\n\n  @Test\n  fun typeOrdinal() {\n    val zero = 0 // Requires an int overload to be invoked\n    val formattedZero = context.getString(FormattedResources.type_ordinal(zero))\n    assertThat(formattedZero).isEqualTo(\"A 0th B\")\n    val formattedOne = context.getString(FormattedResources.type_ordinal(1))\n    assertThat(formattedOne).isEqualTo(\"A 1st B\")\n    val formattedTwo = context.getString(FormattedResources.type_ordinal(2))\n    assertThat(formattedTwo).isEqualTo(\"A 2nd B\")\n    val formattedThree = context.getString(FormattedResources.type_ordinal(3))\n    assertThat(formattedThree).isEqualTo(\"A 3rd B\")\n    val formattedFour = context.getString(FormattedResources.type_ordinal(4))\n    assertThat(formattedFour).isEqualTo(\"A 4th B\")\n    val formattedLong = context.getString(FormattedResources.type_ordinal(Long.MAX_VALUE))\n    val expected =\n      if (Build.VERSION.SDK_INT >= 26) {\n        \"9,223,372,036,854,775,807th\"\n      } else {\n        // ICU versions on older Android platforms lose bits by internally converting Long to\n        // Double:\n        \"9,223,372,036,854,776,000th\"\n      }\n    assertThat(formattedLong).isEqualTo(\"A $expected B\")\n  }\n\n  @Test\n  fun typeSpellout() {\n    val formattedOnes = context.getString(FormattedResources.type_spellout(3))\n    assertThat(formattedOnes).isEqualTo(\"A three B\")\n    val formattedTens = context.getString(FormattedResources.type_spellout(32))\n    assertThat(formattedTens).isEqualTo(\"A thirty-two B\")\n    val formattedHundreds = context.getString(FormattedResources.type_spellout(321))\n    assertThat(formattedHundreds).isEqualTo(\"A three hundred twenty-one B\")\n  }\n\n  @Test\n  fun typePlural() {\n    val formatted0 = context.getString(FormattedResources.type_count_plural(0))\n    assertThat(formatted0).isEqualTo(\"A Z B\")\n    val formatted1 = context.getString(FormattedResources.type_count_plural(1))\n    assertThat(formatted1).isEqualTo(\"A Y B\")\n    val formatted2 = context.getString(FormattedResources.type_count_plural(2))\n    assertThat(formatted2).isEqualTo(\"A X B\")\n  }\n\n  @Test\n  fun typeSelect() {\n    val formattedAlpha = context.getString(FormattedResources.type_verse_select(\"alpha\"))\n    assertThat(formattedAlpha).isEqualTo(\"A Z B\")\n    val formattedBeta = context.getString(FormattedResources.type_verse_select(\"beta\"))\n    assertThat(formattedBeta).isEqualTo(\"A Y B\")\n    val formattedGamma = context.getString(FormattedResources.type_verse_select(\"gamma\"))\n    assertThat(formattedGamma).isEqualTo(\"A X B\")\n  }\n}\n"
  },
  {
    "path": "tests/src/main/res/values/locales.xml",
    "content": "<resources>\n  <string name=\"locale_date\">A {value,date} B</string>\n</resources>\n"
  },
  {
    "path": "tests/src/main/res/values/named.xml",
    "content": "<resources>\n  <string name=\"named_one\">A {one} B</string>\n  <string name=\"named_three\">A {one} B {two} C {three} D</string>\n</resources>\n"
  },
  {
    "path": "tests/src/main/res/values/numbered.xml",
    "content": "<resources>\n  <string name=\"numbered_contiguous_one\">A {0} B</string>\n  <string name=\"numbered_contiguous_three\">A {0} B {1} C {2} D</string>\n  <string name=\"numbered_sparse_one\">A {1} B</string>\n  <string name=\"numbered_sparse_three\">A {1} B {3} C {5} D</string>\n</resources>\n"
  },
  {
    "path": "tests/src/main/res/values/types.xml",
    "content": "<resources>\n  <string name=\"type_none\">A {value} B</string>\n\n  <string name=\"type_number\">A {value,number} B</string>\n  <string name=\"type_number_integer\">A {value,number,integer} B</string>\n  <string name=\"type_number_currency\">A {value,number,currency} B</string>\n  <string name=\"type_number_percent\">A {value,number,percent} B</string>\n  <string name=\"type_number_custom\">A {value,number,#,##,###} B</string>\n\n  <string name=\"type_date\">A {value,date} B</string>\n  <string name=\"type_date_short\">A {value,date,short} B</string>\n  <string name=\"type_date_medium\">A {value,date,medium} B</string>\n  <string name=\"type_date_long\">A {value,date,long} B</string>\n  <string name=\"type_date_full\">A {value,date,full} B</string>\n  <string name=\"type_date_pattern_date_time_zone\">A {value,date,M-dd, ha z} B</string>\n  <string name=\"type_date_pattern_date_time_offset\">A {value,date,M-dd, ha xxx} B</string>\n  <string name=\"type_date_pattern_date_time\">A {value,date,M-dd ha} B</string>\n  <string name=\"type_date_pattern_date_zone\">A {value,date,MMMM (z)} B</string>\n  <string name=\"type_date_pattern_date_offset\">A {value,date,MMMM (xxx)} B</string>\n  <string name=\"type_date_pattern_date\">A {value,date,YYYY-MM-dd} B</string>\n  <string name=\"type_date_pattern_time_zone\">A {value,date,HH:mm zz} B</string>\n  <string name=\"type_date_pattern_time_offset\">A {value,date,HH:mmZ} B</string>\n  <string name=\"type_date_pattern_time\">A {value,date,m \\'past\\' h} B</string>\n  <string name=\"type_date_pattern_zone\">A {value,date,zzzz} B</string>\n  <string name=\"type_date_pattern_offset\">A {value,date,ZZZZ} B</string>\n  <string name=\"type_date_pattern_none\">A {value,date,\\'What is this for?\\'} B</string>\n\n  <string name=\"type_time\">A {value,time} B</string>\n  <string name=\"type_time_short\">A {value,time,short} B</string>\n  <string name=\"type_time_medium\">A {value,time,medium} B</string>\n  <string name=\"type_time_long\">A {value,time,long} B</string>\n  <string name=\"type_time_full\">A {value,time,full} B</string>\n  <string name=\"type_time_pattern_date_time_zone\">A {value,time,M-dd, ha z} B</string>\n  <string name=\"type_time_pattern_date_time_offset\">A {value,time,M-dd, ha x} B</string>\n  <string name=\"type_time_pattern_date_time\">A {value,time,M-dd ha} B</string>\n  <string name=\"type_time_pattern_date_zone\">A {value,time,MMMM (z)} B</string>\n  <string name=\"type_time_pattern_date_offset\">A {value,time,MMMM (x)} B</string>\n  <string name=\"type_time_pattern_date\">A {value,time,YYYY-MM-dd} B</string>\n  <string name=\"type_time_pattern_time_zone\">A {value,time,HH:mm zz} B</string>\n  <string name=\"type_time_pattern_time_offset\">A {value,time,HH:mmZ} B</string>\n  <string name=\"type_time_pattern_time\">A {value,time,HH-mm-ss} B</string>\n  <string name=\"type_time_pattern_zone\">A {value,time,zzzz} B</string>\n  <string name=\"type_time_pattern_offset\">A {value,time,ZZZZ} B</string>\n  <string name=\"type_time_pattern_none\">A {value,time,\\'What is this for?\\'} B</string>\n\n  <string name=\"type_duration\">A {value,duration} B</string>\n  <string name=\"type_ordinal\">A {value,ordinal} B</string>\n  <string name=\"type_spellout\">A {value,spellout} B</string>\n\n  <string name=\"type_count_plural\">A {count, plural,\n      =0 {Z}\n      =1 {Y}\n      other {X}\n    } B</string>\n\n  <string name=\"type_count_select_ordinal\">A {count, selectordinal,\n      one {Z #st}\n      two {Z #nd}\n      few {Z #rd}\n      other {Z #th time}\n    } B</string>\n\n  <string name=\"type_verse_select\">A {verse, select,\n      alpha {Z}\n      beta {Y}\n      other {X}\n    } B</string>\n</resources>\n"
  }
]