├── .github └── workflows │ └── build.yml ├── .gitignore ├── Directory.Build.props ├── Directory.Packages.props ├── NuGet.config ├── OrchardCoreContrib.PoExtractor.sln ├── README.md ├── global.json ├── images ├── OCC.png └── icon.png ├── license.txt ├── src ├── OrchardCoreContrib.PoExtractor.Abstractions │ ├── Extensions │ │ └── StringExtensions.cs │ ├── IMetadataProvider.cs │ ├── IProjectProcessor.cs │ ├── IStringExtractor.cs │ ├── LocalizableString.cs │ ├── LocalizableStringCollection.cs │ ├── LocalizableStringExtractor.cs │ ├── LocalizableStringLocation.cs │ ├── LocalizableStringOccurence.cs │ └── OrchardCoreContrib.PoExtractor.Abstractions.csproj ├── OrchardCoreContrib.PoExtractor.Core │ └── OrchardCoreContrib.PoExtractor.Core.csproj ├── OrchardCoreContrib.PoExtractor.DotNet.CS │ ├── CSharpProjectProcessor.cs │ ├── MetadataProviders │ │ └── CSharpMetadataProvider.cs │ ├── OrchardCoreContrib.PoExtractor.DotNet.CS.csproj │ ├── PluralStringExtractor.cs │ └── SingularStringExtractor.cs ├── OrchardCoreContrib.PoExtractor.DotNet.VB │ ├── MetadataProviders │ │ └── VisualBasicMetadataProvider.cs │ ├── OrchardCoreContrib.PoExtractor.DotNet.VB.csproj │ ├── PluralStringExtractor.cs │ ├── SingularStringExtractor.cs │ └── VisualBasicProjectProcessor.cs ├── OrchardCoreContrib.PoExtractor.DotNet │ ├── DisplayAttributeDescriptionStringExtractor.cs │ ├── DisplayAttributeGroupNameStringExtractor.cs │ ├── DisplayAttributeNameStringExtractor.cs │ ├── DisplayAttributeShortNameStringExtractor.cs │ ├── DisplayAttributeStringExtractor.cs │ ├── ErrorMessageAnnotationStringExtractor.cs │ ├── ExtractingCodeWalker.cs │ ├── LocalizerAccessors.cs │ ├── OrchardCoreContrib.PoExtractor.DotNet.csproj │ └── ProjectExtension.cs ├── OrchardCoreContrib.PoExtractor.Liquid │ ├── ExtractingLiquidWalker.cs │ ├── LiquidExpressionContext.cs │ ├── LiquidProjectProcessor.cs │ ├── LiquidStatementContext.cs │ ├── LiquidStringExtractor.cs │ ├── MetadataProvider │ │ └── LiquidMetadataProvider.cs │ └── OrchardCoreContrib.PoExtractor.Liquid.csproj ├── OrchardCoreContrib.PoExtractor.Razor │ ├── MetadataProviders │ │ └── RazorMetadataProvider.cs │ ├── OrchardCoreContrib.PoExtractor.Razor.csproj │ ├── RazorPageGeneratorResult.cs │ ├── RazorProjectProcessor.cs │ └── ViewCompiler.cs └── OrchardCoreContrib.PoExtractor │ ├── IgnoredProject.cs │ ├── Language.cs │ ├── OrchardCoreContrib.PoExtractor.csproj │ ├── PluginHelper.cs │ ├── PoWriter.cs │ ├── Program.cs │ └── TemplateEngine.cs └── test ├── OrchardCoreContrib.PoExtractor.Core.Tests ├── DisplayAttributeStringExtractorTests.cs ├── ErrorMessageAnnotationStringExtractorTests.cs ├── Extensions │ └── StringExtensionsTests.cs ├── Fakes │ └── FakeCSharpProjectProcessor.cs ├── LocalizableStringCollectionTests.cs ├── LocalizableStringTests.cs ├── OrchardCoreContrib.PoExtractor.Abstractions.Tests.csproj ├── ProjectFiles │ ├── LoginViewModel.cs │ └── PersonModel.cs └── Usings.cs ├── OrchardCoreContrib.PoExtractor.DotNet.CS.Tests ├── MetadataProviders │ └── CSharpMetadataProviderTests.cs ├── OrchardCoreContrib.PoExtractor.DotNet.CS.Tests.csproj ├── PluralStringExtractorTests.cs ├── SingularStringExtractorTests.cs └── Usings.cs ├── OrchardCoreContrib.PoExtractor.DotNet.VB.Tests ├── MetadataProviders │ └── VisualBasicMetadataProviderTests.cs ├── OrchardCoreContrib.PoExtractor.DotNet.VB.Tests.csproj ├── PluralStringExtractorTests.cs ├── SingularStringExtractorTests.cs └── Usings.cs ├── OrchardCoreContrib.PoExtractor.Liquid.Tests ├── LiquidProjectProcessorTests.cs ├── OrchardCoreContrib.PoExtractor.Liquid.Tests.csproj └── ProjectFiles │ └── sample.liquid └── OrchardCoreContrib.PoExtractor.Tests ├── OrchardCoreContrib.PoExtractor.Tests.csproj ├── PluginTestFiles ├── BasicJsonLocalizationProcessor.csx └── i18n │ └── en.json ├── PluginTests.cs └── PoWriterTests.cs /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: Orchard Core Contrib PO Extractor 2 | 3 | on: 4 | pull_request: 5 | branches: [ main ] 6 | 7 | env: 8 | DOTNET_SKIP_FIRST_TIME_EXPERIENCE: true 9 | DOTNET_CLI_TELEMETRY_OPTOUT: true 10 | 11 | jobs: 12 | build: 13 | name: Build windows-latest 14 | runs-on: windows-latest 15 | steps: 16 | - uses: actions/checkout@v3 17 | - name: Setup .NET Core 18 | uses: actions/setup-dotnet@v1 19 | with: 20 | dotnet-version: 8.0.* 21 | - name: Install Dependencies 22 | run: dotnet restore 23 | - name: Build 24 | run: dotnet build --configuration Release --no-restore 25 | - name: Test 26 | run: dotnet test --no-restore --verbosity normal 27 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Created by https://www.gitignore.io/api/aspnetcore 2 | 3 | ### ASPNETCore ### 4 | ## Ignore Visual Studio temporary files, build results, and 5 | ## files generated by popular Visual Studio add-ons. 6 | 7 | # User-specific files 8 | *.suo 9 | *.user 10 | *.userosscache 11 | *.sln.docstates 12 | 13 | # User-specific files (MonoDevelop/Xamarin Studio) 14 | *.userprefs 15 | 16 | # Build results 17 | [Dd]ebug/ 18 | [Dd]ebugPublic/ 19 | [Rr]elease/ 20 | [Rr]eleases/ 21 | x64/ 22 | x86/ 23 | bld/ 24 | [Bb]in/ 25 | [Oo]bj/ 26 | [Ll]og/ 27 | 28 | # Visual Studio 2015 cache/options directory 29 | .vs/ 30 | # Uncomment if you have tasks that create the project's static files in wwwroot 31 | #wwwroot/ 32 | 33 | # MSTest test Results 34 | [Tt]est[Rr]esult*/ 35 | [Bb]uild[Ll]og.* 36 | 37 | # NUNIT 38 | *.VisualState.xml 39 | TestResult.xml 40 | 41 | # Build Results of an ATL Project 42 | [Dd]ebugPS/ 43 | [Rr]eleasePS/ 44 | dlldata.c 45 | 46 | # DNX 47 | project.lock.json 48 | project.fragment.lock.json 49 | artifacts/ 50 | 51 | *_i.c 52 | *_p.c 53 | *_i.h 54 | *.ilk 55 | *.meta 56 | *.obj 57 | *.pch 58 | *.pdb 59 | *.pgc 60 | *.pgd 61 | *.rsp 62 | *.sbr 63 | *.tlb 64 | *.tli 65 | *.tlh 66 | *.tmp 67 | *.tmp_proj 68 | *.log 69 | *.vspscc 70 | *.vssscc 71 | .builds 72 | *.pidb 73 | *.svclog 74 | *.scc 75 | 76 | # Chutzpah Test files 77 | _Chutzpah* 78 | 79 | # Visual C++ cache files 80 | ipch/ 81 | *.aps 82 | *.ncb 83 | *.opendb 84 | *.opensdf 85 | *.sdf 86 | *.cachefile 87 | *.VC.db 88 | *.VC.VC.opendb 89 | 90 | # Visual Studio profiler 91 | *.psess 92 | *.vsp 93 | *.vspx 94 | *.sap 95 | 96 | # TFS 2012 Local Workspace 97 | $tf/ 98 | 99 | # Guidance Automation Toolkit 100 | *.gpState 101 | 102 | # ReSharper is a .NET coding add-in 103 | _ReSharper*/ 104 | *.[Rr]e[Ss]harper 105 | *.DotSettings.user 106 | 107 | # JustCode is a .NET coding add-in 108 | .JustCode 109 | 110 | # TeamCity is a build add-in 111 | _TeamCity* 112 | 113 | # DotCover is a Code Coverage Tool 114 | *.dotCover 115 | 116 | # Visual Studio code coverage results 117 | *.coverage 118 | *.coveragexml 119 | 120 | # NCrunch 121 | _NCrunch_* 122 | .*crunch*.local.xml 123 | nCrunchTemp_* 124 | 125 | # MightyMoose 126 | *.mm.* 127 | AutoTest.Net/ 128 | 129 | # Web workbench (sass) 130 | .sass-cache/ 131 | 132 | # Installshield output folder 133 | [Ee]xpress/ 134 | 135 | # DocProject is a documentation generator add-in 136 | DocProject/buildhelp/ 137 | DocProject/Help/*.HxT 138 | DocProject/Help/*.HxC 139 | DocProject/Help/*.hhc 140 | DocProject/Help/*.hhk 141 | DocProject/Help/*.hhp 142 | DocProject/Help/Html2 143 | DocProject/Help/html 144 | 145 | # Click-Once directory 146 | publish/ 147 | 148 | # Publish Web Output 149 | *.[Pp]ublish.xml 150 | *.azurePubxml 151 | # TODO: Comment the next line if you want to checkin your web deploy settings 152 | # but database connection strings (with potential passwords) will be unencrypted 153 | *.pubxml 154 | *.publishproj 155 | 156 | # Microsoft Azure Web App publish settings. Comment the next line if you want to 157 | # checkin your Azure Web App publish settings, but sensitive information contained 158 | # in these scripts will be unencrypted 159 | PublishScripts/ 160 | 161 | # NuGet Packages 162 | *.nupkg 163 | # The packages folder can be ignored because of Package Restore 164 | **/packages/* 165 | # except build/, which is used as an MSBuild target. 166 | !**/packages/build/ 167 | # Uncomment if necessary however generally it will be regenerated when needed 168 | #!**/packages/repositories.config 169 | # NuGet v3's project.json files produces more ignoreable files 170 | *.nuget.props 171 | *.nuget.targets 172 | 173 | # Microsoft Azure Build Output 174 | csx/ 175 | *.build.csdef 176 | 177 | # Microsoft Azure Emulator 178 | ecf/ 179 | rcf/ 180 | 181 | # Windows Store app package directories and files 182 | AppPackages/ 183 | BundleArtifacts/ 184 | Package.StoreAssociation.xml 185 | _pkginfo.txt 186 | 187 | # Visual Studio cache files 188 | # files ending in .cache can be ignored 189 | *.[Cc]ache 190 | # but keep track of directories ending in .cache 191 | !*.[Cc]ache/ 192 | 193 | # Others 194 | ClientBin/ 195 | ~$* 196 | *~ 197 | *.dbmdl 198 | *.dbproj.schemaview 199 | *.jfm 200 | *.pfx 201 | *.publishsettings 202 | node_modules/ 203 | orleans.codegen.cs 204 | 205 | # Since there are multiple workflows, uncomment next line to ignore bower_components 206 | # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) 207 | #bower_components/ 208 | 209 | # RIA/Silverlight projects 210 | Generated_Code/ 211 | 212 | # Backup & report files from converting an old project file 213 | # to a newer Visual Studio version. Backup files are not needed, 214 | # because we have git ;-) 215 | _UpgradeReport_Files/ 216 | Backup*/ 217 | UpgradeLog*.XML 218 | UpgradeLog*.htm 219 | 220 | # SQL Server files 221 | *.mdf 222 | *.ldf 223 | 224 | # Business Intelligence projects 225 | *.rdl.data 226 | *.bim.layout 227 | *.bim_*.settings 228 | 229 | # Microsoft Fakes 230 | FakesAssemblies/ 231 | 232 | # GhostDoc plugin setting file 233 | *.GhostDoc.xml 234 | 235 | # Node.js Tools for Visual Studio 236 | .ntvs_analysis.dat 237 | 238 | # Visual Studio 6 build log 239 | *.plg 240 | 241 | # Visual Studio 6 workspace options file 242 | *.opt 243 | 244 | # Visual Studio LightSwitch build output 245 | **/*.HTMLClient/GeneratedArtifacts 246 | **/*.DesktopClient/GeneratedArtifacts 247 | **/*.DesktopClient/ModelManifest.xml 248 | **/*.Server/GeneratedArtifacts 249 | **/*.Server/ModelManifest.xml 250 | _Pvt_Extensions 251 | 252 | # Paket dependency manager 253 | .paket/paket.exe 254 | paket-files/ 255 | 256 | # FAKE - F# Make 257 | .fake/ 258 | 259 | # JetBrains Rider 260 | .idea/ 261 | *.sln.iml 262 | 263 | # CodeRush 264 | .cr/ 265 | 266 | # Python Tools for Visual Studio (PTVS) 267 | __pycache__/ 268 | *.pyc 269 | 270 | # Cake - Uncomment if you are using it 271 | # tools/ 272 | 273 | # End of https://www.gitignore.io/api/aspnetcore -------------------------------------------------------------------------------- /Directory.Build.props: -------------------------------------------------------------------------------- 1 |  2 | 3 | net8.0 4 | enable 5 | disable 6 | false 7 | true 8 | 9 | 10 | -------------------------------------------------------------------------------- /Directory.Packages.props: -------------------------------------------------------------------------------- 1 |  2 | 3 | true 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /NuGet.config: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /OrchardCoreContrib.PoExtractor.sln: -------------------------------------------------------------------------------- 1 |  2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio Version 17 4 | VisualStudioVersion = 17.4.33103.184 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "OrchardCoreContrib.PoExtractor.Abstractions.Tests", "test\OrchardCoreContrib.PoExtractor.Core.Tests\OrchardCoreContrib.PoExtractor.Abstractions.Tests.csproj", "{4E0AF6EC-9BD0-4D39-8123-06163362185D}" 7 | EndProject 8 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "OrchardCoreContrib.PoExtractor.Liquid", "src\OrchardCoreContrib.PoExtractor.Liquid\OrchardCoreContrib.PoExtractor.Liquid.csproj", "{9DB27A54-F56F-465E-A1A9-28791C64710B}" 9 | EndProject 10 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "OrchardCoreContrib.PoExtractor", "src\OrchardCoreContrib.PoExtractor\OrchardCoreContrib.PoExtractor.csproj", "{D358C2C3-D568-4C60-BBFA-38AA2BE95C7A}" 11 | EndProject 12 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "OrchardCoreContrib.PoExtractor.Razor", "src\OrchardCoreContrib.PoExtractor.Razor\OrchardCoreContrib.PoExtractor.Razor.csproj", "{D871D2E9-FBD0-4C32-AE1A-0FBC89356345}" 13 | EndProject 14 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "OrchardCoreContrib.PoExtractor.DotNet", "src\OrchardCoreContrib.PoExtractor.DotNet\OrchardCoreContrib.PoExtractor.DotNet.csproj", "{B86D3896-8371-47DF-94B4-634AA7C7A1C7}" 15 | EndProject 16 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "OrchardCoreContrib.PoExtractor.DotNet.CS", "src\OrchardCoreContrib.PoExtractor.DotNet.CS\OrchardCoreContrib.PoExtractor.DotNet.CS.csproj", "{F12A2C3B-728B-4AEC-9495-3FB56300D941}" 17 | EndProject 18 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "OrchardCoreContrib.PoExtractor.DotNet.VB", "src\OrchardCoreContrib.PoExtractor.DotNet.VB\OrchardCoreContrib.PoExtractor.DotNet.VB.csproj", "{458FE156-43B6-4DB9-965D-672A7D318B90}" 19 | EndProject 20 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "OrchardCoreContrib.PoExtractor.Liquid.Tests", "test\OrchardCoreContrib.PoExtractor.Liquid.Tests\OrchardCoreContrib.PoExtractor.Liquid.Tests.csproj", "{042754CC-2E1A-4ED9-8944-E51708A43515}" 21 | EndProject 22 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{FCA7649D-2A78-4580-8866-A00571045744}" 23 | EndProject 24 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "test", "test", "{D1336C32-75A4-411A-90D3-E011D2CEB076}" 25 | EndProject 26 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "OrchardCoreContrib.PoExtractor.Abstractions", "src\OrchardCoreContrib.PoExtractor.Abstractions\OrchardCoreContrib.PoExtractor.Abstractions.csproj", "{3CEF8D52-49EB-49C9-A421-64A59C031B61}" 27 | EndProject 28 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "OrchardCoreContrib.PoExtractor.Tests", "test\OrchardCoreContrib.PoExtractor.Tests\OrchardCoreContrib.PoExtractor.Tests.csproj", "{7989E10F-A776-44FE-B3CF-1EB377EFD665}" 29 | EndProject 30 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "OrchardCoreContrib.PoExtractor.DotNet.CS.Tests", "test\OrchardCoreContrib.PoExtractor.DotNet.CS.Tests\OrchardCoreContrib.PoExtractor.DotNet.CS.Tests.csproj", "{8BD3C4A9-2778-4B2F-8709-79FDC027CA01}" 31 | EndProject 32 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "OrchardCoreContrib.PoExtractor.DotNet.VB.Tests", "test\OrchardCoreContrib.PoExtractor.DotNet.VB.Tests\OrchardCoreContrib.PoExtractor.DotNet.VB.Tests.csproj", "{A5885141-EDF9-4C6C-AFF7-B0BE92079E31}" 33 | EndProject 34 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{DDD306EA-9B35-4984-9E96-CBE0450B82DC}" 35 | ProjectSection(SolutionItems) = preProject 36 | Directory.Build.props = Directory.Build.props 37 | Directory.Packages.props = Directory.Packages.props 38 | README.md = README.md 39 | NuGet.config = NuGet.config 40 | EndProjectSection 41 | EndProject 42 | Global 43 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 44 | Debug|Any CPU = Debug|Any CPU 45 | Release|Any CPU = Release|Any CPU 46 | EndGlobalSection 47 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 48 | {4E0AF6EC-9BD0-4D39-8123-06163362185D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 49 | {4E0AF6EC-9BD0-4D39-8123-06163362185D}.Debug|Any CPU.Build.0 = Debug|Any CPU 50 | {4E0AF6EC-9BD0-4D39-8123-06163362185D}.Release|Any CPU.ActiveCfg = Release|Any CPU 51 | {4E0AF6EC-9BD0-4D39-8123-06163362185D}.Release|Any CPU.Build.0 = Release|Any CPU 52 | {9DB27A54-F56F-465E-A1A9-28791C64710B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 53 | {9DB27A54-F56F-465E-A1A9-28791C64710B}.Debug|Any CPU.Build.0 = Debug|Any CPU 54 | {9DB27A54-F56F-465E-A1A9-28791C64710B}.Release|Any CPU.ActiveCfg = Release|Any CPU 55 | {9DB27A54-F56F-465E-A1A9-28791C64710B}.Release|Any CPU.Build.0 = Release|Any CPU 56 | {D358C2C3-D568-4C60-BBFA-38AA2BE95C7A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 57 | {D358C2C3-D568-4C60-BBFA-38AA2BE95C7A}.Debug|Any CPU.Build.0 = Debug|Any CPU 58 | {D358C2C3-D568-4C60-BBFA-38AA2BE95C7A}.Release|Any CPU.ActiveCfg = Release|Any CPU 59 | {D358C2C3-D568-4C60-BBFA-38AA2BE95C7A}.Release|Any CPU.Build.0 = Release|Any CPU 60 | {D871D2E9-FBD0-4C32-AE1A-0FBC89356345}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 61 | {D871D2E9-FBD0-4C32-AE1A-0FBC89356345}.Debug|Any CPU.Build.0 = Debug|Any CPU 62 | {D871D2E9-FBD0-4C32-AE1A-0FBC89356345}.Release|Any CPU.ActiveCfg = Release|Any CPU 63 | {D871D2E9-FBD0-4C32-AE1A-0FBC89356345}.Release|Any CPU.Build.0 = Release|Any CPU 64 | {B86D3896-8371-47DF-94B4-634AA7C7A1C7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 65 | {B86D3896-8371-47DF-94B4-634AA7C7A1C7}.Debug|Any CPU.Build.0 = Debug|Any CPU 66 | {B86D3896-8371-47DF-94B4-634AA7C7A1C7}.Release|Any CPU.ActiveCfg = Release|Any CPU 67 | {B86D3896-8371-47DF-94B4-634AA7C7A1C7}.Release|Any CPU.Build.0 = Release|Any CPU 68 | {F12A2C3B-728B-4AEC-9495-3FB56300D941}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 69 | {F12A2C3B-728B-4AEC-9495-3FB56300D941}.Debug|Any CPU.Build.0 = Debug|Any CPU 70 | {F12A2C3B-728B-4AEC-9495-3FB56300D941}.Release|Any CPU.ActiveCfg = Release|Any CPU 71 | {F12A2C3B-728B-4AEC-9495-3FB56300D941}.Release|Any CPU.Build.0 = Release|Any CPU 72 | {458FE156-43B6-4DB9-965D-672A7D318B90}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 73 | {458FE156-43B6-4DB9-965D-672A7D318B90}.Debug|Any CPU.Build.0 = Debug|Any CPU 74 | {458FE156-43B6-4DB9-965D-672A7D318B90}.Release|Any CPU.ActiveCfg = Release|Any CPU 75 | {458FE156-43B6-4DB9-965D-672A7D318B90}.Release|Any CPU.Build.0 = Release|Any CPU 76 | {042754CC-2E1A-4ED9-8944-E51708A43515}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 77 | {042754CC-2E1A-4ED9-8944-E51708A43515}.Debug|Any CPU.Build.0 = Debug|Any CPU 78 | {042754CC-2E1A-4ED9-8944-E51708A43515}.Release|Any CPU.ActiveCfg = Release|Any CPU 79 | {042754CC-2E1A-4ED9-8944-E51708A43515}.Release|Any CPU.Build.0 = Release|Any CPU 80 | {3CEF8D52-49EB-49C9-A421-64A59C031B61}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 81 | {3CEF8D52-49EB-49C9-A421-64A59C031B61}.Debug|Any CPU.Build.0 = Debug|Any CPU 82 | {3CEF8D52-49EB-49C9-A421-64A59C031B61}.Release|Any CPU.ActiveCfg = Release|Any CPU 83 | {3CEF8D52-49EB-49C9-A421-64A59C031B61}.Release|Any CPU.Build.0 = Release|Any CPU 84 | {7989E10F-A776-44FE-B3CF-1EB377EFD665}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 85 | {7989E10F-A776-44FE-B3CF-1EB377EFD665}.Debug|Any CPU.Build.0 = Debug|Any CPU 86 | {7989E10F-A776-44FE-B3CF-1EB377EFD665}.Release|Any CPU.ActiveCfg = Release|Any CPU 87 | {7989E10F-A776-44FE-B3CF-1EB377EFD665}.Release|Any CPU.Build.0 = Release|Any CPU 88 | {8BD3C4A9-2778-4B2F-8709-79FDC027CA01}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 89 | {8BD3C4A9-2778-4B2F-8709-79FDC027CA01}.Debug|Any CPU.Build.0 = Debug|Any CPU 90 | {8BD3C4A9-2778-4B2F-8709-79FDC027CA01}.Release|Any CPU.ActiveCfg = Release|Any CPU 91 | {8BD3C4A9-2778-4B2F-8709-79FDC027CA01}.Release|Any CPU.Build.0 = Release|Any CPU 92 | {A5885141-EDF9-4C6C-AFF7-B0BE92079E31}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 93 | {A5885141-EDF9-4C6C-AFF7-B0BE92079E31}.Debug|Any CPU.Build.0 = Debug|Any CPU 94 | {A5885141-EDF9-4C6C-AFF7-B0BE92079E31}.Release|Any CPU.ActiveCfg = Release|Any CPU 95 | {A5885141-EDF9-4C6C-AFF7-B0BE92079E31}.Release|Any CPU.Build.0 = Release|Any CPU 96 | EndGlobalSection 97 | GlobalSection(SolutionProperties) = preSolution 98 | HideSolutionNode = FALSE 99 | EndGlobalSection 100 | GlobalSection(NestedProjects) = preSolution 101 | {4E0AF6EC-9BD0-4D39-8123-06163362185D} = {D1336C32-75A4-411A-90D3-E011D2CEB076} 102 | {9DB27A54-F56F-465E-A1A9-28791C64710B} = {FCA7649D-2A78-4580-8866-A00571045744} 103 | {D358C2C3-D568-4C60-BBFA-38AA2BE95C7A} = {FCA7649D-2A78-4580-8866-A00571045744} 104 | {D871D2E9-FBD0-4C32-AE1A-0FBC89356345} = {FCA7649D-2A78-4580-8866-A00571045744} 105 | {B86D3896-8371-47DF-94B4-634AA7C7A1C7} = {FCA7649D-2A78-4580-8866-A00571045744} 106 | {F12A2C3B-728B-4AEC-9495-3FB56300D941} = {FCA7649D-2A78-4580-8866-A00571045744} 107 | {458FE156-43B6-4DB9-965D-672A7D318B90} = {FCA7649D-2A78-4580-8866-A00571045744} 108 | {042754CC-2E1A-4ED9-8944-E51708A43515} = {D1336C32-75A4-411A-90D3-E011D2CEB076} 109 | {3CEF8D52-49EB-49C9-A421-64A59C031B61} = {FCA7649D-2A78-4580-8866-A00571045744} 110 | {7989E10F-A776-44FE-B3CF-1EB377EFD665} = {D1336C32-75A4-411A-90D3-E011D2CEB076} 111 | {8BD3C4A9-2778-4B2F-8709-79FDC027CA01} = {D1336C32-75A4-411A-90D3-E011D2CEB076} 112 | {A5885141-EDF9-4C6C-AFF7-B0BE92079E31} = {D1336C32-75A4-411A-90D3-E011D2CEB076} 113 | EndGlobalSection 114 | GlobalSection(ExtensibilityGlobals) = postSolution 115 | SolutionGuid = {694D0244-80FF-47D4-A54D-2DB1356B72A1} 116 | EndGlobalSection 117 | EndGlobal 118 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # OrchardCoreContrib.PoExtractor 2 | 3 | ![Orchard Core Contrib](images/OCC.png) 4 | 5 | `OrchardCoreContrib.PoExtractor` is distributed as a dotnet global tool to extracts translatable strings from the C# and VB code, Razor templates and Liquid templates to POT (portable object template) files. It is designed to follow conventions used in the [OrchardCore](https://github.com/OrchardCMS/OrchardCore) project. 6 | 7 | ## Installation 8 | 9 | Install with the following command: 10 | 11 | ```powershell 12 | dotnet tool install --global OrchardCoreContrib.PoExtractor 13 | ``` 14 | 15 | ## Usage 16 | 17 | ```powershell 18 | extractpo [-l|--language {"C#"|"VB"}] [-t|--template {"razor"|"liquid"}] 19 | ``` 20 | 21 | ### Description 22 | 23 | Extracts all translatable strings from projects at the specified input path and saves generated POT files at the specified output path. It creates one POT file per a project. This includes liquid views. 24 | 25 | ### Arguments 26 | 27 | - **`INTPUT_PATH`** 28 | 29 | The path to the input directory, all projects at the the path will be processed. 30 | 31 | - **`OUTPUT_PATH`** 32 | 33 | The path to a directory where POT files will be generated. 34 | 35 | ### Options 36 | 37 | - **`-l|--language {C#|VB}`** 38 | 39 | Specifies the code language to extracts translatable strings from. Default: `C#` language 40 | 41 | - **`-t|--template {"razor"|"liquid"}`** 42 | 43 | Specifies the template engine to extract the translatable strings from. Default: `Razor` & `Liquid` templates. 44 | 45 | - **`-p|--plugin {path or URL to CSX file}`** 46 | 47 | Specifies a path to a C# script file that can define further project processors. (You can find an example script [here](test/OrchardCoreContrib.PoExtractor.Tests/PluginTestFiles/BasicJsonLocalizationProcessor.csx).) This can be used to process localization from code languages or template engines not supported by the above options. You can have multiple of these switches in one call to load several plugins simultaneously. If the argument starts with `https://` then it's treated as a web URL and the script at that address is downloaded into memory and executed instead of a local file. 48 | 49 | When executing the plugins, all _OrchardCoreContrib.PoExtractor_ assemblies are automatically loaded, and two globals are defined: 50 | 51 | - `List ProjectProcessors`: Add an instance of your custom `IProjectProcessor` implementation type to this list. 52 | - `List ProjectFiles`: In the unlikely case that you have to add a new project file type (such as _.fsproj_) add the project file paths to this list. 53 | 54 | > [!TIP] 55 | > You can't import NuGet packages in your script file, but you can import local DLL files using the `#r "path/to/package.dll"` directive. The path can be relative to the script file's location so you can import packages from the build directory of the project you are extracting from. This can be especially useful if you launch the tool as using MSBuild as a post-build action. (For remote scripts loaded with a URL, the path can be relative to the current working directory.) For example: 56 | > 57 | > ```csharp 58 | > #r "src/Modules/OrchardCore.Commerce/bin/Debug/net8.0/OrchardCore.Commerce.dll" 59 | > using OrchardCore.Commerce.Constants; 60 | > Console.WriteLine("Imported resource name: {0}", ResourceNames.ShoppingCart); 61 | > ``` 62 | 63 | ## Uninstallation 64 | 65 | ```powershell 66 | dotnet tool uninstall --global OrchardCoreContrib.PoExtractor 67 | ``` 68 | 69 | ## Limitations 70 | 71 | OrchardCoreContrib.PoExtractor assumes, the code follows several conventions: 72 | 73 | * `IStringLocalizer` or a derived class is accessed via a field named `S` (This is a convention used in Orchard Core) 74 | * `IHtmlLocalizer` or a derived class is accessed via a field named `H` (This is a convention used in Orchard Core) 75 | * `IStringLocalizer` or `IHtmlLocalizer` is accessed via a field named `T` (This is a older convention used in Orchard Core) 76 | * Liquid templates use the filter named `t` (This is a convention used in Fluid) 77 | * context of the localizable string is the full name (with namespace) of the containing class for C# or VB code 78 | * context of the localizable string is the dot-delimited relative path the to view for Razor templates 79 | * context of the localizable string is the dot-delimited relative path the to template for Liquid templates 80 | 81 | ## Example 82 | 83 | C# code: 84 | ```csharp 85 | namespace OrchardCore.ContentFields.Fields { 86 | public class LinkFieldDisplayDriver : ContentFieldDisplayDriver { 87 | private IStringLocalizer S; 88 | 89 | public LinkFieldDisplayDriver(IStringLocalizer localizer) { 90 | S = localizer; 91 | } 92 | 93 | public override async Task UpdateAsync(LinkField field, IUpdateModel updater, UpdateFieldEditorContext context) { 94 | bool modelUpdated = await updater.TryUpdateModelAsync(field, Prefix, f => f.Url, f => f.Text); 95 | 96 | if (modelUpdated) 97 | { 98 | var settings = context.PartFieldDefinition.Settings.ToObject(); 99 | 100 | if (settings.Required && String.IsNullOrWhiteSpace(field.Url)) 101 | { 102 | updater.ModelState.AddModelError(Prefix, S["The url is required for {0}.", context.PartFieldDefinition.DisplayName()]); 103 | } 104 | } 105 | 106 | return Edit(field, context); 107 | } 108 | } 109 | } 110 | ``` 111 | 112 | VB code: 113 | ```vb 114 | Namespace OrchardCore.Modules.GreetingModule 115 | Public Class Greeting 116 | private readonly S As IStringLocalizer(Of Greeting) 117 | 118 | Public Sub New(ByVal localizer As IStringLocalizer(Of Greeting)) 119 | S = localizer 120 | End Sub 121 | 122 | Public Sub Saulation(byVal name As String) 123 | Console.WriteLine(S("Hi {0} ...", name)) 124 | End Sub 125 | End Class 126 | End Namespace 127 | ``` 128 | 129 | Razor view: 130 | ```html 131 | @model OrchardCore.ContentFields.ViewModels.EditLinkFieldViewModel 132 | 133 |
134 |
135 | 136 |
137 |
138 | 139 |
140 |
141 | 142 | 143 |
144 |
145 | 146 | ``` 147 | 148 | Liquid template: 149 | ```html 150 | div class="page-heading"> 151 |

{{ "Page Not Found" | t }}

152 | /div> 153 | 154 | ``` 155 | 156 | Generated POT file: 157 | ``` 158 | #: OrchardCore.ContentFields\Drivers\LinkFieldDriver.cs:59 159 | #. updater.ModelState.AddModelError(Prefix, T["The url is required for {0}.", context.PartFieldDefinition.DisplayName()]); 160 | msgctxt "OrchardCore.ContentFields.Fields.LinkFieldDisplayDriver" 161 | msgid "The url is required for {0}." 162 | msgstr "" 163 | 164 | #: OrchardCore.Modules.GreetingModule\Greeting.vb:94 165 | #. Console.WriteLine(S("Hi {0} ...", name)) 166 | msgctxt "OrchardCore.Modules.GreetingModule.Greeting" 167 | msgid "Hi {0} ..." 168 | msgstr "" 169 | 170 | #: OrchardCore.ContentFields\Views\LinkField.Edit.cshtml:32 171 | #. 172 | msgctxt "OrchardCore.ContentFields.Views.LinkField.Edit" 173 | msgid "Link text" 174 | msgstr "" 175 | 176 | #: TheBlogTheme\Views\Shared\NotFound.liquid:0 177 | msgctxt "TheBlogTheme.Views.Shared.NotFound" 178 | msgid "Page Not Found" 179 | msgstr "" 180 | ``` 181 | 182 | ## Credits 183 | 184 | **PoExtractor** 185 | 186 | https://github.com/lukaskabrt/PoExtractor 187 | 188 | Lukas Kabrt 189 | -------------------------------------------------------------------------------- /global.json: -------------------------------------------------------------------------------- 1 | { 2 | "sdk": { 3 | "version": "8.0.0", 4 | "rollForward": "latestMinor" 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /images/OCC.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OrchardCoreContrib/OrchardCoreContrib.PoExtractor/f799acb7c556054bb2f1af0c3be8ead67eb3392c/images/OCC.png -------------------------------------------------------------------------------- /images/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OrchardCoreContrib/OrchardCoreContrib.PoExtractor/f799acb7c556054bb2f1af0c3be8ead67eb3392c/images/icon.png -------------------------------------------------------------------------------- /license.txt: -------------------------------------------------------------------------------- 1 | Copyright (c) 2017 Lukas Kabrt 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | SOFTWARE. -------------------------------------------------------------------------------- /src/OrchardCoreContrib.PoExtractor.Abstractions/Extensions/StringExtensions.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace OrchardCoreContrib.PoExtractor; 4 | 5 | /// 6 | /// Extension methods for . 7 | /// 8 | public static class StringExtensions 9 | { 10 | /// 11 | /// Removes the given value from the start of the text. 12 | /// 13 | /// The source text. 14 | /// The value to be trimmed. 15 | public static string TrimStart(this string text, string trimText) 16 | { 17 | if (string.IsNullOrEmpty(text)) 18 | { 19 | throw new ArgumentException($"'{nameof(text)}' cannot be null or empty.", nameof(text)); 20 | } 21 | 22 | if (string.IsNullOrEmpty(trimText)) 23 | { 24 | throw new ArgumentException($"'{nameof(trimText)}' cannot be null or empty.", nameof(trimText)); 25 | } 26 | 27 | var index = text.IndexOf(trimText); 28 | 29 | return index < 0 30 | ? text 31 | : text.Remove(index, trimText.Length); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/OrchardCoreContrib.PoExtractor.Abstractions/IMetadataProvider.cs: -------------------------------------------------------------------------------- 1 | namespace OrchardCoreContrib.PoExtractor; 2 | 3 | /// 4 | /// Provides metadata of the translatable text based on information from the AST node. 5 | /// 6 | /// Type of the node. 7 | public interface IMetadataProvider 8 | { 9 | /// 10 | /// Gets context of the translatable text. 11 | /// 12 | /// The AST node representing the translatable text. 13 | /// A string value, that is used in the output file as #msgctx. 14 | string GetContext(TNode node); 15 | 16 | /// 17 | /// Gets location of the translatable text in the source file. 18 | /// 19 | /// The AST node representing the translatable text. 20 | /// An object with the description of the location in the source file. 21 | LocalizableStringLocation GetLocation(TNode node); 22 | } 23 | -------------------------------------------------------------------------------- /src/OrchardCoreContrib.PoExtractor.Abstractions/IProjectProcessor.cs: -------------------------------------------------------------------------------- 1 | namespace OrchardCoreContrib.PoExtractor; 2 | 3 | /// 4 | /// Contract for processing a project to get the localization strings. 5 | /// 6 | public interface IProjectProcessor 7 | { 8 | /// 9 | /// Lookup for the localizable string by process the given project path. 10 | /// 11 | /// Project path. 12 | /// Project base path. 13 | /// List of contain in the processed project. 14 | void Process(string path, string basePath, LocalizableStringCollection localizableStrings); 15 | } 16 | -------------------------------------------------------------------------------- /src/OrchardCoreContrib.PoExtractor.Abstractions/IStringExtractor.cs: -------------------------------------------------------------------------------- 1 | namespace OrchardCoreContrib.PoExtractor; 2 | 3 | /// 4 | /// Extracts a translatable string from a node of the AST tree. 5 | /// 6 | /// Type of the node 7 | public interface IStringExtractor 8 | { 9 | /// 10 | /// Tries to extract a localizable string from the AST node. 11 | /// 12 | /// The AST node. 13 | /// The extracted localizable string. 14 | /// true if a localizable string was successfully extracted, otherwise returns false. 15 | bool TryExtract(TNode node, out LocalizableStringOccurence result); 16 | } 17 | -------------------------------------------------------------------------------- /src/OrchardCoreContrib.PoExtractor.Abstractions/LocalizableString.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | 4 | namespace OrchardCoreContrib.PoExtractor; 5 | 6 | /// 7 | /// Represents a localizable text with all it's occurrences in the project. 8 | /// 9 | public class LocalizableString 10 | { 11 | /// 12 | /// Creates a new instance of the . 13 | /// 14 | public LocalizableString() 15 | { 16 | Locations = []; 17 | } 18 | 19 | /// 20 | /// Creates a new instance of the and properties with data from the source. 21 | /// 22 | /// the with the data. 23 | public LocalizableString(LocalizableStringOccurence source) 24 | { 25 | ArgumentNullException.ThrowIfNull(source); 26 | 27 | Text = source.Text; 28 | TextPlural = source.TextPlural; 29 | Context = source.Context; 30 | 31 | Locations = [ source.Location ]; 32 | } 33 | 34 | /// 35 | /// Gets or sets context of the. 36 | /// 37 | public string Context { get; set; } 38 | 39 | /// 40 | /// Gets or sets the localizable text. 41 | /// 42 | public string Text { get; set; } 43 | 44 | /// 45 | /// Gets or sets the localizable text for the plural. 46 | /// 47 | public string TextPlural { get; set; } 48 | 49 | /// 50 | /// Gets collection of all locations of the text in the project. 51 | /// 52 | public List Locations { get; } 53 | } 54 | -------------------------------------------------------------------------------- /src/OrchardCoreContrib.PoExtractor.Abstractions/LocalizableStringCollection.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | 4 | namespace OrchardCoreContrib.PoExtractor; 5 | 6 | /// 7 | /// Represents collection of the all localizable strings in the project. Localizable strings with the same values are merged. 8 | /// 9 | public class LocalizableStringCollection 10 | { 11 | private readonly Dictionary _values; 12 | 13 | /// 14 | /// Creates a new empty instance of the class. 15 | /// 16 | public LocalizableStringCollection() 17 | { 18 | _values = []; 19 | } 20 | 21 | /// 22 | /// Gets collection of all in the project. 23 | /// 24 | public IEnumerable Values => _values.Values; 25 | 26 | /// 27 | /// Adds to the collection. 28 | /// 29 | /// The item to add. 30 | public void Add(LocalizableStringOccurence item) 31 | { 32 | ArgumentNullException.ThrowIfNull(item); 33 | 34 | var key = item.Context + item.Text; 35 | if (_values.TryGetValue(key, out var localizedString)) 36 | { 37 | localizedString.Locations.Add(item.Location); 38 | } 39 | else 40 | { 41 | _values.Add(key, new LocalizableString(item)); 42 | } 43 | } 44 | 45 | /// 46 | /// Clear collection 47 | /// 48 | public void Clear() => _values.Clear(); 49 | } 50 | -------------------------------------------------------------------------------- /src/OrchardCoreContrib.PoExtractor.Abstractions/LocalizableStringExtractor.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace OrchardCoreContrib.PoExtractor; 4 | 5 | /// 6 | /// Represents a base class for extracting a localizable strings. 7 | /// 8 | /// The type of the node. 9 | /// 10 | /// Creates a new instance of a . 11 | /// 12 | /// The . 13 | public abstract class LocalizableStringExtractor(IMetadataProvider metadataProvider) : IStringExtractor 14 | { 15 | protected IMetadataProvider MetadataProvider { get; } = metadataProvider ?? throw new ArgumentNullException(nameof(metadataProvider)); 16 | 17 | /// 18 | public abstract bool TryExtract(TNode node, out LocalizableStringOccurence result); 19 | 20 | /// 21 | /// Creates a localized string. 22 | /// 23 | /// The localized text. 24 | /// The pluralization form for the localized text. 25 | /// The node in which to get the localized string information. 26 | protected LocalizableStringOccurence CreateLocalizedString(string text, string textPlural, TNode node) 27 | { 28 | if (string.IsNullOrEmpty(text)) 29 | { 30 | throw new ArgumentException($"'{nameof(text)}' cannot be null or empty.", nameof(text)); 31 | } 32 | 33 | var result = new LocalizableStringOccurence 34 | { 35 | Text = text, 36 | TextPlural = textPlural, 37 | Location = metadataProvider.GetLocation(node), 38 | Context = metadataProvider.GetContext(node) 39 | }; 40 | 41 | return result; 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/OrchardCoreContrib.PoExtractor.Abstractions/LocalizableStringLocation.cs: -------------------------------------------------------------------------------- 1 | namespace OrchardCoreContrib.PoExtractor; 2 | 3 | /// 4 | /// Represents a location of the localizable string occurrence in the source code. 5 | /// 6 | public class LocalizableStringLocation 7 | { 8 | /// 9 | /// Gets or sets the name of the source file. 10 | /// 11 | public string SourceFile { get; set; } 12 | 13 | /// 14 | /// Gets or sets the line number in the source file. 15 | /// 16 | public int SourceFileLine { get; set; } 17 | 18 | /// 19 | /// Gets or sets a comment for the occurrence. 20 | /// 21 | /// 22 | /// Typically used to provide better understanding for translators, e.g. copy of the whole line from the source code. 23 | /// 24 | public string Comment { get; set; } 25 | } 26 | -------------------------------------------------------------------------------- /src/OrchardCoreContrib.PoExtractor.Abstractions/LocalizableStringOccurence.cs: -------------------------------------------------------------------------------- 1 | namespace OrchardCoreContrib.PoExtractor; 2 | 3 | /// 4 | /// Represents the specific occurrence of the localizable string in the project. 5 | /// 6 | public class LocalizableStringOccurence 7 | { 8 | /// 9 | /// Gets or sets the context for the localizable string. 10 | /// 11 | public string Context { get; set; } 12 | 13 | /// 14 | /// Gets or sets the localizable text. 15 | /// 16 | public string Text { get; set; } 17 | 18 | /// 19 | /// Gets or sets the localizable pluralization text. 20 | /// 21 | public string TextPlural { get; set; } 22 | 23 | /// 24 | /// Gets or sets the location for the localizable string. 25 | /// 26 | public LocalizableStringLocation Location { get; set; } 27 | } 28 | -------------------------------------------------------------------------------- /src/OrchardCoreContrib.PoExtractor.Abstractions/OrchardCoreContrib.PoExtractor.Abstractions.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | OrchardCoreContrib.PoExtractor 4 | 5 | 6 | -------------------------------------------------------------------------------- /src/OrchardCoreContrib.PoExtractor.Core/OrchardCoreContrib.PoExtractor.Core.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | OrchardCoreContrib.PoExtractor 5 | net6.0 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /src/OrchardCoreContrib.PoExtractor.DotNet.CS/CSharpProjectProcessor.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.CodeAnalysis.CSharp; 2 | using OrchardCoreContrib.PoExtractor.DotNet.CS.MetadataProviders; 3 | using System; 4 | using System.IO; 5 | using System.Linq; 6 | 7 | namespace OrchardCoreContrib.PoExtractor.DotNet.CS; 8 | 9 | /// 10 | /// Extracts localizable strings from all *.cs files in the project path. 11 | /// 12 | public class CSharpProjectProcessor : IProjectProcessor 13 | { 14 | private static readonly string _cSharpExtension = "*.cs"; 15 | 16 | /// 17 | public virtual void Process(string path, string basePath, LocalizableStringCollection localizableStrings) 18 | { 19 | if (string.IsNullOrEmpty(path)) 20 | { 21 | throw new ArgumentException($"'{nameof(path)}' cannot be null or empty.", nameof(path)); 22 | } 23 | 24 | if (string.IsNullOrEmpty(basePath)) 25 | { 26 | throw new ArgumentException($"'{nameof(basePath)}' cannot be null or empty.", nameof(basePath)); 27 | } 28 | 29 | ArgumentNullException.ThrowIfNull(localizableStrings); 30 | 31 | var csharpMetadataProvider = new CSharpMetadataProvider(basePath); 32 | var csharpWalker = new ExtractingCodeWalker( 33 | [ 34 | new SingularStringExtractor(csharpMetadataProvider), 35 | new PluralStringExtractor(csharpMetadataProvider), 36 | new ErrorMessageAnnotationStringExtractor(csharpMetadataProvider), 37 | new DisplayAttributeDescriptionStringExtractor(csharpMetadataProvider), 38 | new DisplayAttributeNameStringExtractor(csharpMetadataProvider), 39 | new DisplayAttributeGroupNameStringExtractor(csharpMetadataProvider), 40 | new DisplayAttributeShortNameStringExtractor(csharpMetadataProvider) 41 | ], localizableStrings); 42 | 43 | foreach (var file in Directory.EnumerateFiles(path, $"*{_cSharpExtension}", SearchOption.AllDirectories).OrderBy(file => file)) 44 | { 45 | if (file.StartsWith(Path.Combine(path, "obj"))) 46 | { 47 | continue; 48 | } 49 | 50 | using var stream = File.OpenRead(file); 51 | using var reader = new StreamReader(stream); 52 | var syntaxTree = CSharpSyntaxTree.ParseText(reader.ReadToEnd(), path: file); 53 | 54 | csharpWalker.Visit(syntaxTree.GetRoot()); 55 | } 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /src/OrchardCoreContrib.PoExtractor.DotNet.CS/MetadataProviders/CSharpMetadataProvider.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.CodeAnalysis; 2 | using Microsoft.CodeAnalysis.CSharp.Syntax; 3 | using System; 4 | using System.Linq; 5 | 6 | namespace OrchardCoreContrib.PoExtractor.DotNet.CS.MetadataProviders; 7 | 8 | /// 9 | /// Provides metadata for C# code files. 10 | /// 11 | public class CSharpMetadataProvider : IMetadataProvider 12 | { 13 | private readonly string _basePath; 14 | 15 | /// 16 | /// Creates a new instance of a . 17 | /// 18 | /// The base path. 19 | public CSharpMetadataProvider(string basePath) 20 | { 21 | ArgumentException.ThrowIfNullOrEmpty(basePath, nameof(basePath)); 22 | 23 | _basePath = basePath; 24 | } 25 | 26 | /// 27 | public string GetContext(SyntaxNode node) 28 | { 29 | ArgumentNullException.ThrowIfNull(node); 30 | 31 | var @namespace = node.Ancestors() 32 | .OfType() 33 | .FirstOrDefault()? 34 | .Name.ToString(); 35 | 36 | if (string.IsNullOrEmpty(@namespace)) 37 | { 38 | @namespace = node.Ancestors() 39 | .OfType() 40 | .FirstOrDefault()? 41 | .Name.ToString(); 42 | } 43 | 44 | var classes = node 45 | .Ancestors() 46 | .OfType() 47 | .Select(c => c.Identifier.ValueText); 48 | 49 | var @class = classes.Count() == 1 50 | ? classes.Single() 51 | : String.Join('.', classes.Reverse()); 52 | 53 | return string.IsNullOrEmpty(@namespace) 54 | ? @class 55 | : $"{@namespace}.{@class}"; 56 | } 57 | 58 | /// 59 | public LocalizableStringLocation GetLocation(SyntaxNode node) 60 | { 61 | ArgumentNullException.ThrowIfNull(node); 62 | 63 | var lineNumber = node 64 | .GetLocation() 65 | .GetMappedLineSpan() 66 | .StartLinePosition.Line; 67 | 68 | return new LocalizableStringLocation 69 | { 70 | SourceFileLine = lineNumber + 1, 71 | SourceFile = node.SyntaxTree.FilePath.TrimStart(_basePath), 72 | Comment = node.SyntaxTree.GetText().Lines[lineNumber].ToString().Trim() 73 | }; 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /src/OrchardCoreContrib.PoExtractor.DotNet.CS/OrchardCoreContrib.PoExtractor.DotNet.CS.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /src/OrchardCoreContrib.PoExtractor.DotNet.CS/PluralStringExtractor.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.CodeAnalysis; 2 | using Microsoft.CodeAnalysis.CSharp; 3 | using Microsoft.CodeAnalysis.CSharp.Syntax; 4 | using System; 5 | using System.Linq; 6 | 7 | namespace OrchardCoreContrib.PoExtractor.DotNet.CS; 8 | 9 | /// 10 | /// Extracts with the singular text from the C# AST node 11 | /// 12 | /// 13 | /// The localizable string is identified by the name convention - T.Plural(count, "1 book", "{0} books") 14 | /// 15 | /// 16 | /// Creates a new instance of a . 17 | /// 18 | /// The . 19 | public class PluralStringExtractor(IMetadataProvider metadataProvider) : LocalizableStringExtractor(metadataProvider) 20 | { 21 | 22 | /// 23 | public override bool TryExtract(SyntaxNode node, out LocalizableStringOccurence result) 24 | { 25 | ArgumentNullException.ThrowIfNull(nameof(node)); 26 | 27 | result = null; 28 | 29 | if (node is InvocationExpressionSyntax invocation && 30 | invocation.Expression is MemberAccessExpressionSyntax accessor && 31 | accessor.Expression is IdentifierNameSyntax identifierName && 32 | LocalizerAccessors.LocalizerIdentifiers.Contains(identifierName.Identifier.Text) && 33 | accessor.Name.Identifier.Text == "Plural") 34 | { 35 | 36 | var arguments = invocation.ArgumentList.Arguments; 37 | if (arguments.Count >= 2 && 38 | arguments[1].Expression is ArrayCreationExpressionSyntax array) 39 | { 40 | if (array.Type.ElementType is PredefinedTypeSyntax arrayType && 41 | arrayType.Keyword.Text == "string" && 42 | array.Initializer.Expressions.Count >= 2 && 43 | array.Initializer.Expressions[0] is LiteralExpressionSyntax singularLiteral && singularLiteral.IsKind(SyntaxKind.StringLiteralExpression) && 44 | array.Initializer.Expressions[1] is LiteralExpressionSyntax pluralLiteral && pluralLiteral.IsKind(SyntaxKind.StringLiteralExpression)) 45 | { 46 | 47 | result = CreateLocalizedString(singularLiteral.Token.ValueText, pluralLiteral.Token.ValueText, node); 48 | 49 | return true; 50 | } 51 | } 52 | else 53 | { 54 | if (arguments.Count >= 3 && 55 | arguments[1].Expression is LiteralExpressionSyntax singularLiteral && singularLiteral.IsKind(SyntaxKind.StringLiteralExpression) && 56 | arguments[2].Expression is LiteralExpressionSyntax pluralLiteral && pluralLiteral.IsKind(SyntaxKind.StringLiteralExpression)) 57 | { 58 | 59 | result = CreateLocalizedString(singularLiteral.Token.ValueText, pluralLiteral.Token.ValueText, node); 60 | 61 | return true; 62 | } 63 | } 64 | } 65 | 66 | return false; 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /src/OrchardCoreContrib.PoExtractor.DotNet.CS/SingularStringExtractor.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.CodeAnalysis; 2 | using Microsoft.CodeAnalysis.CSharp; 3 | using Microsoft.CodeAnalysis.CSharp.Syntax; 4 | using System; 5 | using System.Linq; 6 | 7 | namespace OrchardCoreContrib.PoExtractor.DotNet.CS; 8 | 9 | /// 10 | /// Extracts with the singular text from the C# AST node 11 | /// 12 | /// 13 | /// The localizable string is identified by the name convention - T["TEXT TO TRANSLATE"] 14 | /// 15 | /// 16 | /// Creates a new instance of a . 17 | /// 18 | /// The . 19 | public class SingularStringExtractor(IMetadataProvider metadataProvider) : LocalizableStringExtractor(metadataProvider) 20 | { 21 | 22 | /// 23 | public override bool TryExtract(SyntaxNode node, out LocalizableStringOccurence result) 24 | { 25 | ArgumentNullException.ThrowIfNull(node); 26 | 27 | result = null; 28 | 29 | if (node is ElementAccessExpressionSyntax accessor && 30 | accessor.Expression is IdentifierNameSyntax identifierName && 31 | LocalizerAccessors.LocalizerIdentifiers.Contains(identifierName.Identifier.Text) && 32 | accessor.ArgumentList != null) 33 | { 34 | 35 | var argument = accessor.ArgumentList.Arguments.FirstOrDefault(); 36 | if (argument != null && argument.Expression is LiteralExpressionSyntax literal && literal.IsKind(SyntaxKind.StringLiteralExpression)) 37 | { 38 | result = CreateLocalizedString(literal.Token.ValueText, null, node); 39 | return true; 40 | } 41 | } 42 | 43 | return false; 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/OrchardCoreContrib.PoExtractor.DotNet.VB/MetadataProviders/VisualBasicMetadataProvider.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.CodeAnalysis; 2 | using Microsoft.CodeAnalysis.VisualBasic.Syntax; 3 | using System; 4 | using System.Linq; 5 | 6 | namespace OrchardCoreContrib.PoExtractor.DotNet.VB.MetadataProviders; 7 | 8 | /// 9 | /// Provides metadata for .vb code files 10 | /// 11 | public class VisualBasicMetadataProvider : IMetadataProvider 12 | { 13 | private readonly string _basePath; 14 | 15 | /// 16 | /// Creates a new instance of a . 17 | /// 18 | /// The base path. 19 | public VisualBasicMetadataProvider(string basePath) 20 | { 21 | ArgumentException.ThrowIfNullOrEmpty(basePath, nameof(basePath)); 22 | 23 | _basePath = basePath; 24 | } 25 | 26 | /// 27 | public string GetContext(SyntaxNode node) 28 | { 29 | ArgumentNullException.ThrowIfNull(node); 30 | 31 | var @namespace = node 32 | .Ancestors() 33 | .OfType() 34 | .FirstOrDefault()?.NamespaceStatement.Name 35 | .ToString(); 36 | 37 | var classes = node 38 | .Ancestors() 39 | .OfType() 40 | .Select(c => c.ClassStatement.Identifier.ValueText); 41 | 42 | var @class = classes.Count() == 1 43 | ? classes.Single() 44 | : String.Join('.', classes.Reverse()); 45 | 46 | return $"{@namespace}.{@class}"; 47 | } 48 | 49 | /// 50 | public LocalizableStringLocation GetLocation(SyntaxNode node) 51 | { 52 | ArgumentNullException.ThrowIfNull(node); 53 | 54 | var lineNumber = node.GetLocation().GetMappedLineSpan().StartLinePosition.Line; 55 | 56 | return new LocalizableStringLocation 57 | { 58 | SourceFileLine = lineNumber + 1, 59 | SourceFile = node.SyntaxTree.FilePath.TrimStart(_basePath), 60 | Comment = node.SyntaxTree.GetText().Lines[lineNumber].ToString().Trim() 61 | }; 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /src/OrchardCoreContrib.PoExtractor.DotNet.VB/OrchardCoreContrib.PoExtractor.DotNet.VB.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /src/OrchardCoreContrib.PoExtractor.DotNet.VB/PluralStringExtractor.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.CodeAnalysis; 2 | using Microsoft.CodeAnalysis.VisualBasic; 3 | using Microsoft.CodeAnalysis.VisualBasic.Syntax; 4 | using System; 5 | using System.Linq; 6 | 7 | namespace OrchardCoreContrib.PoExtractor.DotNet.VB; 8 | 9 | /// 10 | /// Extracts with the singular text from the VB AST node. 11 | /// 12 | /// 13 | /// The localizable string is identified by the name convention - T.Plural(count, "1 book", "{0} books"). 14 | /// 15 | /// 16 | /// Creates a new instance of a . 17 | /// 18 | /// The . 19 | public class PluralStringExtractor(IMetadataProvider metadataProvider) : LocalizableStringExtractor(metadataProvider) 20 | { 21 | 22 | /// 23 | public override bool TryExtract(SyntaxNode node, out LocalizableStringOccurence result) 24 | { 25 | ArgumentNullException.ThrowIfNull(node); 26 | 27 | result = null; 28 | 29 | if (node is InvocationExpressionSyntax invocation && 30 | invocation.Expression is MemberAccessExpressionSyntax accessor && 31 | accessor.Expression is IdentifierNameSyntax identifierName && 32 | LocalizerAccessors.LocalizerIdentifiers.Contains(identifierName.Identifier.Text) && 33 | accessor.Name.Identifier.Text == "Plural") 34 | { 35 | var arguments = invocation.ArgumentList.Arguments; 36 | if (arguments.Count >= 2 && 37 | arguments[1].GetExpression() is ArrayCreationExpressionSyntax array) 38 | { 39 | if (array.Type is PredefinedTypeSyntax arrayType && 40 | arrayType.Keyword.Text == "String" && 41 | array.Initializer.Initializers.Count >= 2 && 42 | array.Initializer.Initializers.ElementAt(0) is LiteralExpressionSyntax singularLiteral && singularLiteral.IsKind(SyntaxKind.StringLiteralExpression) && 43 | array.Initializer.Initializers.ElementAt(1) is LiteralExpressionSyntax pluralLiteral && pluralLiteral.IsKind(SyntaxKind.StringLiteralExpression)) 44 | { 45 | 46 | result = CreateLocalizedString(singularLiteral.Token.ValueText, pluralLiteral.Token.ValueText, node); 47 | 48 | return true; 49 | } 50 | } 51 | else 52 | { 53 | if (arguments.Count >= 3 && 54 | arguments[1].GetExpression() is LiteralExpressionSyntax singularLiteral && singularLiteral.IsKind(SyntaxKind.StringLiteralExpression) && 55 | arguments[2].GetExpression() is LiteralExpressionSyntax pluralLiteral && pluralLiteral.IsKind(SyntaxKind.StringLiteralExpression)) 56 | { 57 | 58 | result = CreateLocalizedString(singularLiteral.Token.ValueText, pluralLiteral.Token.ValueText, node); 59 | 60 | return true; 61 | } 62 | } 63 | } 64 | 65 | return false; 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /src/OrchardCoreContrib.PoExtractor.DotNet.VB/SingularStringExtractor.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.CodeAnalysis; 2 | using Microsoft.CodeAnalysis.VisualBasic; 3 | using Microsoft.CodeAnalysis.VisualBasic.Syntax; 4 | using System; 5 | using System.Linq; 6 | 7 | namespace OrchardCoreContrib.PoExtractor.DotNet.VB; 8 | 9 | /// 10 | /// Extracts with the singular text from the C# & VB AST node 11 | /// 12 | /// 13 | /// The localizable string is identified by the name convention - T["TEXT TO TRANSLATE"] 14 | /// 15 | /// 16 | /// Creates a new instance of a . 17 | /// 18 | /// The . 19 | public class SingularStringExtractor(IMetadataProvider metadataProvider) : LocalizableStringExtractor(metadataProvider) 20 | { 21 | /// 22 | public override bool TryExtract(SyntaxNode node, out LocalizableStringOccurence result) 23 | { 24 | ArgumentNullException.ThrowIfNull(node); 25 | 26 | result = null; 27 | 28 | if (node is InvocationExpressionSyntax accessor && 29 | accessor.Expression is IdentifierNameSyntax identifierName && 30 | LocalizerAccessors.LocalizerIdentifiers.Contains(identifierName.Identifier.Text) && 31 | accessor.ArgumentList != null) 32 | { 33 | var argument = accessor.ArgumentList.Arguments.FirstOrDefault(); 34 | if (argument != null && argument.GetExpression() is LiteralExpressionSyntax literal && literal.IsKind(SyntaxKind.StringLiteralExpression)) 35 | { 36 | result = CreateLocalizedString(literal.Token.ValueText, null, node); 37 | return true; 38 | } 39 | } 40 | 41 | return false; 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/OrchardCoreContrib.PoExtractor.DotNet.VB/VisualBasicProjectProcessor.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.CodeAnalysis.VisualBasic; 2 | using OrchardCoreContrib.PoExtractor.DotNet.VB.MetadataProviders; 3 | using System; 4 | using System.IO; 5 | using System.Linq; 6 | 7 | namespace OrchardCoreContrib.PoExtractor.DotNet.VB; 8 | 9 | /// 10 | /// Extracts localizable strings from all *.vb files in the project path. 11 | /// 12 | public class VisualBasicProjectProcessor : IProjectProcessor 13 | { 14 | private static readonly string _visualBasicExtension = "*.vb"; 15 | 16 | /// 17 | public void Process(string path, string basePath, LocalizableStringCollection localizableStrings) 18 | { 19 | if (string.IsNullOrEmpty(path)) 20 | { 21 | throw new ArgumentException($"'{nameof(path)}' cannot be null or empty.", nameof(path)); 22 | } 23 | 24 | if (string.IsNullOrEmpty(basePath)) 25 | { 26 | throw new ArgumentException($"'{nameof(basePath)}' cannot be null or empty.", nameof(basePath)); 27 | } 28 | 29 | ArgumentNullException.ThrowIfNull(localizableStrings); 30 | 31 | var visualBasicMetadataProvider = new VisualBasicMetadataProvider(basePath); 32 | var visualBasicWalker = new ExtractingCodeWalker( 33 | [ 34 | new SingularStringExtractor(visualBasicMetadataProvider), 35 | new PluralStringExtractor(visualBasicMetadataProvider), 36 | new ErrorMessageAnnotationStringExtractor(visualBasicMetadataProvider), 37 | new DisplayAttributeDescriptionStringExtractor(visualBasicMetadataProvider), 38 | new DisplayAttributeNameStringExtractor(visualBasicMetadataProvider), 39 | new DisplayAttributeGroupNameStringExtractor(visualBasicMetadataProvider), 40 | new DisplayAttributeShortNameStringExtractor(visualBasicMetadataProvider) 41 | ], localizableStrings); 42 | 43 | foreach (var file in Directory.EnumerateFiles(path, $"*{_visualBasicExtension}", SearchOption.AllDirectories).OrderBy(file => file)) 44 | { 45 | using var stream = File.OpenRead(file); 46 | using var reader = new StreamReader(stream); 47 | var syntaxTree = VisualBasicSyntaxTree.ParseText(reader.ReadToEnd(), path: file); 48 | 49 | visualBasicWalker.Visit(syntaxTree.GetRoot()); 50 | } 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/OrchardCoreContrib.PoExtractor.DotNet/DisplayAttributeDescriptionStringExtractor.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.CodeAnalysis; 2 | using System.ComponentModel.DataAnnotations; 3 | 4 | namespace OrchardCoreContrib.PoExtractor.DotNet; 5 | 6 | /// 7 | /// Extracts localizable string from Description property. 8 | /// 9 | /// 10 | /// Creates a new instance of a . 11 | /// 12 | /// The . 13 | public class DisplayAttributeDescriptionStringExtractor(IMetadataProvider metadataProvider) 14 | : DisplayAttributeStringExtractor("Description", metadataProvider) 15 | { 16 | } 17 | -------------------------------------------------------------------------------- /src/OrchardCoreContrib.PoExtractor.DotNet/DisplayAttributeGroupNameStringExtractor.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.CodeAnalysis; 2 | using System.ComponentModel.DataAnnotations; 3 | 4 | namespace OrchardCoreContrib.PoExtractor.DotNet; 5 | 6 | /// 7 | /// Extracts localizable string from GroupName property. 8 | /// 9 | /// 10 | /// Creates a new instance of a . 11 | /// 12 | /// The . 13 | public class DisplayAttributeGroupNameStringExtractor(IMetadataProvider metadataProvider) 14 | : DisplayAttributeStringExtractor("GroupName", metadataProvider) 15 | { 16 | } 17 | -------------------------------------------------------------------------------- /src/OrchardCoreContrib.PoExtractor.DotNet/DisplayAttributeNameStringExtractor.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.CodeAnalysis; 2 | using System.ComponentModel.DataAnnotations; 3 | 4 | namespace OrchardCoreContrib.PoExtractor.DotNet; 5 | 6 | /// 7 | /// Extracts localizable string from Name property. 8 | /// 9 | /// 10 | /// Creates a new instanceof a . 11 | /// 12 | /// The . 13 | public class DisplayAttributeNameStringExtractor(IMetadataProvider metadataProvider) 14 | : DisplayAttributeStringExtractor("Name", metadataProvider) 15 | { 16 | } 17 | -------------------------------------------------------------------------------- /src/OrchardCoreContrib.PoExtractor.DotNet/DisplayAttributeShortNameStringExtractor.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.CodeAnalysis; 2 | using System.ComponentModel.DataAnnotations; 3 | 4 | namespace OrchardCoreContrib.PoExtractor.DotNet; 5 | 6 | /// 7 | /// Extracts localizable string from ShortName property. 8 | /// 9 | /// 10 | /// Creates a new instance of a . 11 | /// 12 | /// The . 13 | public class DisplayAttributeShortNameStringExtractor(IMetadataProvider metadataProvider) 14 | : DisplayAttributeStringExtractor("ShortName", metadataProvider) 15 | { 16 | } 17 | -------------------------------------------------------------------------------- /src/OrchardCoreContrib.PoExtractor.DotNet/DisplayAttributeStringExtractor.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.CodeAnalysis; 2 | using Microsoft.CodeAnalysis.CSharp; 3 | using Microsoft.CodeAnalysis.CSharp.Syntax; 4 | using System.ComponentModel.DataAnnotations; 5 | 6 | namespace OrchardCoreContrib.PoExtractor.DotNet; 7 | 8 | /// 9 | /// Extracts localizable string from . 10 | /// 11 | /// 12 | /// Creates a new instance of a . 13 | /// 14 | /// The argument name. 15 | /// The . 16 | public abstract class DisplayAttributeStringExtractor(string argumentName, IMetadataProvider metadataProvider) 17 | : LocalizableStringExtractor(metadataProvider) 18 | { 19 | private const string DisplayAttributeName = "Display"; 20 | 21 | /// 22 | public override bool TryExtract(SyntaxNode node, out LocalizableStringOccurence result) 23 | { 24 | result = null; 25 | 26 | if (node is AttributeArgumentSyntax argument 27 | && argument.Expression.Parent.ToFullString().StartsWith(argumentName) 28 | && node.Parent?.Parent is AttributeSyntax accessor 29 | && accessor.Name.ToString() == DisplayAttributeName 30 | && argument.Expression is LiteralExpressionSyntax literal 31 | && literal.IsKind(SyntaxKind.StringLiteralExpression)) 32 | { 33 | result = CreateLocalizedString(literal.Token.ValueText, null, node); 34 | return true; 35 | } 36 | 37 | return false; 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/OrchardCoreContrib.PoExtractor.DotNet/ErrorMessageAnnotationStringExtractor.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.CodeAnalysis; 2 | using Microsoft.CodeAnalysis.CSharp; 3 | using Microsoft.CodeAnalysis.CSharp.Syntax; 4 | using System; 5 | using System.Linq; 6 | 7 | namespace OrchardCoreContrib.PoExtractor.DotNet; 8 | 9 | /// 10 | /// Extracts localizable string from data annotations error messages. 11 | /// 12 | /// 13 | /// Creates a new instance of a . 14 | /// 15 | /// The . 16 | public class ErrorMessageAnnotationStringExtractor(IMetadataProvider metadataProvider) 17 | : LocalizableStringExtractor(metadataProvider) 18 | { 19 | private const string ErrorMessageAttributeName = "ErrorMessage"; 20 | 21 | /// 22 | public override bool TryExtract(SyntaxNode node, out LocalizableStringOccurence result) 23 | { 24 | ArgumentNullException.ThrowIfNull(node, nameof(node)); 25 | 26 | result = null; 27 | 28 | if (node is AttributeSyntax accessor && accessor.ArgumentList != null) 29 | { 30 | var argument = accessor.ArgumentList.Arguments 31 | .Where(a => a.Expression.Parent.ToFullString().StartsWith(ErrorMessageAttributeName)) 32 | .FirstOrDefault(); 33 | 34 | if (argument != null && argument.Expression is LiteralExpressionSyntax literal && literal.IsKind(SyntaxKind.StringLiteralExpression)) 35 | { 36 | result = CreateLocalizedString(literal.Token.ValueText, null, node); 37 | return true; 38 | } 39 | } 40 | 41 | return false; 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/OrchardCoreContrib.PoExtractor.DotNet/ExtractingCodeWalker.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.CodeAnalysis; 2 | using System; 3 | using System.Collections.Generic; 4 | 5 | namespace OrchardCoreContrib.PoExtractor.DotNet; 6 | 7 | /// 8 | /// Traverses C# & VB AST and extracts localizable strings using provided collection of 9 | /// 10 | /// 11 | /// Initializes a new instance of the class. 12 | /// 13 | /// the collection of extractors to use 14 | /// The where the results are saved. 15 | public class ExtractingCodeWalker(IEnumerable> extractors, LocalizableStringCollection strings) : SyntaxWalker 16 | { 17 | private readonly LocalizableStringCollection _strings = strings ?? throw new ArgumentNullException(nameof(strings)); 18 | private readonly IEnumerable> _extractors = extractors ?? throw new ArgumentNullException(nameof(extractors)); 19 | 20 | /// 21 | public override void Visit(SyntaxNode node) 22 | { 23 | ArgumentNullException.ThrowIfNull(node, nameof(node)); 24 | 25 | base.Visit(node); 26 | 27 | foreach (var extractor in _extractors) 28 | { 29 | if (extractor.TryExtract(node, out var result)) 30 | { 31 | _strings.Add(result); 32 | } 33 | } 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/OrchardCoreContrib.PoExtractor.DotNet/LocalizerAccessors.cs: -------------------------------------------------------------------------------- 1 | namespace OrchardCoreContrib.PoExtractor.DotNet; 2 | 3 | /// 4 | /// Represents a class that contains a set of localizer identifier accessors. 5 | /// 6 | public static class LocalizerAccessors 7 | { 8 | /// 9 | /// Gets the localizer identifier for IStringLocalizer or IHtmlStringLocalizer in views. 10 | /// 11 | public static readonly string DefaultLocalizerIdentifier = "T"; 12 | 13 | /// 14 | /// Gets the localizer identifier for IStringLocalizer. 15 | /// 16 | public static readonly string StringLocalizerIdentifier = "S"; 17 | 18 | /// 19 | /// Gets the localizer identifier for IHtmlStringLocalizer. 20 | /// 21 | public static readonly string HtmlLocalizerIdentifier = "H"; 22 | 23 | /// 24 | /// Gets the localizer identifiers. 25 | /// 26 | public static string[] LocalizerIdentifiers = 27 | [ 28 | DefaultLocalizerIdentifier, 29 | StringLocalizerIdentifier, 30 | HtmlLocalizerIdentifier 31 | ]; 32 | } 33 | -------------------------------------------------------------------------------- /src/OrchardCoreContrib.PoExtractor.DotNet/OrchardCoreContrib.PoExtractor.DotNet.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /src/OrchardCoreContrib.PoExtractor.DotNet/ProjectExtension.cs: -------------------------------------------------------------------------------- 1 | namespace OrchardCoreContrib.PoExtractor.DotNet 2 | { 3 | /// 4 | /// Represents a class that contains .NET projects extensions. 5 | /// 6 | public class ProjectExtension 7 | { 8 | /// 9 | /// Gets the CSharp project extension. 10 | /// 11 | public static readonly string CS = ".csproj"; 12 | 13 | /// 14 | /// Gets the Visual Basic project extension. 15 | /// 16 | public static readonly string VB = ".vbproj"; 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/OrchardCoreContrib.PoExtractor.Liquid/ExtractingLiquidWalker.cs: -------------------------------------------------------------------------------- 1 | using Fluid.Ast; 2 | using System; 3 | using System.Collections.Generic; 4 | 5 | namespace OrchardCoreContrib.PoExtractor.Liquid; 6 | 7 | /// 8 | /// Traverses Fluid AST and extracts localizable strings using provided collection of 9 | /// 10 | /// 11 | /// Initializes a new instance of the class 12 | /// 13 | /// the collection of extractors to use 14 | /// the where the results are saved 15 | public class ExtractingLiquidWalker(IEnumerable> extractors, LocalizableStringCollection localizableStrings) 16 | { 17 | private string _filePath; 18 | 19 | private readonly LocalizableStringCollection _localizableStrings = localizableStrings ?? throw new ArgumentNullException(nameof(localizableStrings)); 20 | private readonly IEnumerable> _extractors = extractors ?? throw new ArgumentNullException(nameof(extractors)); 21 | 22 | /// 23 | /// Visits liquid statement. 24 | /// 25 | /// The statement context. 26 | public void Visit(LiquidStatementContext statementContext) 27 | { 28 | ArgumentNullException.ThrowIfNull(statementContext); 29 | 30 | _filePath = statementContext.FilePath; 31 | 32 | Visit(statementContext.Statement); 33 | } 34 | 35 | private void Visit(Statement node) 36 | { 37 | switch (node) 38 | { 39 | case AssignStatement assign: 40 | Visit(assign.Value); 41 | break; 42 | case CaseStatement @case: 43 | Visit(@case.Statements); 44 | Visit(@case.Whens); 45 | Visit(@case.Else); 46 | Visit(@case.Expression); 47 | break; 48 | case CycleStatement cycle: 49 | Visit(cycle.Group); 50 | Visit(cycle.Values); 51 | break; 52 | case ElseIfStatement elseIf: 53 | Visit(elseIf.Condition); 54 | Visit(elseIf.Statements); 55 | break; 56 | case IfStatement @if: 57 | Visit(@if.Condition); 58 | Visit(@if.Statements); 59 | Visit(@if.ElseIfs); 60 | Visit(@if.Else); 61 | break; 62 | case OutputStatement output: 63 | Visit(output.Expression); 64 | Visit(output.Filters); 65 | break; 66 | case UnlessStatement unless: 67 | Visit(unless.Condition); 68 | Visit(unless.Statements); 69 | break; 70 | case WhenStatement @when: 71 | Visit(when.Options); 72 | Visit(when.Statements); 73 | break; 74 | case TagStatement tag: 75 | if (tag.Statements != null) 76 | { 77 | foreach (var item in tag.Statements) 78 | { 79 | Visit(item); 80 | } 81 | } 82 | 83 | break; 84 | } 85 | } 86 | 87 | private void Visit(IEnumerable statements) 88 | { 89 | if (statements == null) 90 | { 91 | return; 92 | } 93 | 94 | foreach (var statement in statements) 95 | { 96 | Visit(statement); 97 | } 98 | } 99 | private void Visit(Expression expression) 100 | { 101 | switch (expression) 102 | { 103 | case BinaryExpression binary: 104 | Visit(binary.Left); 105 | Visit(binary.Right); 106 | break; 107 | case FilterExpression filter: 108 | ProcessFilterExpression(filter); 109 | break; 110 | 111 | } 112 | } 113 | 114 | private void Visit(IEnumerable expressions) 115 | { 116 | if (expressions == null) 117 | { 118 | return; 119 | } 120 | 121 | foreach (var expression in expressions) 122 | { 123 | Visit(expression); 124 | } 125 | } 126 | 127 | private void ProcessFilterExpression(FilterExpression filter) 128 | { 129 | foreach (var extractor in _extractors) 130 | { 131 | if (extractor.TryExtract(new LiquidExpressionContext() { Expression = filter, FilePath = _filePath }, out var result)) 132 | { 133 | _localizableStrings.Add(result); 134 | } 135 | } 136 | 137 | Visit(filter.Input); 138 | } 139 | } 140 | -------------------------------------------------------------------------------- /src/OrchardCoreContrib.PoExtractor.Liquid/LiquidExpressionContext.cs: -------------------------------------------------------------------------------- 1 | using Fluid.Ast; 2 | 3 | namespace OrchardCoreContrib.PoExtractor.Liquid; 4 | 5 | /// 6 | /// Represents a liquid expression context. 7 | /// 8 | public class LiquidExpressionContext 9 | { 10 | /// 11 | /// Gets or sets the liquid file path. 12 | /// 13 | public string FilePath { get; set; } 14 | 15 | /// 16 | /// Gets or sets the expression. 17 | /// 18 | public FilterExpression Expression { get; set; } 19 | } 20 | -------------------------------------------------------------------------------- /src/OrchardCoreContrib.PoExtractor.Liquid/LiquidProjectProcessor.cs: -------------------------------------------------------------------------------- 1 | using Fluid; 2 | using Fluid.Parser; 3 | using Microsoft.Extensions.Options; 4 | using OrchardCore.DisplayManagement.Liquid; 5 | using OrchardCoreContrib.PoExtractor.Liquid.MetadataProviders; 6 | using System; 7 | using System.IO; 8 | using System.Linq; 9 | 10 | namespace OrchardCoreContrib.PoExtractor.Liquid; 11 | 12 | /// 13 | /// Extracts localizable strings from all *.liquid files in the project path 14 | /// 15 | public class LiquidProjectProcessor : IProjectProcessor 16 | { 17 | private static readonly string _liquidExtension = "*.liquid"; 18 | 19 | private readonly LiquidViewParser _parser; 20 | 21 | /// 22 | /// Initializes a new instance of the 23 | /// 24 | public LiquidProjectProcessor() 25 | { 26 | var parserOptions = Options.Create(new LiquidViewOptions()); 27 | 28 | _parser = new LiquidViewParser(parserOptions); 29 | } 30 | 31 | /// 32 | public void Process(string path, string basePath, LocalizableStringCollection localizableStrings) 33 | { 34 | ArgumentException.ThrowIfNullOrEmpty(path, nameof(path)); 35 | ArgumentException.ThrowIfNullOrEmpty(basePath, nameof(basePath)); 36 | ArgumentNullException.ThrowIfNull(localizableStrings); 37 | 38 | var liquidMetadataProvider = new LiquidMetadataProvider(basePath); 39 | var liquidVisitor = new ExtractingLiquidWalker([new LiquidStringExtractor(liquidMetadataProvider)], localizableStrings); 40 | 41 | foreach (var file in Directory.EnumerateFiles(path, $"*{_liquidExtension}", SearchOption.AllDirectories).OrderBy(file => file)) 42 | { 43 | using var stream = File.OpenRead(file); 44 | using var reader = new StreamReader(stream); 45 | if (_parser.TryParse(reader.ReadToEnd(), out var template, out var errors)) 46 | { 47 | ProcessTemplate(template, liquidVisitor, file); 48 | } 49 | } 50 | } 51 | 52 | private static void ProcessTemplate(IFluidTemplate template, ExtractingLiquidWalker visitor, string path) 53 | { 54 | if (template is CompositeFluidTemplate compositeTemplate) 55 | { 56 | foreach (var innerTemplate in compositeTemplate.Templates) 57 | { 58 | ProcessTemplate(innerTemplate, visitor, path); 59 | } 60 | } 61 | else if (template is FluidTemplate singleTemplate) 62 | { 63 | ProcessTemplate(singleTemplate, visitor, path); 64 | } 65 | } 66 | 67 | private static void ProcessTemplate(FluidTemplate template, ExtractingLiquidWalker visitor, string path) 68 | { 69 | foreach (var statement in template.Statements) 70 | { 71 | visitor.Visit(new LiquidStatementContext() { Statement = statement, FilePath = path }); 72 | } 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /src/OrchardCoreContrib.PoExtractor.Liquid/LiquidStatementContext.cs: -------------------------------------------------------------------------------- 1 | using Fluid.Ast; 2 | 3 | namespace OrchardCoreContrib.PoExtractor.Liquid; 4 | 5 | /// 6 | /// Represents a liquid statement context. 7 | /// 8 | public class LiquidStatementContext 9 | { 10 | /// 11 | /// Gets or sets liquid file path. 12 | /// 13 | public string FilePath { get; set; } 14 | 15 | /// 16 | /// Gets or sets the liquid statement. 17 | /// 18 | public Statement Statement { get; set; } 19 | } 20 | -------------------------------------------------------------------------------- /src/OrchardCoreContrib.PoExtractor.Liquid/LiquidStringExtractor.cs: -------------------------------------------------------------------------------- 1 | using Fluid.Ast; 2 | using System; 3 | 4 | namespace OrchardCoreContrib.PoExtractor.Liquid; 5 | 6 | /// 7 | /// Extracts localizable strings the Fluid AST node 8 | /// 9 | /// 10 | /// The localizable string is identified by the name convention of the filter - "TEXT TO TRANSLATE" | t 11 | /// 12 | /// 13 | /// Creates a new instance of a . 14 | /// 15 | /// The . 16 | public class LiquidStringExtractor(IMetadataProvider metadataProvider) 17 | : LocalizableStringExtractor(metadataProvider) 18 | { 19 | private static readonly string _localizationFilterName = "t"; 20 | 21 | /// 22 | public override bool TryExtract(LiquidExpressionContext expressionContext, out LocalizableStringOccurence result) 23 | { 24 | ArgumentNullException.ThrowIfNull(expressionContext); 25 | 26 | result = null; 27 | var filter = expressionContext.Expression; 28 | 29 | if (filter.Name == _localizationFilterName) 30 | { 31 | if (filter.Input is LiteralExpression literal) 32 | { 33 | var text = literal 34 | .EvaluateAsync(new Fluid.TemplateContext()) 35 | .GetAwaiter() 36 | .GetResult() 37 | .ToStringValue(); 38 | 39 | result = CreateLocalizedString(text, null, expressionContext); 40 | 41 | return true; 42 | } 43 | } 44 | 45 | return false; 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/OrchardCoreContrib.PoExtractor.Liquid/MetadataProvider/LiquidMetadataProvider.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.IO; 3 | 4 | namespace OrchardCoreContrib.PoExtractor.Liquid.MetadataProviders; 5 | 6 | /// 7 | /// Provides metadata for .liquid files. 8 | /// 9 | public class LiquidMetadataProvider : IMetadataProvider 10 | { 11 | private readonly string _basePath; 12 | 13 | /// 14 | /// Creates a new instance of a . 15 | /// 16 | /// The base path. 17 | public LiquidMetadataProvider(string basePath) 18 | { 19 | ArgumentException.ThrowIfNullOrEmpty(basePath, nameof(basePath)); 20 | 21 | _basePath = basePath; 22 | } 23 | 24 | /// 25 | public string GetContext(LiquidExpressionContext expressionContext) 26 | { 27 | ArgumentNullException.ThrowIfNull(expressionContext, nameof(expressionContext)); 28 | 29 | var path = expressionContext.FilePath.TrimStart(_basePath); 30 | 31 | return path.Replace(Path.DirectorySeparatorChar, '.').Replace(".liquid", string.Empty); 32 | } 33 | 34 | /// 35 | public LocalizableStringLocation GetLocation(LiquidExpressionContext expressionContext) 36 | { 37 | ArgumentNullException.ThrowIfNull(expressionContext, nameof(expressionContext)); 38 | 39 | return new() { SourceFile = expressionContext.FilePath.TrimStart(_basePath) }; 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/OrchardCoreContrib.PoExtractor.Liquid/OrchardCoreContrib.PoExtractor.Liquid.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | OrchardCoreContrib.PoExtractor.Liquid 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /src/OrchardCoreContrib.PoExtractor.Razor/MetadataProviders/RazorMetadataProvider.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.CodeAnalysis; 2 | using Microsoft.CodeAnalysis.CSharp; 3 | using Microsoft.CodeAnalysis.CSharp.Syntax; 4 | using System; 5 | using System.IO; 6 | using System.Linq; 7 | 8 | namespace OrchardCoreContrib.PoExtractor.Razor.MetadataProviders; 9 | 10 | /// 11 | /// Provides metadata for Razor .cshtml files. 12 | /// 13 | public class RazorMetadataProvider : IMetadataProvider 14 | { 15 | private static readonly string _razorPageExtension = ".cshtml"; 16 | private static readonly string _razorComponentExtension = ".razor"; 17 | 18 | private string[] _sourceCache; 19 | private string _sourceCachePath; 20 | 21 | private readonly string _basePath; 22 | 23 | /// 24 | /// Creates a new instance of a . 25 | /// 26 | /// The base path. 27 | public RazorMetadataProvider(string basePath) 28 | { 29 | _basePath = basePath; 30 | } 31 | 32 | /// 33 | public string GetContext(SyntaxNode node) 34 | { 35 | ArgumentNullException.ThrowIfNull(node); 36 | 37 | var path = node.SyntaxTree.FilePath.TrimStart(_basePath); 38 | path = RemoveRazorFileExtension(path); 39 | 40 | return path.Replace(Path.DirectorySeparatorChar, '.'); 41 | } 42 | 43 | private static string RemoveRazorFileExtension(string path) 44 | { 45 | return path 46 | .Replace(_razorPageExtension, string.Empty) 47 | .Replace(_razorComponentExtension, string.Empty); 48 | } 49 | 50 | /// 51 | public LocalizableStringLocation GetLocation(SyntaxNode node) 52 | { 53 | ArgumentNullException.ThrowIfNull(node); 54 | 55 | var result = new LocalizableStringLocation 56 | { 57 | SourceFile = node.SyntaxTree.FilePath.TrimStart(_basePath) 58 | }; 59 | 60 | var statement = node 61 | .Ancestors() 62 | .OfType() 63 | .FirstOrDefault(); 64 | 65 | if (statement != null) 66 | { 67 | var lineTriviaSyntax = statement 68 | .DescendantTrivia() 69 | .OfType() 70 | .Where(o => o.IsKind(SyntaxKind.LineDirectiveTrivia) && o.HasStructure) 71 | .FirstOrDefault(); 72 | 73 | if (lineTriviaSyntax.GetStructure() is LineDirectiveTriviaSyntax lineTrivia && lineTrivia.HashToken.Text == "#" && lineTrivia.DirectiveNameToken.Text == "line") 74 | { 75 | if (int.TryParse(lineTrivia.Line.Text, out var lineNumber)) 76 | { 77 | result.SourceFileLine = lineNumber; 78 | result.Comment = GetSourceCodeLine(node.SyntaxTree.FilePath, lineNumber)?.Trim(); 79 | } 80 | } 81 | } 82 | 83 | return result; 84 | } 85 | 86 | private string GetSourceCodeLine(string path, int line) 87 | { 88 | if (_sourceCachePath != path) 89 | { 90 | _sourceCache = null; 91 | _sourceCachePath = null; 92 | 93 | try 94 | { 95 | _sourceCache = File.ReadAllLines(path); 96 | _sourceCachePath = path; 97 | } 98 | catch 99 | { 100 | } 101 | } 102 | 103 | var zeroBasedLineNumber = line - 1; 104 | if (_sourceCache != null && _sourceCache.Length > zeroBasedLineNumber) 105 | { 106 | return _sourceCache[zeroBasedLineNumber]; 107 | } 108 | 109 | return null; 110 | } 111 | } 112 | -------------------------------------------------------------------------------- /src/OrchardCoreContrib.PoExtractor.Razor/OrchardCoreContrib.PoExtractor.Razor.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /src/OrchardCoreContrib.PoExtractor.Razor/RazorPageGeneratorResult.cs: -------------------------------------------------------------------------------- 1 | namespace OrchardCoreContrib.PoExtractor.Razor 2 | { 3 | /// 4 | /// Represents a result for generated razor page. 5 | /// 6 | public class RazorPageGeneratorResult 7 | { 8 | /// 9 | /// Gets or sets the file path. 10 | /// 11 | public string FilePath { get; set; } 12 | 13 | /// 14 | /// Gets or sets the razor enerated code. 15 | /// 16 | public string GeneratedCode { get; set; } 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/OrchardCoreContrib.PoExtractor.Razor/RazorProjectProcessor.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.CodeAnalysis; 2 | using Microsoft.CodeAnalysis.CSharp; 3 | using OrchardCoreContrib.PoExtractor.DotNet; 4 | using OrchardCoreContrib.PoExtractor.DotNet.CS; 5 | using OrchardCoreContrib.PoExtractor.Razor.MetadataProviders; 6 | using System; 7 | 8 | namespace OrchardCoreContrib.PoExtractor.Razor 9 | { 10 | /// 11 | /// Extracts localizable strings from all *.cshtml files in the folder Views under the project path 12 | /// 13 | public class RazorProjectProcessor : CSharpProjectProcessor 14 | { 15 | /// 16 | public override void Process(string path, string basePath, LocalizableStringCollection strings) 17 | { 18 | if (string.IsNullOrEmpty(path)) 19 | { 20 | throw new ArgumentException($"'{nameof(path)}' cannot be null or empty.", nameof(path)); 21 | } 22 | 23 | if (string.IsNullOrEmpty(basePath)) 24 | { 25 | throw new ArgumentException($"'{nameof(basePath)}' cannot be null or empty.", nameof(basePath)); 26 | } 27 | 28 | if (strings is null) 29 | { 30 | throw new ArgumentNullException(nameof(strings)); 31 | } 32 | 33 | var razorMetadataProvider = new RazorMetadataProvider(basePath); 34 | var razorWalker = new ExtractingCodeWalker(new IStringExtractor[] 35 | { 36 | new SingularStringExtractor(razorMetadataProvider), 37 | new PluralStringExtractor(razorMetadataProvider), 38 | new ErrorMessageAnnotationStringExtractor(razorMetadataProvider), 39 | new DisplayAttributeDescriptionStringExtractor(razorMetadataProvider), 40 | new DisplayAttributeNameStringExtractor(razorMetadataProvider), 41 | new DisplayAttributeGroupNameStringExtractor(razorMetadataProvider), 42 | new DisplayAttributeShortNameStringExtractor(razorMetadataProvider) 43 | }, strings); 44 | 45 | var compiledViews = ViewCompiler.CompileViews(path); 46 | 47 | foreach (var view in compiledViews) 48 | { 49 | try 50 | { 51 | var syntaxTree = CSharpSyntaxTree.ParseText(view.GeneratedCode, path: view.FilePath); 52 | 53 | razorWalker.Visit(syntaxTree.GetRoot()); 54 | } 55 | catch 56 | { 57 | Console.WriteLine("Process failed for: {0}", view.FilePath); 58 | } 59 | } 60 | } 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /src/OrchardCoreContrib.PoExtractor.Razor/ViewCompiler.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Razor.Language; 2 | using System; 3 | using System.Collections.Generic; 4 | using System.IO; 5 | using System.Linq; 6 | 7 | namespace OrchardCoreContrib.PoExtractor.Razor 8 | { 9 | /// 10 | /// Represents a utility class to compile razor views. 11 | /// 12 | public static class ViewCompiler 13 | { 14 | /// 15 | /// Complies the views on a given project. 16 | /// 17 | /// The project directory. 18 | public static IEnumerable CompileViews(string projectDirectory) 19 | { 20 | if (string.IsNullOrEmpty(projectDirectory)) 21 | { 22 | throw new ArgumentException($"'{nameof(projectDirectory)}' cannot be null or empty.", nameof(projectDirectory)); 23 | } 24 | 25 | var projectEngine = CreateProjectEngine("OrchardCoreContrib.PoExtractor.GeneratedCode", projectDirectory); 26 | 27 | foreach (var item in projectEngine.FileSystem.EnumerateItems(projectDirectory).OrderBy(rzrProjItem => rzrProjItem.FileName)) 28 | { 29 | yield return GenerateCodeFile(projectEngine, item); 30 | } 31 | } 32 | 33 | private static RazorProjectEngine CreateProjectEngine(string rootNamespace, string projectDirectory) 34 | { 35 | if (string.IsNullOrEmpty(rootNamespace)) 36 | { 37 | throw new ArgumentException($"'{nameof(rootNamespace)}' cannot be null or empty.", nameof(rootNamespace)); 38 | } 39 | 40 | if (string.IsNullOrEmpty(projectDirectory)) 41 | { 42 | throw new ArgumentException($"'{nameof(projectDirectory)}' cannot be null or empty.", nameof(projectDirectory)); 43 | } 44 | 45 | var fileSystem = RazorProjectFileSystem.Create(projectDirectory); 46 | var projectEngine = RazorProjectEngine.Create(RazorConfiguration.Default, fileSystem, builder => 47 | { 48 | 49 | builder 50 | .SetNamespace(rootNamespace) 51 | .ConfigureClass((document, @class) => 52 | { 53 | @class.ClassName = Path.GetFileNameWithoutExtension(document.Source.FilePath); 54 | }); 55 | #if NETSTANDARD2_0 56 | FunctionsDirective.Register(builder); 57 | InheritsDirective.Register(builder); 58 | SectionDirective.Register(builder); 59 | #endif 60 | }); 61 | 62 | return projectEngine; 63 | } 64 | 65 | private static RazorPageGeneratorResult GenerateCodeFile(RazorProjectEngine projectEngine, RazorProjectItem projectItem) 66 | { 67 | var codeDocument = projectEngine.Process(projectItem); 68 | var cSharpDocument = codeDocument.GetCSharpDocument(); 69 | 70 | return new RazorPageGeneratorResult 71 | { 72 | FilePath = projectItem.PhysicalPath, 73 | GeneratedCode = cSharpDocument.GeneratedCode, 74 | }; 75 | } 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /src/OrchardCoreContrib.PoExtractor/IgnoredProject.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | 3 | namespace OrchardCoreContrib.PoExtractor; 4 | 5 | public class IgnoredProject 6 | { 7 | public static readonly string Docs = "src\\docs"; 8 | 9 | public static readonly string Cms = "src\\OrchardCore.Cms.Web"; 10 | 11 | public static readonly string Mvc = "src\\OrchardCore.Mvc.Web"; 12 | 13 | public static readonly string Templates = "src\\Templates"; 14 | 15 | public static readonly string Test = "test"; 16 | 17 | private static readonly List _ignoredProjects = [ Docs, Cms, Mvc, Templates ]; 18 | 19 | public static void Add(string project) => _ignoredProjects.Add(project); 20 | 21 | public static IEnumerable ToList() => _ignoredProjects; 22 | } 23 | -------------------------------------------------------------------------------- /src/OrchardCoreContrib.PoExtractor/Language.cs: -------------------------------------------------------------------------------- 1 | namespace OrchardCoreContrib.PoExtractor; 2 | 3 | public class Language 4 | { 5 | public static readonly string CSharp = "C#"; 6 | 7 | public static readonly string VisualBasic = "VB"; 8 | } 9 | -------------------------------------------------------------------------------- /src/OrchardCoreContrib.PoExtractor/OrchardCoreContrib.PoExtractor.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | Exe 4 | true 5 | OrchardCoreContrib.PoExtractor 6 | extractpo 7 | true 8 | 1.2.0 9 | The Orchard Core Contrib Team 10 | 11 | .NET Core global tool for extracting translatable strings from the OrchardCore source files. 12 | MIT 13 | https://github.com/OrchardCoreContrib/OrchardCoreContrib.PoExtractor 14 | https://github.com/OrchardCoreContrib/OrchardCoreContrib.PoExtractor 15 | git 16 | true 17 | Orchard Core, Orchard Core Contrib, Localization, PO 18 | https://github.com/OrchardCoreContrib/OrchardCoreContrib.PoExtractor/releases 19 | OrchardCoreContrib.PoExtractor 20 | icon.png 21 | Orchard Core Contrib Portable Object Extraction Tool 22 | true 23 | 2019 Orchard Core Contrib 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | -------------------------------------------------------------------------------- /src/OrchardCoreContrib.PoExtractor/PluginHelper.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.CodeAnalysis.CSharp.Scripting; 2 | using Microsoft.CodeAnalysis.Scripting; 3 | 4 | namespace OrchardCoreContrib.PoExtractor; 5 | 6 | public static class PluginHelper 7 | { 8 | public static async Task ProcessPluginsAsync( 9 | IEnumerable plugins, 10 | List projectProcessors, 11 | List projectFiles) 12 | { 13 | var sharedOptions = ScriptOptions.Default.AddReferences(typeof(Program).Assembly); 14 | 15 | foreach (var plugin in plugins) 16 | { 17 | string code; 18 | ScriptOptions options; 19 | 20 | if (plugin.StartsWith("https://", StringComparison.OrdinalIgnoreCase)) 21 | { 22 | code = await new HttpClient().GetStringAsync(plugin); 23 | options = sharedOptions.WithFilePath(Path.Join( 24 | Environment.CurrentDirectory, 25 | Path.GetFileName(new Uri(plugin).AbsolutePath))); 26 | } 27 | else 28 | { 29 | code = await File.ReadAllTextAsync(plugin); 30 | options = sharedOptions.WithFilePath(Path.GetFullPath(plugin)); 31 | } 32 | 33 | await CSharpScript.EvaluateAsync(code, options, new PluginContext(projectProcessors, projectFiles)); 34 | } 35 | } 36 | 37 | public record PluginContext(List ProjectProcessors, List ProjectFiles); 38 | } 39 | -------------------------------------------------------------------------------- /src/OrchardCoreContrib.PoExtractor/PoWriter.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.IO; 4 | using System.Text; 5 | 6 | namespace OrchardCoreContrib.PoExtractor; 7 | 8 | /// 9 | /// Writes objects in the Portable Object format to a stream 10 | /// 11 | public class PoWriter : IDisposable 12 | { 13 | public const string PortaleObjectTemplateExtension = ".pot"; 14 | 15 | private readonly StreamWriter _writer; 16 | 17 | /// 18 | /// Creates a new instance of the , that writes records to the file 19 | /// 20 | /// the path to the file 21 | /// This function creates a new file or overwrites the existing file, if it already exists 22 | public PoWriter(string path) 23 | { 24 | _writer = new StreamWriter(File.Create(path)); 25 | } 26 | 27 | /// 28 | /// Creates a new instance of the , that writes records to the stream 29 | /// 30 | /// 31 | public PoWriter(Stream stream) 32 | { 33 | _writer = new StreamWriter(stream); 34 | } 35 | 36 | /// 37 | /// Writes a object to the output 38 | /// 39 | /// the object to write 40 | public void WriteRecord(LocalizableString record) 41 | { 42 | foreach (var location in record.Locations) 43 | { 44 | _writer.WriteLine($"#: {location.SourceFile}:{location.SourceFileLine}"); 45 | 46 | if (!string.IsNullOrEmpty(location.Comment)) 47 | { 48 | _writer.WriteLine($"#. {location.Comment}"); 49 | } 50 | } 51 | 52 | if (!string.IsNullOrEmpty(record.Context)) 53 | { 54 | _writer.WriteLine($"msgctxt \"{Escape(record.Context)}\""); 55 | } 56 | 57 | _writer.WriteLine($"msgid \"{Escape(record.Text)}\""); 58 | 59 | if (string.IsNullOrEmpty(record.TextPlural)) 60 | { 61 | _writer.WriteLine($"msgstr \"\""); 62 | } 63 | else 64 | { 65 | _writer.WriteLine($"msgid_plural \"{Escape(record.TextPlural)}\""); 66 | _writer.WriteLine($"msgstr[0] \"\""); 67 | } 68 | 69 | 70 | _writer.WriteLine(); 71 | } 72 | 73 | /// 74 | /// Writes a collection of objects to the output 75 | /// 76 | /// the collection to write 77 | public void WriteRecord(IEnumerable records) 78 | { 79 | foreach (var record in records) 80 | { 81 | WriteRecord(record); 82 | } 83 | } 84 | 85 | 86 | /// 87 | public void Dispose() 88 | { 89 | Dispose(true); 90 | GC.SuppressFinalize(this); 91 | } 92 | 93 | protected virtual void Dispose(bool disposing) 94 | { 95 | if (disposing) 96 | { 97 | _writer.Close(); 98 | _writer.Dispose(); 99 | } 100 | } 101 | 102 | private static string Escape(string text) 103 | { 104 | var sb = new StringBuilder(text); 105 | sb.Replace("\\", "\\\\"); // \ -> \\ 106 | sb.Replace("\"", "\\\""); // " -> \" 107 | sb.Replace("\r", "\\r"); 108 | sb.Replace("\n", "\\n"); 109 | 110 | return sb.ToString(); 111 | } 112 | } 113 | -------------------------------------------------------------------------------- /src/OrchardCoreContrib.PoExtractor/Program.cs: -------------------------------------------------------------------------------- 1 | using System.ComponentModel.DataAnnotations; 2 | using McMaster.Extensions.CommandLineUtils; 3 | using OrchardCore.Modules; 4 | using OrchardCoreContrib.PoExtractor.DotNet; 5 | using OrchardCoreContrib.PoExtractor.DotNet.CS; 6 | using OrchardCoreContrib.PoExtractor.DotNet.VB; 7 | using OrchardCoreContrib.PoExtractor.Liquid; 8 | using OrchardCoreContrib.PoExtractor.Razor; 9 | 10 | namespace OrchardCoreContrib.PoExtractor; 11 | 12 | public class Program 13 | { 14 | public static void Main(string[] args) 15 | { 16 | var app = new CommandLineApplication(); 17 | 18 | app.HelpOption(); 19 | 20 | // Arguments 21 | var inputPath = app.Argument("Input Path", "The path to the input directory, all projects at the the path will be processed.") 22 | .IsRequired(); 23 | var outputPath = app.Argument("Output Path", "The path to a directory where POT files will be generated.") 24 | .IsRequired(); 25 | 26 | // Options 27 | var language = app.Option("-l|--language ", "Specifies the code language to extracts translatable strings from.", CommandOptionType.SingleValue, option => 28 | { 29 | option.Accepts(cfg => cfg.Values("C#", "VB")); 30 | option.DefaultValue = "C#"; 31 | }); 32 | var template = app.Option("-t|--template