diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml new file mode 100644 index 0000000..65b45f1 --- /dev/null +++ b/.github/FUNDING.yml @@ -0,0 +1,4 @@ +# These are supported funding model platforms + +github: markbates +patreon: buffalo diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index b1a6d0a..68804c1 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -2,25 +2,21 @@ name: Tests on: [push] jobs: - - tests-off: - name: (GOPATH) ${{matrix.go-version}} ${{matrix.os}} + tests-on: + name: ${{matrix.go-version}} ${{matrix.os}} runs-on: ${{ matrix.os }} strategy: matrix: go-version: [1.12.x, 1.13.x] os: [macos-latest, windows-latest, ubuntu-latest] - env: - GO111MODULE: on steps: - - name: Checkout code - uses: actions/checkout@v1 - with: - fetch-depth: 1 - path: src/github.com/${{ github.repository }} - - name: Test - env: - GOPATH: ${{runner.workspace}} - run: | - go mod tidy -v - go test -cover -race ./... \ No newline at end of file + - name: Checkout Code + uses: actions/checkout@v1 + with: + fetch-depth: 1 + - name: Test + run: | + go mod tidy -v + go test -race ./... + + diff --git a/.gitignore b/.gitignore index 0a95508..d70f9c3 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,3 @@ .idea/ -.vscode/ \ No newline at end of file +.vscode/ +testdata/output diff --git a/LICENSE.txt b/LICENSE similarity index 100% rename from LICENSE.txt rename to LICENSE diff --git a/go.mod b/go.mod index f4e1ca0..2a0703b 100644 --- a/go.mod +++ b/go.mod @@ -3,9 +3,10 @@ module github.com/gobuffalo/mw-i18n go 1.13 require ( - github.com/gobuffalo/buffalo v0.15.3 + github.com/gobuffalo/buffalo v0.15.4 github.com/gobuffalo/httptest v1.4.0 github.com/gobuffalo/packd v0.3.0 - github.com/nicksnyder/go-i18n v1.10.0 + github.com/pelletier/go-toml v1.6.0 github.com/stretchr/testify v1.4.0 + gopkg.in/yaml.v2 v2.2.8 ) diff --git a/go.sum b/go.sum index d9d12eb..d1f3a5b 100644 --- a/go.sum +++ b/go.sum @@ -47,8 +47,9 @@ github.com/dgryski/go-sip13 v0.0.0-20181026042036-e10d5fee7954/go.mod h1:vAd38F8 github.com/dustin/go-humanize v0.0.0-20180713052910-9f541cc9db5d/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk= github.com/dustin/go-humanize v1.0.0 h1:VSnTsYCnlFHaM2/igO1h6X3HA71jcobQuxemgkq4zYo= github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk= -github.com/fatih/color v1.7.0 h1:DkWD4oS2D8LGGgTQ6IvwJJXSL5Vp2ffcQg58nFV38Ys= github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= +github.com/fatih/color v1.9.0 h1:8xPHl4/q1VyqGIPif1F+1V3Y3lSmrq01EabUW3CoW5s= +github.com/fatih/color v1.9.0/go.mod h1:eQcE1qtQxscV5RaZvpXrrb8Drkc3/DdQ+uUYCNjL+zU= github.com/fatih/structs v1.0.0/go.mod h1:9NiDSp5zOcgEDl+j00MP/WkGVPOlPRLejGD8Ga6PJ7M= github.com/fatih/structs v1.1.0 h1:Q7juDM0QtcnhCpeyLGQKyg4TOIghuNXrkL32pHAUMxo= github.com/fatih/structs v1.1.0/go.mod h1:9NiDSp5zOcgEDl+j00MP/WkGVPOlPRLejGD8Ga6PJ7M= @@ -86,8 +87,8 @@ github.com/gobuffalo/buffalo v0.14.6/go.mod h1:71Un+T2JGgwXLjBqYFdGSooz/OUjw15BJ github.com/gobuffalo/buffalo v0.14.9/go.mod h1:M8XWw+Rcotn7C4NYpCEGBg3yX+O1TeD1pBfmiILhgHw= github.com/gobuffalo/buffalo v0.14.10/go.mod h1:49A7/JYlsCyTkVHtvKl91w6rG35ZiywwjWMVC1zKWsQ= github.com/gobuffalo/buffalo v0.14.11/go.mod h1:RO5OwOJQjv6/TukzszV5ELA54lg84D1kZwla6oAkTlo= -github.com/gobuffalo/buffalo v0.15.3 h1:QLBWxNr/pWULR2G3+bvU957FDv9YgqDtejaMpBZ5qR0= -github.com/gobuffalo/buffalo v0.15.3/go.mod h1:m5pcCKN9S+e2d5Ns6Ezc7D6SCycNwfCSocGpd0k5TNA= +github.com/gobuffalo/buffalo v0.15.4 h1:/+m4a7EFckj6JQTMe6MgyCmmmx254EMN/9C5KPwxWlI= +github.com/gobuffalo/buffalo v0.15.4/go.mod h1:KAEep39iQPGw18Wa71KKkDCCKdMlEwZ+21tab42MFLY= github.com/gobuffalo/buffalo-docker v1.0.5/go.mod h1:NZ3+21WIdqOUlOlM2onCt7cwojYp4Qwlsngoolw8zlE= github.com/gobuffalo/buffalo-docker v1.0.6/go.mod h1:UlqKHJD8CQvyIb+pFq+m/JQW2w2mXuhxsaKaTj1X1XI= github.com/gobuffalo/buffalo-docker v1.0.7/go.mod h1:BdB8AhcmjwR6Lo3rDPMzyh/+eNjYnZ1TAO0eZeLkhig= @@ -139,6 +140,7 @@ github.com/gobuffalo/clara v0.4.1/go.mod h1:3QgAPqYgPqAzhfGbNLlp4UztaZRi2SOg+ZrZ github.com/gobuffalo/clara v0.6.0/go.mod h1:RKZxkcH80pLykRi2hLkoxGMxA8T06Dc9fN/pFvutMFY= github.com/gobuffalo/clara v0.7.0/go.mod h1:pen7ZMmnuYUYVF/3BbnvidYVAbMEfkyO4O+Tc+FKICU= github.com/gobuffalo/clara v0.9.1/go.mod h1:OQ3HmSqLQJHaMmKhuTkmBCvBLL4BhgjweNpywRGulWo= +github.com/gobuffalo/clara v0.10.1/go.mod h1:XcB5V5Vx5wuq/cXZOV0kAPetk7CYxSLFG5YvpyTxzxI= github.com/gobuffalo/depgen v0.0.0-20190219190223-ba8c93fa0c2c/go.mod h1:CE/HUV4vDCXtJayRf6WoMWgezb1yH4QHg8GNK8FL0JI= github.com/gobuffalo/depgen v0.0.0-20190315122043-8442b3fa16db/go.mod h1:3STtPUQYuzV0gBVOY3vy6CfMm/ljR4pABfrTeHNLHUY= github.com/gobuffalo/depgen v0.0.0-20190315124901-e02f65b90669/go.mod h1:yTQe8xo5pGIDOApkeO95DjePS4ZOSSSx+ItkqJHxUG4= @@ -209,8 +211,8 @@ github.com/gobuffalo/flect v0.1.1/go.mod h1:8JCgGVbRjJhVgD6399mQr4fx5rRfGKVzFjbj github.com/gobuffalo/flect v0.1.3/go.mod h1:8JCgGVbRjJhVgD6399mQr4fx5rRfGKVzFjbj6RE/9UI= github.com/gobuffalo/flect v0.1.5/go.mod h1:W3K3X9ksuZfir8f/LrfVtWmCDQFfayuylOJ7sz/Fj80= github.com/gobuffalo/flect v0.1.6/go.mod h1:W3K3X9ksuZfir8f/LrfVtWmCDQFfayuylOJ7sz/Fj80= -github.com/gobuffalo/flect v0.1.7 h1:qQqM2eGdM6tJX8yHKYBM0wVHBLjUT7Qs6uk5jnAhOwI= -github.com/gobuffalo/flect v0.1.7/go.mod h1:W3K3X9ksuZfir8f/LrfVtWmCDQFfayuylOJ7sz/Fj80= +github.com/gobuffalo/flect v0.2.0 h1:EWCvMGGxOjsgwlWaP+f4+Hh6yrrte7JeFL2S6b+0hdM= +github.com/gobuffalo/flect v0.2.0/go.mod h1:W3K3X9ksuZfir8f/LrfVtWmCDQFfayuylOJ7sz/Fj80= github.com/gobuffalo/genny v0.0.0-20180924032338-7af3a40f2252/go.mod h1:tUTQOogrr7tAQnhajMSH6rv1BVev34H2sa1xNHMy94g= github.com/gobuffalo/genny v0.0.0-20181003150629-3786a0744c5d/go.mod h1:WAd8HmjMVrnkAZbmfgH5dLBUchsZfqzp/WS5sQz+uTM= github.com/gobuffalo/genny v0.0.0-20181005145118-318a41a134cc/go.mod h1:WAd8HmjMVrnkAZbmfgH5dLBUchsZfqzp/WS5sQz+uTM= @@ -252,8 +254,9 @@ github.com/gobuffalo/genny v0.1.1/go.mod h1:5TExbEyY48pfunL4QSXxlDOmdsD44RRq4mVZ github.com/gobuffalo/genny v0.2.0/go.mod h1:rWs4Z12d1Zbf19rlsn0nurr75KqhYp52EAGGxTbBhNk= github.com/gobuffalo/genny v0.3.0/go.mod h1:ywJ2CoXrTZj7rbS8HTbzv7uybnLKlsNSBhEQ+yFI3E8= github.com/gobuffalo/genny v0.4.0/go.mod h1:Kdo8wsw5zmooVvEfMkfv4JI9Ogz/PMvBNvl133soylI= -github.com/gobuffalo/genny v0.4.1 h1:ylgRyFoVGtfq92Ziq0kyi0Sdwh//pqWEwg+vD3eK1ZA= github.com/gobuffalo/genny v0.4.1/go.mod h1:dpded+KBgICFciAb+6R5Lo+1VxzofjqHgKqFYIL8M7U= +github.com/gobuffalo/genny v0.6.0 h1:d7c6d66ZrTHHty01hDX1/TcTWvAJQxRZl885KWX5kHY= +github.com/gobuffalo/genny v0.6.0/go.mod h1:Vigx9VDiNscYpa/LwrURqGXLSIbzTfapt9+K6gF1kTA= github.com/gobuffalo/gitgen v0.0.0-20190219185555-91c2c5f0aad5/go.mod h1:ZzGIrxBvCJEluaU4i3CN0GFlu1Qmb3yK8ziV02evJ1E= github.com/gobuffalo/gitgen v0.0.0-20190315122116-cc086187d211/go.mod h1:vEHJk/E9DmhejeLeNt7UVvlSGv3ziL+djtTr3yyzcOw= github.com/gobuffalo/github_flavored_markdown v1.0.4/go.mod h1:uRowCdK+q8d/RF0Kt3/DSalaIXbb0De/dmTqMQdkQ4I= @@ -272,10 +275,14 @@ github.com/gobuffalo/helpers v0.0.0-20190506214229-8e6f634af7c3/go.mod h1:HlNpmw github.com/gobuffalo/helpers v0.2.1/go.mod h1:5UhA1EfGvyPZfzo9PqhKkSgmLolaTpnWYDbqCJcmiAE= github.com/gobuffalo/helpers v0.2.2/go.mod h1:xYbzUdCUpVzLwLnqV8HIjT6hmG0Cs7YIBCJkNM597jw= github.com/gobuffalo/helpers v0.2.4/go.mod h1:NX7v27yxPDOPTgUFYmJ5ow37EbxdoLraucOGvMNawyk= -github.com/gobuffalo/helpers v0.4.0 h1:DR/iYihrVCXv1cYeIGSK3EZz2CljO+DqDLQPWZAod9c= github.com/gobuffalo/helpers v0.4.0/go.mod h1:2q/ZnVxCehM4/y1bNz3+wXsvWvWUY+iTUr7mPC6QqGQ= +github.com/gobuffalo/helpers v0.5.0/go.mod h1:stpgxJ2C7T99NLyAxGUnYMM2zAtBk5NKQR0SIbd05j4= +github.com/gobuffalo/helpers v0.6.0 h1:CL1xOSGeKCaKD1IUpo4RfrkDU83kmkMG4H3dXAS7dw0= +github.com/gobuffalo/helpers v0.6.0/go.mod h1:pncVrer7x/KRvnL5aJABLAuT/RhKRR9klL6dkUOhyv8= github.com/gobuffalo/here v0.2.3/go.mod h1:2a6G14FaAKOGJMK/5UNa4Og/+iyFS5cq3MnlvFR7YDk= github.com/gobuffalo/here v0.4.0/go.mod h1:bTNk/uKZgycuB358iR0D32dI9kHBClBGpXjW2HVHkNo= +github.com/gobuffalo/here v0.5.1/go.mod h1:wAG085dHOYqUpf+Ap+WOdrPTp5IYcDAs/x7PLa8Y5fM= +github.com/gobuffalo/here v0.6.0/go.mod h1:wAG085dHOYqUpf+Ap+WOdrPTp5IYcDAs/x7PLa8Y5fM= github.com/gobuffalo/httptest v1.0.2/go.mod h1:7T1IbSrg60ankme0aDLVnEY0h056g9M1/ZvpVThtB7E= github.com/gobuffalo/httptest v1.0.3/go.mod h1:7T1IbSrg60ankme0aDLVnEY0h056g9M1/ZvpVThtB7E= github.com/gobuffalo/httptest v1.0.4/go.mod h1:7T1IbSrg60ankme0aDLVnEY0h056g9M1/ZvpVThtB7E= @@ -458,8 +465,9 @@ github.com/gobuffalo/pop v4.11.2+incompatible/go.mod h1:DwBz3SD5SsHpTZiTubcsFWcV github.com/gobuffalo/pop v4.11.3+incompatible/go.mod h1:DwBz3SD5SsHpTZiTubcsFWcVDpJWGsxjVjMPnkiThWg= github.com/gobuffalo/pop v4.12.0+incompatible/go.mod h1:DwBz3SD5SsHpTZiTubcsFWcVDpJWGsxjVjMPnkiThWg= github.com/gobuffalo/pop v4.12.1+incompatible/go.mod h1:DwBz3SD5SsHpTZiTubcsFWcVDpJWGsxjVjMPnkiThWg= -github.com/gobuffalo/pop v4.12.2+incompatible h1:WFHMzzHbVLulZnEium1VlYRnWkzHz39FzVLov6rZdDI= github.com/gobuffalo/pop v4.12.2+incompatible/go.mod h1:DwBz3SD5SsHpTZiTubcsFWcVDpJWGsxjVjMPnkiThWg= +github.com/gobuffalo/pop v4.13.1+incompatible h1:AhbqPxNOBN/DBb2DBaiBqzOXIBQXxEYzngHHJ+ytP4g= +github.com/gobuffalo/pop v4.13.1+incompatible/go.mod h1:DwBz3SD5SsHpTZiTubcsFWcVDpJWGsxjVjMPnkiThWg= github.com/gobuffalo/release v1.0.35/go.mod h1:VtHFAKs61vO3wboCec5xr9JPTjYyWYcvaM3lclkc4x4= github.com/gobuffalo/release v1.0.38/go.mod h1:VtHFAKs61vO3wboCec5xr9JPTjYyWYcvaM3lclkc4x4= github.com/gobuffalo/release v1.0.42/go.mod h1:RPs7EtafH4oylgetOJpGP0yCZZUiO4vqHfTHJjSdpug= @@ -480,7 +488,6 @@ github.com/gobuffalo/release v1.4.0/go.mod h1:f4uUPnD9dxrWxVy9yy0k/mvDf3EzhFtf7/ github.com/gobuffalo/release v1.7.0/go.mod h1:xH2NjAueVSY89XgC4qx24ojEQ4zQ9XCGVs5eXwJTkEs= github.com/gobuffalo/release v1.8.3/go.mod h1:gCk/x5WD+aIGkPodO4CuLxdnhYn9Jgp7yFYxntK/8mk= github.com/gobuffalo/release v1.13.4/go.mod h1:5Cc4TSNxP4QFV2ZUYcgPiBBV7YyRomHecGTQuuy26G4= -github.com/gobuffalo/release v1.15.0/go.mod h1:LwotdbqCKcpQ+bKAvk1vvXLZhM8ftBTI5AlE7mx/1H8= github.com/gobuffalo/shoulders v1.0.1/go.mod h1:V33CcVmaQ4gRUmHKwq1fiTXuf8Gp/qjQBUL5tHPmvbA= github.com/gobuffalo/shoulders v1.0.3/go.mod h1:LqMcHhKRuBPMAYElqOe3POHiZ1x7Ry0BE8ZZ84Bx+k4= github.com/gobuffalo/shoulders v1.0.4/go.mod h1:LqMcHhKRuBPMAYElqOe3POHiZ1x7Ry0BE8ZZ84Bx+k4= @@ -497,12 +504,16 @@ github.com/gobuffalo/tags v2.1.0+incompatible/go.mod h1:9XmhOkyaB7UzvuY4UoZO4s67 github.com/gobuffalo/tags v2.1.5+incompatible/go.mod h1:9XmhOkyaB7UzvuY4UoZO4s67q8/xRMVJEaakauVQYeY= github.com/gobuffalo/tags v2.1.7+incompatible h1:GUxxh34f9SI4U0Pj3ZqvopO9SlzuqSf+g4ZGSPSszt4= github.com/gobuffalo/tags v2.1.7+incompatible/go.mod h1:9XmhOkyaB7UzvuY4UoZO4s67q8/xRMVJEaakauVQYeY= +github.com/gobuffalo/tags/v3 v3.0.2 h1:gxE6c6fA5radwQeg59aPIeYgCG8YA8AZd3Oh6fh5UXA= +github.com/gobuffalo/tags/v3 v3.0.2/go.mod h1:ZQeN6TCTiwAFnS0dNcbDtSgZDwNKSpqajvVtt6mlYpA= github.com/gobuffalo/uuid v2.0.3+incompatible/go.mod h1:ErhIzkRhm0FtRuiE/PeORqcw4cVi1RtSpnwYrxuvkfE= github.com/gobuffalo/uuid v2.0.4+incompatible/go.mod h1:ErhIzkRhm0FtRuiE/PeORqcw4cVi1RtSpnwYrxuvkfE= github.com/gobuffalo/uuid v2.0.5+incompatible h1:c5uWRuEnYggYCrT9AJm0U2v1QTG7OVDAvxhj8tIV5Gc= github.com/gobuffalo/uuid v2.0.5+incompatible/go.mod h1:ErhIzkRhm0FtRuiE/PeORqcw4cVi1RtSpnwYrxuvkfE= github.com/gobuffalo/validate v2.0.3+incompatible h1:6f4JCEz11Zi6iIlexMv7Jz10RBPvgI795AOaubtCwTE= github.com/gobuffalo/validate v2.0.3+incompatible/go.mod h1:N+EtDe0J8252BgfzQUChBgfd6L93m9weay53EWFVsMM= +github.com/gobuffalo/validate/v3 v3.0.0 h1:dF7Bg8NMF9Zv8bZvUMXYJXxZdj+eSZ8z/lGM7/jVFUE= +github.com/gobuffalo/validate/v3 v3.0.0/go.mod h1:HFpjq+AIiA2RHoQnQVTFKF/ZpUPXwyw82LgyDPxQ9r0= github.com/gobuffalo/x v0.0.0-20181003152136-452098b06085/go.mod h1:WevpGD+5YOreDJznWevcn8NTmQEW5STSBgIkpkjzqXc= github.com/gobuffalo/x v0.0.0-20181007152206-913e47c59ca7/go.mod h1:9rDPXaB3kXdKWzMc4odGQQdG2e2DIEmANy5aSJ9yesY= github.com/gobuffalo/x v0.0.0-20181025165825-f204f550da9d/go.mod h1:Qh2Pb/Ak1Ko2mzHlGPigrnxkhO4WTTCI1jJM58sbgtE= @@ -529,6 +540,7 @@ github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Z github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-github v17.0.0+incompatible/go.mod h1:zLgOLi98H3fifZn+44m+umXrS52loVEgC2AApnigrVQ= github.com/google/go-querystring v1.0.0/go.mod h1:odCYkC5MyYFN7vkCjXpyrEuKhc/BUO6wN/zVPAxq5ck= github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= @@ -604,6 +616,7 @@ github.com/joho/godotenv v1.2.0/go.mod h1:7hK45KPybAkOC6peb+G5yklZfMxEjkZhHbwpqx github.com/joho/godotenv v1.3.0 h1:Zjp+RcGpHhGlrMbJzXTrZZPrWj+1vfm90La1wgB6Bhc= github.com/joho/godotenv v1.3.0/go.mod h1:7hK45KPybAkOC6peb+G5yklZfMxEjkZhHbwpqxOKXbg= github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo= +github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU= github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w= github.com/karrick/godirwalk v1.7.5/go.mod h1:2c9FRhkDxdIbgkOnCEvnSWs71Bhugbl46shStcFDJ34= github.com/karrick/godirwalk v1.7.7/go.mod h1:2c9FRhkDxdIbgkOnCEvnSWs71Bhugbl46shStcFDJ34= @@ -612,8 +625,8 @@ github.com/karrick/godirwalk v1.8.0/go.mod h1:H5KPZjojv4lE+QYImBI8xVtrBRgYrIVsaR github.com/karrick/godirwalk v1.10.3/go.mod h1:RoGL9dQei4vP9ilrpETWE8CLOZ1kiN0LhBygSwrAsHA= github.com/karrick/godirwalk v1.10.12/go.mod h1:RoGL9dQei4vP9ilrpETWE8CLOZ1kiN0LhBygSwrAsHA= github.com/karrick/godirwalk v1.12.0/go.mod h1:H5KPZjojv4lE+QYImBI8xVtrBRgYrIVsaRPx4tDPEn4= -github.com/karrick/godirwalk v1.13.0 h1:GJq8GHQEAPsjwqfGhLNXBO5P0dS2HYdDRVWe+P4E/EQ= -github.com/karrick/godirwalk v1.13.0/go.mod h1:H5KPZjojv4lE+QYImBI8xVtrBRgYrIVsaRPx4tDPEn4= +github.com/karrick/godirwalk v1.14.1 h1:eFeFrvfLc79AXZ5782mZsVx6T41uxVORlddagiORs4E= +github.com/karrick/godirwalk v1.14.1/go.mod h1:H5KPZjojv4lE+QYImBI8xVtrBRgYrIVsaRPx4tDPEn4= github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 h1:Z9n2FFNUXsshfwJMBgNA0RU6/i7WVaAegv3PtuIHPMs= github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51/go.mod h1:CzGEWj7cYgsdH8dAjBGEr58BoE7ScuLd+fwFZ44+/x8= github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q= @@ -666,8 +679,9 @@ github.com/markbates/refresh v1.4.11/go.mod h1:awpJuyo4zgexB/JaHfmBX0sRdvOjo2dXw github.com/markbates/refresh v1.5.0/go.mod h1:ZYMLkxV+x7wXQ2Xd7bXAPyF0EXiEWAMfiy/4URYb1+M= github.com/markbates/refresh v1.6.0/go.mod h1:p8jWGABFUaFf/cSw0pxbo0MQVujiz5NTQ0bmCHLC4ac= github.com/markbates/refresh v1.7.1/go.mod h1:hcGVJc3m5EeskliwSVJxcTHzUtMz2h8gBtCS0V94CgE= -github.com/markbates/refresh v1.8.0 h1:ELMS9kKyO/H6cJrqFo6qCyE0cRx2JeHWC9yusDkVeM8= github.com/markbates/refresh v1.8.0/go.mod h1:ppl0l94oz3OKBAx3MV65vCDWPo51JQnypdtFUmps1NM= +github.com/markbates/refresh v1.10.0 h1:xn+ZSDPED3SvQJDnHkX3vLhFkEVKGWBKWkPKiLYGC4Q= +github.com/markbates/refresh v1.10.0/go.mod h1:txAFIPNbphfNCZELWAQ440wIKnmZKRX64TBdKiAMWfg= github.com/markbates/safe v1.0.0/go.mod h1:nAqgmRi7cY2nqMc92/bSEeQA+R4OheNU2T1kNSCBdG0= github.com/markbates/safe v1.0.1 h1:yjZkbvRM6IzKj9tlu/zMJLS0n/V351OZWRnF3QfaUxI= github.com/markbates/safe v1.0.1/go.mod h1:nAqgmRi7cY2nqMc92/bSEeQA+R4OheNU2T1kNSCBdG0= @@ -677,14 +691,17 @@ github.com/markbates/willie v1.0.9/go.mod h1:fsrFVWl91+gXpx/6dv715j7i11fYPfZ9ZGf github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU= github.com/mattn/go-colorable v0.1.0/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU= github.com/mattn/go-colorable v0.1.1/go.mod h1:FuOcm+DKB9mbwrcAfNl7/TZVBZ6rcnceauSikq3lYCQ= -github.com/mattn/go-colorable v0.1.2 h1:/bC9yWikZXAL9uJdulbSfyVNIR3n3trXl+v8+1sx8mU= github.com/mattn/go-colorable v0.1.2/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE= +github.com/mattn/go-colorable v0.1.4 h1:snbPLB8fVfU9iwbbo30TPtbLRzwWu6aJS6Xh4eaaviA= +github.com/mattn/go-colorable v0.1.4/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE= github.com/mattn/go-isatty v0.0.4/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= github.com/mattn/go-isatty v0.0.5/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= github.com/mattn/go-isatty v0.0.7/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= -github.com/mattn/go-isatty v0.0.9 h1:d5US/mDsogSGW37IV293h//ZFaeajb69h+EHFsv2xGg= github.com/mattn/go-isatty v0.0.9/go.mod h1:YNRxwqDuOph6SZLI9vUUz6OYw3QyUt7WiY2yME+cCiQ= +github.com/mattn/go-isatty v0.0.10/go.mod h1:qgIWMr58cqv1PHHyhnkY9lrL7etaEgOFcMEpPG5Rm84= +github.com/mattn/go-isatty v0.0.11 h1:FxPOTFNqGkuDUGi3H/qkUbQO4ZiBa2brKq5r0l8TGeM= +github.com/mattn/go-isatty v0.0.11/go.mod h1:PhnuNfih5lzO57/f3n+odYbM4JtupLOxQOAqxQCu2WE= github.com/mattn/go-sqlite3 v1.9.0/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc= github.com/mattn/go-sqlite3 v1.10.0 h1:jbhqpg7tQe4SupckyijYiy0mJJ/pRyHvXf7JdWK860o= github.com/mattn/go-sqlite3 v1.10.0/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc= @@ -700,8 +717,9 @@ github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh github.com/monoculum/formam v0.0.0-20180901015400-4e68be1d79ba/go.mod h1:RKgILGEJq24YyJ2ban8EO0RUVSJlF1pGsEvoLEACr/Q= github.com/monoculum/formam v0.0.0-20190307031628-bc555adff0cd/go.mod h1:JKa2av1XVkGjhxdLS59nDoXa2JpmIHpnURWNbzCtXtc= github.com/monoculum/formam v0.0.0-20190730134247-0612307a4099/go.mod h1:JKa2av1XVkGjhxdLS59nDoXa2JpmIHpnURWNbzCtXtc= -github.com/monoculum/formam v0.0.0-20190830100315-7ff9597b1407 h1:ZU5O9BawmEx9Mu1lxn9NLIwO9DrqRfjE+HWKU+e9GKQ= github.com/monoculum/formam v0.0.0-20190830100315-7ff9597b1407/go.mod h1:JKa2av1XVkGjhxdLS59nDoXa2JpmIHpnURWNbzCtXtc= +github.com/monoculum/formam v0.0.0-20191229172733-952f0766a724 h1:qlTmDrFZLQIGXnd1JE58dqyLnKyIJjR9WBeDQcT3O8w= +github.com/monoculum/formam v0.0.0-20191229172733-952f0766a724/go.mod h1:JKa2av1XVkGjhxdLS59nDoXa2JpmIHpnURWNbzCtXtc= github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= github.com/neelance/astrewrite v0.0.0-20160511093645-99348263ae86/go.mod h1:kHJEU3ofeGjhHklVoIGuVj85JJwZ6kWPaJwCIxgnFmo= github.com/neelance/sourcemap v0.0.0-20151028013722-8c68805598ab/go.mod h1:Qr6/a/Q4r9LP1IltGz7tA7iOK1WonHEYhu1HRBA7ZiM= @@ -712,18 +730,22 @@ github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+W github.com/onsi/ginkgo v1.7.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= github.com/onsi/ginkgo v1.8.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= github.com/onsi/ginkgo v1.9.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= -github.com/onsi/ginkgo v1.10.1 h1:q/mM8GF/n0shIN8SaAZ0V+jnLPzen6WIVZdiwrRlMlo= github.com/onsi/ginkgo v1.10.1/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= +github.com/onsi/ginkgo v1.10.3 h1:OoxbjfXVZyod1fmWYhI7SEyaD8B00ynP3T+D5GiyHOY= +github.com/onsi/ginkgo v1.10.3/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= github.com/onsi/gomega v1.4.1/go.mod h1:C1qb7wdrVGGVU+Z6iS04AVkA3Q65CEZX59MT0QO5uiA= github.com/onsi/gomega v1.4.2/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= github.com/onsi/gomega v1.4.3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= github.com/onsi/gomega v1.5.0/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= github.com/onsi/gomega v1.6.0/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= -github.com/onsi/gomega v1.7.0 h1:XPnZz8VVBHjVsy1vzJmRwIcSwiUO+JFfrv/xGiigmME= github.com/onsi/gomega v1.7.0/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= +github.com/onsi/gomega v1.7.1 h1:K0jcRCwNQM3vFGh1ppMtDh/+7ApJrjldlX8fA0jDTLQ= +github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY= github.com/openzipkin/zipkin-go v0.1.1/go.mod h1:NtoC/o8u3JlF1lSlyPNswIbeQH9bJTmOf0Erfk+hxe8= github.com/pelletier/go-toml v1.2.0 h1:T5zMGML61Wp+FlcbWjRDT7yAxhJNAiPPLOFECq181zc= github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic= +github.com/pelletier/go-toml v1.6.0 h1:aetoXYr0Tv7xRU/V4B4IZJ2QcbtMUFoNb3ORp7TzIK4= +github.com/pelletier/go-toml v1.6.0/go.mod h1:5N711Q9dKgbdkxHL+MEfF31hpT7l0S0s/t2kKREewys= github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I= github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= @@ -792,6 +814,8 @@ github.com/sirupsen/logrus v1.4.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPx github.com/sirupsen/logrus v1.4.1/go.mod h1:ni0Sbl8bgC9z8RoU9G6nDWqqs/fq4eDPysMBDgk/93Q= github.com/sirupsen/logrus v1.4.2 h1:SPIRibHv4MatM3XXNO2BJeFLZwZ2LvZgfQ5+UNI2im4= github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= +github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc= +github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA= github.com/soheilhy/cmux v0.1.4/go.mod h1:IM3LyeVVIOuxMH7sFAkER9+bJ4dT7Ms6E4xg4kGIyLM= github.com/sourcegraph/annotate v0.0.0-20160123013949-f4cad6c6324d h1:yKm7XZV6j9Ev6lojP2XaIshpT4ymkqhMeSghO5Ps00E= github.com/sourcegraph/annotate v0.0.0-20160123013949-f4cad6c6324d/go.mod h1:UdhH50NIW0fCiwBSr0co2m7BnFLdv4fQTgdqdJTHFeE= @@ -817,7 +841,7 @@ github.com/spf13/viper v1.3.0/go.mod h1:ZiWeW+zYFKm7srdB9IoDzzZXaJaI5eL9QjNiN/DM github.com/spf13/viper v1.3.1/go.mod h1:ZiWeW+zYFKm7srdB9IoDzzZXaJaI5eL9QjNiN/DMA2s= github.com/spf13/viper v1.3.2/go.mod h1:ZiWeW+zYFKm7srdB9IoDzzZXaJaI5eL9QjNiN/DMA2s= github.com/spf13/viper v1.4.0/go.mod h1:PTJ7Z/lr49W6bUbkmS1V3by4uWynFiR9p7+dSq/yZzE= -github.com/spf13/viper v1.5.0/go.mod h1:AkYRkVJF8TkSG/xet6PzXX+l39KhhXa2pdqVSxnTcn4= +github.com/spf13/viper v1.6.2/go.mod h1:t3iDnF5Jlj76alVNuyFBk5oUMCvsrkbvZK0WQdfDi5k= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.2.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE= @@ -874,12 +898,14 @@ golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8U golang.org/x/crypto v0.0.0-20190611184440-5c40567a22f8/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20190621222207-cc06ce4a13d4/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20190820162420-60c769a6c586/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20191122220453-ac88ee75c92c h1:/nJuwDLoL/zrqY6gf57vxC+Pi+pZ8bfhpPkicO5H7W4= golang.org/x/crypto v0.0.0-20191122220453-ac88ee75c92c/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/lint v0.0.0-20180702182130-06c8688daad7/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180816102801-aaf60122140d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= @@ -961,8 +987,10 @@ golang.org/x/sys v0.0.0-20190419153524-e8e3143a4f4a/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190515120540-06a5c4944438/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190813064441-fde4db37ae7a h1:aYOabOQFp6Vj6W1F80affTUvO9UxmJRx8K0gsfABByQ= golang.org/x/sys v0.0.0-20190813064441-fde4db37ae7a/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191008105621-543471e840be/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191026070338-33540a1f6037 h1:YyJpGZS1sBuBCzLAR1VEpK193GlqGZbnPFnPV/5Rsb4= +golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.2 h1:tW2bmiBqwgJj/UpqtC8EpXEZVYOwU0yG4iWbprSVAcs= @@ -1018,6 +1046,7 @@ golang.org/x/tools v0.0.0-20190221204921-83362c3779f5/go.mod h1:9Yl7xja0Znq3iFh3 golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= golang.org/x/tools v0.0.0-20190315044204-8b67d361bba2/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= golang.org/x/tools v0.0.0-20190318200714-bb1270c20edf/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= golang.org/x/tools v0.0.0-20190329151228-23e29df326fe/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= golang.org/x/tools v0.0.0-20190404132500-923d25813098/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= golang.org/x/tools v0.0.0-20190407030857-0fdf0c73855b/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= @@ -1039,11 +1068,14 @@ golang.org/x/tools v0.0.0-20190906203814-12febf440ab1/go.mod h1:b+2E5dAYhXwXZwtn golang.org/x/tools v0.0.0-20190907020128-2ca718005c18/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191004055002-72853e10c5a3/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191015150414-f936694f27bf/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20191127171310-c1736c0f0a5b/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191224055732-dd894d0a8a40/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200117220505-0cba7a3a9ee9/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= golang.org/x/xerrors v0.0.0-20190410155217-1f06c39b4373/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20190513163551-3ee3066db522/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7 h1:9zdDQZ7Thm29KFXgAX/+yaf3eVbP7djjWp/dXAppNCc= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= google.golang.org/api v0.0.0-20180910000450-7ca32eb868bf/go.mod h1:4mhQ8q/RsB7i+udVvVy5NUi08OU8ZlA0gRVgrF7VFY0= google.golang.org/api v0.0.0-20181030000543-1d582fd0359e/go.mod h1:4mhQ8q/RsB7i+udVvVy5NUi08OU8ZlA0gRVgrF7VFY0= google.golang.org/api v0.1.0/go.mod h1:UGEZY7KEX120AnNLIHFMKIo4obdJhkp2tPbaPlQx13Y= @@ -1077,6 +1109,7 @@ gopkg.in/gemnasium/logrus-airbrake-hook.v2 v2.1.2/go.mod h1:Xk6kEKp8OKb+X14hQBKW gopkg.in/gomail.v2 v2.0.0-20160411212932-81ebce5c23df/go.mod h1:LRQQ+SO6ZHR7tOkpBDuZnXENFzX8qRjMDMyPD6BRkCw= gopkg.in/inconshreveable/log15.v2 v2.0.0-20180818164646-67afb5ed74ec/go.mod h1:aPpfJ7XW+gOuirDoZ8gHhLh3kZ1B08FtV2bbmy7Jv3s= gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw= +gopkg.in/ini.v1 v1.51.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= gopkg.in/mail.v2 v2.0.0-20180731213649-a0242b2233b4/go.mod h1:htwXN1Qh09vZJ1NVKxQqHPBaCBbzKhp5GzuJEA4VJWw= gopkg.in/resty.v1 v1.12.0/go.mod h1:mDo4pnntr5jdWRML875a/NmxYqAlA73dVijT2AXvQQo= gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ= @@ -1087,6 +1120,8 @@ gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.7 h1:VUgggvou5XRW9mHwD/yXxIYSMtY0zoKQf/v226p2nyo= gopkg.in/yaml.v2 v2.2.7/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.8 h1:obN1ZagJSUGI0Ek/LBmuj4SNLPfIny3KsKFopxRdj10= +gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= grpc.go4.org v0.0.0-20170609214715-11d0a25b4919/go.mod h1:77eQGdRu53HpSqPFJFmuJdjuHRquDANNeA4x7B8WQ9o= honnef.co/go/tools v0.0.0-20180728063816-88497007e858/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= diff --git a/i18n.go b/i18n.go index f92a414..1255689 100644 --- a/i18n.go +++ b/i18n.go @@ -7,10 +7,10 @@ import ( "strings" "github.com/gobuffalo/buffalo" + "github.com/gobuffalo/mw-i18n/internal/go-i18n/i18n" + "github.com/gobuffalo/mw-i18n/internal/go-i18n/i18n/language" + "github.com/gobuffalo/mw-i18n/internal/go-i18n/i18n/translation" "github.com/gobuffalo/packd" - "github.com/nicksnyder/go-i18n/i18n" - "github.com/nicksnyder/go-i18n/i18n/language" - "github.com/nicksnyder/go-i18n/i18n/translation" ) // LanguageExtractor can be implemented for custom finding of search @@ -133,7 +133,7 @@ func (t *Translator) Middleware() buffalo.MiddlewareFunc { // Translate returns the translation of the string identified by translationID. // -// See https://github.com/nicksnyder/go-i18n +// See https://github.com/gobuffalo/i18n-mw/internal/go-i18n // // If there is no translation for translationID, then the translationID itself is returned. // This makes it easy to identify missing translations in your app. diff --git a/i18n_test.go b/i18n_test.go index 2e74a7c..60b41fb 100644 --- a/i18n_test.go +++ b/i18n_test.go @@ -1,7 +1,6 @@ package i18n_test import ( - "github.com/gobuffalo/packd" "io/ioutil" "log" "os" @@ -10,10 +9,12 @@ import ( "testing" "time" + "github.com/gobuffalo/packd" + "github.com/gobuffalo/buffalo" "github.com/gobuffalo/buffalo/render" "github.com/gobuffalo/httptest" - "github.com/gobuffalo/mw-i18n" + i18n "github.com/gobuffalo/mw-i18n" "github.com/stretchr/testify/require" ) @@ -217,7 +218,6 @@ func Test_i18n_URL_prefix(t *testing.T) { r.Equal("Hello, World!", strings.TrimSpace(res.Body.String())) } - func Test_i18n_TranslateWithLang(t *testing.T) { r := require.New(t) @@ -257,13 +257,12 @@ func Test_i18n_TranslateWithLang(t *testing.T) { // Test French format-loop original = "test-format-loop" want = "M. Mark Bates" - params := struct{FirstName, LastName string}{"Mark", "Bates"} + params := struct{ FirstName, LastName string }{"Mark", "Bates"} res, err = transl.TranslateWithLang(lang, original, params) r.NoError(err) r.Equal(want, res) } - func Test_Refresh(t *testing.T) { r := require.New(t) diff --git a/internal/go-i18n/CHANGELOG b/internal/go-i18n/CHANGELOG new file mode 100644 index 0000000..c7949b1 --- /dev/null +++ b/internal/go-i18n/CHANGELOG @@ -0,0 +1,5 @@ +Feb 24, 2015 +- Add Korean + +Feb 18, 2015 +- Added ParseTranslationFileBytes so translation files may be loaded from an arbitrary serialization format, such as go-bindata. diff --git a/internal/go-i18n/LICENSE b/internal/go-i18n/LICENSE new file mode 100644 index 0000000..609cce7 --- /dev/null +++ b/internal/go-i18n/LICENSE @@ -0,0 +1,19 @@ +Copyright (c) 2014 Nick Snyder https://github.com/nicksnyder + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/internal/go-i18n/README.md b/internal/go-i18n/README.md new file mode 100644 index 0000000..7136dc8 --- /dev/null +++ b/internal/go-i18n/README.md @@ -0,0 +1,186 @@ +go-i18n [![Build Status](https://travis-ci.org/nicksnyder/go-i18n.svg?branch=master)](http://travis-ci.org/nicksnyder/go-i18n) [![Sourcegraph](https://sourcegraph.com/github.com/nicksnyder/go-i18n/-/badge.svg)](https://sourcegraph.com/github.com/nicksnyder/go-i18n?badge) +======= + +go-i18n is a Go [package](#i18n-package) and a [command](#goi18n-command) that helps you translate Go programs into multiple languages. +* Supports [pluralized strings](http://cldr.unicode.org/index/cldr-spec/plural-rules) for all 200+ languages in the [Unicode Common Locale Data Repository (CLDR)](http://www.unicode.org/cldr/charts/28/supplemental/language_plural_rules.html). + * Code and tests are [automatically generated](https://github.com/nicksnyder/go-i18n/tree/master/i18n/language/codegen) from [CLDR data](http://cldr.unicode.org/index/downloads) +* Supports strings with named variables using [text/template](http://golang.org/pkg/text/template/) syntax. +* Translation files are simple JSON, TOML or YAML. +* [Documented](http://godoc.org/github.com/nicksnyder/go-i18n) and [tested](https://travis-ci.org/nicksnyder/go-i18n)! + +Package i18n [![GoDoc](http://godoc.org/github.com/nicksnyder/go-i18n?status.svg)](http://godoc.org/github.com/nicksnyder/go-i18n/i18n) +------------ + +The i18n package provides runtime APIs for fetching translated strings. + +Command goi18n [![GoDoc](http://godoc.org/github.com/nicksnyder/go-i18n?status.svg)](http://godoc.org/github.com/nicksnyder/go-i18n/goi18n) +-------------- + +The goi18n command provides functionality for managing the translation process. + +Installation +------------ + +Make sure you have [setup GOPATH](http://golang.org/doc/code.html#GOPATH). + + go get -u github.com/nicksnyder/go-i18n/goi18n + goi18n -help + +Workflow +-------- + +A typical workflow looks like this: + +1. Add a new string to your source code. + + ```go + T("settings_title") + ``` + +2. Add the string to en-US.all.json + + ```json + [ + { + "id": "settings_title", + "translation": "Settings" + } + ] + ``` + +3. Run goi18n + + ``` + goi18n path/to/*.all.json + ``` + +4. Send `path/to/*.untranslated.json` to get translated. +5. Run goi18n again to merge the translations + + ```sh + goi18n path/to/*.all.json path/to/*.untranslated.json + ``` + +Translation files +----------------- + +A translation file stores translated and untranslated strings. + +Here is an example of the default file format that go-i18n supports: + +```json +[ + { + "id": "d_days", + "translation": { + "one": "{{.Count}} day", + "other": "{{.Count}} days" + } + }, + { + "id": "my_height_in_meters", + "translation": { + "one": "I am {{.Count}} meter tall.", + "other": "I am {{.Count}} meters tall." + } + }, + { + "id": "person_greeting", + "translation": "Hello {{.Person}}" + }, + { + "id": "person_unread_email_count", + "translation": { + "one": "{{.Person}} has {{.Count}} unread email.", + "other": "{{.Person}} has {{.Count}} unread emails." + } + }, + { + "id": "person_unread_email_count_timeframe", + "translation": { + "one": "{{.Person}} has {{.Count}} unread email in the past {{.Timeframe}}.", + "other": "{{.Person}} has {{.Count}} unread emails in the past {{.Timeframe}}." + } + }, + { + "id": "program_greeting", + "translation": "Hello world" + }, + { + "id": "your_unread_email_count", + "translation": { + "one": "You have {{.Count}} unread email.", + "other": "You have {{.Count}} unread emails." + } + } +] +``` + +To use a different file format, write a parser for the format and add the parsed translations using [AddTranslation](https://godoc.org/github.com/nicksnyder/go-i18n/i18n#AddTranslation). + +Note that TOML only supports the flat format, which is described below. + +More examples of translation files: [JSON](https://github.com/nicksnyder/go-i18n/tree/master/goi18n/testdata/input), [TOML](https://github.com/nicksnyder/go-i18n/blob/master/goi18n/testdata/input/flat/ar-ar.one.toml), [YAML](https://github.com/nicksnyder/go-i18n/blob/master/goi18n/testdata/input/yaml/en-us.one.yaml). + +Flat Format +------------- + +You can also write shorter translation files with flat format. +E.g the example above can be written in this way: + +```json +{ + "d_days": { + "one": "{{.Count}} day.", + "other": "{{.Count}} days." + }, + + "my_height_in_meters": { + "one": "I am {{.Count}} meter tall.", + "other": "I am {{.Count}} meters tall." + }, + + "person_greeting": { + "other": "Hello {{.Person}}" + }, + + "person_unread_email_count": { + "one": "{{.Person}} has {{.Count}} unread email.", + "other": "{{.Person}} has {{.Count}} unread emails." + }, + + "person_unread_email_count_timeframe": { + "one": "{{.Person}} has {{.Count}} unread email in the past {{.Timeframe}}.", + "other": "{{.Person}} has {{.Count}} unread emails in the past {{.Timeframe}}." + }, + + "program_greeting": { + "other": "Hello world" + }, + + "your_unread_email_count": { + "one": "You have {{.Count}} unread email.", + "other": "You have {{.Count}} unread emails." + } +} +``` + +The logic of flat format is, what it is structure of structures +and name of substructures (ids) should be always a string. +If there is only one key in substructure and it is "other", then it's non-plural +translation, else plural. + +More examples of flat format translation files can be found in [goi18n/testdata/input/flat](https://github.com/nicksnyder/go-i18n/tree/master/goi18n/testdata/input/flat). + +Contributions +------------- + +If you would like to submit a pull request, please + +1. Write tests +2. Format code with [goimports](https://github.com/bradfitz/goimports). +3. Read the [common code review comments](https://github.com/golang/go/wiki/CodeReviewComments). + +License +------- +go-i18n is available under the MIT license. See the [LICENSE](LICENSE) file for more info. diff --git a/internal/go-i18n/goi18n/constants_command.go b/internal/go-i18n/goi18n/constants_command.go new file mode 100644 index 0000000..a2681b9 --- /dev/null +++ b/internal/go-i18n/goi18n/constants_command.go @@ -0,0 +1,229 @@ +package main + +import ( + "flag" + "fmt" + "os" + "path/filepath" + "reflect" + "regexp" + "sort" + "strconv" + "strings" + "text/template" + "unicode" + + "github.com/gobuffalo/mw-i18n/internal/go-i18n/i18n/bundle" + "github.com/gobuffalo/mw-i18n/internal/go-i18n/i18n/language" + "github.com/gobuffalo/mw-i18n/internal/go-i18n/i18n/translation" +) + +type constantsCommand struct { + translationFiles []string + packageName string + outdir string +} + +type templateConstants struct { + ID string + Name string + Comments []string +} + +type templateHeader struct { + PackageName string + Constants []templateConstants +} + +var constTemplate = template.Must(template.New("").Parse(`// DON'T CHANGE THIS FILE MANUALLY +// This file was generated using the command: +// $ goi18n constants + +package {{.PackageName}} +{{range .Constants}} +// {{.Name}} is the identifier for the following localizable string template(s):{{range .Comments}} +// {{.}}{{end}} +const {{.Name}} = "{{.ID}}" +{{end}}`)) + +func (cc *constantsCommand) execute() error { + if len(cc.translationFiles) != 1 { + return fmt.Errorf("need one translation file") + } + + bundle := bundle.New() + + if err := bundle.LoadTranslationFile(cc.translationFiles[0]); err != nil { + return fmt.Errorf("failed to load translation file %s because %s\n", cc.translationFiles[0], err) + } + + translations := bundle.Translations() + lang := translations[bundle.LanguageTags()[0]] + + // create an array of id to organize + keys := make([]string, len(lang)) + i := 0 + + for id := range lang { + keys[i] = id + i++ + } + sort.Strings(keys) + + tmpl := &templateHeader{ + PackageName: cc.packageName, + Constants: make([]templateConstants, len(keys)), + } + + for i, id := range keys { + tmpl.Constants[i].ID = id + tmpl.Constants[i].Name = toCamelCase(id) + tmpl.Constants[i].Comments = toComments(lang[id]) + } + + filename := filepath.Join(cc.outdir, cc.packageName+".go") + f, err := os.Create(filename) + if err != nil { + return fmt.Errorf("failed to create file %s because %s", filename, err) + } + + defer f.Close() + + if err = constTemplate.Execute(f, tmpl); err != nil { + return fmt.Errorf("failed to write file %s because %s", filename, err) + } + + return nil +} + +func (cc *constantsCommand) parse(arguments []string) { + flags := flag.NewFlagSet("constants", flag.ExitOnError) + flags.Usage = usageConstants + + packageName := flags.String("package", "R", "") + outdir := flags.String("outdir", ".", "") + + flags.Parse(arguments) + + cc.translationFiles = flags.Args() + cc.packageName = *packageName + cc.outdir = *outdir +} + +func (cc *constantsCommand) SetArgs(args []string) { + cc.translationFiles = args +} + +func usageConstants() { + fmt.Printf(`Generate constant file from translation file. + +Usage: + + goi18n constants [options] [file] + +Translation files: + + A translation file contains the strings and translations for a single language. + + Translation file names must have a suffix of a supported format (e.g. .json) and + contain a valid language tag as defined by RFC 5646 (e.g. en-us, fr, zh-hant, etc.). + +Options: + + -package name + goi18n generates the constant file under the package name. + Default: R + + -outdir directory + goi18n writes the constant file to this directory. + Default: . + +`) +} + +// commonInitialisms is a set of common initialisms. +// Only add entries that are highly unlikely to be non-initialisms. +// For instance, "ID" is fine (Freudian code is rare), but "AND" is not. +// https://github.com/golang/lint/blob/master/lint.go +var commonInitialisms = map[string]bool{ + "API": true, + "ASCII": true, + "CPU": true, + "CSS": true, + "DNS": true, + "EOF": true, + "GUID": true, + "HTML": true, + "HTTP": true, + "HTTPS": true, + "ID": true, + "IP": true, + "JSON": true, + "LHS": true, + "QPS": true, + "RAM": true, + "RHS": true, + "RPC": true, + "SLA": true, + "SMTP": true, + "SQL": true, + "SSH": true, + "TCP": true, + "TLS": true, + "TTL": true, + "UDP": true, + "UI": true, + "UID": true, + "UUID": true, + "URI": true, + "URL": true, + "UTF8": true, + "VM": true, + "XML": true, + "XSRF": true, + "XSS": true, +} + +func toCamelCase(id string) string { + var result string + + r := regexp.MustCompile(`[\-\.\_\s]`) + words := r.Split(id, -1) + + for _, w := range words { + upper := strings.ToUpper(w) + if commonInitialisms[upper] { + result += upper + continue + } + + if len(w) > 0 { + u := []rune(w) + u[0] = unicode.ToUpper(u[0]) + result += string(u) + } + } + return result +} + +func toComments(trans translation.Translation) []string { + var result []string + data := trans.MarshalInterface().(map[string]interface{}) + + t := data["translation"] + + switch v := reflect.ValueOf(t); v.Kind() { + case reflect.Map: + for _, k := range []language.Plural{"zero", "one", "two", "few", "many", "other"} { + vt := v.MapIndex(reflect.ValueOf(k)) + if !vt.IsValid() { + continue + } + result = append(result, string(k)+": "+strconv.Quote(fmt.Sprint(vt.Interface()))) + } + default: + result = append(result, strconv.Quote(fmt.Sprint(t))) + } + + return result +} diff --git a/internal/go-i18n/goi18n/constants_command_test.go b/internal/go-i18n/goi18n/constants_command_test.go new file mode 100644 index 0000000..43dea3f --- /dev/null +++ b/internal/go-i18n/goi18n/constants_command_test.go @@ -0,0 +1,42 @@ +package main + +import "testing" + +func TestConstantsExecute(t *testing.T) { + resetDir(t, "testdata/output") + + cc := &constantsCommand{ + translationFiles: []string{"testdata/input/en-us.constants.json"}, + packageName: "R", + outdir: "testdata/output", + } + + if err := cc.execute(); err != nil { + t.Fatal(err) + } + + expectEqualFiles(t, "testdata/output/R.go", "testdata/expected/R.go") +} + +func TestToCamelCase(t *testing.T) { + expectEqual := func(test, expected string) { + result := toCamelCase(test) + if result != expected { + t.Fatalf("failed toCamelCase the test %s was expected %s but the result was %s", test, expected, result) + } + } + + expectEqual("", "") + expectEqual("a", "A") + expectEqual("_", "") + expectEqual("__code__", "Code") + expectEqual("test", "Test") + expectEqual("test_one", "TestOne") + expectEqual("test.two", "TestTwo") + expectEqual("test_alpha_beta", "TestAlphaBeta") + expectEqual("word word", "WordWord") + expectEqual("test_id", "TestID") + expectEqual("tcp_name", "TCPName") + expectEqual("こんにちは", "こんにちは") + expectEqual("test_a", "TestA") +} diff --git a/internal/go-i18n/goi18n/doc.go b/internal/go-i18n/goi18n/doc.go new file mode 100644 index 0000000..c34ecd5 --- /dev/null +++ b/internal/go-i18n/goi18n/doc.go @@ -0,0 +1,93 @@ +// The goi18n command formats and merges translation files. +// +// go get -u github.com/gobuffalo/mw-i18n/internal/go-i18n/goi18n +// goi18n -help +// +// Help documentation: +// +// goi18n manages translation files. +// +// Usage: +// +// goi18n merge Merge translation files +// goi18n constants Generate constant file from translation file +// +// For more details execute: +// +// goi18n [command] -help +// +// Merge translation files. +// +// Usage: +// +// goi18n merge [options] [files...] +// +// Translation files: +// +// A translation file contains the strings and translations for a single language. +// +// Translation file names must have a suffix of a supported format (e.g. .json) and +// contain a valid language tag as defined by RFC 5646 (e.g. en-us, fr, zh-hant, etc.). +// +// For each language represented by at least one input translation file, goi18n will produce 2 output files: +// +// xx-yy.all.format +// This file contains all strings for the language (translated and untranslated). +// Use this file when loading strings at runtime. +// +// xx-yy.untranslated.format +// This file contains the strings that have not been translated for this language. +// The translations for the strings in this file will be extracted from the source language. +// After they are translated, merge them back into xx-yy.all.format using goi18n. +// +// Merging: +// +// goi18n will merge multiple translation files for the same language. +// Duplicate translations will be merged into the existing translation. +// Non-empty fields in the duplicate translation will overwrite those fields in the existing translation. +// Empty fields in the duplicate translation are ignored. +// +// Adding a new language: +// +// To produce translation files for a new language, create an empty translation file with the +// appropriate name and pass it in to goi18n. +// +// Options: +// +// -sourceLanguage tag +// goi18n uses the strings from this language to seed the translations for other languages. +// Default: en-us +// +// -outdir directory +// goi18n writes the output translation files to this directory. +// Default: . +// +// -format format +// goi18n encodes the output translation files in this format. +// Supported formats: json, yaml +// Default: json +// +// Generate constant file from translation file. +// +// Usage: +// +// goi18n constants [options] [file] +// +// Translation files: +// +// A translation file contains the strings and translations for a single language. +// +// Translation file names must have a suffix of a supported format (e.g. .json) and +// contain a valid language tag as defined by RFC 5646 (e.g. en-us, fr, zh-hant, etc.). +// +// Options: +// +// -package name +// goi18n generates the constant file under the package name. +// Default: R +// +// -outdir directory +// goi18n writes the constant file to this directory. +// Default: . +// +package main diff --git a/internal/go-i18n/goi18n/gendoc.sh b/internal/go-i18n/goi18n/gendoc.sh new file mode 100644 index 0000000..f30df34 --- /dev/null +++ b/internal/go-i18n/goi18n/gendoc.sh @@ -0,0 +1,12 @@ +go install +echo "// The goi18n command formats and merges translation files." > doc.go +echo "//" >> doc.go +echo "// go get -u github.com/nicksnyder/go-i18n/goi18n" >> doc.go +echo "// goi18n -help" >> doc.go +echo "//" >> doc.go +echo "// Help documentation:" >> doc.go +echo "//" >> doc.go +goi18n | sed -e 's/^/\/\/ /' >> doc.go +goi18n merge -help | sed -e 's/^/\/\/ /' >> doc.go +goi18n constants -help | sed -e 's/^/\/\/ /' >> doc.go +echo "package main" >> doc.go diff --git a/internal/go-i18n/goi18n/goi18n.go b/internal/go-i18n/goi18n/goi18n.go new file mode 100644 index 0000000..3bd763f --- /dev/null +++ b/internal/go-i18n/goi18n/goi18n.go @@ -0,0 +1,55 @@ +package main + +import ( + "flag" + "fmt" + "os" +) + +type command interface { + execute() error + parse(arguments []string) +} + +func main() { + flag.Usage = usage + + if len(os.Args) == 1 { + usage() + } + + var cmd command + + switch os.Args[1] { + case "merge": + cmd = &mergeCommand{} + cmd.parse(os.Args[2:]) + case "constants": + cmd = &constantsCommand{} + cmd.parse(os.Args[2:]) + default: + cmd = &mergeCommand{} + cmd.parse(os.Args[1:]) + } + + if err := cmd.execute(); err != nil { + fmt.Println(err.Error()) + os.Exit(1) + } +} + +func usage() { + fmt.Printf(`goi18n manages translation files. + +Usage: + + goi18n merge Merge translation files + goi18n constants Generate constant file from translation file + +For more details execute: + + goi18n [command] -help + +`) + os.Exit(1) +} diff --git a/internal/go-i18n/goi18n/merge_command.go b/internal/go-i18n/goi18n/merge_command.go new file mode 100644 index 0000000..fe0b9de --- /dev/null +++ b/internal/go-i18n/goi18n/merge_command.go @@ -0,0 +1,239 @@ +package main + +import ( + "encoding/json" + "flag" + "fmt" + "io/ioutil" + "path/filepath" + "reflect" + "sort" + + "gopkg.in/yaml.v2" + + "github.com/gobuffalo/mw-i18n/internal/go-i18n/i18n/bundle" + "github.com/gobuffalo/mw-i18n/internal/go-i18n/i18n/language" + "github.com/gobuffalo/mw-i18n/internal/go-i18n/i18n/translation" + toml "github.com/pelletier/go-toml" +) + +type mergeCommand struct { + translationFiles []string + sourceLanguage string + outdir string + format string + flat bool +} + +func (mc *mergeCommand) execute() error { + if len(mc.translationFiles) < 1 { + return fmt.Errorf("need at least one translation file to parse") + } + + if lang := language.Parse(mc.sourceLanguage); lang == nil { + return fmt.Errorf("invalid source locale: %s", mc.sourceLanguage) + } + + bundle := bundle.New() + for _, tf := range mc.translationFiles { + if err := bundle.LoadTranslationFile(tf); err != nil { + return fmt.Errorf("failed to load translation file %s: %s\n", tf, err) + } + } + + translations := bundle.Translations() + sourceLanguageTag := language.NormalizeTag(mc.sourceLanguage) + sourceTranslations := translations[sourceLanguageTag] + if sourceTranslations == nil { + return fmt.Errorf("no translations found for source locale %s", sourceLanguageTag) + } + for translationID, src := range sourceTranslations { + for _, localeTranslations := range translations { + if dst := localeTranslations[translationID]; dst == nil || reflect.TypeOf(src) != reflect.TypeOf(dst) { + localeTranslations[translationID] = src.UntranslatedCopy() + } + } + } + + for localeID, localeTranslations := range translations { + lang := language.MustParse(localeID)[0] + all := filter(localeTranslations, func(t translation.Translation) translation.Translation { + return t.Normalize(lang) + }) + if err := mc.writeFile("all", all, localeID); err != nil { + return err + } + + untranslated := filter(localeTranslations, func(t translation.Translation) translation.Translation { + if t.Incomplete(lang) { + return t.Normalize(lang).Backfill(sourceTranslations[t.ID()]) + } + return nil + }) + if err := mc.writeFile("untranslated", untranslated, localeID); err != nil { + return err + } + } + return nil +} + +func (mc *mergeCommand) parse(arguments []string) { + flags := flag.NewFlagSet("merge", flag.ExitOnError) + flags.Usage = usageMerge + + sourceLanguage := flags.String("sourceLanguage", "en-us", "") + outdir := flags.String("outdir", ".", "") + format := flags.String("format", "json", "") + flat := flags.Bool("flat", true, "") + + flags.Parse(arguments) + + mc.translationFiles = flags.Args() + mc.sourceLanguage = *sourceLanguage + mc.outdir = *outdir + mc.format = *format + if *format == "toml" { + mc.flat = true + } else { + mc.flat = *flat + } +} + +func (mc *mergeCommand) SetArgs(args []string) { + mc.translationFiles = args +} + +func (mc *mergeCommand) writeFile(label string, translations []translation.Translation, localeID string) error { + sort.Sort(translation.SortableByID(translations)) + + var convert func([]translation.Translation) interface{} + if mc.flat { + convert = marshalFlatInterface + } else { + convert = marshalInterface + } + + buf, err := mc.marshal(convert(translations)) + if err != nil { + return fmt.Errorf("failed to marshal %s strings to %s: %s", localeID, mc.format, err) + } + + filename := filepath.Join(mc.outdir, fmt.Sprintf("%s.%s.%s", localeID, label, mc.format)) + + if err := ioutil.WriteFile(filename, buf, 0666); err != nil { + return fmt.Errorf("failed to write %s: %s", filename, err) + } + return nil +} + +func filter(translations map[string]translation.Translation, f func(translation.Translation) translation.Translation) []translation.Translation { + filtered := make([]translation.Translation, 0, len(translations)) + for _, translation := range translations { + if t := f(translation); t != nil { + filtered = append(filtered, t) + } + } + return filtered + +} + +func marshalFlatInterface(translations []translation.Translation) interface{} { + mi := make(map[string]interface{}, len(translations)) + for _, translation := range translations { + mi[translation.ID()] = translation.MarshalFlatInterface() + } + return mi +} + +func marshalInterface(translations []translation.Translation) interface{} { + mi := make([]interface{}, len(translations)) + for i, translation := range translations { + mi[i] = translation.MarshalInterface() + } + return mi +} + +func (mc mergeCommand) marshal(v interface{}) ([]byte, error) { + switch mc.format { + case "json": + return json.MarshalIndent(v, "", " ") + case "toml": + return marshalTOML(v) + case "yaml": + return yaml.Marshal(v) + } + return nil, fmt.Errorf("unsupported format: %s\n", mc.format) +} + +func marshalTOML(v interface{}) ([]byte, error) { + m, ok := v.(map[string]interface{}) + if !ok { + return nil, fmt.Errorf("invalid format for marshaling to TOML") + } + tree, err := toml.TreeFromMap(m) + if err != nil { + return nil, err + } + s, err := tree.ToTomlString() + return []byte(s), err +} + +func usageMerge() { + fmt.Printf(`Merge translation files. + +Usage: + + goi18n merge [options] [files...] + +Translation files: + + A translation file contains the strings and translations for a single language. + + Translation file names must have a suffix of a supported format (e.g. .json) and + contain a valid language tag as defined by RFC 5646 (e.g. en-us, fr, zh-hant, etc.). + + For each language represented by at least one input translation file, goi18n will produce 2 output files: + + xx-yy.all.format + This file contains all strings for the language (translated and untranslated). + Use this file when loading strings at runtime. + + xx-yy.untranslated.format + This file contains the strings that have not been translated for this language. + The translations for the strings in this file will be extracted from the source language. + After they are translated, merge them back into xx-yy.all.format using goi18n. + +Merging: + + goi18n will merge multiple translation files for the same language. + Duplicate translations will be merged into the existing translation. + Non-empty fields in the duplicate translation will overwrite those fields in the existing translation. + Empty fields in the duplicate translation are ignored. + +Adding a new language: + + To produce translation files for a new language, create an empty translation file with the + appropriate name and pass it in to goi18n. + +Options: + + -sourceLanguage tag + goi18n uses the strings from this language to seed the translations for other languages. + Default: en-us + + -outdir directory + goi18n writes the output translation files to this directory. + Default: . + + -format format + goi18n encodes the output translation files in this format. + Supported formats: json, toml, yaml + Default: json + + -flat + goi18n writes the output translation files in flat format. + Usage of '-format toml' automitically sets this flag. + Default: true + +`) +} diff --git a/internal/go-i18n/goi18n/merge_command_flat_test.go b/internal/go-i18n/goi18n/merge_command_flat_test.go new file mode 100644 index 0000000..caa892d --- /dev/null +++ b/internal/go-i18n/goi18n/merge_command_flat_test.go @@ -0,0 +1,36 @@ +package main + +import "testing" + +func TestMergeExecuteFlat(t *testing.T) { + files := []string{ + "testdata/input/flat/en-us.one.yaml", + "testdata/input/flat/en-us.two.json", + "testdata/input/flat/fr-fr.json", + "testdata/input/flat/ar-ar.one.toml", + "testdata/input/flat/ar-ar.two.json", + } + testFlatMergeExecute(t, files) +} + +func testFlatMergeExecute(t *testing.T, files []string) { + resetDir(t, "testdata/output/flat") + + mc := &mergeCommand{ + translationFiles: files, + sourceLanguage: "en-us", + outdir: "testdata/output/flat", + format: "json", + flat: true, + } + if err := mc.execute(); err != nil { + t.Fatal(err) + } + + expectEqualFiles(t, "testdata/output/flat/en-us.all.json", "testdata/expected/flat/en-us.all.json") + expectEqualFiles(t, "testdata/output/flat/ar-ar.all.json", "testdata/expected/flat/ar-ar.all.json") + expectEqualFiles(t, "testdata/output/flat/fr-fr.all.json", "testdata/expected/flat/fr-fr.all.json") + expectEqualFiles(t, "testdata/output/flat/en-us.untranslated.json", "testdata/expected/flat/en-us.untranslated.json") + expectEqualFiles(t, "testdata/output/flat/ar-ar.untranslated.json", "testdata/expected/flat/ar-ar.untranslated.json") + expectEqualFiles(t, "testdata/output/flat/fr-fr.untranslated.json", "testdata/expected/flat/fr-fr.untranslated.json") +} diff --git a/internal/go-i18n/goi18n/merge_command_test.go b/internal/go-i18n/goi18n/merge_command_test.go new file mode 100644 index 0000000..425a6b6 --- /dev/null +++ b/internal/go-i18n/goi18n/merge_command_test.go @@ -0,0 +1,75 @@ +package main + +import ( + "bytes" + "io/ioutil" + "os" + "testing" +) + +func TestMergeExecuteJSON(t *testing.T) { + files := []string{ + "testdata/input/en-us.one.json", + "testdata/input/en-us.two.json", + "testdata/input/fr-fr.json", + "testdata/input/ar-ar.one.json", + "testdata/input/ar-ar.two.json", + } + testMergeExecute(t, files) +} + +func TestMergeExecuteYAML(t *testing.T) { + files := []string{ + "testdata/input/yaml/en-us.one.yaml", + "testdata/input/yaml/en-us.two.json", + "testdata/input/yaml/fr-fr.json", + "testdata/input/yaml/ar-ar.one.json", + "testdata/input/yaml/ar-ar.two.json", + } + testMergeExecute(t, files) +} + +func testMergeExecute(t *testing.T, files []string) { + resetDir(t, "testdata/output") + + mc := &mergeCommand{ + translationFiles: files, + sourceLanguage: "en-us", + outdir: "testdata/output", + format: "json", + flat: false, + } + if err := mc.execute(); err != nil { + t.Fatal(err) + } + + expectEqualFiles(t, "testdata/output/en-us.all.json", "testdata/expected/en-us.all.json") + expectEqualFiles(t, "testdata/output/ar-ar.all.json", "testdata/expected/ar-ar.all.json") + expectEqualFiles(t, "testdata/output/fr-fr.all.json", "testdata/expected/fr-fr.all.json") + expectEqualFiles(t, "testdata/output/en-us.untranslated.json", "testdata/expected/en-us.untranslated.json") + expectEqualFiles(t, "testdata/output/ar-ar.untranslated.json", "testdata/expected/ar-ar.untranslated.json") + expectEqualFiles(t, "testdata/output/fr-fr.untranslated.json", "testdata/expected/fr-fr.untranslated.json") +} + +func resetDir(t *testing.T, dir string) { + if err := os.RemoveAll(dir); err != nil { + t.Fatal(err) + } + if err := os.Mkdir(dir, 0777); err != nil { + t.Fatal(err) + } +} + +func expectEqualFiles(t *testing.T, expectedName, actualName string) { + actual, err := ioutil.ReadFile(actualName) + if err != nil { + t.Fatal(err) + } + expected, err := ioutil.ReadFile(expectedName) + if err != nil { + t.Fatal(err) + } + if !bytes.Equal(actual, expected) { + t.Errorf("contents of files did not match: %s, %s", expectedName, actualName) + } +} diff --git a/internal/go-i18n/goi18n/testdata/en-us.flat.json b/internal/go-i18n/goi18n/testdata/en-us.flat.json new file mode 100644 index 0000000..f67d21c --- /dev/null +++ b/internal/go-i18n/goi18n/testdata/en-us.flat.json @@ -0,0 +1,34 @@ +{ + "program_greeting": { + "other": "Hello world" + }, + + "person_greeting": { + "other": "Hello {{.Person}}" + }, + + "my_height_in_meters": { + "one": "I am {{.Count}} meter tall.", + "other": "I am {{.Count}} meters tall." + }, + + "your_unread_email_count": { + "one": "You have {{.Count}} unread email.", + "other": "You have {{.Count}} unread emails." + }, + + "person_unread_email_count": { + "one": "{{.Person}} has {{.Count}} unread email.", + "other": "{{.Person}} has {{.Count}} unread emails." + }, + + "person_unread_email_count_timeframe": { + "one": "{{.Person}} has {{.Count}} unread email in the past {{.Timeframe}}.", + "other": "{{.Person}} has {{.Count}} unread emails in the past {{.Timeframe}}." + }, + + "d_days": { + "one": "{{.Count}} day.", + "other": "{{.Count}} days." + } +} diff --git a/internal/go-i18n/goi18n/testdata/en-us.flat.toml b/internal/go-i18n/goi18n/testdata/en-us.flat.toml new file mode 100644 index 0000000..5623a6c --- /dev/null +++ b/internal/go-i18n/goi18n/testdata/en-us.flat.toml @@ -0,0 +1,25 @@ +[program_greeting] +other = "Hello world" + +[person_greeting] +other = "Hello {{.Person}}" + +[my_height_in_meters] +one = "I am {{.Count}} meter tall." +other = "I am {{.Count}} meters tall." + +[your_unread_email_count] +one = "You have {{.Count}} unread email." +other = "You have {{.Count}} unread emails." + +[person_unread_email_count] +one = "{{.Person}} has {{.Count}} unread email." +other = "{{.Person}} has {{.Count}} unread emails." + +[person_unread_email_count_timeframe] +one = "{{.Person}} has {{.Count}} unread email in the past {{.Timeframe}}." +other = "{{.Person}} has {{.Count}} unread emails in the past {{.Timeframe}}." + +[d_days] +one = "{{.Count}} day" +other = "{{.Count}} days" diff --git a/internal/go-i18n/goi18n/testdata/en-us.flat.yaml b/internal/go-i18n/goi18n/testdata/en-us.flat.yaml new file mode 100644 index 0000000..6641289 --- /dev/null +++ b/internal/go-i18n/goi18n/testdata/en-us.flat.yaml @@ -0,0 +1,29 @@ + + +# Comment +# Comment +program_greeting: + other: "Hello world" + +person_greeting: + other: "Hello {{.Person}}" + +my_height_in_meters: + one: "I am {{.Count}} meter tall." + other: "I am {{.Count}} meters tall." + +your_unread_email_count: + one: "You have {{.Count}} unread email." + other: "You have {{.Count}} unread emails." + +person_unread_email_count: + one: "{{.Person}} has {{.Count}} unread email." + other: "{{.Person}} has {{.Count}} unread emails." + +person_unread_email_count_timeframe: + one: "{{.Person}} has {{.Count}} unread email in the past {{.Timeframe}}." + other: "{{.Person}} has {{.Count}} unread emails in the past {{.Timeframe}}." + +d_days: + one: "{{.Count}} day" + other: "{{.Count}} days" diff --git a/internal/go-i18n/goi18n/testdata/en-us.yaml b/internal/go-i18n/goi18n/testdata/en-us.yaml new file mode 100644 index 0000000..a8f5ed9 --- /dev/null +++ b/internal/go-i18n/goi18n/testdata/en-us.yaml @@ -0,0 +1,35 @@ + + + +# Comment +# Comment +- id: program_greeting + translation: "Hello world" + +- id: person_greeting + translation: "Hello {{.Person}}" + +- id: my_height_in_meters + translation: + one: "I am {{.Count}} meter tall." + other: "I am {{.Count}} meters tall." + +- id: your_unread_email_count + translation: + one: "You have {{.Count}} unread email." + other: "You have {{.Count}} unread emails." + +- id: person_unread_email_count + translation: + one: "{{.Person}} has {{.Count}} unread email." + other: "{{.Person}} has {{.Count}} unread emails." + +- id: person_unread_email_count_timeframe + translation: + one: "{{.Person}} has {{.Count}} unread email in the past {{.Timeframe}}." + other: "{{.Person}} has {{.Count}} unread emails in the past {{.Timeframe}}." + +- id: d_days + translation: + one: "{{.Count}} day" + other: "{{.Count}} days" diff --git a/internal/go-i18n/goi18n/testdata/expected/R.go b/internal/go-i18n/goi18n/testdata/expected/R.go new file mode 100644 index 0000000..9b5334a --- /dev/null +++ b/internal/go-i18n/goi18n/testdata/expected/R.go @@ -0,0 +1,38 @@ +// DON'T CHANGE THIS FILE MANUALLY +// This file was generated using the command: +// $ goi18n constants + +package R + +// DDays is the identifier for the following localizable string template(s): +// one: "{{.Count}} day" +// other: "{{.Count}} days" +const DDays = "d_days" + +// MyHeightInMeters is the identifier for the following localizable string template(s): +// one: "I am {{.Count}} meter tall." +// other: "I am {{.Count}} meters tall." +const MyHeightInMeters = "my_height_in_meters" + +// PersonGreeting is the identifier for the following localizable string template(s): +// "Hello {{.Person}}" +const PersonGreeting = "person_greeting" + +// PersonUnreadEmailCount is the identifier for the following localizable string template(s): +// one: "{{.Person}} has {{.Count}} unread email." +// other: "{{.Person}} has {{.Count}} unread emails." +const PersonUnreadEmailCount = "person_unread_email_count" + +// PersonUnreadEmailCountTimeframe is the identifier for the following localizable string template(s): +// one: "{{.Person}} has {{.Count}} unread email in the past {{.Timeframe}}." +// other: "{{.Person}} has {{.Count}} unread emails in the past {{.Timeframe}}." +const PersonUnreadEmailCountTimeframe = "person_unread_email_count_timeframe" + +// ProgramGreeting is the identifier for the following localizable string template(s): +// "Hello world" +const ProgramGreeting = "program_greeting" + +// YourUnreadEmailCount is the identifier for the following localizable string template(s): +// one: "You have {{.Count}} unread email." +// other: "You have {{.Count}} unread emails." +const YourUnreadEmailCount = "your_unread_email_count" diff --git a/internal/go-i18n/goi18n/testdata/expected/ar-ar.all.json b/internal/go-i18n/goi18n/testdata/expected/ar-ar.all.json new file mode 100644 index 0000000..26a72ff --- /dev/null +++ b/internal/go-i18n/goi18n/testdata/expected/ar-ar.all.json @@ -0,0 +1,65 @@ +[ + { + "id": "d_days", + "translation": { + "few": "new arabic few translation of d_days", + "many": "arabic many translation of d_days", + "one": "arabic one translation of d_days", + "other": "", + "two": "", + "zero": "" + } + }, + { + "id": "my_height_in_meters", + "translation": { + "few": "", + "many": "", + "one": "", + "other": "", + "two": "", + "zero": "" + } + }, + { + "id": "person_greeting", + "translation": "new arabic translation of person_greeting" + }, + { + "id": "person_unread_email_count", + "translation": { + "few": "arabic few translation of person_unread_email_count", + "many": "arabic many translation of person_unread_email_count", + "one": "arabic one translation of person_unread_email_count", + "other": "arabic other translation of person_unread_email_count", + "two": "arabic two translation of person_unread_email_count", + "zero": "arabic zero translation of person_unread_email_count" + } + }, + { + "id": "person_unread_email_count_timeframe", + "translation": { + "few": "", + "many": "", + "one": "", + "other": "", + "two": "", + "zero": "" + } + }, + { + "id": "program_greeting", + "translation": "" + }, + { + "id": "your_unread_email_count", + "translation": { + "few": "", + "many": "", + "one": "", + "other": "", + "two": "", + "zero": "" + } + } +] \ No newline at end of file diff --git a/internal/go-i18n/goi18n/testdata/expected/ar-ar.untranslated.json b/internal/go-i18n/goi18n/testdata/expected/ar-ar.untranslated.json new file mode 100644 index 0000000..a19fa0b --- /dev/null +++ b/internal/go-i18n/goi18n/testdata/expected/ar-ar.untranslated.json @@ -0,0 +1,50 @@ +[ + { + "id": "d_days", + "translation": { + "few": "new arabic few translation of d_days", + "many": "arabic many translation of d_days", + "one": "arabic one translation of d_days", + "other": "{{.Count}} days", + "two": "{{.Count}} days", + "zero": "{{.Count}} days" + } + }, + { + "id": "my_height_in_meters", + "translation": { + "few": "I am {{.Count}} meters tall.", + "many": "I am {{.Count}} meters tall.", + "one": "I am {{.Count}} meters tall.", + "other": "I am {{.Count}} meters tall.", + "two": "I am {{.Count}} meters tall.", + "zero": "I am {{.Count}} meters tall." + } + }, + { + "id": "person_unread_email_count_timeframe", + "translation": { + "few": "{{.Person}} has {{.Count}} unread emails in the past {{.Timeframe}}.", + "many": "{{.Person}} has {{.Count}} unread emails in the past {{.Timeframe}}.", + "one": "{{.Person}} has {{.Count}} unread emails in the past {{.Timeframe}}.", + "other": "{{.Person}} has {{.Count}} unread emails in the past {{.Timeframe}}.", + "two": "{{.Person}} has {{.Count}} unread emails in the past {{.Timeframe}}.", + "zero": "{{.Person}} has {{.Count}} unread emails in the past {{.Timeframe}}." + } + }, + { + "id": "program_greeting", + "translation": "Hello world" + }, + { + "id": "your_unread_email_count", + "translation": { + "few": "You have {{.Count}} unread emails.", + "many": "You have {{.Count}} unread emails.", + "one": "You have {{.Count}} unread emails.", + "other": "You have {{.Count}} unread emails.", + "two": "You have {{.Count}} unread emails.", + "zero": "You have {{.Count}} unread emails." + } + } +] \ No newline at end of file diff --git a/internal/go-i18n/goi18n/testdata/expected/en-us.all.json b/internal/go-i18n/goi18n/testdata/expected/en-us.all.json new file mode 100644 index 0000000..5aedc23 --- /dev/null +++ b/internal/go-i18n/goi18n/testdata/expected/en-us.all.json @@ -0,0 +1,45 @@ +[ + { + "id": "d_days", + "translation": { + "one": "{{.Count}} day", + "other": "{{.Count}} days" + } + }, + { + "id": "my_height_in_meters", + "translation": { + "one": "I am {{.Count}} meter tall.", + "other": "I am {{.Count}} meters tall." + } + }, + { + "id": "person_greeting", + "translation": "Hello {{.Person}}" + }, + { + "id": "person_unread_email_count", + "translation": { + "one": "{{.Person}} has {{.Count}} unread email.", + "other": "{{.Person}} has {{.Count}} unread emails." + } + }, + { + "id": "person_unread_email_count_timeframe", + "translation": { + "one": "{{.Person}} has {{.Count}} unread email in the past {{.Timeframe}}.", + "other": "{{.Person}} has {{.Count}} unread emails in the past {{.Timeframe}}." + } + }, + { + "id": "program_greeting", + "translation": "Hello world" + }, + { + "id": "your_unread_email_count", + "translation": { + "one": "You have {{.Count}} unread email.", + "other": "You have {{.Count}} unread emails." + } + } +] \ No newline at end of file diff --git a/internal/go-i18n/goi18n/testdata/expected/en-us.untranslated.json b/internal/go-i18n/goi18n/testdata/expected/en-us.untranslated.json new file mode 100644 index 0000000..0637a08 --- /dev/null +++ b/internal/go-i18n/goi18n/testdata/expected/en-us.untranslated.json @@ -0,0 +1 @@ +[] \ No newline at end of file diff --git a/internal/go-i18n/goi18n/testdata/expected/flat/ar-ar.all.json b/internal/go-i18n/goi18n/testdata/expected/flat/ar-ar.all.json new file mode 100644 index 0000000..1adb99c --- /dev/null +++ b/internal/go-i18n/goi18n/testdata/expected/flat/ar-ar.all.json @@ -0,0 +1,43 @@ +{ + "d_days": { + "few": "new arabic few translation of d_days", + "many": "arabic many translation of d_days", + "one": "arabic one translation of d_days", + "other": "", + "two": "", + "zero": "" + }, + "my_height_in_meters": { + "few": "", + "many": "", + "one": "", + "other": "", + "two": "", + "zero": "" + }, + "person_greeting": { + "other": "new arabic translation of person_greeting" + }, + "person_unread_email_count": { + "few": "arabic few translation of person_unread_email_count", + "many": "arabic many translation of person_unread_email_count", + "one": "arabic one translation of person_unread_email_count", + "other": "arabic other translation of person_unread_email_count", + "two": "arabic two translation of person_unread_email_count", + "zero": "arabic zero translation of person_unread_email_count" + }, + "person_unread_email_count_timeframe": { + "other": "" + }, + "program_greeting": { + "other": "" + }, + "your_unread_email_count": { + "few": "", + "many": "", + "one": "", + "other": "", + "two": "", + "zero": "" + } +} \ No newline at end of file diff --git a/internal/go-i18n/goi18n/testdata/expected/flat/ar-ar.untranslated.json b/internal/go-i18n/goi18n/testdata/expected/flat/ar-ar.untranslated.json new file mode 100644 index 0000000..ea7aa7d --- /dev/null +++ b/internal/go-i18n/goi18n/testdata/expected/flat/ar-ar.untranslated.json @@ -0,0 +1,32 @@ +{ + "d_days": { + "few": "new arabic few translation of d_days", + "many": "arabic many translation of d_days", + "one": "arabic one translation of d_days", + "other": "{{.Count}} days", + "two": "{{.Count}} days", + "zero": "{{.Count}} days" + }, + "my_height_in_meters": { + "few": "I am {{.Count}} meters tall.", + "many": "I am {{.Count}} meters tall.", + "one": "I am {{.Count}} meters tall.", + "other": "I am {{.Count}} meters tall.", + "two": "I am {{.Count}} meters tall.", + "zero": "I am {{.Count}} meters tall." + }, + "person_unread_email_count_timeframe": { + "other": "{{.Person}} has {{.Count}} unread emails in the past {{.Timeframe}}." + }, + "program_greeting": { + "other": "Hello world" + }, + "your_unread_email_count": { + "few": "You have {{.Count}} unread emails.", + "many": "You have {{.Count}} unread emails.", + "one": "You have {{.Count}} unread emails.", + "other": "You have {{.Count}} unread emails.", + "two": "You have {{.Count}} unread emails.", + "zero": "You have {{.Count}} unread emails." + } +} \ No newline at end of file diff --git a/internal/go-i18n/goi18n/testdata/expected/flat/en-us.all.json b/internal/go-i18n/goi18n/testdata/expected/flat/en-us.all.json new file mode 100644 index 0000000..766b2a7 --- /dev/null +++ b/internal/go-i18n/goi18n/testdata/expected/flat/en-us.all.json @@ -0,0 +1,27 @@ +{ + "d_days": { + "one": "{{.Count}} day", + "other": "{{.Count}} days" + }, + "my_height_in_meters": { + "one": "I am {{.Count}} meter tall.", + "other": "I am {{.Count}} meters tall." + }, + "person_greeting": { + "other": "Hello {{.Person}}" + }, + "person_unread_email_count": { + "one": "{{.Person}} has {{.Count}} unread email.", + "other": "{{.Person}} has {{.Count}} unread emails." + }, + "person_unread_email_count_timeframe": { + "other": "{{.Person}} has {{.Count}} unread emails in the past {{.Timeframe}}." + }, + "program_greeting": { + "other": "Hello world" + }, + "your_unread_email_count": { + "one": "You have {{.Count}} unread email.", + "other": "You have {{.Count}} unread emails." + } +} \ No newline at end of file diff --git a/internal/go-i18n/goi18n/testdata/expected/flat/en-us.untranslated.json b/internal/go-i18n/goi18n/testdata/expected/flat/en-us.untranslated.json new file mode 100644 index 0000000..9e26dfe --- /dev/null +++ b/internal/go-i18n/goi18n/testdata/expected/flat/en-us.untranslated.json @@ -0,0 +1 @@ +{} \ No newline at end of file diff --git a/internal/go-i18n/goi18n/testdata/expected/flat/en-us.untranslated.json.json b/internal/go-i18n/goi18n/testdata/expected/flat/en-us.untranslated.json.json new file mode 100644 index 0000000..e69de29 diff --git a/internal/go-i18n/goi18n/testdata/expected/flat/fr-fr.all.json b/internal/go-i18n/goi18n/testdata/expected/flat/fr-fr.all.json new file mode 100644 index 0000000..b0ee031 --- /dev/null +++ b/internal/go-i18n/goi18n/testdata/expected/flat/fr-fr.all.json @@ -0,0 +1,27 @@ +{ + "d_days": { + "one": "", + "other": "" + }, + "my_height_in_meters": { + "one": "", + "other": "" + }, + "person_greeting": { + "other": "" + }, + "person_unread_email_count": { + "one": "", + "other": "" + }, + "person_unread_email_count_timeframe": { + "other": "" + }, + "program_greeting": { + "other": "" + }, + "your_unread_email_count": { + "one": "", + "other": "" + } +} \ No newline at end of file diff --git a/internal/go-i18n/goi18n/testdata/expected/flat/fr-fr.untranslated.json b/internal/go-i18n/goi18n/testdata/expected/flat/fr-fr.untranslated.json new file mode 100644 index 0000000..e6d5c4f --- /dev/null +++ b/internal/go-i18n/goi18n/testdata/expected/flat/fr-fr.untranslated.json @@ -0,0 +1,27 @@ +{ + "d_days": { + "one": "{{.Count}} days", + "other": "{{.Count}} days" + }, + "my_height_in_meters": { + "one": "I am {{.Count}} meters tall.", + "other": "I am {{.Count}} meters tall." + }, + "person_greeting": { + "other": "Hello {{.Person}}" + }, + "person_unread_email_count": { + "one": "{{.Person}} has {{.Count}} unread emails.", + "other": "{{.Person}} has {{.Count}} unread emails." + }, + "person_unread_email_count_timeframe": { + "other": "{{.Person}} has {{.Count}} unread emails in the past {{.Timeframe}}." + }, + "program_greeting": { + "other": "Hello world" + }, + "your_unread_email_count": { + "one": "You have {{.Count}} unread emails.", + "other": "You have {{.Count}} unread emails." + } +} \ No newline at end of file diff --git a/internal/go-i18n/goi18n/testdata/expected/fr-fr.all.json b/internal/go-i18n/goi18n/testdata/expected/fr-fr.all.json new file mode 100644 index 0000000..20e0b87 --- /dev/null +++ b/internal/go-i18n/goi18n/testdata/expected/fr-fr.all.json @@ -0,0 +1,45 @@ +[ + { + "id": "d_days", + "translation": { + "one": "", + "other": "" + } + }, + { + "id": "my_height_in_meters", + "translation": { + "one": "", + "other": "" + } + }, + { + "id": "person_greeting", + "translation": "" + }, + { + "id": "person_unread_email_count", + "translation": { + "one": "", + "other": "" + } + }, + { + "id": "person_unread_email_count_timeframe", + "translation": { + "one": "", + "other": "" + } + }, + { + "id": "program_greeting", + "translation": "" + }, + { + "id": "your_unread_email_count", + "translation": { + "one": "", + "other": "" + } + } +] \ No newline at end of file diff --git a/internal/go-i18n/goi18n/testdata/expected/fr-fr.untranslated.json b/internal/go-i18n/goi18n/testdata/expected/fr-fr.untranslated.json new file mode 100644 index 0000000..e2d3967 --- /dev/null +++ b/internal/go-i18n/goi18n/testdata/expected/fr-fr.untranslated.json @@ -0,0 +1,45 @@ +[ + { + "id": "d_days", + "translation": { + "one": "{{.Count}} days", + "other": "{{.Count}} days" + } + }, + { + "id": "my_height_in_meters", + "translation": { + "one": "I am {{.Count}} meters tall.", + "other": "I am {{.Count}} meters tall." + } + }, + { + "id": "person_greeting", + "translation": "Hello {{.Person}}" + }, + { + "id": "person_unread_email_count", + "translation": { + "one": "{{.Person}} has {{.Count}} unread emails.", + "other": "{{.Person}} has {{.Count}} unread emails." + } + }, + { + "id": "person_unread_email_count_timeframe", + "translation": { + "one": "{{.Person}} has {{.Count}} unread emails in the past {{.Timeframe}}.", + "other": "{{.Person}} has {{.Count}} unread emails in the past {{.Timeframe}}." + } + }, + { + "id": "program_greeting", + "translation": "Hello world" + }, + { + "id": "your_unread_email_count", + "translation": { + "one": "You have {{.Count}} unread emails.", + "other": "You have {{.Count}} unread emails." + } + } +] \ No newline at end of file diff --git a/internal/go-i18n/goi18n/testdata/input/ar-ar.one.json b/internal/go-i18n/goi18n/testdata/input/ar-ar.one.json new file mode 100644 index 0000000..f5af1d6 --- /dev/null +++ b/internal/go-i18n/goi18n/testdata/input/ar-ar.one.json @@ -0,0 +1,54 @@ +[ + { + "id": "d_days", + "translation": { + "few": "arabic few translation of d_days", + "many": "arabic many translation of d_days", + "one": "", + "other": "", + "two": "", + "zero": "" + } + }, + { + "id": "person_greeting", + "translation": "arabic translation of person_greeting" + }, + { + "id": "person_unread_email_count", + "translation": { + "few": "arabic few translation of person_unread_email_count", + "many": "arabic many translation of person_unread_email_count", + "one": "arabic one translation of person_unread_email_count", + "other": "", + "two": "", + "zero": "" + } + }, + { + "id": "person_unread_email_count_timeframe", + "translation": { + "few": "", + "many": "", + "one": "", + "other": "", + "two": "", + "zero": "" + } + }, + { + "id": "program_greeting", + "translation": "" + }, + { + "id": "your_unread_email_count", + "translation": { + "few": "", + "many": "", + "one": "", + "other": "", + "two": "", + "zero": "" + } + } +] diff --git a/internal/go-i18n/goi18n/testdata/input/ar-ar.two.json b/internal/go-i18n/goi18n/testdata/input/ar-ar.two.json new file mode 100644 index 0000000..e98d7e9 --- /dev/null +++ b/internal/go-i18n/goi18n/testdata/input/ar-ar.two.json @@ -0,0 +1,54 @@ +[ + { + "id": "d_days", + "translation": { + "few": "new arabic few translation of d_days", + "many": "", + "one": "arabic one translation of d_days", + "other": "", + "two": "", + "zero": "" + } + }, + { + "id": "person_greeting", + "translation": "new arabic translation of person_greeting" + }, + { + "id": "person_unread_email_count", + "translation": { + "few": "", + "many": "", + "one": "", + "other": "arabic other translation of person_unread_email_count", + "two": "arabic two translation of person_unread_email_count", + "zero": "arabic zero translation of person_unread_email_count" + } + }, + { + "id": "person_unread_email_count_timeframe", + "translation": { + "few": "", + "many": "", + "one": "", + "other": "", + "two": "", + "zero": "" + } + }, + { + "id": "program_greeting", + "translation": "" + }, + { + "id": "your_unread_email_count", + "translation": { + "few": "", + "many": "", + "one": "", + "other": "", + "two": "", + "zero": "" + } + } +] diff --git a/internal/go-i18n/goi18n/testdata/input/en-us.constants.json b/internal/go-i18n/goi18n/testdata/input/en-us.constants.json new file mode 100644 index 0000000..5aedc23 --- /dev/null +++ b/internal/go-i18n/goi18n/testdata/input/en-us.constants.json @@ -0,0 +1,45 @@ +[ + { + "id": "d_days", + "translation": { + "one": "{{.Count}} day", + "other": "{{.Count}} days" + } + }, + { + "id": "my_height_in_meters", + "translation": { + "one": "I am {{.Count}} meter tall.", + "other": "I am {{.Count}} meters tall." + } + }, + { + "id": "person_greeting", + "translation": "Hello {{.Person}}" + }, + { + "id": "person_unread_email_count", + "translation": { + "one": "{{.Person}} has {{.Count}} unread email.", + "other": "{{.Person}} has {{.Count}} unread emails." + } + }, + { + "id": "person_unread_email_count_timeframe", + "translation": { + "one": "{{.Person}} has {{.Count}} unread email in the past {{.Timeframe}}.", + "other": "{{.Person}} has {{.Count}} unread emails in the past {{.Timeframe}}." + } + }, + { + "id": "program_greeting", + "translation": "Hello world" + }, + { + "id": "your_unread_email_count", + "translation": { + "one": "You have {{.Count}} unread email.", + "other": "You have {{.Count}} unread emails." + } + } +] \ No newline at end of file diff --git a/internal/go-i18n/goi18n/testdata/input/en-us.one.json b/internal/go-i18n/goi18n/testdata/input/en-us.one.json new file mode 100644 index 0000000..63a9d6f --- /dev/null +++ b/internal/go-i18n/goi18n/testdata/input/en-us.one.json @@ -0,0 +1,30 @@ +[ + { + "id": "program_greeting", + "translation": "Hello world" + }, + { + "id": "your_unread_email_count", + "translation": { + "one": "You have {{.Count}} unread email.", + "other": "You have {{.Count}} unread emails." + } + }, + { + "id": "my_height_in_meters", + "translation": { + "one": "I am {{.Count}} meter tall.", + "other": "I am {{.Count}} meters tall." + } + }, + { + "id": "person_unread_email_count_timeframe", + "translation": { + "one": "{{.Person}} has {{.Count}} unread email in the past {{.Timeframe}}." + } + }, + { + "id": "d_days", + "translation": "this should get overwritten" + } +] diff --git a/internal/go-i18n/goi18n/testdata/input/en-us.two.json b/internal/go-i18n/goi18n/testdata/input/en-us.two.json new file mode 100644 index 0000000..dcc715c --- /dev/null +++ b/internal/go-i18n/goi18n/testdata/input/en-us.two.json @@ -0,0 +1,26 @@ +[ + { + "id": "person_greeting", + "translation": "Hello {{.Person}}" + }, + { + "id": "person_unread_email_count", + "translation": { + "one": "{{.Person}} has {{.Count}} unread email.", + "other": "{{.Person}} has {{.Count}} unread emails." + } + }, + { + "id": "person_unread_email_count_timeframe", + "translation": { + "other": "{{.Person}} has {{.Count}} unread emails in the past {{.Timeframe}}." + } + }, + { + "id": "d_days", + "translation": { + "one": "{{.Count}} day", + "other": "{{.Count}} days" + } + } +] diff --git a/internal/go-i18n/goi18n/testdata/input/flat/ar-ar.one.toml b/internal/go-i18n/goi18n/testdata/input/flat/ar-ar.one.toml new file mode 100644 index 0000000..364a62c --- /dev/null +++ b/internal/go-i18n/goi18n/testdata/input/flat/ar-ar.one.toml @@ -0,0 +1,37 @@ +[d_days] +few = "arabic few translation of d_days" +many = "arabic many translation of d_days" +one = "" +other = "" +two = "" +zero = "" + +[person_greeting] +other = "arabic translation of person_greeting" + +[person_unread_email_count] +few = "arabic few translation of person_unread_email_count" +many = "arabic many translation of person_unread_email_count" +one = "arabic one translation of person_unread_email_count" +other = "" +two = "" +zero = "" + +[person_unread_email_count_timeframe] +few = "" +many = "" +one = "" +other = "" +two = "" +zero = "" + +[program_greeting] +other = "" + +[your_unread_email_count] +few = "" +many = "" +one = "" +other = "" +two = "" +zero = "" diff --git a/internal/go-i18n/goi18n/testdata/input/flat/ar-ar.two.json b/internal/go-i18n/goi18n/testdata/input/flat/ar-ar.two.json new file mode 100644 index 0000000..5e6fba4 --- /dev/null +++ b/internal/go-i18n/goi18n/testdata/input/flat/ar-ar.two.json @@ -0,0 +1,45 @@ +{ + "d_days": { + "few": "new arabic few translation of d_days", + "many": "", + "one": "arabic one translation of d_days", + "other": "", + "two": "", + "zero": "" + }, + + "person_greeting": { + "other": "new arabic translation of person_greeting" + }, + + "person_unread_email_count": { + "few": "", + "many": "", + "one": "", + "other": "arabic other translation of person_unread_email_count", + "two": "arabic two translation of person_unread_email_count", + "zero": "arabic zero translation of person_unread_email_count" + }, + + "person_unread_email_count_timeframe": { + "few": "", + "many": "", + "one": "", + "other": "", + "two": "", + "zero": "" + }, + + "program_greeting": { + "other": "" + }, + + "your_unread_email_count": { + "few": "", + "many": "", + "one": "", + "other": "", + "two": "", + "zero": "" + } +} diff --git a/internal/go-i18n/goi18n/testdata/input/flat/en-us.constants.json b/internal/go-i18n/goi18n/testdata/input/flat/en-us.constants.json new file mode 100644 index 0000000..c41b2b9 --- /dev/null +++ b/internal/go-i18n/goi18n/testdata/input/flat/en-us.constants.json @@ -0,0 +1,34 @@ +{ + "d_days": { + "one": "{{.Count}} day", + "other": "{{.Count}} days" + }, + + "my_height_in_meters": { + "one": "I am {{.Count}} meter tall.", + "other": "I am {{.Count}} meters tall." + }, + + "person_greeting": { + "other": "Hello {{.Person}}" + }, + + "person_unread_email_count": { + "one": "{{.Person}} has {{.Count}} unread email.", + "other": "{{.Person}} has {{.Count}} unread emails." + }, + + "person_unread_email_count_timeframe": { + "one": "{{.Person}} has {{.Count}} unread email in the past {{.Timeframe}}.", + "other": "{{.Person}} has {{.Count}} unread emails in the past {{.Timeframe}}." + }, + + "program_greeting": { + "other": "Hello world" + }, + + "your_unread_email_count": { + "one": "You have {{.Count}} unread email.", + "other": "You have {{.Count}} unread emails." + } +} diff --git a/internal/go-i18n/goi18n/testdata/input/flat/en-us.one.yaml b/internal/go-i18n/goi18n/testdata/input/flat/en-us.one.yaml new file mode 100644 index 0000000..02ae001 --- /dev/null +++ b/internal/go-i18n/goi18n/testdata/input/flat/en-us.one.yaml @@ -0,0 +1,16 @@ +program_greeting: + other: "Hello world" + +your_unread_email_count: + one: "You have {{.Count}} unread email." + other: "You have {{.Count}} unread emails." + +my_height_in_meters: + one: "I am {{.Count}} meter tall." + other: "I am {{.Count}} meters tall." + +person_unread_email_count_timeframe: + other: "{{.Person}} has {{.Count}} unread email in the past {{.Timeframe}}." + +d_days: + other: "this should get overwritten" diff --git a/internal/go-i18n/goi18n/testdata/input/flat/en-us.two.json b/internal/go-i18n/goi18n/testdata/input/flat/en-us.two.json new file mode 100644 index 0000000..06bd28d --- /dev/null +++ b/internal/go-i18n/goi18n/testdata/input/flat/en-us.two.json @@ -0,0 +1,19 @@ +{ + "person_greeting": { + "other": "Hello {{.Person}}" + }, + + "person_unread_email_count": { + "one": "{{.Person}} has {{.Count}} unread email.", + "other": "{{.Person}} has {{.Count}} unread emails." + }, + + "person_unread_email_count_timeframe": { + "other": "{{.Person}} has {{.Count}} unread emails in the past {{.Timeframe}}." + }, + + "d_days": { + "one": "{{.Count}} day", + "other": "{{.Count}} days" + } +} diff --git a/internal/go-i18n/goi18n/testdata/input/flat/fr-fr.json b/internal/go-i18n/goi18n/testdata/input/flat/fr-fr.json new file mode 100644 index 0000000..e69de29 diff --git a/internal/go-i18n/goi18n/testdata/input/fr-fr.json b/internal/go-i18n/goi18n/testdata/input/fr-fr.json new file mode 100644 index 0000000..e69de29 diff --git a/internal/go-i18n/goi18n/testdata/input/yaml/ar-ar.one.json b/internal/go-i18n/goi18n/testdata/input/yaml/ar-ar.one.json new file mode 100644 index 0000000..f5af1d6 --- /dev/null +++ b/internal/go-i18n/goi18n/testdata/input/yaml/ar-ar.one.json @@ -0,0 +1,54 @@ +[ + { + "id": "d_days", + "translation": { + "few": "arabic few translation of d_days", + "many": "arabic many translation of d_days", + "one": "", + "other": "", + "two": "", + "zero": "" + } + }, + { + "id": "person_greeting", + "translation": "arabic translation of person_greeting" + }, + { + "id": "person_unread_email_count", + "translation": { + "few": "arabic few translation of person_unread_email_count", + "many": "arabic many translation of person_unread_email_count", + "one": "arabic one translation of person_unread_email_count", + "other": "", + "two": "", + "zero": "" + } + }, + { + "id": "person_unread_email_count_timeframe", + "translation": { + "few": "", + "many": "", + "one": "", + "other": "", + "two": "", + "zero": "" + } + }, + { + "id": "program_greeting", + "translation": "" + }, + { + "id": "your_unread_email_count", + "translation": { + "few": "", + "many": "", + "one": "", + "other": "", + "two": "", + "zero": "" + } + } +] diff --git a/internal/go-i18n/goi18n/testdata/input/yaml/ar-ar.two.json b/internal/go-i18n/goi18n/testdata/input/yaml/ar-ar.two.json new file mode 100644 index 0000000..e98d7e9 --- /dev/null +++ b/internal/go-i18n/goi18n/testdata/input/yaml/ar-ar.two.json @@ -0,0 +1,54 @@ +[ + { + "id": "d_days", + "translation": { + "few": "new arabic few translation of d_days", + "many": "", + "one": "arabic one translation of d_days", + "other": "", + "two": "", + "zero": "" + } + }, + { + "id": "person_greeting", + "translation": "new arabic translation of person_greeting" + }, + { + "id": "person_unread_email_count", + "translation": { + "few": "", + "many": "", + "one": "", + "other": "arabic other translation of person_unread_email_count", + "two": "arabic two translation of person_unread_email_count", + "zero": "arabic zero translation of person_unread_email_count" + } + }, + { + "id": "person_unread_email_count_timeframe", + "translation": { + "few": "", + "many": "", + "one": "", + "other": "", + "two": "", + "zero": "" + } + }, + { + "id": "program_greeting", + "translation": "" + }, + { + "id": "your_unread_email_count", + "translation": { + "few": "", + "many": "", + "one": "", + "other": "", + "two": "", + "zero": "" + } + } +] diff --git a/internal/go-i18n/goi18n/testdata/input/yaml/en-us.one.yaml b/internal/go-i18n/goi18n/testdata/input/yaml/en-us.one.yaml new file mode 100644 index 0000000..3ca8e38 --- /dev/null +++ b/internal/go-i18n/goi18n/testdata/input/yaml/en-us.one.yaml @@ -0,0 +1,19 @@ +- id: program_greeting + translation: Hello world + +- id: your_unread_email_count + translation: + one: You have {{.Count}} unread email. + other: You have {{.Count}} unread emails. + +- id: my_height_in_meters + translation: + one: I am {{.Count}} meter tall. + other: I am {{.Count}} meters tall. + +- id: person_unread_email_count_timeframe + translation: + one: "{{.Person}} has {{.Count}} unread email in the past {{.Timeframe}}." + +- id: d_days + translation: this should get overwritten \ No newline at end of file diff --git a/internal/go-i18n/goi18n/testdata/input/yaml/en-us.two.json b/internal/go-i18n/goi18n/testdata/input/yaml/en-us.two.json new file mode 100644 index 0000000..dcc715c --- /dev/null +++ b/internal/go-i18n/goi18n/testdata/input/yaml/en-us.two.json @@ -0,0 +1,26 @@ +[ + { + "id": "person_greeting", + "translation": "Hello {{.Person}}" + }, + { + "id": "person_unread_email_count", + "translation": { + "one": "{{.Person}} has {{.Count}} unread email.", + "other": "{{.Person}} has {{.Count}} unread emails." + } + }, + { + "id": "person_unread_email_count_timeframe", + "translation": { + "other": "{{.Person}} has {{.Count}} unread emails in the past {{.Timeframe}}." + } + }, + { + "id": "d_days", + "translation": { + "one": "{{.Count}} day", + "other": "{{.Count}} days" + } + } +] diff --git a/internal/go-i18n/goi18n/testdata/input/yaml/fr-fr.json b/internal/go-i18n/goi18n/testdata/input/yaml/fr-fr.json new file mode 100644 index 0000000..e69de29 diff --git a/internal/go-i18n/i18n/bundle/bundle.go b/internal/go-i18n/i18n/bundle/bundle.go new file mode 100644 index 0000000..c214444 --- /dev/null +++ b/internal/go-i18n/i18n/bundle/bundle.go @@ -0,0 +1,444 @@ +// Package bundle manages translations for multiple languages. +package bundle + +import ( + "bytes" + "encoding/json" + "fmt" + "io/ioutil" + "path/filepath" + "reflect" + "sync" + "unicode" + + "github.com/gobuffalo/mw-i18n/internal/go-i18n/i18n/language" + "github.com/gobuffalo/mw-i18n/internal/go-i18n/i18n/translation" + toml "github.com/pelletier/go-toml" + "gopkg.in/yaml.v2" +) + +// TranslateFunc is a copy of i18n.TranslateFunc to avoid a circular dependency. +type TranslateFunc func(translationID string, args ...interface{}) string + +// Bundle stores the translations for multiple languages. +type Bundle struct { + // The primary translations for a language tag and translation id. + translations map[string]map[string]translation.Translation + + // Translations that can be used when an exact language match is not possible. + fallbackTranslations map[string]map[string]translation.Translation + + sync.RWMutex +} + +// New returns an empty bundle. +func New() *Bundle { + return &Bundle{ + translations: make(map[string]map[string]translation.Translation), + fallbackTranslations: make(map[string]map[string]translation.Translation), + } +} + +// MustLoadTranslationFile is similar to LoadTranslationFile +// except it panics if an error happens. +func (b *Bundle) MustLoadTranslationFile(filename string) { + if err := b.LoadTranslationFile(filename); err != nil { + panic(err) + } +} + +// LoadTranslationFile loads the translations from filename into memory. +// +// The language that the translations are associated with is parsed from the filename (e.g. en-US.json). +// +// Generally you should load translation files once during your program's initialization. +func (b *Bundle) LoadTranslationFile(filename string) error { + buf, err := ioutil.ReadFile(filename) + if err != nil { + return err + } + return b.ParseTranslationFileBytes(filename, buf) +} + +// ParseTranslationFileBytes is similar to LoadTranslationFile except it parses the bytes in buf. +// +// It is useful for parsing translation files embedded with go-bindata. +func (b *Bundle) ParseTranslationFileBytes(filename string, buf []byte) error { + basename := filepath.Base(filename) + langs := language.Parse(basename) + switch l := len(langs); { + case l == 0: + return fmt.Errorf("no language found in %q", basename) + case l > 1: + return fmt.Errorf("multiple languages found in filename %q: %v; expected one", basename, langs) + } + translations, err := parseTranslations(filename, buf) + if err != nil { + return err + } + b.AddTranslation(langs[0], translations...) + return nil +} + +func parseTranslations(filename string, buf []byte) ([]translation.Translation, error) { + if len(buf) == 0 { + return []translation.Translation{}, nil + } + + ext := filepath.Ext(filename) + + // `github.com/pelletier/go-toml` lacks an Unmarshal function, + // so we should parse TOML separately. + if ext == ".toml" { + tree, err := toml.LoadReader(bytes.NewReader(buf)) + if err != nil { + return nil, err + } + + m := make(map[string]map[string]interface{}) + for k, v := range tree.ToMap() { + m[k] = v.(map[string]interface{}) + } + + return parseFlatFormat(m) + } + + // Then parse other formats. + if isStandardFormat(ext, buf) { + var standardFormat []map[string]interface{} + if err := unmarshal(ext, buf, &standardFormat); err != nil { + return nil, fmt.Errorf("failed to unmarshal %v: %v", filename, err) + } + return parseStandardFormat(standardFormat) + } else { + var flatFormat map[string]map[string]interface{} + if err := unmarshal(ext, buf, &flatFormat); err != nil { + return nil, fmt.Errorf("failed to unmarshal %v: %v", filename, err) + } + return parseFlatFormat(flatFormat) + } +} + +func isStandardFormat(ext string, buf []byte) bool { + buf = deleteLeadingComments(ext, buf) + firstRune := rune(buf[0]) + return (ext == ".json" && firstRune == '[') || (ext == ".yaml" && firstRune == '-') +} + +// deleteLeadingComments deletes leading newlines and comments in buf. +// It only works for ext == ".yaml". +func deleteLeadingComments(ext string, buf []byte) []byte { + if ext != ".yaml" { + return buf + } + + for { + buf = bytes.TrimLeftFunc(buf, unicode.IsSpace) + if buf[0] == '#' { + buf = deleteLine(buf) + } else { + break + } + } + + return buf +} + +func deleteLine(buf []byte) []byte { + index := bytes.IndexRune(buf, '\n') + if index == -1 { // If there is only one line without newline ... + return nil // ... delete it and return nothing. + } + if index == len(buf)-1 { // If there is only one line with newline ... + return nil // ... do the same as above. + } + return buf[index+1:] +} + +// unmarshal finds an appropriate unmarshal function for ext +// (extension of filename) and unmarshals buf to out. out must be a pointer. +func unmarshal(ext string, buf []byte, out interface{}) error { + switch ext { + case ".json": + return json.Unmarshal(buf, out) + case ".yaml": + return yaml.Unmarshal(buf, out) + } + + return fmt.Errorf("unsupported file extension %v", ext) +} + +func parseStandardFormat(data []map[string]interface{}) ([]translation.Translation, error) { + translations := make([]translation.Translation, 0, len(data)) + for i, translationData := range data { + t, err := translation.NewTranslation(translationData) + if err != nil { + return nil, fmt.Errorf("unable to parse translation #%d because %s\n%v", i, err, translationData) + } + translations = append(translations, t) + } + return translations, nil +} + +// parseFlatFormat just converts data from flat format to standard format +// and passes it to parseStandardFormat. +// +// Flat format logic: +// key of data must be a string and data[key] must be always map[string]interface{}, +// but if there is only "other" key in it then it is non-plural, else plural. +func parseFlatFormat(data map[string]map[string]interface{}) ([]translation.Translation, error) { + var standardFormatData []map[string]interface{} + for id, translationData := range data { + dataObject := make(map[string]interface{}) + dataObject["id"] = id + if len(translationData) == 1 { // non-plural form + _, otherExists := translationData["other"] + if otherExists { + dataObject["translation"] = translationData["other"] + } + } else { // plural form + dataObject["translation"] = translationData + } + + standardFormatData = append(standardFormatData, dataObject) + } + + return parseStandardFormat(standardFormatData) +} + +// AddTranslation adds translations for a language. +// +// It is useful if your translations are in a format not supported by LoadTranslationFile. +func (b *Bundle) AddTranslation(lang *language.Language, translations ...translation.Translation) { + b.Lock() + defer b.Unlock() + if b.translations[lang.Tag] == nil { + b.translations[lang.Tag] = make(map[string]translation.Translation, len(translations)) + } + currentTranslations := b.translations[lang.Tag] + for _, newTranslation := range translations { + if currentTranslation := currentTranslations[newTranslation.ID()]; currentTranslation != nil { + currentTranslations[newTranslation.ID()] = currentTranslation.Merge(newTranslation) + } else { + currentTranslations[newTranslation.ID()] = newTranslation + } + } + + // lang can provide translations for less specific language tags. + for _, tag := range lang.MatchingTags() { + b.fallbackTranslations[tag] = currentTranslations + } +} + +// Translations returns all translations in the bundle. +func (b *Bundle) Translations() map[string]map[string]translation.Translation { + t := make(map[string]map[string]translation.Translation) + b.RLock() + for tag, translations := range b.translations { + t[tag] = make(map[string]translation.Translation) + for id, translation := range translations { + t[tag][id] = translation + } + } + b.RUnlock() + return t +} + +// LanguageTags returns the tags of all languages that that have been added. +func (b *Bundle) LanguageTags() []string { + var tags []string + b.RLock() + for k := range b.translations { + tags = append(tags, k) + } + b.RUnlock() + return tags +} + +// LanguageTranslationIDs returns the ids of all translations that have been added for a given language. +func (b *Bundle) LanguageTranslationIDs(languageTag string) []string { + var ids []string + b.RLock() + for id := range b.translations[languageTag] { + ids = append(ids, id) + } + b.RUnlock() + return ids +} + +// MustTfunc is similar to Tfunc except it panics if an error happens. +func (b *Bundle) MustTfunc(pref string, prefs ...string) TranslateFunc { + tfunc, err := b.Tfunc(pref, prefs...) + if err != nil { + panic(err) + } + return tfunc +} + +// MustTfuncAndLanguage is similar to TfuncAndLanguage except it panics if an error happens. +func (b *Bundle) MustTfuncAndLanguage(pref string, prefs ...string) (TranslateFunc, *language.Language) { + tfunc, language, err := b.TfuncAndLanguage(pref, prefs...) + if err != nil { + panic(err) + } + return tfunc, language +} + +// Tfunc is similar to TfuncAndLanguage except is doesn't return the Language. +func (b *Bundle) Tfunc(pref string, prefs ...string) (TranslateFunc, error) { + tfunc, _, err := b.TfuncAndLanguage(pref, prefs...) + return tfunc, err +} + +// TfuncAndLanguage returns a TranslateFunc for the first Language that +// has a non-zero number of translations in the bundle. +// +// The returned Language matches the the first language preference that could be satisfied, +// but this may not strictly match the language of the translations used to satisfy that preference. +// +// For example, the user may request "zh". If there are no translations for "zh" but there are translations +// for "zh-cn", then the translations for "zh-cn" will be used but the returned Language will be "zh". +// +// It can parse languages from Accept-Language headers (RFC 2616), +// but it assumes weights are monotonically decreasing. +func (b *Bundle) TfuncAndLanguage(pref string, prefs ...string) (TranslateFunc, *language.Language, error) { + lang := b.supportedLanguage(pref, prefs...) + var err error + if lang == nil { + err = fmt.Errorf("no supported languages found %#v", append(prefs, pref)) + } + return func(translationID string, args ...interface{}) string { + return b.translate(lang, translationID, args...) + }, lang, err +} + +// supportedLanguage returns the first language which +// has a non-zero number of translations in the bundle. +func (b *Bundle) supportedLanguage(pref string, prefs ...string) *language.Language { + lang := b.translatedLanguage(pref) + if lang == nil { + for _, pref := range prefs { + lang = b.translatedLanguage(pref) + if lang != nil { + break + } + } + } + return lang +} + +func (b *Bundle) translatedLanguage(src string) *language.Language { + langs := language.Parse(src) + b.RLock() + defer b.RUnlock() + for _, lang := range langs { + if len(b.translations[lang.Tag]) > 0 || + len(b.fallbackTranslations[lang.Tag]) > 0 { + return lang + } + } + return nil +} + +func (b *Bundle) translate(lang *language.Language, translationID string, args ...interface{}) string { + if lang == nil { + return translationID + } + + translation := b.translation(lang, translationID) + if translation == nil { + return translationID + } + + var data interface{} + var count interface{} + if argc := len(args); argc > 0 { + if isNumber(args[0]) { + count = args[0] + if argc > 1 { + data = args[1] + } + } else { + data = args[0] + } + } + + if count != nil { + if data == nil { + data = map[string]interface{}{"Count": count} + } else { + dataMap := toMap(data) + dataMap["Count"] = count + data = dataMap + } + } else { + dataMap := toMap(data) + if c, ok := dataMap["Count"]; ok { + count = c + } + } + + p, _ := lang.Plural(count) + template := translation.Template(p) + if template == nil { + return translationID + } + + s := template.Execute(data) + if s == "" { + return translationID + } + return s +} + +func (b *Bundle) translation(lang *language.Language, translationID string) translation.Translation { + b.RLock() + defer b.RUnlock() + translations := b.translations[lang.Tag] + if translations == nil { + translations = b.fallbackTranslations[lang.Tag] + if translations == nil { + return nil + } + } + return translations[translationID] +} + +func isNumber(n interface{}) bool { + switch n.(type) { + case int, int8, int16, int32, int64, string: + return true + } + return false +} + +func toMap(input interface{}) map[string]interface{} { + if data, ok := input.(map[string]interface{}); ok { + return data + } + v := reflect.ValueOf(input) + switch v.Kind() { + case reflect.Ptr: + return toMap(v.Elem().Interface()) + case reflect.Struct: + return structToMap(v) + default: + return nil + } +} + +// Converts the top level of a struct to a map[string]interface{}. +// Code inspired by github.com/fatih/structs. +func structToMap(v reflect.Value) map[string]interface{} { + out := make(map[string]interface{}) + t := v.Type() + for i := 0; i < t.NumField(); i++ { + field := t.Field(i) + if field.PkgPath != "" { + // unexported field. skip. + continue + } + out[field.Name] = v.FieldByName(field.Name).Interface() + } + return out +} diff --git a/internal/go-i18n/i18n/bundle/bundle_test.go b/internal/go-i18n/i18n/bundle/bundle_test.go new file mode 100644 index 0000000..f10a009 --- /dev/null +++ b/internal/go-i18n/i18n/bundle/bundle_test.go @@ -0,0 +1,364 @@ +package bundle + +import ( + "fmt" + "strconv" + "sync" + "testing" + + "reflect" + "sort" + + "github.com/gobuffalo/mw-i18n/internal/go-i18n/i18n/language" + "github.com/gobuffalo/mw-i18n/internal/go-i18n/i18n/translation" +) + +func TestMustLoadTranslationFile(t *testing.T) { + t.Skipf("not implemented") +} + +func TestLoadTranslationFile(t *testing.T) { + t.Skipf("not implemented") +} + +func TestParseTranslationFileBytes(t *testing.T) { + t.Skipf("not implemented") +} + +func TestAddTranslation(t *testing.T) { + t.Skipf("not implemented") +} + +func TestMustTfunc(t *testing.T) { + defer func() { + if r := recover(); r == nil { + t.Errorf("expected MustTfunc to panic") + } + }() + New().MustTfunc("invalid") +} + +func TestLanguageTagsAndTranslationIDs(t *testing.T) { + b := New() + translationID := "translation_id" + englishLanguage := languageWithTag("en-US") + frenchLanguage := languageWithTag("fr-FR") + spanishLanguage := languageWithTag("es") + addFakeTranslation(t, b, englishLanguage, "English"+translationID) + addFakeTranslation(t, b, frenchLanguage, translationID) + addFakeTranslation(t, b, spanishLanguage, translationID) + + tags := b.LanguageTags() + sort.Strings(tags) + compareTo := []string{englishLanguage.Tag, spanishLanguage.Tag, frenchLanguage.Tag} + if !reflect.DeepEqual(tags, compareTo) { + t.Errorf("LanguageTags() = %#v; expected: %#v", tags, compareTo) + } + + ids := b.LanguageTranslationIDs(englishLanguage.Tag) + sort.Strings(ids) + compareTo = []string{"English" + translationID} + if !reflect.DeepEqual(ids, compareTo) { + t.Errorf("LanguageTranslationIDs() = %#v; expected: %#v", ids, compareTo) + } +} + +func TestTfuncAndLanguage(t *testing.T) { + b := New() + translationID := "translation_id" + englishLanguage := languageWithTag("en-US") + frenchLanguage := languageWithTag("fr-FR") + spanishLanguage := languageWithTag("es") + chineseLanguage := languageWithTag("zh-hans-cn") + englishTranslation := addFakeTranslation(t, b, englishLanguage, translationID) + frenchTranslation := addFakeTranslation(t, b, frenchLanguage, translationID) + spanishTranslation := addFakeTranslation(t, b, spanishLanguage, translationID) + chineseTranslation := addFakeTranslation(t, b, chineseLanguage, translationID) + + tests := []struct { + languageIDs []string + result string + expectedLanguage *language.Language + }{ + { + []string{"invalid"}, + translationID, + nil, + }, + { + []string{"invalid", "invalid2"}, + translationID, + nil, + }, + { + []string{"invalid", "en-US"}, + englishTranslation, + englishLanguage, + }, + { + []string{"en-US", "invalid"}, + englishTranslation, + englishLanguage, + }, + { + []string{"en-US", "fr-FR"}, + englishTranslation, + englishLanguage, + }, + { + []string{"invalid", "es"}, + spanishTranslation, + spanishLanguage, + }, + { + []string{"zh-CN,fr-XX,es"}, + spanishTranslation, + spanishLanguage, + }, + { + []string{"fr"}, + frenchTranslation, + + // The language is still "fr" even though the translation is provided by "fr-FR" + languageWithTag("fr"), + }, + { + []string{"zh"}, + chineseTranslation, + + // The language is still "zh" even though the translation is provided by "zh-hans-cn" + languageWithTag("zh"), + }, + { + []string{"zh-hans"}, + chineseTranslation, + + // The language is still "zh-hans" even though the translation is provided by "zh-hans-cn" + languageWithTag("zh-hans"), + }, + { + []string{"zh-hans-cn"}, + chineseTranslation, + languageWithTag("zh-hans-cn"), + }, + } + + for i, test := range tests { + tf, lang, err := b.TfuncAndLanguage(test.languageIDs[0], test.languageIDs[1:]...) + if err != nil && test.expectedLanguage != nil { + t.Errorf("Tfunc(%v) = error{%q}; expected no error", test.languageIDs, err) + } + if err == nil && test.expectedLanguage == nil { + t.Errorf("Tfunc(%v) = nil error; expected error", test.languageIDs) + } + if result := tf(translationID); result != test.result { + t.Errorf("translation %d was %s; expected %s", i, result, test.result) + } + if (lang == nil && test.expectedLanguage != nil) || + (lang != nil && test.expectedLanguage == nil) || + (lang != nil && test.expectedLanguage != nil && lang.String() != test.expectedLanguage.String()) { + t.Errorf("lang %d was %s; expected %s", i, lang, test.expectedLanguage) + } + } +} + +func TestConcurrent(t *testing.T) { + b := New() + // bootstrap bundle + translationID := "translation_id" // +1 + englishLanguage := languageWithTag("en-US") + addFakeTranslation(t, b, englishLanguage, translationID) + + tf, err := b.Tfunc(englishLanguage.Tag) + if err != nil { + t.Errorf("Tfunc(%v) = error{%q}; expected no error", []string{englishLanguage.Tag}, err) + } + + const iterations = 1000 + var wg sync.WaitGroup + wg.Add(iterations) + + // Using go routines insert 1000 ints into our map. + go func() { + for i := 0; i < iterations/2; i++ { + // Add item to map. + translationID := strconv.FormatInt(int64(i), 10) + addFakeTranslation(t, b, englishLanguage, translationID) + + // Retrieve item from map. + tf(translationID) + + wg.Done() + } // Call go routine with current index. + }() + + go func() { + for i := iterations / 2; i < iterations; i++ { + // Add item to map. + translationID := strconv.FormatInt(int64(i), 10) + addFakeTranslation(t, b, englishLanguage, translationID) + + // Retrieve item from map. + tf(translationID) + + wg.Done() + } // Call go routine with current index. + }() + + // Wait for all go routines to finish. + wg.Wait() + + // Make sure map contains 1000+1 elements. + count := len(b.Translations()[englishLanguage.Tag]) + if count != iterations+1 { + t.Error("Expecting 1001 elements, got", count) + } +} + +func addFakeTranslation(t *testing.T, b *Bundle, lang *language.Language, translationID string) string { + translation := fakeTranslation(lang, translationID) + b.AddTranslation(lang, testNewTranslation(t, map[string]interface{}{ + "id": translationID, + "translation": translation, + })) + return translation +} + +func fakeTranslation(lang *language.Language, translationID string) string { + return fmt.Sprintf("%s(%s)", lang.Tag, translationID) +} + +func testNewTranslation(t *testing.T, data map[string]interface{}) translation.Translation { + translation, err := translation.NewTranslation(data) + if err != nil { + t.Fatal(err) + } + return translation +} + +func languageWithTag(tag string) *language.Language { + return language.MustParse(tag)[0] +} + +func createBenchmarkTranslateFunc(b *testing.B, translationTemplate interface{}, count interface{}, expected string) func(data interface{}) { + bundle := New() + lang := "en-US" + translationID := "translation_id" + translation, err := translation.NewTranslation(map[string]interface{}{ + "id": translationID, + "translation": translationTemplate, + }) + if err != nil { + b.Fatal(err) + } + bundle.AddTranslation(languageWithTag(lang), translation) + tf, err := bundle.Tfunc(lang) + if err != nil { + b.Fatal(err) + } + return func(data interface{}) { + var result string + if count == nil { + result = tf(translationID, data) + } else { + result = tf(translationID, count, data) + } + if result != expected { + b.Fatalf("expected %q, got %q", expected, result) + } + } +} + +func createBenchmarkPluralTranslateFunc(b *testing.B) func(data interface{}) { + translationTemplate := map[string]interface{}{ + "one": "{{.Person}} is {{.Count}} year old.", + "other": "{{.Person}} is {{.Count}} years old.", + } + count := 26 + expected := "Bob is 26 years old." + return createBenchmarkTranslateFunc(b, translationTemplate, count, expected) +} + +func createBenchmarkNonPluralTranslateFunc(b *testing.B) func(data interface{}) { + translationTemplate := "Hi {{.Person}}!" + expected := "Hi Bob!" + return createBenchmarkTranslateFunc(b, translationTemplate, nil, expected) +} + +func BenchmarkTranslateNonPluralWithMap(b *testing.B) { + data := map[string]interface{}{ + "Person": "Bob", + } + tf := createBenchmarkNonPluralTranslateFunc(b) + b.ResetTimer() + for i := 0; i < b.N; i++ { + tf(data) + } +} + +func BenchmarkTranslateNonPluralWithStruct(b *testing.B) { + data := struct{ Person string }{Person: "Bob"} + tf := createBenchmarkNonPluralTranslateFunc(b) + b.ResetTimer() + for i := 0; i < b.N; i++ { + tf(data) + } +} + +func BenchmarkTranslateNonPluralWithStructPointer(b *testing.B) { + data := &struct{ Person string }{Person: "Bob"} + tf := createBenchmarkNonPluralTranslateFunc(b) + b.ResetTimer() + for i := 0; i < b.N; i++ { + tf(data) + } +} + +func BenchmarkTranslatePluralWithMap(b *testing.B) { + data := map[string]interface{}{ + "Person": "Bob", + } + tf := createBenchmarkPluralTranslateFunc(b) + b.ResetTimer() + for i := 0; i < b.N; i++ { + tf(data) + } +} + +func BenchmarkTranslatePluralWithMapAndCountField(b *testing.B) { + data := map[string]interface{}{ + "Person": "Bob", + "Count": 26, + } + + translationTemplate := map[string]interface{}{ + "one": "{{.Person}} is {{.Count}} year old.", + "other": "{{.Person}} is {{.Count}} years old.", + } + expected := "Bob is 26 years old." + + tf := createBenchmarkTranslateFunc(b, translationTemplate, nil, expected) + + b.ResetTimer() + for i := 0; i < b.N; i++ { + tf(data) + } +} + +func BenchmarkTranslatePluralWithStruct(b *testing.B) { + data := struct{ Person string }{Person: "Bob"} + tf := createBenchmarkPluralTranslateFunc(b) + b.ResetTimer() + for i := 0; i < b.N; i++ { + tf(data) + } +} + +func BenchmarkTranslatePluralWithStructPointer(b *testing.B) { + data := &struct{ Person string }{Person: "Bob"} + tf := createBenchmarkPluralTranslateFunc(b) + b.ResetTimer() + for i := 0; i < b.N; i++ { + tf(data) + } +} diff --git a/internal/go-i18n/i18n/example_test.go b/internal/go-i18n/i18n/example_test.go new file mode 100644 index 0000000..8120376 --- /dev/null +++ b/internal/go-i18n/i18n/example_test.go @@ -0,0 +1,97 @@ +package i18n_test + +import ( + "fmt" + + "github.com/gobuffalo/mw-i18n/internal/go-i18n/i18n" +) + +func Example() { + i18n.MustLoadTranslationFile("../goi18n/testdata/expected/en-us.all.json") + + T, _ := i18n.Tfunc("en-US") + + bobMap := map[string]interface{}{"Person": "Bob"} + bobStruct := struct{ Person string }{Person: "Bob"} + + fmt.Println(T("program_greeting")) + fmt.Println(T("person_greeting", bobMap)) + fmt.Println(T("person_greeting", bobStruct)) + + fmt.Println(T("your_unread_email_count", 0)) + fmt.Println(T("your_unread_email_count", 1)) + fmt.Println(T("your_unread_email_count", 2)) + fmt.Println(T("my_height_in_meters", "1.7")) + + fmt.Println(T("person_unread_email_count", 0, bobMap)) + fmt.Println(T("person_unread_email_count", 1, bobMap)) + fmt.Println(T("person_unread_email_count", 2, bobMap)) + fmt.Println(T("person_unread_email_count", 0, bobStruct)) + fmt.Println(T("person_unread_email_count", 1, bobStruct)) + fmt.Println(T("person_unread_email_count", 2, bobStruct)) + + type Count struct{ Count int } + fmt.Println(T("your_unread_email_count", Count{0})) + fmt.Println(T("your_unread_email_count", Count{1})) + fmt.Println(T("your_unread_email_count", Count{2})) + + fmt.Println(T("your_unread_email_count", map[string]interface{}{"Count": 0})) + fmt.Println(T("your_unread_email_count", map[string]interface{}{"Count": "1"})) + fmt.Println(T("your_unread_email_count", map[string]interface{}{"Count": "3.14"})) + + fmt.Println(T("person_unread_email_count_timeframe", 3, map[string]interface{}{ + "Person": "Bob", + "Timeframe": T("d_days", 0), + })) + fmt.Println(T("person_unread_email_count_timeframe", 3, map[string]interface{}{ + "Person": "Bob", + "Timeframe": T("d_days", 1), + })) + fmt.Println(T("person_unread_email_count_timeframe", 3, map[string]interface{}{ + "Person": "Bob", + "Timeframe": T("d_days", 2), + })) + + fmt.Println(T("person_unread_email_count_timeframe", 1, map[string]interface{}{ + "Count": 30, + "Person": "Bob", + "Timeframe": T("d_days", 0), + })) + fmt.Println(T("person_unread_email_count_timeframe", 2, map[string]interface{}{ + "Count": 20, + "Person": "Bob", + "Timeframe": T("d_days", 1), + })) + fmt.Println(T("person_unread_email_count_timeframe", 3, map[string]interface{}{ + "Count": 10, + "Person": "Bob", + "Timeframe": T("d_days", 2), + })) + + // Output: + // Hello world + // Hello Bob + // Hello Bob + // You have 0 unread emails. + // You have 1 unread email. + // You have 2 unread emails. + // I am 1.7 meters tall. + // Bob has 0 unread emails. + // Bob has 1 unread email. + // Bob has 2 unread emails. + // Bob has 0 unread emails. + // Bob has 1 unread email. + // Bob has 2 unread emails. + // You have 0 unread emails. + // You have 1 unread email. + // You have 2 unread emails. + // You have 0 unread emails. + // You have 1 unread email. + // You have 3.14 unread emails. + // Bob has 3 unread emails in the past 0 days. + // Bob has 3 unread emails in the past 1 day. + // Bob has 3 unread emails in the past 2 days. + // Bob has 1 unread email in the past 0 days. + // Bob has 2 unread emails in the past 1 day. + // Bob has 3 unread emails in the past 2 days. +} diff --git a/internal/go-i18n/i18n/exampletemplate_test.go b/internal/go-i18n/i18n/exampletemplate_test.go new file mode 100644 index 0000000..6292910 --- /dev/null +++ b/internal/go-i18n/i18n/exampletemplate_test.go @@ -0,0 +1,63 @@ +package i18n_test + +import ( + "github.com/gobuffalo/mw-i18n/internal/go-i18n/i18n" + "os" + "text/template" +) + +var funcMap = map[string]interface{}{ + "T": i18n.IdentityTfunc, +} + +var tmpl = template.Must(template.New("").Funcs(funcMap).Parse(` +{{T "program_greeting"}} +{{T "person_greeting" .}} +{{T "your_unread_email_count" 0}} +{{T "your_unread_email_count" 1}} +{{T "your_unread_email_count" 2}} +{{T "person_unread_email_count" 0 .}} +{{T "person_unread_email_count" 1 .}} +{{T "person_unread_email_count" 2 .}} +`)) + +func Example_template() { + i18n.MustLoadTranslationFile("../goi18n/testdata/expected/en-us.all.json") + + T, _ := i18n.Tfunc("en-US") + tmpl.Funcs(map[string]interface{}{ + "T": T, + }) + + tmpl.Execute(os.Stdout, map[string]interface{}{ + "Person": "Bob", + "Timeframe": T("d_days", 1), + }) + + tmpl.Execute(os.Stdout, struct { + Person string + Timeframe string + }{ + Person: "Bob", + Timeframe: T("d_days", 1), + }) + + // Output: + // Hello world + // Hello Bob + // You have 0 unread emails. + // You have 1 unread email. + // You have 2 unread emails. + // Bob has 0 unread emails. + // Bob has 1 unread email. + // Bob has 2 unread emails. + // + // Hello world + // Hello Bob + // You have 0 unread emails. + // You have 1 unread email. + // You have 2 unread emails. + // Bob has 0 unread emails. + // Bob has 1 unread email. + // Bob has 2 unread emails. +} diff --git a/internal/go-i18n/i18n/exampleyaml_test.go b/internal/go-i18n/i18n/exampleyaml_test.go new file mode 100644 index 0000000..0f27dd2 --- /dev/null +++ b/internal/go-i18n/i18n/exampleyaml_test.go @@ -0,0 +1,62 @@ +package i18n_test + +import ( + "fmt" + "github.com/gobuffalo/mw-i18n/internal/go-i18n/i18n" +) + +func ExampleYAML() { + i18n.MustLoadTranslationFile("../goi18n/testdata/en-us.yaml") + + T, _ := i18n.Tfunc("en-US") + + bobMap := map[string]interface{}{"Person": "Bob"} + bobStruct := struct{ Person string }{Person: "Bob"} + + fmt.Println(T("program_greeting")) + fmt.Println(T("person_greeting", bobMap)) + fmt.Println(T("person_greeting", bobStruct)) + + fmt.Println(T("your_unread_email_count", 0)) + fmt.Println(T("your_unread_email_count", 1)) + fmt.Println(T("your_unread_email_count", 2)) + fmt.Println(T("my_height_in_meters", "1.7")) + + fmt.Println(T("person_unread_email_count", 0, bobMap)) + fmt.Println(T("person_unread_email_count", 1, bobMap)) + fmt.Println(T("person_unread_email_count", 2, bobMap)) + fmt.Println(T("person_unread_email_count", 0, bobStruct)) + fmt.Println(T("person_unread_email_count", 1, bobStruct)) + fmt.Println(T("person_unread_email_count", 2, bobStruct)) + + fmt.Println(T("person_unread_email_count_timeframe", 3, map[string]interface{}{ + "Person": "Bob", + "Timeframe": T("d_days", 0), + })) + fmt.Println(T("person_unread_email_count_timeframe", 3, map[string]interface{}{ + "Person": "Bob", + "Timeframe": T("d_days", 1), + })) + fmt.Println(T("person_unread_email_count_timeframe", 3, map[string]interface{}{ + "Person": "Bob", + "Timeframe": T("d_days", 2), + })) + + // Output: + // Hello world + // Hello Bob + // Hello Bob + // You have 0 unread emails. + // You have 1 unread email. + // You have 2 unread emails. + // I am 1.7 meters tall. + // Bob has 0 unread emails. + // Bob has 1 unread email. + // Bob has 2 unread emails. + // Bob has 0 unread emails. + // Bob has 1 unread email. + // Bob has 2 unread emails. + // Bob has 3 unread emails in the past 0 days. + // Bob has 3 unread emails in the past 1 day. + // Bob has 3 unread emails in the past 2 days. +} diff --git a/internal/go-i18n/i18n/i18n.go b/internal/go-i18n/i18n/i18n.go new file mode 100644 index 0000000..c435d40 --- /dev/null +++ b/internal/go-i18n/i18n/i18n.go @@ -0,0 +1,158 @@ +// Package i18n supports string translations with variable substitution and CLDR pluralization. +// It is intended to be used in conjunction with the goi18n command, although that is not strictly required. +// +// Initialization +// +// Your Go program should load translations during its initialization. +// i18n.MustLoadTranslationFile("path/to/fr-FR.all.json") +// If your translations are in a file format not supported by (Must)?LoadTranslationFile, +// then you can use the AddTranslation function to manually add translations. +// +// Fetching a translation +// +// Use Tfunc or MustTfunc to fetch a TranslateFunc that will return the translated string for a specific language. +// func handleRequest(w http.ResponseWriter, r *http.Request) { +// cookieLang := r.Cookie("lang") +// acceptLang := r.Header.Get("Accept-Language") +// defaultLang = "en-US" // known valid language +// T, err := i18n.Tfunc(cookieLang, acceptLang, defaultLang) +// fmt.Println(T("Hello world")) +// } +// +// Usually it is a good idea to identify strings by a generic id rather than the English translation, +// but the rest of this documentation will continue to use the English translation for readability. +// T("Hello world") // ok +// T("programGreeting") // better! +// +// Variables +// +// TranslateFunc supports strings that have variables using the text/template syntax. +// T("Hello {{.Person}}", map[string]interface{}{ +// "Person": "Bob", +// }) +// +// Pluralization +// +// TranslateFunc supports the pluralization of strings using the CLDR pluralization rules defined here: +// http://www.unicode.org/cldr/charts/latest/supplemental/language_plural_rules.html +// T("You have {{.Count}} unread emails.", 2) +// T("I am {{.Count}} meters tall.", "1.7") +// +// Plural strings may also have variables. +// T("{{.Person}} has {{.Count}} unread emails", 2, map[string]interface{}{ +// "Person": "Bob", +// }) +// +// Sentences with multiple plural components can be supported with nesting. +// T("{{.Person}} has {{.Count}} unread emails in the past {{.Timeframe}}.", 3, map[string]interface{}{ +// "Person": "Bob", +// "Timeframe": T("{{.Count}} days", 2), +// }) +// +// Templates +// +// You can use the .Funcs() method of a text/template or html/template to register a TranslateFunc +// for usage inside of that template. +package i18n + +import ( + "github.com/gobuffalo/mw-i18n/internal/go-i18n/i18n/bundle" + "github.com/gobuffalo/mw-i18n/internal/go-i18n/i18n/language" + "github.com/gobuffalo/mw-i18n/internal/go-i18n/i18n/translation" +) + +// TranslateFunc returns the translation of the string identified by translationID. +// +// If there is no translation for translationID, then the translationID itself is returned. +// This makes it easy to identify missing translations in your app. +// +// If translationID is a non-plural form, then the first variadic argument may be a map[string]interface{} +// or struct that contains template data. +// +// If translationID is a plural form, the function accepts two parameter signatures +// 1. T(count int, data struct{}) +// The first variadic argument must be an integer type +// (int, int8, int16, int32, int64) or a float formatted as a string (e.g. "123.45"). +// The second variadic argument may be a map[string]interface{} or struct{} that contains template data. +// 2. T(data struct{}) +// data must be a struct{} or map[string]interface{} that contains a Count field and the template data, +// Count field must be an integer type (int, int8, int16, int32, int64) +// or a float formatted as a string (e.g. "123.45"). +type TranslateFunc func(translationID string, args ...interface{}) string + +// IdentityTfunc returns a TranslateFunc that always returns the translationID passed to it. +// +// It is a useful placeholder when parsing a text/template or html/template +// before the actual Tfunc is available. +func IdentityTfunc() TranslateFunc { + return func(translationID string, args ...interface{}) string { + return translationID + } +} + +var defaultBundle = bundle.New() + +// MustLoadTranslationFile is similar to LoadTranslationFile +// except it panics if an error happens. +func MustLoadTranslationFile(filename string) { + defaultBundle.MustLoadTranslationFile(filename) +} + +// LoadTranslationFile loads the translations from filename into memory. +// +// The language that the translations are associated with is parsed from the filename (e.g. en-US.json). +// +// Generally you should load translation files once during your program's initialization. +func LoadTranslationFile(filename string) error { + return defaultBundle.LoadTranslationFile(filename) +} + +// ParseTranslationFileBytes is similar to LoadTranslationFile except it parses the bytes in buf. +// +// It is useful for parsing translation files embedded with go-bindata. +func ParseTranslationFileBytes(filename string, buf []byte) error { + return defaultBundle.ParseTranslationFileBytes(filename, buf) +} + +// AddTranslation adds translations for a language. +// +// It is useful if your translations are in a format not supported by LoadTranslationFile. +func AddTranslation(lang *language.Language, translations ...translation.Translation) { + defaultBundle.AddTranslation(lang, translations...) +} + +// LanguageTags returns the tags of all languages that have been added. +func LanguageTags() []string { + return defaultBundle.LanguageTags() +} + +// LanguageTranslationIDs returns the ids of all translations that have been added for a given language. +func LanguageTranslationIDs(languageTag string) []string { + return defaultBundle.LanguageTranslationIDs(languageTag) +} + +// MustTfunc is similar to Tfunc except it panics if an error happens. +func MustTfunc(languageSource string, languageSources ...string) TranslateFunc { + return TranslateFunc(defaultBundle.MustTfunc(languageSource, languageSources...)) +} + +// Tfunc returns a TranslateFunc that will be bound to the first language which +// has a non-zero number of translations. +// +// It can parse languages from Accept-Language headers (RFC 2616). +func Tfunc(languageSource string, languageSources ...string) (TranslateFunc, error) { + tfunc, err := defaultBundle.Tfunc(languageSource, languageSources...) + return TranslateFunc(tfunc), err +} + +// MustTfuncAndLanguage is similar to TfuncAndLanguage except it panics if an error happens. +func MustTfuncAndLanguage(languageSource string, languageSources ...string) (TranslateFunc, *language.Language) { + tfunc, lang := defaultBundle.MustTfuncAndLanguage(languageSource, languageSources...) + return TranslateFunc(tfunc), lang +} + +// TfuncAndLanguage is similar to Tfunc except it also returns the language which TranslateFunc is bound to. +func TfuncAndLanguage(languageSource string, languageSources ...string) (TranslateFunc, *language.Language, error) { + tfunc, lang, err := defaultBundle.TfuncAndLanguage(languageSource, languageSources...) + return TranslateFunc(tfunc), lang, err +} diff --git a/internal/go-i18n/i18n/language/codegen/generate.sh b/internal/go-i18n/i18n/language/codegen/generate.sh new file mode 100644 index 0000000..a9fae84 --- /dev/null +++ b/internal/go-i18n/i18n/language/codegen/generate.sh @@ -0,0 +1,5 @@ +#!/bin/sh +go build && ./codegen -cout ../pluralspec_gen.go -tout ../pluralspec_gen_test.go && \ + gofmt -w=true ../pluralspec_gen.go && \ + gofmt -w=true ../pluralspec_gen_test.go && \ + rm codegen diff --git a/internal/go-i18n/i18n/language/codegen/main.go b/internal/go-i18n/i18n/language/codegen/main.go new file mode 100644 index 0000000..5897103 --- /dev/null +++ b/internal/go-i18n/i18n/language/codegen/main.go @@ -0,0 +1,132 @@ +package main + +import ( + "encoding/xml" + "flag" + "fmt" + "io/ioutil" + "os" + "text/template" +) + +var usage = `%[1]s generates Go code to support CLDR plural rules. + +Usage: %[1]s [options] + +Options: + +` + +func main() { + flag.Usage = func() { + fmt.Fprintf(os.Stderr, usage, os.Args[0]) + flag.PrintDefaults() + } + var in, cout, tout string + flag.StringVar(&in, "i", "plurals.xml", "the input XML file containing CLDR plural rules") + flag.StringVar(&cout, "cout", "", "the code output file") + flag.StringVar(&tout, "tout", "", "the test output file") + flag.BoolVar(&verbose, "v", false, "verbose output") + flag.Parse() + + buf, err := ioutil.ReadFile(in) + if err != nil { + fatalf("failed to read file: %s", err) + } + + var data SupplementalData + if err := xml.Unmarshal(buf, &data); err != nil { + fatalf("failed to unmarshal xml: %s", err) + } + + count := 0 + for _, pg := range data.PluralGroups { + count += len(pg.SplitLocales()) + } + infof("parsed %d locales", count) + + if cout != "" { + file := openWritableFile(cout) + if err := codeTemplate.Execute(file, data); err != nil { + fatalf("unable to execute code template because %s", err) + } else { + infof("generated %s", cout) + } + } else { + infof("not generating code file (use -cout)") + } + + if tout != "" { + file := openWritableFile(tout) + if err := testTemplate.Execute(file, data); err != nil { + fatalf("unable to execute test template because %s", err) + } else { + infof("generated %s", tout) + } + } else { + infof("not generating test file (use -tout)") + } +} + +func openWritableFile(name string) *os.File { + file, err := os.OpenFile(name, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0644) + if err != nil { + fatalf("failed to write file %s because %s", name, err) + } + return file +} + +var codeTemplate = template.Must(template.New("spec").Parse(`package language +// This file is generated by i18n/language/codegen/generate.sh + +func init() { +{{range .PluralGroups}} + RegisterPluralSpec({{printf "%#v" .SplitLocales}}, &PluralSpec{ + Plurals: newPluralSet({{range $i, $e := .PluralRules}}{{if $i}}, {{end}}{{$e.CountTitle}}{{end}}), + PluralFunc: func(ops *Operands) Plural { {{range .PluralRules}}{{if .GoCondition}} + // {{.Condition}} + if {{.GoCondition}} { + return {{.CountTitle}} + }{{end}}{{end}} + return Other + }, + }){{end}} +} +`)) + +var testTemplate = template.Must(template.New("spec").Parse(`package language +// This file is generated by i18n/language/codegen/generate.sh + +import "testing" + +{{range .PluralGroups}} +func Test{{.Name}}(t *testing.T) { + var tests []pluralTest + {{range .PluralRules}} + {{if .IntegerExamples}}tests = appendIntegerTests(tests, {{.CountTitle}}, {{printf "%#v" .IntegerExamples}}){{end}} + {{if .DecimalExamples}}tests = appendDecimalTests(tests, {{.CountTitle}}, {{printf "%#v" .DecimalExamples}}){{end}} + {{end}} + locales := {{printf "%#v" .SplitLocales}} + for _, locale := range locales { + runTests(t, locale, tests) + } +} +{{end}} +`)) + +func infof(format string, args ...interface{}) { + fmt.Fprintf(os.Stderr, format+"\n", args...) +} + +var verbose bool + +func verbosef(format string, args ...interface{}) { + if verbose { + infof(format, args...) + } +} + +func fatalf(format string, args ...interface{}) { + infof("fatal: "+format+"\n", args...) + os.Exit(1) +} diff --git a/internal/go-i18n/i18n/language/codegen/plurals.xml b/internal/go-i18n/i18n/language/codegen/plurals.xml new file mode 100644 index 0000000..3310c8e --- /dev/null +++ b/internal/go-i18n/i18n/language/codegen/plurals.xml @@ -0,0 +1,226 @@ + + + + + + + + + + + + @integer 0~15, 100, 1000, 10000, 100000, 1000000, … @decimal 0.0~1.5, 10.0, 100.0, 1000.0, 10000.0, 100000.0, 1000000.0, … + + + + + + i = 0 or n = 1 @integer 0, 1 @decimal 0.0~1.0, 0.00~0.04 + @integer 2~17, 100, 1000, 10000, 100000, 1000000, … @decimal 1.1~2.6, 10.0, 100.0, 1000.0, 10000.0, 100000.0, 1000000.0, … + + + i = 0,1 @integer 0, 1 @decimal 0.0~1.5 + @integer 2~17, 100, 1000, 10000, 100000, 1000000, … @decimal 2.0~3.5, 10.0, 100.0, 1000.0, 10000.0, 100000.0, 1000000.0, … + + + i = 0..1 @integer 0, 1 @decimal 0.0~1.5 + @integer 2~17, 100, 1000, 10000, 100000, 1000000, … @decimal 2.0~3.5, 10.0, 100.0, 1000.0, 10000.0, 100000.0, 1000000.0, … + + + i = 1 and v = 0 @integer 1 + @integer 0, 2~16, 100, 1000, 10000, 100000, 1000000, … @decimal 0.0~1.5, 10.0, 100.0, 1000.0, 10000.0, 100000.0, 1000000.0, … + + + n = 0,1 or i = 0 and f = 1 @integer 0, 1 @decimal 0.0, 0.1, 1.0, 0.00, 0.01, 1.00, 0.000, 0.001, 1.000, 0.0000, 0.0001, 1.0000 + @integer 2~17, 100, 1000, 10000, 100000, 1000000, … @decimal 0.2~0.9, 1.1~1.8, 10.0, 100.0, 1000.0, 10000.0, 100000.0, 1000000.0, … + + + n = 0..1 @integer 0, 1 @decimal 0.0, 1.0, 0.00, 1.00, 0.000, 1.000, 0.0000, 1.0000 + @integer 2~17, 100, 1000, 10000, 100000, 1000000, … @decimal 0.1~0.9, 1.1~1.7, 10.0, 100.0, 1000.0, 10000.0, 100000.0, 1000000.0, … + + + n = 0..1 or n = 11..99 @integer 0, 1, 11~24 @decimal 0.0, 1.0, 11.0, 12.0, 13.0, 14.0, 15.0, 16.0, 17.0, 18.0, 19.0, 20.0, 21.0, 22.0, 23.0, 24.0 + @integer 2~10, 100~106, 1000, 10000, 100000, 1000000, … @decimal 0.1~0.9, 1.1~1.7, 10.0, 100.0, 1000.0, 10000.0, 100000.0, 1000000.0, … + + + n = 1 @integer 1 @decimal 1.0, 1.00, 1.000, 1.0000 + @integer 0, 2~16, 100, 1000, 10000, 100000, 1000000, … @decimal 0.0~0.9, 1.1~1.6, 10.0, 100.0, 1000.0, 10000.0, 100000.0, 1000000.0, … + + + n = 1 or t != 0 and i = 0,1 @integer 1 @decimal 0.1~1.6 + @integer 0, 2~16, 100, 1000, 10000, 100000, 1000000, … @decimal 0.0, 2.0~3.4, 10.0, 100.0, 1000.0, 10000.0, 100000.0, 1000000.0, … + + + t = 0 and i % 10 = 1 and i % 100 != 11 or t != 0 @integer 1, 21, 31, 41, 51, 61, 71, 81, 101, 1001, … @decimal 0.1~1.6, 10.1, 100.1, 1000.1, … + @integer 0, 2~16, 100, 1000, 10000, 100000, 1000000, … @decimal 0.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 10.0, 100.0, 1000.0, 10000.0, 100000.0, 1000000.0, … + + + v = 0 and i % 10 = 1 or f % 10 = 1 @integer 1, 11, 21, 31, 41, 51, 61, 71, 101, 1001, … @decimal 0.1, 1.1, 2.1, 3.1, 4.1, 5.1, 6.1, 7.1, 10.1, 100.1, 1000.1, … + @integer 0, 2~10, 12~17, 100, 1000, 10000, 100000, 1000000, … @decimal 0.0, 0.2~1.0, 1.2~1.7, 10.0, 100.0, 1000.0, 10000.0, 100000.0, 1000000.0, … + + + v = 0 and i = 1,2,3 or v = 0 and i % 10 != 4,6,9 or v != 0 and f % 10 != 4,6,9 @integer 0~3, 5, 7, 8, 10~13, 15, 17, 18, 20, 21, 100, 1000, 10000, 100000, 1000000, … @decimal 0.0~0.3, 0.5, 0.7, 0.8, 1.0~1.3, 1.5, 1.7, 1.8, 2.0, 2.1, 10.0, 100.0, 1000.0, 10000.0, 100000.0, 1000000.0, … + @integer 4, 6, 9, 14, 16, 19, 24, 26, 104, 1004, … @decimal 0.4, 0.6, 0.9, 1.4, 1.6, 1.9, 2.4, 2.6, 10.4, 100.4, 1000.4, … + + + + + + n % 10 = 0 or n % 100 = 11..19 or v = 2 and f % 100 = 11..19 @integer 0, 10~20, 30, 40, 50, 60, 100, 1000, 10000, 100000, 1000000, … @decimal 0.0, 10.0, 11.0, 12.0, 13.0, 14.0, 15.0, 16.0, 100.0, 1000.0, 10000.0, 100000.0, 1000000.0, … + n % 10 = 1 and n % 100 != 11 or v = 2 and f % 10 = 1 and f % 100 != 11 or v != 2 and f % 10 = 1 @integer 1, 21, 31, 41, 51, 61, 71, 81, 101, 1001, … @decimal 0.1, 1.0, 1.1, 2.1, 3.1, 4.1, 5.1, 6.1, 7.1, 10.1, 100.1, 1000.1, … + @integer 2~9, 22~29, 102, 1002, … @decimal 0.2~0.9, 1.2~1.9, 10.2, 100.2, 1000.2, … + + + n = 0 @integer 0 @decimal 0.0, 0.00, 0.000, 0.0000 + i = 0,1 and n != 0 @integer 1 @decimal 0.1~1.6 + @integer 2~17, 100, 1000, 10000, 100000, 1000000, … @decimal 2.0~3.5, 10.0, 100.0, 1000.0, 10000.0, 100000.0, 1000000.0, … + + + n = 0 @integer 0 @decimal 0.0, 0.00, 0.000, 0.0000 + n = 1 @integer 1 @decimal 1.0, 1.00, 1.000, 1.0000 + @integer 2~17, 100, 1000, 10000, 100000, 1000000, … @decimal 0.1~0.9, 1.1~1.7, 10.0, 100.0, 1000.0, 10000.0, 100000.0, 1000000.0, … + + + + + + n = 1 @integer 1 @decimal 1.0, 1.00, 1.000, 1.0000 + n = 2 @integer 2 @decimal 2.0, 2.00, 2.000, 2.0000 + @integer 0, 3~17, 100, 1000, 10000, 100000, 1000000, … @decimal 0.0~0.9, 1.1~1.6, 10.0, 100.0, 1000.0, 10000.0, 100000.0, 1000000.0, … + + + + + + i = 0 or n = 1 @integer 0, 1 @decimal 0.0~1.0, 0.00~0.04 + n = 2..10 @integer 2~10 @decimal 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0, 10.0, 2.00, 3.00, 4.00, 5.00, 6.00, 7.00, 8.00 + @integer 11~26, 100, 1000, 10000, 100000, 1000000, … @decimal 1.1~1.9, 2.1~2.7, 10.1, 100.0, 1000.0, 10000.0, 100000.0, 1000000.0, … + + + i = 1 and v = 0 @integer 1 + v != 0 or n = 0 or n != 1 and n % 100 = 1..19 @integer 0, 2~16, 101, 1001, … @decimal 0.0~1.5, 10.0, 100.0, 1000.0, 10000.0, 100000.0, 1000000.0, … + @integer 20~35, 100, 1000, 10000, 100000, 1000000, … + + + v = 0 and i % 10 = 1 and i % 100 != 11 or f % 10 = 1 and f % 100 != 11 @integer 1, 21, 31, 41, 51, 61, 71, 81, 101, 1001, … @decimal 0.1, 1.1, 2.1, 3.1, 4.1, 5.1, 6.1, 7.1, 10.1, 100.1, 1000.1, … + v = 0 and i % 10 = 2..4 and i % 100 != 12..14 or f % 10 = 2..4 and f % 100 != 12..14 @integer 2~4, 22~24, 32~34, 42~44, 52~54, 62, 102, 1002, … @decimal 0.2~0.4, 1.2~1.4, 2.2~2.4, 3.2~3.4, 4.2~4.4, 5.2, 10.2, 100.2, 1000.2, … + @integer 0, 5~19, 100, 1000, 10000, 100000, 1000000, … @decimal 0.0, 0.5~1.0, 1.5~2.0, 2.5~2.7, 10.0, 100.0, 1000.0, 10000.0, 100000.0, 1000000.0, … + + + + + + n = 1,11 @integer 1, 11 @decimal 1.0, 11.0, 1.00, 11.00, 1.000, 11.000, 1.0000 + n = 2,12 @integer 2, 12 @decimal 2.0, 12.0, 2.00, 12.00, 2.000, 12.000, 2.0000 + n = 3..10,13..19 @integer 3~10, 13~19 @decimal 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0, 10.0, 13.0, 14.0, 15.0, 16.0, 17.0, 18.0, 19.0, 3.00 + @integer 0, 20~34, 100, 1000, 10000, 100000, 1000000, … @decimal 0.0~0.9, 1.1~1.6, 10.1, 100.0, 1000.0, 10000.0, 100000.0, 1000000.0, … + + + v = 0 and i % 100 = 1 @integer 1, 101, 201, 301, 401, 501, 601, 701, 1001, … + v = 0 and i % 100 = 2 @integer 2, 102, 202, 302, 402, 502, 602, 702, 1002, … + v = 0 and i % 100 = 3..4 or v != 0 @integer 3, 4, 103, 104, 203, 204, 303, 304, 403, 404, 503, 504, 603, 604, 703, 704, 1003, … @decimal 0.0~1.5, 10.0, 100.0, 1000.0, 10000.0, 100000.0, 1000000.0, … + @integer 0, 5~19, 100, 1000, 10000, 100000, 1000000, … + + + v = 0 and i % 100 = 1 or f % 100 = 1 @integer 1, 101, 201, 301, 401, 501, 601, 701, 1001, … @decimal 0.1, 1.1, 2.1, 3.1, 4.1, 5.1, 6.1, 7.1, 10.1, 100.1, 1000.1, … + v = 0 and i % 100 = 2 or f % 100 = 2 @integer 2, 102, 202, 302, 402, 502, 602, 702, 1002, … @decimal 0.2, 1.2, 2.2, 3.2, 4.2, 5.2, 6.2, 7.2, 10.2, 100.2, 1000.2, … + v = 0 and i % 100 = 3..4 or f % 100 = 3..4 @integer 3, 4, 103, 104, 203, 204, 303, 304, 403, 404, 503, 504, 603, 604, 703, 704, 1003, … @decimal 0.3, 0.4, 1.3, 1.4, 2.3, 2.4, 3.3, 3.4, 4.3, 4.4, 5.3, 5.4, 6.3, 6.4, 7.3, 7.4, 10.3, 100.3, 1000.3, … + @integer 0, 5~19, 100, 1000, 10000, 100000, 1000000, … @decimal 0.0, 0.5~1.0, 1.5~2.0, 2.5~2.7, 10.0, 100.0, 1000.0, 10000.0, 100000.0, 1000000.0, … + + + + + + i = 1 and v = 0 @integer 1 + i = 2 and v = 0 @integer 2 + v = 0 and n != 0..10 and n % 10 = 0 @integer 20, 30, 40, 50, 60, 70, 80, 90, 100, 1000, 10000, 100000, 1000000, … + @integer 0, 3~17, 101, 1001, … @decimal 0.0~1.5, 10.0, 100.0, 1000.0, 10000.0, 100000.0, 1000000.0, … + + + + + + i = 1 and v = 0 @integer 1 + i = 2..4 and v = 0 @integer 2~4 + v != 0 @decimal 0.0~1.5, 10.0, 100.0, 1000.0, 10000.0, 100000.0, 1000000.0, … + @integer 0, 5~19, 100, 1000, 10000, 100000, 1000000, … + + + i = 1 and v = 0 @integer 1 + v = 0 and i % 10 = 2..4 and i % 100 != 12..14 @integer 2~4, 22~24, 32~34, 42~44, 52~54, 62, 102, 1002, … + v = 0 and i != 1 and i % 10 = 0..1 or v = 0 and i % 10 = 5..9 or v = 0 and i % 100 = 12..14 @integer 0, 5~19, 100, 1000, 10000, 100000, 1000000, … + @decimal 0.0~1.5, 10.0, 100.0, 1000.0, 10000.0, 100000.0, 1000000.0, … + + + n % 10 = 1 and n % 100 != 11 @integer 1, 21, 31, 41, 51, 61, 71, 81, 101, 1001, … @decimal 1.0, 21.0, 31.0, 41.0, 51.0, 61.0, 71.0, 81.0, 101.0, 1001.0, … + n % 10 = 2..4 and n % 100 != 12..14 @integer 2~4, 22~24, 32~34, 42~44, 52~54, 62, 102, 1002, … @decimal 2.0, 3.0, 4.0, 22.0, 23.0, 24.0, 32.0, 33.0, 102.0, 1002.0, … + n % 10 = 0 or n % 10 = 5..9 or n % 100 = 11..14 @integer 0, 5~19, 100, 1000, 10000, 100000, 1000000, … @decimal 0.0, 5.0, 6.0, 7.0, 8.0, 9.0, 10.0, 11.0, 100.0, 1000.0, 10000.0, 100000.0, 1000000.0, … + @decimal 0.1~0.9, 1.1~1.7, 10.1, 100.1, 1000.1, … + + + n % 10 = 1 and n % 100 != 11..19 @integer 1, 21, 31, 41, 51, 61, 71, 81, 101, 1001, … @decimal 1.0, 21.0, 31.0, 41.0, 51.0, 61.0, 71.0, 81.0, 101.0, 1001.0, … + n % 10 = 2..9 and n % 100 != 11..19 @integer 2~9, 22~29, 102, 1002, … @decimal 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0, 22.0, 102.0, 1002.0, … + f != 0 @decimal 0.1~0.9, 1.1~1.7, 10.1, 100.1, 1000.1, … + @integer 0, 10~20, 30, 40, 50, 60, 100, 1000, 10000, 100000, 1000000, … @decimal 0.0, 10.0, 11.0, 12.0, 13.0, 14.0, 15.0, 16.0, 100.0, 1000.0, 10000.0, 100000.0, 1000000.0, … + + + n = 1 @integer 1 @decimal 1.0, 1.00, 1.000, 1.0000 + n = 0 or n % 100 = 2..10 @integer 0, 2~10, 102~107, 1002, … @decimal 0.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 10.0, 102.0, 1002.0, … + n % 100 = 11..19 @integer 11~19, 111~117, 1011, … @decimal 11.0, 12.0, 13.0, 14.0, 15.0, 16.0, 17.0, 18.0, 111.0, 1011.0, … + @integer 20~35, 100, 1000, 10000, 100000, 1000000, … @decimal 0.1~0.9, 1.1~1.7, 10.1, 100.0, 1000.0, 10000.0, 100000.0, 1000000.0, … + + + v = 0 and i % 10 = 1 and i % 100 != 11 @integer 1, 21, 31, 41, 51, 61, 71, 81, 101, 1001, … + v = 0 and i % 10 = 2..4 and i % 100 != 12..14 @integer 2~4, 22~24, 32~34, 42~44, 52~54, 62, 102, 1002, … + v = 0 and i % 10 = 0 or v = 0 and i % 10 = 5..9 or v = 0 and i % 100 = 11..14 @integer 0, 5~19, 100, 1000, 10000, 100000, 1000000, … + @decimal 0.0~1.5, 10.0, 100.0, 1000.0, 10000.0, 100000.0, 1000000.0, … + + + + + + n % 10 = 1 and n % 100 != 11,71,91 @integer 1, 21, 31, 41, 51, 61, 81, 101, 1001, … @decimal 1.0, 21.0, 31.0, 41.0, 51.0, 61.0, 81.0, 101.0, 1001.0, … + n % 10 = 2 and n % 100 != 12,72,92 @integer 2, 22, 32, 42, 52, 62, 82, 102, 1002, … @decimal 2.0, 22.0, 32.0, 42.0, 52.0, 62.0, 82.0, 102.0, 1002.0, … + n % 10 = 3..4,9 and n % 100 != 10..19,70..79,90..99 @integer 3, 4, 9, 23, 24, 29, 33, 34, 39, 43, 44, 49, 103, 1003, … @decimal 3.0, 4.0, 9.0, 23.0, 24.0, 29.0, 33.0, 34.0, 103.0, 1003.0, … + n != 0 and n % 1000000 = 0 @integer 1000000, … @decimal 1000000.0, 1000000.00, 1000000.000, … + @integer 0, 5~8, 10~20, 100, 1000, 10000, 100000, … @decimal 0.0~0.9, 1.1~1.6, 10.0, 100.0, 1000.0, 10000.0, 100000.0, … + + + n = 1 @integer 1 @decimal 1.0, 1.00, 1.000, 1.0000 + n = 2 @integer 2 @decimal 2.0, 2.00, 2.000, 2.0000 + n = 3..6 @integer 3~6 @decimal 3.0, 4.0, 5.0, 6.0, 3.00, 4.00, 5.00, 6.00, 3.000, 4.000, 5.000, 6.000, 3.0000, 4.0000, 5.0000, 6.0000 + n = 7..10 @integer 7~10 @decimal 7.0, 8.0, 9.0, 10.0, 7.00, 8.00, 9.00, 10.00, 7.000, 8.000, 9.000, 10.000, 7.0000, 8.0000, 9.0000, 10.0000 + @integer 0, 11~25, 100, 1000, 10000, 100000, 1000000, … @decimal 0.0~0.9, 1.1~1.6, 10.1, 100.0, 1000.0, 10000.0, 100000.0, 1000000.0, … + + + v = 0 and i % 10 = 1 @integer 1, 11, 21, 31, 41, 51, 61, 71, 101, 1001, … + v = 0 and i % 10 = 2 @integer 2, 12, 22, 32, 42, 52, 62, 72, 102, 1002, … + v = 0 and i % 100 = 0,20,40,60,80 @integer 0, 20, 40, 60, 80, 100, 120, 140, 1000, 10000, 100000, 1000000, … + v != 0 @decimal 0.0~1.5, 10.0, 100.0, 1000.0, 10000.0, 100000.0, 1000000.0, … + @integer 3~10, 13~19, 23, 103, 1003, … + + + + + + n = 0 @integer 0 @decimal 0.0, 0.00, 0.000, 0.0000 + n = 1 @integer 1 @decimal 1.0, 1.00, 1.000, 1.0000 + n = 2 @integer 2 @decimal 2.0, 2.00, 2.000, 2.0000 + n % 100 = 3..10 @integer 3~10, 103~110, 1003, … @decimal 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0, 10.0, 103.0, 1003.0, … + n % 100 = 11..99 @integer 11~26, 111, 1011, … @decimal 11.0, 12.0, 13.0, 14.0, 15.0, 16.0, 17.0, 18.0, 111.0, 1011.0, … + @integer 100~102, 200~202, 300~302, 400~402, 500~502, 600, 1000, 10000, 100000, 1000000, … @decimal 0.1~0.9, 1.1~1.7, 10.1, 100.0, 1000.0, 10000.0, 100000.0, 1000000.0, … + + + n = 0 @integer 0 @decimal 0.0, 0.00, 0.000, 0.0000 + n = 1 @integer 1 @decimal 1.0, 1.00, 1.000, 1.0000 + n = 2 @integer 2 @decimal 2.0, 2.00, 2.000, 2.0000 + n = 3 @integer 3 @decimal 3.0, 3.00, 3.000, 3.0000 + n = 6 @integer 6 @decimal 6.0, 6.00, 6.000, 6.0000 + @integer 4, 5, 7~20, 100, 1000, 10000, 100000, 1000000, … @decimal 0.1~0.9, 1.1~1.7, 10.0, 100.0, 1000.0, 10000.0, 100000.0, 1000000.0, … + + + diff --git a/internal/go-i18n/i18n/language/codegen/xml.go b/internal/go-i18n/i18n/language/codegen/xml.go new file mode 100644 index 0000000..9d39053 --- /dev/null +++ b/internal/go-i18n/i18n/language/codegen/xml.go @@ -0,0 +1,143 @@ +package main + +import ( + "encoding/xml" + "fmt" + "regexp" + "strings" +) + +// SupplementalData is the top level struct of plural.xml +type SupplementalData struct { + XMLName xml.Name `xml:"supplementalData"` + PluralGroups []PluralGroup `xml:"plurals>pluralRules"` +} + +// PluralGroup is a group of locales with the same plural rules. +type PluralGroup struct { + Locales string `xml:"locales,attr"` + PluralRules []PluralRule `xml:"pluralRule"` +} + +// Name returns a unique name for this plural group. +func (pg *PluralGroup) Name() string { + n := strings.Title(pg.Locales) + return strings.Replace(n, " ", "", -1) +} + +// SplitLocales returns all the locales in the PluralGroup as a slice. +func (pg *PluralGroup) SplitLocales() []string { + return strings.Split(pg.Locales, " ") +} + +// PluralRule is the rule for a single plural form. +type PluralRule struct { + Count string `xml:"count,attr"` + Rule string `xml:",innerxml"` +} + +// CountTitle returns the title case of the PluralRule's count. +func (pr *PluralRule) CountTitle() string { + return strings.Title(pr.Count) +} + +// Condition returns the condition where the PluralRule applies. +func (pr *PluralRule) Condition() string { + i := strings.Index(pr.Rule, "@") + return pr.Rule[:i] +} + +// Examples returns the integer and decimal exmaples for the PLuralRule. +func (pr *PluralRule) Examples() (integer []string, decimal []string) { + ex := strings.Replace(pr.Rule, ", …", "", -1) + ddelim := "@decimal" + if i := strings.Index(ex, ddelim); i > 0 { + dex := strings.TrimSpace(ex[i+len(ddelim):]) + decimal = strings.Split(dex, ", ") + ex = ex[:i] + } + idelim := "@integer" + if i := strings.Index(ex, idelim); i > 0 { + iex := strings.TrimSpace(ex[i+len(idelim):]) + integer = strings.Split(iex, ", ") + } + return integer, decimal +} + +// IntegerExamples returns the integer exmaples for the PLuralRule. +func (pr *PluralRule) IntegerExamples() []string { + integer, _ := pr.Examples() + return integer +} + +// DecimalExamples returns the decimal exmaples for the PLuralRule. +func (pr *PluralRule) DecimalExamples() []string { + _, decimal := pr.Examples() + return decimal +} + +var relationRegexp = regexp.MustCompile("([niftvw])(?: % ([0-9]+))? (!=|=)(.*)") + +// GoCondition converts the XML condition to valid Go code. +func (pr *PluralRule) GoCondition() string { + var ors []string + for _, and := range strings.Split(pr.Condition(), "or") { + var ands []string + for _, relation := range strings.Split(and, "and") { + parts := relationRegexp.FindStringSubmatch(relation) + if parts == nil { + continue + } + lvar, lmod, op, rhs := strings.Title(parts[1]), parts[2], parts[3], strings.TrimSpace(parts[4]) + if op == "=" { + op = "==" + } + lvar = "ops." + lvar + var rhor []string + var rany []string + for _, rh := range strings.Split(rhs, ",") { + if parts := strings.Split(rh, ".."); len(parts) == 2 { + from, to := parts[0], parts[1] + if lvar == "ops.N" { + if lmod != "" { + rhor = append(rhor, fmt.Sprintf("ops.NmodInRange(%s, %s, %s)", lmod, from, to)) + } else { + rhor = append(rhor, fmt.Sprintf("ops.NinRange(%s, %s)", from, to)) + } + } else if lmod != "" { + rhor = append(rhor, fmt.Sprintf("intInRange(%s %% %s, %s, %s)", lvar, lmod, from, to)) + } else { + rhor = append(rhor, fmt.Sprintf("intInRange(%s, %s, %s)", lvar, from, to)) + } + } else { + rany = append(rany, rh) + } + } + + if len(rany) > 0 { + rh := strings.Join(rany, ",") + if lvar == "ops.N" { + if lmod != "" { + rhor = append(rhor, fmt.Sprintf("ops.NmodEqualsAny(%s, %s)", lmod, rh)) + } else { + rhor = append(rhor, fmt.Sprintf("ops.NequalsAny(%s)", rh)) + } + } else if lmod != "" { + rhor = append(rhor, fmt.Sprintf("intEqualsAny(%s %% %s, %s)", lvar, lmod, rh)) + } else { + rhor = append(rhor, fmt.Sprintf("intEqualsAny(%s, %s)", lvar, rh)) + } + } + r := strings.Join(rhor, " || ") + if len(rhor) > 1 { + r = "(" + r + ")" + } + if op == "!=" { + r = "!" + r + } + ands = append(ands, r) + } + ors = append(ors, strings.Join(ands, " && ")) + } + return strings.Join(ors, " ||\n") +} diff --git a/internal/go-i18n/i18n/language/language.go b/internal/go-i18n/i18n/language/language.go new file mode 100644 index 0000000..b045a27 --- /dev/null +++ b/internal/go-i18n/i18n/language/language.go @@ -0,0 +1,99 @@ +// Package language defines languages that implement CLDR pluralization. +package language + +import ( + "fmt" + "strings" +) + +// Language is a written human language. +type Language struct { + // Tag uniquely identifies the language as defined by RFC 5646. + // + // Most language tags are a two character language code (ISO 639-1) + // optionally followed by a dash and a two character country code (ISO 3166-1). + // (e.g. en, pt-br) + Tag string + *PluralSpec +} + +func (l *Language) String() string { + return l.Tag +} + +// MatchingTags returns the set of language tags that map to this Language. +// e.g. "zh-hans-cn" yields {"zh", "zh-hans", "zh-hans-cn"} +// BUG: This should be computed once and stored as a field on Language for efficiency, +// but this would require changing how Languages are constructed. +func (l *Language) MatchingTags() []string { + parts := strings.Split(l.Tag, "-") + var prefix, matches []string + for _, part := range parts { + prefix = append(prefix, part) + match := strings.Join(prefix, "-") + matches = append(matches, match) + } + return matches +} + +// Parse returns a slice of supported languages found in src or nil if none are found. +// It can parse language tags and Accept-Language headers. +func Parse(src string) []*Language { + var langs []*Language + start := 0 + for end, chr := range src { + switch chr { + case ',', ';', '.': + tag := strings.TrimSpace(src[start:end]) + if spec := GetPluralSpec(tag); spec != nil { + langs = append(langs, &Language{NormalizeTag(tag), spec}) + } + start = end + 1 + } + } + if start > 0 { + tag := strings.TrimSpace(src[start:]) + if spec := GetPluralSpec(tag); spec != nil { + langs = append(langs, &Language{NormalizeTag(tag), spec}) + } + return dedupe(langs) + } + if spec := GetPluralSpec(src); spec != nil { + langs = append(langs, &Language{NormalizeTag(src), spec}) + } + return langs +} + +func dedupe(langs []*Language) []*Language { + found := make(map[string]struct{}, len(langs)) + deduped := make([]*Language, 0, len(langs)) + for _, lang := range langs { + if _, ok := found[lang.Tag]; !ok { + found[lang.Tag] = struct{}{} + deduped = append(deduped, lang) + } + } + return deduped +} + +// MustParse is similar to Parse except it panics instead of retuning a nil Language. +func MustParse(src string) []*Language { + langs := Parse(src) + if len(langs) == 0 { + panic(fmt.Errorf("unable to parse language from %q", src)) + } + return langs +} + +// Add adds support for a new language. +func Add(l *Language) { + tag := NormalizeTag(l.Tag) + pluralSpecs[tag] = l.PluralSpec +} + +// NormalizeTag returns a language tag with all lower-case characters +// and dashes "-" instead of underscores "_" +func NormalizeTag(tag string) string { + tag = strings.ToLower(tag) + return strings.Replace(tag, "_", "-", -1) +} diff --git a/internal/go-i18n/i18n/language/language_test.go b/internal/go-i18n/i18n/language/language_test.go new file mode 100644 index 0000000..1ab3314 --- /dev/null +++ b/internal/go-i18n/i18n/language/language_test.go @@ -0,0 +1,85 @@ +package language + +import ( + "reflect" + "testing" +) + +func TestParse(t *testing.T) { + tests := []struct { + src string + lang []*Language + }{ + {"en", []*Language{{"en", pluralSpecs["en"]}}}, + {"en-US", []*Language{{"en-us", pluralSpecs["en"]}}}, + {"en_US", []*Language{{"en-us", pluralSpecs["en"]}}}, + {"en-GB", []*Language{{"en-gb", pluralSpecs["en"]}}}, + {"zh-CN", []*Language{{"zh-cn", pluralSpecs["zh"]}}}, + {"zh-TW", []*Language{{"zh-tw", pluralSpecs["zh"]}}}, + {"pt-BR", []*Language{{"pt-br", pluralSpecs["pt"]}}}, + {"pt_BR", []*Language{{"pt-br", pluralSpecs["pt"]}}}, + {"pt-PT", []*Language{{"pt-pt", pluralSpecs["pt"]}}}, + {"pt_PT", []*Language{{"pt-pt", pluralSpecs["pt"]}}}, + {"zh-Hans-CN", []*Language{{"zh-hans-cn", pluralSpecs["zh"]}}}, + {"zh-Hant-TW", []*Language{{"zh-hant-tw", pluralSpecs["zh"]}}}, + {"en-US-en-US", []*Language{{"en-us-en-us", pluralSpecs["en"]}}}, + {".en-US..en-US.", []*Language{{"en-us", pluralSpecs["en"]}}}, + { + "it, xx-zz, xx-ZZ, zh, en-gb;q=0.8, en;q=0.7, es-ES;q=0.6, de-xx", + []*Language{ + {"it", pluralSpecs["it"]}, + {"zh", pluralSpecs["zh"]}, + {"en-gb", pluralSpecs["en"]}, + {"en", pluralSpecs["en"]}, + {"es-es", pluralSpecs["es"]}, + {"de-xx", pluralSpecs["de"]}, + }, + }, + { + "it-qq,xx,xx-zz,xx-ZZ,zh,en-gb;q=0.8,en;q=0.7,es-ES;q=0.6,de-xx", + []*Language{ + {"it-qq", pluralSpecs["it"]}, + {"zh", pluralSpecs["zh"]}, + {"en-gb", pluralSpecs["en"]}, + {"en", pluralSpecs["en"]}, + {"es-es", pluralSpecs["es"]}, + {"de-xx", pluralSpecs["de"]}, + }, + }, + {"en.json", []*Language{{"en", pluralSpecs["en"]}}}, + {"en-US.json", []*Language{{"en-us", pluralSpecs["en"]}}}, + {"en-us.json", []*Language{{"en-us", pluralSpecs["en"]}}}, + {"en-xx.json", []*Language{{"en-xx", pluralSpecs["en"]}}}, + {"xx-Yyen-US", nil}, + {"en US", nil}, + {"", nil}, + {"-", nil}, + {"_", nil}, + {"-en", nil}, + {"_en", nil}, + {"-en-", nil}, + {"_en_", nil}, + {"xx", nil}, + } + for _, test := range tests { + lang := Parse(test.src) + if !reflect.DeepEqual(lang, test.lang) { + t.Errorf("Parse(%q) = %s expected %s", test.src, lang, test.lang) + } + } +} + +func TestMatchingTags(t *testing.T) { + tests := []struct { + lang *Language + matches []string + }{ + {&Language{"zh-hans-cn", nil}, []string{"zh", "zh-hans", "zh-hans-cn"}}, + {&Language{"foo", nil}, []string{"foo"}}, + } + for _, test := range tests { + if actual := test.lang.MatchingTags(); !reflect.DeepEqual(test.matches, actual) { + t.Errorf("matchingTags(%q) = %q expected %q", test.lang.Tag, actual, test.matches) + } + } +} diff --git a/internal/go-i18n/i18n/language/operands.go b/internal/go-i18n/i18n/language/operands.go new file mode 100644 index 0000000..49ee7dc --- /dev/null +++ b/internal/go-i18n/i18n/language/operands.go @@ -0,0 +1,119 @@ +package language + +import ( + "fmt" + "strconv" + "strings" +) + +// http://unicode.org/reports/tr35/tr35-numbers.html#Operands +type Operands struct { + N float64 // absolute value of the source number (integer and decimals) + I int64 // integer digits of n + V int64 // number of visible fraction digits in n, with trailing zeros + W int64 // number of visible fraction digits in n, without trailing zeros + F int64 // visible fractional digits in n, with trailing zeros + T int64 // visible fractional digits in n, without trailing zeros +} + +// NmodEqualAny returns true if o represents an integer equal to any of the arguments. +func (o *Operands) NequalsAny(any ...int64) bool { + for _, i := range any { + if o.I == i && o.T == 0 { + return true + } + } + return false +} + +// NmodEqualAny returns true if o represents an integer equal to any of the arguments modulo mod. +func (o *Operands) NmodEqualsAny(mod int64, any ...int64) bool { + modI := o.I % mod + for _, i := range any { + if modI == i && o.T == 0 { + return true + } + } + return false +} + +// NmodInRange returns true if o represents an integer in the closed interval [from, to]. +func (o *Operands) NinRange(from, to int64) bool { + return o.T == 0 && from <= o.I && o.I <= to +} + +// NmodInRange returns true if o represents an integer in the closed interval [from, to] modulo mod. +func (o *Operands) NmodInRange(mod, from, to int64) bool { + modI := o.I % mod + return o.T == 0 && from <= modI && modI <= to +} + +func newOperands(v interface{}) (*Operands, error) { + switch v := v.(type) { + case int: + return newOperandsInt64(int64(v)), nil + case int8: + return newOperandsInt64(int64(v)), nil + case int16: + return newOperandsInt64(int64(v)), nil + case int32: + return newOperandsInt64(int64(v)), nil + case int64: + return newOperandsInt64(v), nil + case string: + return newOperandsString(v) + case float32, float64: + return nil, fmt.Errorf("floats should be formatted into a string") + default: + return nil, fmt.Errorf("invalid type %T; expected integer or string", v) + } +} + +func newOperandsInt64(i int64) *Operands { + if i < 0 { + i = -i + } + return &Operands{float64(i), i, 0, 0, 0, 0} +} + +func newOperandsString(s string) (*Operands, error) { + if s[0] == '-' { + s = s[1:] + } + n, err := strconv.ParseFloat(s, 64) + if err != nil { + return nil, err + } + ops := &Operands{N: n} + parts := strings.SplitN(s, ".", 2) + ops.I, err = strconv.ParseInt(parts[0], 10, 64) + if err != nil { + return nil, err + } + if len(parts) == 1 { + return ops, nil + } + fraction := parts[1] + ops.V = int64(len(fraction)) + for i := ops.V - 1; i >= 0; i-- { + if fraction[i] != '0' { + ops.W = i + 1 + break + } + } + if ops.V > 0 { + f, err := strconv.ParseInt(fraction, 10, 0) + if err != nil { + return nil, err + } + ops.F = f + } + if ops.W > 0 { + t, err := strconv.ParseInt(fraction[:ops.W], 10, 0) + if err != nil { + return nil, err + } + ops.T = t + } + return ops, nil +} diff --git a/internal/go-i18n/i18n/language/operands_test.go b/internal/go-i18n/i18n/language/operands_test.go new file mode 100644 index 0000000..e4f3390 --- /dev/null +++ b/internal/go-i18n/i18n/language/operands_test.go @@ -0,0 +1,45 @@ +package language + +import ( + "reflect" + "testing" +) + +func TestNewOperands(t *testing.T) { + tests := []struct { + input interface{} + ops *Operands + err bool + }{ + {int64(0), &Operands{0.0, 0, 0, 0, 0, 0}, false}, + {int64(1), &Operands{1.0, 1, 0, 0, 0, 0}, false}, + {"0", &Operands{0.0, 0, 0, 0, 0, 0}, false}, + {"1", &Operands{1.0, 1, 0, 0, 0, 0}, false}, + {"1.0", &Operands{1.0, 1, 1, 0, 0, 0}, false}, + {"1.00", &Operands{1.0, 1, 2, 0, 0, 0}, false}, + {"1.3", &Operands{1.3, 1, 1, 1, 3, 3}, false}, + {"1.30", &Operands{1.3, 1, 2, 1, 30, 3}, false}, + {"1.03", &Operands{1.03, 1, 2, 2, 3, 3}, false}, + {"1.230", &Operands{1.23, 1, 3, 2, 230, 23}, false}, + {"20.0230", &Operands{20.023, 20, 4, 3, 230, 23}, false}, + {20.0230, nil, true}, + } + for _, test := range tests { + ops, err := newOperands(test.input) + if err != nil && !test.err { + t.Errorf("newOperands(%#v) unexpected error: %s", test.input, err) + } else if err == nil && test.err { + t.Errorf("newOperands(%#v) returned %#v; expected error", test.input, ops) + } else if !reflect.DeepEqual(ops, test.ops) { + t.Errorf("newOperands(%#v) returned %#v; expected %#v", test.input, ops, test.ops) + } + } +} + +func BenchmarkNewOperand(b *testing.B) { + for i := 0; i < b.N; i++ { + if _, err := newOperands("1234.56780000"); err != nil { + b.Fatal(err) + } + } +} diff --git a/internal/go-i18n/i18n/language/plural.go b/internal/go-i18n/i18n/language/plural.go new file mode 100644 index 0000000..1f3ea5c --- /dev/null +++ b/internal/go-i18n/i18n/language/plural.go @@ -0,0 +1,40 @@ +package language + +import ( + "fmt" +) + +// Plural represents a language pluralization form as defined here: +// http://cldr.unicode.org/index/cldr-spec/plural-rules +type Plural string + +// All defined plural categories. +const ( + Invalid Plural = "invalid" + Zero = "zero" + One = "one" + Two = "two" + Few = "few" + Many = "many" + Other = "other" +) + +// NewPlural returns src as a Plural +// or Invalid and a non-nil error if src is not a valid Plural. +func NewPlural(src string) (Plural, error) { + switch src { + case "zero": + return Zero, nil + case "one": + return One, nil + case "two": + return Two, nil + case "few": + return Few, nil + case "many": + return Many, nil + case "other": + return Other, nil + } + return Invalid, fmt.Errorf("invalid plural category %s", src) +} diff --git a/internal/go-i18n/i18n/language/plural_test.go b/internal/go-i18n/i18n/language/plural_test.go new file mode 100644 index 0000000..6336d29 --- /dev/null +++ b/internal/go-i18n/i18n/language/plural_test.go @@ -0,0 +1,28 @@ +package language + +import ( + "testing" +) + +func TestNewPlural(t *testing.T) { + tests := []struct { + src string + plural Plural + err bool + }{ + {"zero", Zero, false}, + {"one", One, false}, + {"two", Two, false}, + {"few", Few, false}, + {"many", Many, false}, + {"other", Other, false}, + {"asdf", Invalid, true}, + } + for _, test := range tests { + plural, err := NewPlural(test.src) + wrongErr := (err != nil && !test.err) || (err == nil && test.err) + if plural != test.plural || wrongErr { + t.Errorf("NewPlural(%#v) returned %#v,%#v; expected %#v", test.src, plural, err, test.plural) + } + } +} diff --git a/internal/go-i18n/i18n/language/pluralspec.go b/internal/go-i18n/i18n/language/pluralspec.go new file mode 100644 index 0000000..fc31e88 --- /dev/null +++ b/internal/go-i18n/i18n/language/pluralspec.go @@ -0,0 +1,75 @@ +package language + +import "strings" + +// PluralSpec defines the CLDR plural rules for a language. +// http://www.unicode.org/cldr/charts/latest/supplemental/language_plural_rules.html +// http://unicode.org/reports/tr35/tr35-numbers.html#Operands +type PluralSpec struct { + Plurals map[Plural]struct{} + PluralFunc func(*Operands) Plural +} + +var pluralSpecs = make(map[string]*PluralSpec) + +func normalizePluralSpecID(id string) string { + id = strings.Replace(id, "_", "-", -1) + id = strings.ToLower(id) + return id +} + +// RegisterPluralSpec registers a new plural spec for the language ids. +func RegisterPluralSpec(ids []string, ps *PluralSpec) { + for _, id := range ids { + id = normalizePluralSpecID(id) + pluralSpecs[id] = ps + } +} + +// Plural returns the plural category for number as defined by +// the language's CLDR plural rules. +func (ps *PluralSpec) Plural(number interface{}) (Plural, error) { + ops, err := newOperands(number) + if err != nil { + return Invalid, err + } + return ps.PluralFunc(ops), nil +} + +// GetPluralSpec returns the PluralSpec that matches the longest prefix of tag. +// It returns nil if no PluralSpec matches tag. +func GetPluralSpec(tag string) *PluralSpec { + tag = NormalizeTag(tag) + subtag := tag + for { + if spec := pluralSpecs[subtag]; spec != nil { + return spec + } + end := strings.LastIndex(subtag, "-") + if end == -1 { + return nil + } + subtag = subtag[:end] + } +} + +func newPluralSet(plurals ...Plural) map[Plural]struct{} { + set := make(map[Plural]struct{}, len(plurals)) + for _, plural := range plurals { + set[plural] = struct{}{} + } + return set +} + +func intInRange(i, from, to int64) bool { + return from <= i && i <= to +} + +func intEqualsAny(i int64, any ...int64) bool { + for _, a := range any { + if i == a { + return true + } + } + return false +} diff --git a/internal/go-i18n/i18n/language/pluralspec_gen.go b/internal/go-i18n/i18n/language/pluralspec_gen.go new file mode 100644 index 0000000..0268bb9 --- /dev/null +++ b/internal/go-i18n/i18n/language/pluralspec_gen.go @@ -0,0 +1,557 @@ +package language + +// This file is generated by i18n/language/codegen/generate.sh + +func init() { + + RegisterPluralSpec([]string{"bm", "bo", "dz", "id", "ig", "ii", "in", "ja", "jbo", "jv", "jw", "kde", "kea", "km", "ko", "lkt", "lo", "ms", "my", "nqo", "root", "sah", "ses", "sg", "th", "to", "vi", "wo", "yo", "yue", "zh"}, &PluralSpec{ + Plurals: newPluralSet(Other), + PluralFunc: func(ops *Operands) Plural { + return Other + }, + }) + RegisterPluralSpec([]string{"am", "as", "bn", "fa", "gu", "hi", "kn", "mr", "zu"}, &PluralSpec{ + Plurals: newPluralSet(One, Other), + PluralFunc: func(ops *Operands) Plural { + // i = 0 or n = 1 + if intEqualsAny(ops.I, 0) || + ops.NequalsAny(1) { + return One + } + return Other + }, + }) + RegisterPluralSpec([]string{"ff", "fr", "hy", "kab"}, &PluralSpec{ + Plurals: newPluralSet(One, Other), + PluralFunc: func(ops *Operands) Plural { + // i = 0,1 + if intEqualsAny(ops.I, 0, 1) { + return One + } + return Other + }, + }) + RegisterPluralSpec([]string{"pt"}, &PluralSpec{ + Plurals: newPluralSet(One, Other), + PluralFunc: func(ops *Operands) Plural { + // i = 0..1 + if intInRange(ops.I, 0, 1) { + return One + } + return Other + }, + }) + RegisterPluralSpec([]string{"ast", "ca", "de", "en", "et", "fi", "fy", "gl", "it", "ji", "nl", "sv", "sw", "ur", "yi"}, &PluralSpec{ + Plurals: newPluralSet(One, Other), + PluralFunc: func(ops *Operands) Plural { + // i = 1 and v = 0 + if intEqualsAny(ops.I, 1) && intEqualsAny(ops.V, 0) { + return One + } + return Other + }, + }) + RegisterPluralSpec([]string{"si"}, &PluralSpec{ + Plurals: newPluralSet(One, Other), + PluralFunc: func(ops *Operands) Plural { + // n = 0,1 or i = 0 and f = 1 + if ops.NequalsAny(0, 1) || + intEqualsAny(ops.I, 0) && intEqualsAny(ops.F, 1) { + return One + } + return Other + }, + }) + RegisterPluralSpec([]string{"ak", "bh", "guw", "ln", "mg", "nso", "pa", "ti", "wa"}, &PluralSpec{ + Plurals: newPluralSet(One, Other), + PluralFunc: func(ops *Operands) Plural { + // n = 0..1 + if ops.NinRange(0, 1) { + return One + } + return Other + }, + }) + RegisterPluralSpec([]string{"tzm"}, &PluralSpec{ + Plurals: newPluralSet(One, Other), + PluralFunc: func(ops *Operands) Plural { + // n = 0..1 or n = 11..99 + if ops.NinRange(0, 1) || + ops.NinRange(11, 99) { + return One + } + return Other + }, + }) + RegisterPluralSpec([]string{"af", "asa", "az", "bem", "bez", "bg", "brx", "ce", "cgg", "chr", "ckb", "dv", "ee", "el", "eo", "es", "eu", "fo", "fur", "gsw", "ha", "haw", "hu", "jgo", "jmc", "ka", "kaj", "kcg", "kk", "kkj", "kl", "ks", "ksb", "ku", "ky", "lb", "lg", "mas", "mgo", "ml", "mn", "nah", "nb", "nd", "ne", "nn", "nnh", "no", "nr", "ny", "nyn", "om", "or", "os", "pap", "ps", "rm", "rof", "rwk", "saq", "sdh", "seh", "sn", "so", "sq", "ss", "ssy", "st", "syr", "ta", "te", "teo", "tig", "tk", "tn", "tr", "ts", "ug", "uz", "ve", "vo", "vun", "wae", "xh", "xog"}, &PluralSpec{ + Plurals: newPluralSet(One, Other), + PluralFunc: func(ops *Operands) Plural { + // n = 1 + if ops.NequalsAny(1) { + return One + } + return Other + }, + }) + RegisterPluralSpec([]string{"da"}, &PluralSpec{ + Plurals: newPluralSet(One, Other), + PluralFunc: func(ops *Operands) Plural { + // n = 1 or t != 0 and i = 0,1 + if ops.NequalsAny(1) || + !intEqualsAny(ops.T, 0) && intEqualsAny(ops.I, 0, 1) { + return One + } + return Other + }, + }) + RegisterPluralSpec([]string{"is"}, &PluralSpec{ + Plurals: newPluralSet(One, Other), + PluralFunc: func(ops *Operands) Plural { + // t = 0 and i % 10 = 1 and i % 100 != 11 or t != 0 + if intEqualsAny(ops.T, 0) && intEqualsAny(ops.I%10, 1) && !intEqualsAny(ops.I%100, 11) || + !intEqualsAny(ops.T, 0) { + return One + } + return Other + }, + }) + RegisterPluralSpec([]string{"mk"}, &PluralSpec{ + Plurals: newPluralSet(One, Other), + PluralFunc: func(ops *Operands) Plural { + // v = 0 and i % 10 = 1 or f % 10 = 1 + if intEqualsAny(ops.V, 0) && intEqualsAny(ops.I%10, 1) || + intEqualsAny(ops.F%10, 1) { + return One + } + return Other + }, + }) + RegisterPluralSpec([]string{"fil", "tl"}, &PluralSpec{ + Plurals: newPluralSet(One, Other), + PluralFunc: func(ops *Operands) Plural { + // v = 0 and i = 1,2,3 or v = 0 and i % 10 != 4,6,9 or v != 0 and f % 10 != 4,6,9 + if intEqualsAny(ops.V, 0) && intEqualsAny(ops.I, 1, 2, 3) || + intEqualsAny(ops.V, 0) && !intEqualsAny(ops.I%10, 4, 6, 9) || + !intEqualsAny(ops.V, 0) && !intEqualsAny(ops.F%10, 4, 6, 9) { + return One + } + return Other + }, + }) + RegisterPluralSpec([]string{"lv", "prg"}, &PluralSpec{ + Plurals: newPluralSet(Zero, One, Other), + PluralFunc: func(ops *Operands) Plural { + // n % 10 = 0 or n % 100 = 11..19 or v = 2 and f % 100 = 11..19 + if ops.NmodEqualsAny(10, 0) || + ops.NmodInRange(100, 11, 19) || + intEqualsAny(ops.V, 2) && intInRange(ops.F%100, 11, 19) { + return Zero + } + // n % 10 = 1 and n % 100 != 11 or v = 2 and f % 10 = 1 and f % 100 != 11 or v != 2 and f % 10 = 1 + if ops.NmodEqualsAny(10, 1) && !ops.NmodEqualsAny(100, 11) || + intEqualsAny(ops.V, 2) && intEqualsAny(ops.F%10, 1) && !intEqualsAny(ops.F%100, 11) || + !intEqualsAny(ops.V, 2) && intEqualsAny(ops.F%10, 1) { + return One + } + return Other + }, + }) + RegisterPluralSpec([]string{"lag"}, &PluralSpec{ + Plurals: newPluralSet(Zero, One, Other), + PluralFunc: func(ops *Operands) Plural { + // n = 0 + if ops.NequalsAny(0) { + return Zero + } + // i = 0,1 and n != 0 + if intEqualsAny(ops.I, 0, 1) && !ops.NequalsAny(0) { + return One + } + return Other + }, + }) + RegisterPluralSpec([]string{"ksh"}, &PluralSpec{ + Plurals: newPluralSet(Zero, One, Other), + PluralFunc: func(ops *Operands) Plural { + // n = 0 + if ops.NequalsAny(0) { + return Zero + } + // n = 1 + if ops.NequalsAny(1) { + return One + } + return Other + }, + }) + RegisterPluralSpec([]string{"iu", "kw", "naq", "se", "sma", "smi", "smj", "smn", "sms"}, &PluralSpec{ + Plurals: newPluralSet(One, Two, Other), + PluralFunc: func(ops *Operands) Plural { + // n = 1 + if ops.NequalsAny(1) { + return One + } + // n = 2 + if ops.NequalsAny(2) { + return Two + } + return Other + }, + }) + RegisterPluralSpec([]string{"shi"}, &PluralSpec{ + Plurals: newPluralSet(One, Few, Other), + PluralFunc: func(ops *Operands) Plural { + // i = 0 or n = 1 + if intEqualsAny(ops.I, 0) || + ops.NequalsAny(1) { + return One + } + // n = 2..10 + if ops.NinRange(2, 10) { + return Few + } + return Other + }, + }) + RegisterPluralSpec([]string{"mo", "ro"}, &PluralSpec{ + Plurals: newPluralSet(One, Few, Other), + PluralFunc: func(ops *Operands) Plural { + // i = 1 and v = 0 + if intEqualsAny(ops.I, 1) && intEqualsAny(ops.V, 0) { + return One + } + // v != 0 or n = 0 or n != 1 and n % 100 = 1..19 + if !intEqualsAny(ops.V, 0) || + ops.NequalsAny(0) || + !ops.NequalsAny(1) && ops.NmodInRange(100, 1, 19) { + return Few + } + return Other + }, + }) + RegisterPluralSpec([]string{"bs", "hr", "sh", "sr"}, &PluralSpec{ + Plurals: newPluralSet(One, Few, Other), + PluralFunc: func(ops *Operands) Plural { + // v = 0 and i % 10 = 1 and i % 100 != 11 or f % 10 = 1 and f % 100 != 11 + if intEqualsAny(ops.V, 0) && intEqualsAny(ops.I%10, 1) && !intEqualsAny(ops.I%100, 11) || + intEqualsAny(ops.F%10, 1) && !intEqualsAny(ops.F%100, 11) { + return One + } + // v = 0 and i % 10 = 2..4 and i % 100 != 12..14 or f % 10 = 2..4 and f % 100 != 12..14 + if intEqualsAny(ops.V, 0) && intInRange(ops.I%10, 2, 4) && !intInRange(ops.I%100, 12, 14) || + intInRange(ops.F%10, 2, 4) && !intInRange(ops.F%100, 12, 14) { + return Few + } + return Other + }, + }) + RegisterPluralSpec([]string{"gd"}, &PluralSpec{ + Plurals: newPluralSet(One, Two, Few, Other), + PluralFunc: func(ops *Operands) Plural { + // n = 1,11 + if ops.NequalsAny(1, 11) { + return One + } + // n = 2,12 + if ops.NequalsAny(2, 12) { + return Two + } + // n = 3..10,13..19 + if ops.NinRange(3, 10) || ops.NinRange(13, 19) { + return Few + } + return Other + }, + }) + RegisterPluralSpec([]string{"sl"}, &PluralSpec{ + Plurals: newPluralSet(One, Two, Few, Other), + PluralFunc: func(ops *Operands) Plural { + // v = 0 and i % 100 = 1 + if intEqualsAny(ops.V, 0) && intEqualsAny(ops.I%100, 1) { + return One + } + // v = 0 and i % 100 = 2 + if intEqualsAny(ops.V, 0) && intEqualsAny(ops.I%100, 2) { + return Two + } + // v = 0 and i % 100 = 3..4 or v != 0 + if intEqualsAny(ops.V, 0) && intInRange(ops.I%100, 3, 4) || + !intEqualsAny(ops.V, 0) { + return Few + } + return Other + }, + }) + RegisterPluralSpec([]string{"dsb", "hsb"}, &PluralSpec{ + Plurals: newPluralSet(One, Two, Few, Other), + PluralFunc: func(ops *Operands) Plural { + // v = 0 and i % 100 = 1 or f % 100 = 1 + if intEqualsAny(ops.V, 0) && intEqualsAny(ops.I%100, 1) || + intEqualsAny(ops.F%100, 1) { + return One + } + // v = 0 and i % 100 = 2 or f % 100 = 2 + if intEqualsAny(ops.V, 0) && intEqualsAny(ops.I%100, 2) || + intEqualsAny(ops.F%100, 2) { + return Two + } + // v = 0 and i % 100 = 3..4 or f % 100 = 3..4 + if intEqualsAny(ops.V, 0) && intInRange(ops.I%100, 3, 4) || + intInRange(ops.F%100, 3, 4) { + return Few + } + return Other + }, + }) + RegisterPluralSpec([]string{"he", "iw"}, &PluralSpec{ + Plurals: newPluralSet(One, Two, Many, Other), + PluralFunc: func(ops *Operands) Plural { + // i = 1 and v = 0 + if intEqualsAny(ops.I, 1) && intEqualsAny(ops.V, 0) { + return One + } + // i = 2 and v = 0 + if intEqualsAny(ops.I, 2) && intEqualsAny(ops.V, 0) { + return Two + } + // v = 0 and n != 0..10 and n % 10 = 0 + if intEqualsAny(ops.V, 0) && !ops.NinRange(0, 10) && ops.NmodEqualsAny(10, 0) { + return Many + } + return Other + }, + }) + RegisterPluralSpec([]string{"cs", "sk"}, &PluralSpec{ + Plurals: newPluralSet(One, Few, Many, Other), + PluralFunc: func(ops *Operands) Plural { + // i = 1 and v = 0 + if intEqualsAny(ops.I, 1) && intEqualsAny(ops.V, 0) { + return One + } + // i = 2..4 and v = 0 + if intInRange(ops.I, 2, 4) && intEqualsAny(ops.V, 0) { + return Few + } + // v != 0 + if !intEqualsAny(ops.V, 0) { + return Many + } + return Other + }, + }) + RegisterPluralSpec([]string{"pl"}, &PluralSpec{ + Plurals: newPluralSet(One, Few, Many, Other), + PluralFunc: func(ops *Operands) Plural { + // i = 1 and v = 0 + if intEqualsAny(ops.I, 1) && intEqualsAny(ops.V, 0) { + return One + } + // v = 0 and i % 10 = 2..4 and i % 100 != 12..14 + if intEqualsAny(ops.V, 0) && intInRange(ops.I%10, 2, 4) && !intInRange(ops.I%100, 12, 14) { + return Few + } + // v = 0 and i != 1 and i % 10 = 0..1 or v = 0 and i % 10 = 5..9 or v = 0 and i % 100 = 12..14 + if intEqualsAny(ops.V, 0) && !intEqualsAny(ops.I, 1) && intInRange(ops.I%10, 0, 1) || + intEqualsAny(ops.V, 0) && intInRange(ops.I%10, 5, 9) || + intEqualsAny(ops.V, 0) && intInRange(ops.I%100, 12, 14) { + return Many + } + return Other + }, + }) + RegisterPluralSpec([]string{"be"}, &PluralSpec{ + Plurals: newPluralSet(One, Few, Many, Other), + PluralFunc: func(ops *Operands) Plural { + // n % 10 = 1 and n % 100 != 11 + if ops.NmodEqualsAny(10, 1) && !ops.NmodEqualsAny(100, 11) { + return One + } + // n % 10 = 2..4 and n % 100 != 12..14 + if ops.NmodInRange(10, 2, 4) && !ops.NmodInRange(100, 12, 14) { + return Few + } + // n % 10 = 0 or n % 10 = 5..9 or n % 100 = 11..14 + if ops.NmodEqualsAny(10, 0) || + ops.NmodInRange(10, 5, 9) || + ops.NmodInRange(100, 11, 14) { + return Many + } + return Other + }, + }) + RegisterPluralSpec([]string{"lt"}, &PluralSpec{ + Plurals: newPluralSet(One, Few, Many, Other), + PluralFunc: func(ops *Operands) Plural { + // n % 10 = 1 and n % 100 != 11..19 + if ops.NmodEqualsAny(10, 1) && !ops.NmodInRange(100, 11, 19) { + return One + } + // n % 10 = 2..9 and n % 100 != 11..19 + if ops.NmodInRange(10, 2, 9) && !ops.NmodInRange(100, 11, 19) { + return Few + } + // f != 0 + if !intEqualsAny(ops.F, 0) { + return Many + } + return Other + }, + }) + RegisterPluralSpec([]string{"mt"}, &PluralSpec{ + Plurals: newPluralSet(One, Few, Many, Other), + PluralFunc: func(ops *Operands) Plural { + // n = 1 + if ops.NequalsAny(1) { + return One + } + // n = 0 or n % 100 = 2..10 + if ops.NequalsAny(0) || + ops.NmodInRange(100, 2, 10) { + return Few + } + // n % 100 = 11..19 + if ops.NmodInRange(100, 11, 19) { + return Many + } + return Other + }, + }) + RegisterPluralSpec([]string{"ru", "uk"}, &PluralSpec{ + Plurals: newPluralSet(One, Few, Many, Other), + PluralFunc: func(ops *Operands) Plural { + // v = 0 and i % 10 = 1 and i % 100 != 11 + if intEqualsAny(ops.V, 0) && intEqualsAny(ops.I%10, 1) && !intEqualsAny(ops.I%100, 11) { + return One + } + // v = 0 and i % 10 = 2..4 and i % 100 != 12..14 + if intEqualsAny(ops.V, 0) && intInRange(ops.I%10, 2, 4) && !intInRange(ops.I%100, 12, 14) { + return Few + } + // v = 0 and i % 10 = 0 or v = 0 and i % 10 = 5..9 or v = 0 and i % 100 = 11..14 + if intEqualsAny(ops.V, 0) && intEqualsAny(ops.I%10, 0) || + intEqualsAny(ops.V, 0) && intInRange(ops.I%10, 5, 9) || + intEqualsAny(ops.V, 0) && intInRange(ops.I%100, 11, 14) { + return Many + } + return Other + }, + }) + RegisterPluralSpec([]string{"br"}, &PluralSpec{ + Plurals: newPluralSet(One, Two, Few, Many, Other), + PluralFunc: func(ops *Operands) Plural { + // n % 10 = 1 and n % 100 != 11,71,91 + if ops.NmodEqualsAny(10, 1) && !ops.NmodEqualsAny(100, 11, 71, 91) { + return One + } + // n % 10 = 2 and n % 100 != 12,72,92 + if ops.NmodEqualsAny(10, 2) && !ops.NmodEqualsAny(100, 12, 72, 92) { + return Two + } + // n % 10 = 3..4,9 and n % 100 != 10..19,70..79,90..99 + if (ops.NmodInRange(10, 3, 4) || ops.NmodEqualsAny(10, 9)) && !(ops.NmodInRange(100, 10, 19) || ops.NmodInRange(100, 70, 79) || ops.NmodInRange(100, 90, 99)) { + return Few + } + // n != 0 and n % 1000000 = 0 + if !ops.NequalsAny(0) && ops.NmodEqualsAny(1000000, 0) { + return Many + } + return Other + }, + }) + RegisterPluralSpec([]string{"ga"}, &PluralSpec{ + Plurals: newPluralSet(One, Two, Few, Many, Other), + PluralFunc: func(ops *Operands) Plural { + // n = 1 + if ops.NequalsAny(1) { + return One + } + // n = 2 + if ops.NequalsAny(2) { + return Two + } + // n = 3..6 + if ops.NinRange(3, 6) { + return Few + } + // n = 7..10 + if ops.NinRange(7, 10) { + return Many + } + return Other + }, + }) + RegisterPluralSpec([]string{"gv"}, &PluralSpec{ + Plurals: newPluralSet(One, Two, Few, Many, Other), + PluralFunc: func(ops *Operands) Plural { + // v = 0 and i % 10 = 1 + if intEqualsAny(ops.V, 0) && intEqualsAny(ops.I%10, 1) { + return One + } + // v = 0 and i % 10 = 2 + if intEqualsAny(ops.V, 0) && intEqualsAny(ops.I%10, 2) { + return Two + } + // v = 0 and i % 100 = 0,20,40,60,80 + if intEqualsAny(ops.V, 0) && intEqualsAny(ops.I%100, 0, 20, 40, 60, 80) { + return Few + } + // v != 0 + if !intEqualsAny(ops.V, 0) { + return Many + } + return Other + }, + }) + RegisterPluralSpec([]string{"ar", "ars"}, &PluralSpec{ + Plurals: newPluralSet(Zero, One, Two, Few, Many, Other), + PluralFunc: func(ops *Operands) Plural { + // n = 0 + if ops.NequalsAny(0) { + return Zero + } + // n = 1 + if ops.NequalsAny(1) { + return One + } + // n = 2 + if ops.NequalsAny(2) { + return Two + } + // n % 100 = 3..10 + if ops.NmodInRange(100, 3, 10) { + return Few + } + // n % 100 = 11..99 + if ops.NmodInRange(100, 11, 99) { + return Many + } + return Other + }, + }) + RegisterPluralSpec([]string{"cy"}, &PluralSpec{ + Plurals: newPluralSet(Zero, One, Two, Few, Many, Other), + PluralFunc: func(ops *Operands) Plural { + // n = 0 + if ops.NequalsAny(0) { + return Zero + } + // n = 1 + if ops.NequalsAny(1) { + return One + } + // n = 2 + if ops.NequalsAny(2) { + return Two + } + // n = 3 + if ops.NequalsAny(3) { + return Few + } + // n = 6 + if ops.NequalsAny(6) { + return Many + } + return Other + }, + }) +} diff --git a/internal/go-i18n/i18n/language/pluralspec_gen_test.go b/internal/go-i18n/i18n/language/pluralspec_gen_test.go new file mode 100644 index 0000000..4cfa97b --- /dev/null +++ b/internal/go-i18n/i18n/language/pluralspec_gen_test.go @@ -0,0 +1,631 @@ +package language + +// This file is generated by i18n/language/codegen/generate.sh + +import "testing" + +func TestBmBoDzIdIgIiInJaJboJvJwKdeKeaKmKoLktLoMsMyNqoRootSahSesSgThToViWoYoYueZh(t *testing.T) { + var tests []pluralTest + + tests = appendIntegerTests(tests, Other, []string{"0~15", "100", "1000", "10000", "100000", "1000000"}) + tests = appendDecimalTests(tests, Other, []string{"0.0~1.5", "10.0", "100.0", "1000.0", "10000.0", "100000.0", "1000000.0"}) + + locales := []string{"bm", "bo", "dz", "id", "ig", "ii", "in", "ja", "jbo", "jv", "jw", "kde", "kea", "km", "ko", "lkt", "lo", "ms", "my", "nqo", "root", "sah", "ses", "sg", "th", "to", "vi", "wo", "yo", "yue", "zh"} + for _, locale := range locales { + runTests(t, locale, tests) + } +} + +func TestAmAsBnFaGuHiKnMrZu(t *testing.T) { + var tests []pluralTest + + tests = appendIntegerTests(tests, One, []string{"0", "1"}) + tests = appendDecimalTests(tests, One, []string{"0.0~1.0", "0.00~0.04"}) + + tests = appendIntegerTests(tests, Other, []string{"2~17", "100", "1000", "10000", "100000", "1000000"}) + tests = appendDecimalTests(tests, Other, []string{"1.1~2.6", "10.0", "100.0", "1000.0", "10000.0", "100000.0", "1000000.0"}) + + locales := []string{"am", "as", "bn", "fa", "gu", "hi", "kn", "mr", "zu"} + for _, locale := range locales { + runTests(t, locale, tests) + } +} + +func TestFfFrHyKab(t *testing.T) { + var tests []pluralTest + + tests = appendIntegerTests(tests, One, []string{"0", "1"}) + tests = appendDecimalTests(tests, One, []string{"0.0~1.5"}) + + tests = appendIntegerTests(tests, Other, []string{"2~17", "100", "1000", "10000", "100000", "1000000"}) + tests = appendDecimalTests(tests, Other, []string{"2.0~3.5", "10.0", "100.0", "1000.0", "10000.0", "100000.0", "1000000.0"}) + + locales := []string{"ff", "fr", "hy", "kab"} + for _, locale := range locales { + runTests(t, locale, tests) + } +} + +func TestPt(t *testing.T) { + var tests []pluralTest + + tests = appendIntegerTests(tests, One, []string{"0", "1"}) + tests = appendDecimalTests(tests, One, []string{"0.0~1.5"}) + + tests = appendIntegerTests(tests, Other, []string{"2~17", "100", "1000", "10000", "100000", "1000000"}) + tests = appendDecimalTests(tests, Other, []string{"2.0~3.5", "10.0", "100.0", "1000.0", "10000.0", "100000.0", "1000000.0"}) + + locales := []string{"pt"} + for _, locale := range locales { + runTests(t, locale, tests) + } +} + +func TestAstCaDeEnEtFiFyGlItJiNlSvSwUrYi(t *testing.T) { + var tests []pluralTest + + tests = appendIntegerTests(tests, One, []string{"1"}) + + tests = appendIntegerTests(tests, Other, []string{"0", "2~16", "100", "1000", "10000", "100000", "1000000"}) + tests = appendDecimalTests(tests, Other, []string{"0.0~1.5", "10.0", "100.0", "1000.0", "10000.0", "100000.0", "1000000.0"}) + + locales := []string{"ast", "ca", "de", "en", "et", "fi", "fy", "gl", "it", "ji", "nl", "sv", "sw", "ur", "yi"} + for _, locale := range locales { + runTests(t, locale, tests) + } +} + +func TestSi(t *testing.T) { + var tests []pluralTest + + tests = appendIntegerTests(tests, One, []string{"0", "1"}) + tests = appendDecimalTests(tests, One, []string{"0.0", "0.1", "1.0", "0.00", "0.01", "1.00", "0.000", "0.001", "1.000", "0.0000", "0.0001", "1.0000"}) + + tests = appendIntegerTests(tests, Other, []string{"2~17", "100", "1000", "10000", "100000", "1000000"}) + tests = appendDecimalTests(tests, Other, []string{"0.2~0.9", "1.1~1.8", "10.0", "100.0", "1000.0", "10000.0", "100000.0", "1000000.0"}) + + locales := []string{"si"} + for _, locale := range locales { + runTests(t, locale, tests) + } +} + +func TestAkBhGuwLnMgNsoPaTiWa(t *testing.T) { + var tests []pluralTest + + tests = appendIntegerTests(tests, One, []string{"0", "1"}) + tests = appendDecimalTests(tests, One, []string{"0.0", "1.0", "0.00", "1.00", "0.000", "1.000", "0.0000", "1.0000"}) + + tests = appendIntegerTests(tests, Other, []string{"2~17", "100", "1000", "10000", "100000", "1000000"}) + tests = appendDecimalTests(tests, Other, []string{"0.1~0.9", "1.1~1.7", "10.0", "100.0", "1000.0", "10000.0", "100000.0", "1000000.0"}) + + locales := []string{"ak", "bh", "guw", "ln", "mg", "nso", "pa", "ti", "wa"} + for _, locale := range locales { + runTests(t, locale, tests) + } +} + +func TestTzm(t *testing.T) { + var tests []pluralTest + + tests = appendIntegerTests(tests, One, []string{"0", "1", "11~24"}) + tests = appendDecimalTests(tests, One, []string{"0.0", "1.0", "11.0", "12.0", "13.0", "14.0", "15.0", "16.0", "17.0", "18.0", "19.0", "20.0", "21.0", "22.0", "23.0", "24.0"}) + + tests = appendIntegerTests(tests, Other, []string{"2~10", "100~106", "1000", "10000", "100000", "1000000"}) + tests = appendDecimalTests(tests, Other, []string{"0.1~0.9", "1.1~1.7", "10.0", "100.0", "1000.0", "10000.0", "100000.0", "1000000.0"}) + + locales := []string{"tzm"} + for _, locale := range locales { + runTests(t, locale, tests) + } +} + +func TestAfAsaAzBemBezBgBrxCeCggChrCkbDvEeElEoEsEuFoFurGswHaHawHuJgoJmcKaKajKcgKkKkjKlKsKsbKuKyLbLgMasMgoMlMnNahNbNdNeNnNnhNoNrNyNynOmOrOsPapPsRmRofRwkSaqSdhSehSnSoSqSsSsyStSyrTaTeTeoTigTkTnTrTsUgUzVeVoVunWaeXhXog(t *testing.T) { + var tests []pluralTest + + tests = appendIntegerTests(tests, One, []string{"1"}) + tests = appendDecimalTests(tests, One, []string{"1.0", "1.00", "1.000", "1.0000"}) + + tests = appendIntegerTests(tests, Other, []string{"0", "2~16", "100", "1000", "10000", "100000", "1000000"}) + tests = appendDecimalTests(tests, Other, []string{"0.0~0.9", "1.1~1.6", "10.0", "100.0", "1000.0", "10000.0", "100000.0", "1000000.0"}) + + locales := []string{"af", "asa", "az", "bem", "bez", "bg", "brx", "ce", "cgg", "chr", "ckb", "dv", "ee", "el", "eo", "es", "eu", "fo", "fur", "gsw", "ha", "haw", "hu", "jgo", "jmc", "ka", "kaj", "kcg", "kk", "kkj", "kl", "ks", "ksb", "ku", "ky", "lb", "lg", "mas", "mgo", "ml", "mn", "nah", "nb", "nd", "ne", "nn", "nnh", "no", "nr", "ny", "nyn", "om", "or", "os", "pap", "ps", "rm", "rof", "rwk", "saq", "sdh", "seh", "sn", "so", "sq", "ss", "ssy", "st", "syr", "ta", "te", "teo", "tig", "tk", "tn", "tr", "ts", "ug", "uz", "ve", "vo", "vun", "wae", "xh", "xog"} + for _, locale := range locales { + runTests(t, locale, tests) + } +} + +func TestDa(t *testing.T) { + var tests []pluralTest + + tests = appendIntegerTests(tests, One, []string{"1"}) + tests = appendDecimalTests(tests, One, []string{"0.1~1.6"}) + + tests = appendIntegerTests(tests, Other, []string{"0", "2~16", "100", "1000", "10000", "100000", "1000000"}) + tests = appendDecimalTests(tests, Other, []string{"0.0", "2.0~3.4", "10.0", "100.0", "1000.0", "10000.0", "100000.0", "1000000.0"}) + + locales := []string{"da"} + for _, locale := range locales { + runTests(t, locale, tests) + } +} + +func TestIs(t *testing.T) { + var tests []pluralTest + + tests = appendIntegerTests(tests, One, []string{"1", "21", "31", "41", "51", "61", "71", "81", "101", "1001"}) + tests = appendDecimalTests(tests, One, []string{"0.1~1.6", "10.1", "100.1", "1000.1"}) + + tests = appendIntegerTests(tests, Other, []string{"0", "2~16", "100", "1000", "10000", "100000", "1000000"}) + tests = appendDecimalTests(tests, Other, []string{"0.0", "2.0", "3.0", "4.0", "5.0", "6.0", "7.0", "8.0", "10.0", "100.0", "1000.0", "10000.0", "100000.0", "1000000.0"}) + + locales := []string{"is"} + for _, locale := range locales { + runTests(t, locale, tests) + } +} + +func TestMk(t *testing.T) { + var tests []pluralTest + + tests = appendIntegerTests(tests, One, []string{"1", "11", "21", "31", "41", "51", "61", "71", "101", "1001"}) + tests = appendDecimalTests(tests, One, []string{"0.1", "1.1", "2.1", "3.1", "4.1", "5.1", "6.1", "7.1", "10.1", "100.1", "1000.1"}) + + tests = appendIntegerTests(tests, Other, []string{"0", "2~10", "12~17", "100", "1000", "10000", "100000", "1000000"}) + tests = appendDecimalTests(tests, Other, []string{"0.0", "0.2~1.0", "1.2~1.7", "10.0", "100.0", "1000.0", "10000.0", "100000.0", "1000000.0"}) + + locales := []string{"mk"} + for _, locale := range locales { + runTests(t, locale, tests) + } +} + +func TestFilTl(t *testing.T) { + var tests []pluralTest + + tests = appendIntegerTests(tests, One, []string{"0~3", "5", "7", "8", "10~13", "15", "17", "18", "20", "21", "100", "1000", "10000", "100000", "1000000"}) + tests = appendDecimalTests(tests, One, []string{"0.0~0.3", "0.5", "0.7", "0.8", "1.0~1.3", "1.5", "1.7", "1.8", "2.0", "2.1", "10.0", "100.0", "1000.0", "10000.0", "100000.0", "1000000.0"}) + + tests = appendIntegerTests(tests, Other, []string{"4", "6", "9", "14", "16", "19", "24", "26", "104", "1004"}) + tests = appendDecimalTests(tests, Other, []string{"0.4", "0.6", "0.9", "1.4", "1.6", "1.9", "2.4", "2.6", "10.4", "100.4", "1000.4"}) + + locales := []string{"fil", "tl"} + for _, locale := range locales { + runTests(t, locale, tests) + } +} + +func TestLvPrg(t *testing.T) { + var tests []pluralTest + + tests = appendIntegerTests(tests, Zero, []string{"0", "10~20", "30", "40", "50", "60", "100", "1000", "10000", "100000", "1000000"}) + tests = appendDecimalTests(tests, Zero, []string{"0.0", "10.0", "11.0", "12.0", "13.0", "14.0", "15.0", "16.0", "100.0", "1000.0", "10000.0", "100000.0", "1000000.0"}) + + tests = appendIntegerTests(tests, One, []string{"1", "21", "31", "41", "51", "61", "71", "81", "101", "1001"}) + tests = appendDecimalTests(tests, One, []string{"0.1", "1.0", "1.1", "2.1", "3.1", "4.1", "5.1", "6.1", "7.1", "10.1", "100.1", "1000.1"}) + + tests = appendIntegerTests(tests, Other, []string{"2~9", "22~29", "102", "1002"}) + tests = appendDecimalTests(tests, Other, []string{"0.2~0.9", "1.2~1.9", "10.2", "100.2", "1000.2"}) + + locales := []string{"lv", "prg"} + for _, locale := range locales { + runTests(t, locale, tests) + } +} + +func TestLag(t *testing.T) { + var tests []pluralTest + + tests = appendIntegerTests(tests, Zero, []string{"0"}) + tests = appendDecimalTests(tests, Zero, []string{"0.0", "0.00", "0.000", "0.0000"}) + + tests = appendIntegerTests(tests, One, []string{"1"}) + tests = appendDecimalTests(tests, One, []string{"0.1~1.6"}) + + tests = appendIntegerTests(tests, Other, []string{"2~17", "100", "1000", "10000", "100000", "1000000"}) + tests = appendDecimalTests(tests, Other, []string{"2.0~3.5", "10.0", "100.0", "1000.0", "10000.0", "100000.0", "1000000.0"}) + + locales := []string{"lag"} + for _, locale := range locales { + runTests(t, locale, tests) + } +} + +func TestKsh(t *testing.T) { + var tests []pluralTest + + tests = appendIntegerTests(tests, Zero, []string{"0"}) + tests = appendDecimalTests(tests, Zero, []string{"0.0", "0.00", "0.000", "0.0000"}) + + tests = appendIntegerTests(tests, One, []string{"1"}) + tests = appendDecimalTests(tests, One, []string{"1.0", "1.00", "1.000", "1.0000"}) + + tests = appendIntegerTests(tests, Other, []string{"2~17", "100", "1000", "10000", "100000", "1000000"}) + tests = appendDecimalTests(tests, Other, []string{"0.1~0.9", "1.1~1.7", "10.0", "100.0", "1000.0", "10000.0", "100000.0", "1000000.0"}) + + locales := []string{"ksh"} + for _, locale := range locales { + runTests(t, locale, tests) + } +} + +func TestIuKwNaqSeSmaSmiSmjSmnSms(t *testing.T) { + var tests []pluralTest + + tests = appendIntegerTests(tests, One, []string{"1"}) + tests = appendDecimalTests(tests, One, []string{"1.0", "1.00", "1.000", "1.0000"}) + + tests = appendIntegerTests(tests, Two, []string{"2"}) + tests = appendDecimalTests(tests, Two, []string{"2.0", "2.00", "2.000", "2.0000"}) + + tests = appendIntegerTests(tests, Other, []string{"0", "3~17", "100", "1000", "10000", "100000", "1000000"}) + tests = appendDecimalTests(tests, Other, []string{"0.0~0.9", "1.1~1.6", "10.0", "100.0", "1000.0", "10000.0", "100000.0", "1000000.0"}) + + locales := []string{"iu", "kw", "naq", "se", "sma", "smi", "smj", "smn", "sms"} + for _, locale := range locales { + runTests(t, locale, tests) + } +} + +func TestShi(t *testing.T) { + var tests []pluralTest + + tests = appendIntegerTests(tests, One, []string{"0", "1"}) + tests = appendDecimalTests(tests, One, []string{"0.0~1.0", "0.00~0.04"}) + + tests = appendIntegerTests(tests, Few, []string{"2~10"}) + tests = appendDecimalTests(tests, Few, []string{"2.0", "3.0", "4.0", "5.0", "6.0", "7.0", "8.0", "9.0", "10.0", "2.00", "3.00", "4.00", "5.00", "6.00", "7.00", "8.00"}) + + tests = appendIntegerTests(tests, Other, []string{"11~26", "100", "1000", "10000", "100000", "1000000"}) + tests = appendDecimalTests(tests, Other, []string{"1.1~1.9", "2.1~2.7", "10.1", "100.0", "1000.0", "10000.0", "100000.0", "1000000.0"}) + + locales := []string{"shi"} + for _, locale := range locales { + runTests(t, locale, tests) + } +} + +func TestMoRo(t *testing.T) { + var tests []pluralTest + + tests = appendIntegerTests(tests, One, []string{"1"}) + + tests = appendIntegerTests(tests, Few, []string{"0", "2~16", "101", "1001"}) + tests = appendDecimalTests(tests, Few, []string{"0.0~1.5", "10.0", "100.0", "1000.0", "10000.0", "100000.0", "1000000.0"}) + + tests = appendIntegerTests(tests, Other, []string{"20~35", "100", "1000", "10000", "100000", "1000000"}) + + locales := []string{"mo", "ro"} + for _, locale := range locales { + runTests(t, locale, tests) + } +} + +func TestBsHrShSr(t *testing.T) { + var tests []pluralTest + + tests = appendIntegerTests(tests, One, []string{"1", "21", "31", "41", "51", "61", "71", "81", "101", "1001"}) + tests = appendDecimalTests(tests, One, []string{"0.1", "1.1", "2.1", "3.1", "4.1", "5.1", "6.1", "7.1", "10.1", "100.1", "1000.1"}) + + tests = appendIntegerTests(tests, Few, []string{"2~4", "22~24", "32~34", "42~44", "52~54", "62", "102", "1002"}) + tests = appendDecimalTests(tests, Few, []string{"0.2~0.4", "1.2~1.4", "2.2~2.4", "3.2~3.4", "4.2~4.4", "5.2", "10.2", "100.2", "1000.2"}) + + tests = appendIntegerTests(tests, Other, []string{"0", "5~19", "100", "1000", "10000", "100000", "1000000"}) + tests = appendDecimalTests(tests, Other, []string{"0.0", "0.5~1.0", "1.5~2.0", "2.5~2.7", "10.0", "100.0", "1000.0", "10000.0", "100000.0", "1000000.0"}) + + locales := []string{"bs", "hr", "sh", "sr"} + for _, locale := range locales { + runTests(t, locale, tests) + } +} + +func TestGd(t *testing.T) { + var tests []pluralTest + + tests = appendIntegerTests(tests, One, []string{"1", "11"}) + tests = appendDecimalTests(tests, One, []string{"1.0", "11.0", "1.00", "11.00", "1.000", "11.000", "1.0000"}) + + tests = appendIntegerTests(tests, Two, []string{"2", "12"}) + tests = appendDecimalTests(tests, Two, []string{"2.0", "12.0", "2.00", "12.00", "2.000", "12.000", "2.0000"}) + + tests = appendIntegerTests(tests, Few, []string{"3~10", "13~19"}) + tests = appendDecimalTests(tests, Few, []string{"3.0", "4.0", "5.0", "6.0", "7.0", "8.0", "9.0", "10.0", "13.0", "14.0", "15.0", "16.0", "17.0", "18.0", "19.0", "3.00"}) + + tests = appendIntegerTests(tests, Other, []string{"0", "20~34", "100", "1000", "10000", "100000", "1000000"}) + tests = appendDecimalTests(tests, Other, []string{"0.0~0.9", "1.1~1.6", "10.1", "100.0", "1000.0", "10000.0", "100000.0", "1000000.0"}) + + locales := []string{"gd"} + for _, locale := range locales { + runTests(t, locale, tests) + } +} + +func TestSl(t *testing.T) { + var tests []pluralTest + + tests = appendIntegerTests(tests, One, []string{"1", "101", "201", "301", "401", "501", "601", "701", "1001"}) + + tests = appendIntegerTests(tests, Two, []string{"2", "102", "202", "302", "402", "502", "602", "702", "1002"}) + + tests = appendIntegerTests(tests, Few, []string{"3", "4", "103", "104", "203", "204", "303", "304", "403", "404", "503", "504", "603", "604", "703", "704", "1003"}) + tests = appendDecimalTests(tests, Few, []string{"0.0~1.5", "10.0", "100.0", "1000.0", "10000.0", "100000.0", "1000000.0"}) + + tests = appendIntegerTests(tests, Other, []string{"0", "5~19", "100", "1000", "10000", "100000", "1000000"}) + + locales := []string{"sl"} + for _, locale := range locales { + runTests(t, locale, tests) + } +} + +func TestDsbHsb(t *testing.T) { + var tests []pluralTest + + tests = appendIntegerTests(tests, One, []string{"1", "101", "201", "301", "401", "501", "601", "701", "1001"}) + tests = appendDecimalTests(tests, One, []string{"0.1", "1.1", "2.1", "3.1", "4.1", "5.1", "6.1", "7.1", "10.1", "100.1", "1000.1"}) + + tests = appendIntegerTests(tests, Two, []string{"2", "102", "202", "302", "402", "502", "602", "702", "1002"}) + tests = appendDecimalTests(tests, Two, []string{"0.2", "1.2", "2.2", "3.2", "4.2", "5.2", "6.2", "7.2", "10.2", "100.2", "1000.2"}) + + tests = appendIntegerTests(tests, Few, []string{"3", "4", "103", "104", "203", "204", "303", "304", "403", "404", "503", "504", "603", "604", "703", "704", "1003"}) + tests = appendDecimalTests(tests, Few, []string{"0.3", "0.4", "1.3", "1.4", "2.3", "2.4", "3.3", "3.4", "4.3", "4.4", "5.3", "5.4", "6.3", "6.4", "7.3", "7.4", "10.3", "100.3", "1000.3"}) + + tests = appendIntegerTests(tests, Other, []string{"0", "5~19", "100", "1000", "10000", "100000", "1000000"}) + tests = appendDecimalTests(tests, Other, []string{"0.0", "0.5~1.0", "1.5~2.0", "2.5~2.7", "10.0", "100.0", "1000.0", "10000.0", "100000.0", "1000000.0"}) + + locales := []string{"dsb", "hsb"} + for _, locale := range locales { + runTests(t, locale, tests) + } +} + +func TestHeIw(t *testing.T) { + var tests []pluralTest + + tests = appendIntegerTests(tests, One, []string{"1"}) + + tests = appendIntegerTests(tests, Two, []string{"2"}) + + tests = appendIntegerTests(tests, Many, []string{"20", "30", "40", "50", "60", "70", "80", "90", "100", "1000", "10000", "100000", "1000000"}) + + tests = appendIntegerTests(tests, Other, []string{"0", "3~17", "101", "1001"}) + tests = appendDecimalTests(tests, Other, []string{"0.0~1.5", "10.0", "100.0", "1000.0", "10000.0", "100000.0", "1000000.0"}) + + locales := []string{"he", "iw"} + for _, locale := range locales { + runTests(t, locale, tests) + } +} + +func TestCsSk(t *testing.T) { + var tests []pluralTest + + tests = appendIntegerTests(tests, One, []string{"1"}) + + tests = appendIntegerTests(tests, Few, []string{"2~4"}) + + tests = appendDecimalTests(tests, Many, []string{"0.0~1.5", "10.0", "100.0", "1000.0", "10000.0", "100000.0", "1000000.0"}) + + tests = appendIntegerTests(tests, Other, []string{"0", "5~19", "100", "1000", "10000", "100000", "1000000"}) + + locales := []string{"cs", "sk"} + for _, locale := range locales { + runTests(t, locale, tests) + } +} + +func TestPl(t *testing.T) { + var tests []pluralTest + + tests = appendIntegerTests(tests, One, []string{"1"}) + + tests = appendIntegerTests(tests, Few, []string{"2~4", "22~24", "32~34", "42~44", "52~54", "62", "102", "1002"}) + + tests = appendIntegerTests(tests, Many, []string{"0", "5~19", "100", "1000", "10000", "100000", "1000000"}) + + tests = appendDecimalTests(tests, Other, []string{"0.0~1.5", "10.0", "100.0", "1000.0", "10000.0", "100000.0", "1000000.0"}) + + locales := []string{"pl"} + for _, locale := range locales { + runTests(t, locale, tests) + } +} + +func TestBe(t *testing.T) { + var tests []pluralTest + + tests = appendIntegerTests(tests, One, []string{"1", "21", "31", "41", "51", "61", "71", "81", "101", "1001"}) + tests = appendDecimalTests(tests, One, []string{"1.0", "21.0", "31.0", "41.0", "51.0", "61.0", "71.0", "81.0", "101.0", "1001.0"}) + + tests = appendIntegerTests(tests, Few, []string{"2~4", "22~24", "32~34", "42~44", "52~54", "62", "102", "1002"}) + tests = appendDecimalTests(tests, Few, []string{"2.0", "3.0", "4.0", "22.0", "23.0", "24.0", "32.0", "33.0", "102.0", "1002.0"}) + + tests = appendIntegerTests(tests, Many, []string{"0", "5~19", "100", "1000", "10000", "100000", "1000000"}) + tests = appendDecimalTests(tests, Many, []string{"0.0", "5.0", "6.0", "7.0", "8.0", "9.0", "10.0", "11.0", "100.0", "1000.0", "10000.0", "100000.0", "1000000.0"}) + + tests = appendDecimalTests(tests, Other, []string{"0.1~0.9", "1.1~1.7", "10.1", "100.1", "1000.1"}) + + locales := []string{"be"} + for _, locale := range locales { + runTests(t, locale, tests) + } +} + +func TestLt(t *testing.T) { + var tests []pluralTest + + tests = appendIntegerTests(tests, One, []string{"1", "21", "31", "41", "51", "61", "71", "81", "101", "1001"}) + tests = appendDecimalTests(tests, One, []string{"1.0", "21.0", "31.0", "41.0", "51.0", "61.0", "71.0", "81.0", "101.0", "1001.0"}) + + tests = appendIntegerTests(tests, Few, []string{"2~9", "22~29", "102", "1002"}) + tests = appendDecimalTests(tests, Few, []string{"2.0", "3.0", "4.0", "5.0", "6.0", "7.0", "8.0", "9.0", "22.0", "102.0", "1002.0"}) + + tests = appendDecimalTests(tests, Many, []string{"0.1~0.9", "1.1~1.7", "10.1", "100.1", "1000.1"}) + + tests = appendIntegerTests(tests, Other, []string{"0", "10~20", "30", "40", "50", "60", "100", "1000", "10000", "100000", "1000000"}) + tests = appendDecimalTests(tests, Other, []string{"0.0", "10.0", "11.0", "12.0", "13.0", "14.0", "15.0", "16.0", "100.0", "1000.0", "10000.0", "100000.0", "1000000.0"}) + + locales := []string{"lt"} + for _, locale := range locales { + runTests(t, locale, tests) + } +} + +func TestMt(t *testing.T) { + var tests []pluralTest + + tests = appendIntegerTests(tests, One, []string{"1"}) + tests = appendDecimalTests(tests, One, []string{"1.0", "1.00", "1.000", "1.0000"}) + + tests = appendIntegerTests(tests, Few, []string{"0", "2~10", "102~107", "1002"}) + tests = appendDecimalTests(tests, Few, []string{"0.0", "2.0", "3.0", "4.0", "5.0", "6.0", "7.0", "8.0", "10.0", "102.0", "1002.0"}) + + tests = appendIntegerTests(tests, Many, []string{"11~19", "111~117", "1011"}) + tests = appendDecimalTests(tests, Many, []string{"11.0", "12.0", "13.0", "14.0", "15.0", "16.0", "17.0", "18.0", "111.0", "1011.0"}) + + tests = appendIntegerTests(tests, Other, []string{"20~35", "100", "1000", "10000", "100000", "1000000"}) + tests = appendDecimalTests(tests, Other, []string{"0.1~0.9", "1.1~1.7", "10.1", "100.0", "1000.0", "10000.0", "100000.0", "1000000.0"}) + + locales := []string{"mt"} + for _, locale := range locales { + runTests(t, locale, tests) + } +} + +func TestRuUk(t *testing.T) { + var tests []pluralTest + + tests = appendIntegerTests(tests, One, []string{"1", "21", "31", "41", "51", "61", "71", "81", "101", "1001"}) + + tests = appendIntegerTests(tests, Few, []string{"2~4", "22~24", "32~34", "42~44", "52~54", "62", "102", "1002"}) + + tests = appendIntegerTests(tests, Many, []string{"0", "5~19", "100", "1000", "10000", "100000", "1000000"}) + + tests = appendDecimalTests(tests, Other, []string{"0.0~1.5", "10.0", "100.0", "1000.0", "10000.0", "100000.0", "1000000.0"}) + + locales := []string{"ru", "uk"} + for _, locale := range locales { + runTests(t, locale, tests) + } +} + +func TestBr(t *testing.T) { + var tests []pluralTest + + tests = appendIntegerTests(tests, One, []string{"1", "21", "31", "41", "51", "61", "81", "101", "1001"}) + tests = appendDecimalTests(tests, One, []string{"1.0", "21.0", "31.0", "41.0", "51.0", "61.0", "81.0", "101.0", "1001.0"}) + + tests = appendIntegerTests(tests, Two, []string{"2", "22", "32", "42", "52", "62", "82", "102", "1002"}) + tests = appendDecimalTests(tests, Two, []string{"2.0", "22.0", "32.0", "42.0", "52.0", "62.0", "82.0", "102.0", "1002.0"}) + + tests = appendIntegerTests(tests, Few, []string{"3", "4", "9", "23", "24", "29", "33", "34", "39", "43", "44", "49", "103", "1003"}) + tests = appendDecimalTests(tests, Few, []string{"3.0", "4.0", "9.0", "23.0", "24.0", "29.0", "33.0", "34.0", "103.0", "1003.0"}) + + tests = appendIntegerTests(tests, Many, []string{"1000000"}) + tests = appendDecimalTests(tests, Many, []string{"1000000.0", "1000000.00", "1000000.000"}) + + tests = appendIntegerTests(tests, Other, []string{"0", "5~8", "10~20", "100", "1000", "10000", "100000"}) + tests = appendDecimalTests(tests, Other, []string{"0.0~0.9", "1.1~1.6", "10.0", "100.0", "1000.0", "10000.0", "100000.0"}) + + locales := []string{"br"} + for _, locale := range locales { + runTests(t, locale, tests) + } +} + +func TestGa(t *testing.T) { + var tests []pluralTest + + tests = appendIntegerTests(tests, One, []string{"1"}) + tests = appendDecimalTests(tests, One, []string{"1.0", "1.00", "1.000", "1.0000"}) + + tests = appendIntegerTests(tests, Two, []string{"2"}) + tests = appendDecimalTests(tests, Two, []string{"2.0", "2.00", "2.000", "2.0000"}) + + tests = appendIntegerTests(tests, Few, []string{"3~6"}) + tests = appendDecimalTests(tests, Few, []string{"3.0", "4.0", "5.0", "6.0", "3.00", "4.00", "5.00", "6.00", "3.000", "4.000", "5.000", "6.000", "3.0000", "4.0000", "5.0000", "6.0000"}) + + tests = appendIntegerTests(tests, Many, []string{"7~10"}) + tests = appendDecimalTests(tests, Many, []string{"7.0", "8.0", "9.0", "10.0", "7.00", "8.00", "9.00", "10.00", "7.000", "8.000", "9.000", "10.000", "7.0000", "8.0000", "9.0000", "10.0000"}) + + tests = appendIntegerTests(tests, Other, []string{"0", "11~25", "100", "1000", "10000", "100000", "1000000"}) + tests = appendDecimalTests(tests, Other, []string{"0.0~0.9", "1.1~1.6", "10.1", "100.0", "1000.0", "10000.0", "100000.0", "1000000.0"}) + + locales := []string{"ga"} + for _, locale := range locales { + runTests(t, locale, tests) + } +} + +func TestGv(t *testing.T) { + var tests []pluralTest + + tests = appendIntegerTests(tests, One, []string{"1", "11", "21", "31", "41", "51", "61", "71", "101", "1001"}) + + tests = appendIntegerTests(tests, Two, []string{"2", "12", "22", "32", "42", "52", "62", "72", "102", "1002"}) + + tests = appendIntegerTests(tests, Few, []string{"0", "20", "40", "60", "80", "100", "120", "140", "1000", "10000", "100000", "1000000"}) + + tests = appendDecimalTests(tests, Many, []string{"0.0~1.5", "10.0", "100.0", "1000.0", "10000.0", "100000.0", "1000000.0"}) + + tests = appendIntegerTests(tests, Other, []string{"3~10", "13~19", "23", "103", "1003"}) + + locales := []string{"gv"} + for _, locale := range locales { + runTests(t, locale, tests) + } +} + +func TestArArs(t *testing.T) { + var tests []pluralTest + + tests = appendIntegerTests(tests, Zero, []string{"0"}) + tests = appendDecimalTests(tests, Zero, []string{"0.0", "0.00", "0.000", "0.0000"}) + + tests = appendIntegerTests(tests, One, []string{"1"}) + tests = appendDecimalTests(tests, One, []string{"1.0", "1.00", "1.000", "1.0000"}) + + tests = appendIntegerTests(tests, Two, []string{"2"}) + tests = appendDecimalTests(tests, Two, []string{"2.0", "2.00", "2.000", "2.0000"}) + + tests = appendIntegerTests(tests, Few, []string{"3~10", "103~110", "1003"}) + tests = appendDecimalTests(tests, Few, []string{"3.0", "4.0", "5.0", "6.0", "7.0", "8.0", "9.0", "10.0", "103.0", "1003.0"}) + + tests = appendIntegerTests(tests, Many, []string{"11~26", "111", "1011"}) + tests = appendDecimalTests(tests, Many, []string{"11.0", "12.0", "13.0", "14.0", "15.0", "16.0", "17.0", "18.0", "111.0", "1011.0"}) + + tests = appendIntegerTests(tests, Other, []string{"100~102", "200~202", "300~302", "400~402", "500~502", "600", "1000", "10000", "100000", "1000000"}) + tests = appendDecimalTests(tests, Other, []string{"0.1~0.9", "1.1~1.7", "10.1", "100.0", "1000.0", "10000.0", "100000.0", "1000000.0"}) + + locales := []string{"ar", "ars"} + for _, locale := range locales { + runTests(t, locale, tests) + } +} + +func TestCy(t *testing.T) { + var tests []pluralTest + + tests = appendIntegerTests(tests, Zero, []string{"0"}) + tests = appendDecimalTests(tests, Zero, []string{"0.0", "0.00", "0.000", "0.0000"}) + + tests = appendIntegerTests(tests, One, []string{"1"}) + tests = appendDecimalTests(tests, One, []string{"1.0", "1.00", "1.000", "1.0000"}) + + tests = appendIntegerTests(tests, Two, []string{"2"}) + tests = appendDecimalTests(tests, Two, []string{"2.0", "2.00", "2.000", "2.0000"}) + + tests = appendIntegerTests(tests, Few, []string{"3"}) + tests = appendDecimalTests(tests, Few, []string{"3.0", "3.00", "3.000", "3.0000"}) + + tests = appendIntegerTests(tests, Many, []string{"6"}) + tests = appendDecimalTests(tests, Many, []string{"6.0", "6.00", "6.000", "6.0000"}) + + tests = appendIntegerTests(tests, Other, []string{"4", "5", "7~20", "100", "1000", "10000", "100000", "1000000"}) + tests = appendDecimalTests(tests, Other, []string{"0.1~0.9", "1.1~1.7", "10.0", "100.0", "1000.0", "10000.0", "100000.0", "1000000.0"}) + + locales := []string{"cy"} + for _, locale := range locales { + runTests(t, locale, tests) + } +} diff --git a/internal/go-i18n/i18n/language/pluralspec_test.go b/internal/go-i18n/i18n/language/pluralspec_test.go new file mode 100644 index 0000000..e31cfb7 --- /dev/null +++ b/internal/go-i18n/i18n/language/pluralspec_test.go @@ -0,0 +1,716 @@ +package language + +import ( + "fmt" + "strconv" + "strings" + "testing" +) + +const onePlusEpsilon = "1.00000000000000000000000000000001" + +func TestGetPluralSpec(t *testing.T) { + tests := []struct { + src string + spec *PluralSpec + }{ + {"pl", pluralSpecs["pl"]}, + {"en", pluralSpecs["en"]}, + {"en-US", pluralSpecs["en"]}, + {"en_US", pluralSpecs["en"]}, + {"en-GB", pluralSpecs["en"]}, + {"zh-CN", pluralSpecs["zh"]}, + {"zh-TW", pluralSpecs["zh"]}, + {"pt-BR", pluralSpecs["pt"]}, + {"pt_BR", pluralSpecs["pt"]}, + {"pt-PT", pluralSpecs["pt"]}, + {"pt_PT", pluralSpecs["pt"]}, + {"zh-Hans-CN", pluralSpecs["zh"]}, + {"zh-Hant-TW", pluralSpecs["zh"]}, + {"zh-CN", pluralSpecs["zh"]}, + {"zh-TW", pluralSpecs["zh"]}, + {"zh-Hans", pluralSpecs["zh"]}, + {"zh-Hant", pluralSpecs["zh"]}, + {"ko-KR", pluralSpecs["ko"]}, + {"ko_KR", pluralSpecs["ko"]}, + {"ko-KP", pluralSpecs["ko"]}, + {"ko_KP", pluralSpecs["ko"]}, + {"en-US-en-US", pluralSpecs["en"]}, + {"th", pluralSpecs["th"]}, + {"th-TH", pluralSpecs["th"]}, + {"hr", pluralSpecs["hr"]}, + {"bs", pluralSpecs["bs"]}, + {"sr", pluralSpecs["sr"]}, + {"ti", pluralSpecs["ti"]}, + {"vi", pluralSpecs["vi"]}, + {"vi-VN", pluralSpecs["vi"]}, + {"mk", pluralSpecs["mk"]}, + {"mk-MK", pluralSpecs["mk"]}, + {"lv", pluralSpecs["lv"]}, + {"lv-LV", pluralSpecs["lv"]}, + {".en-US..en-US.", nil}, + {"zh, en-gb;q=0.8, en;q=0.7", nil}, + {"zh,en-gb;q=0.8,en;q=0.7", nil}, + {"xx, en-gb;q=0.8, en;q=0.7", nil}, + {"xx,en-gb;q=0.8,en;q=0.7", nil}, + {"xx-YY,xx;q=0.8,en-US,en;q=0.8,de;q=0.6,nl;q=0.4", nil}, + {"/foo/es/en.json", nil}, + {"xx-Yyen-US", nil}, + {"en US", nil}, + {"", nil}, + {"-", nil}, + {"_", nil}, + {".", nil}, + {"-en", nil}, + {"_en", nil}, + {"-en-", nil}, + {"_en_", nil}, + {"xx", nil}, + } + for _, test := range tests { + spec := GetPluralSpec(test.src) + if spec != test.spec { + t.Errorf("getPluralSpec(%v) = %v expected %v", test.src, spec, test.spec) + } + } +} + +type pluralTest struct { + num interface{} + plural Plural +} + +func appendIntegerTests(tests []pluralTest, plural Plural, examples []string) []pluralTest { + for _, ex := range expandExamples(examples) { + i, err := strconv.ParseInt(ex, 10, 64) + if err != nil { + panic(err) + } + tests = append(tests, pluralTest{ex, plural}, pluralTest{i, plural}) + } + return tests +} + +func appendDecimalTests(tests []pluralTest, plural Plural, examples []string) []pluralTest { + for _, ex := range expandExamples(examples) { + tests = append(tests, pluralTest{ex, plural}) + } + return tests +} + +func expandExamples(examples []string) []string { + var expanded []string + for _, ex := range examples { + if parts := strings.Split(ex, "~"); len(parts) == 2 { + for ex := parts[0]; ; ex = increment(ex) { + expanded = append(expanded, ex) + if ex == parts[1] { + break + } + } + } else { + expanded = append(expanded, ex) + } + } + return expanded +} + +func increment(dec string) string { + runes := []rune(dec) + carry := true + for i := len(runes) - 1; carry && i >= 0; i-- { + switch runes[i] { + case '.': + continue + case '9': + runes[i] = '0' + default: + runes[i]++ + carry = false + } + } + if carry { + runes = append([]rune{'1'}, runes...) + } + return string(runes) +} + +// +// Below here are tests that were manually written before tests were automatically generated. +// These are kept around as sanity checks for our code generation. +// + +func TestArabic(t *testing.T) { + tests := []pluralTest{ + {0, Zero}, + {"0", Zero}, + {"0.0", Zero}, + {"0.00", Zero}, + {1, One}, + {"1", One}, + {"1.0", One}, + {"1.00", One}, + {onePlusEpsilon, Other}, + {2, Two}, + {"2", Two}, + {"2.0", Two}, + {"2.00", Two}, + {3, Few}, + {"3", Few}, + {"3.0", Few}, + {"3.00", Few}, + {10, Few}, + {"10", Few}, + {"10.0", Few}, + {"10.00", Few}, + {103, Few}, + {"103", Few}, + {"103.0", Few}, + {"103.00", Few}, + {110, Few}, + {"110", Few}, + {"110.0", Few}, + {"110.00", Few}, + {11, Many}, + {"11", Many}, + {"11.0", Many}, + {"11.00", Many}, + {99, Many}, + {"99", Many}, + {"99.0", Many}, + {"99.00", Many}, + {111, Many}, + {"111", Many}, + {"111.0", Many}, + {"111.00", Many}, + {199, Many}, + {"199", Many}, + {"199.0", Many}, + {"199.00", Many}, + {100, Other}, + {"100", Other}, + {"100.0", Other}, + {"100.00", Other}, + {102, Other}, + {"102", Other}, + {"102.0", Other}, + {"102.00", Other}, + {200, Other}, + {"200", Other}, + {"200.0", Other}, + {"200.00", Other}, + {202, Other}, + {"202", Other}, + {"202.0", Other}, + {"202.00", Other}, + } + tests = appendFloatTests(tests, 0.1, 0.9, Other) + tests = appendFloatTests(tests, 1.1, 1.9, Other) + tests = appendFloatTests(tests, 2.1, 2.9, Other) + tests = appendFloatTests(tests, 3.1, 3.9, Other) + tests = appendFloatTests(tests, 4.1, 4.9, Other) + runTests(t, "ar", tests) +} + +func TestBelarusian(t *testing.T) { + tests := []pluralTest{ + {0, Many}, + {1, One}, + {2, Few}, + {3, Few}, + {4, Few}, + {5, Many}, + {19, Many}, + {20, Many}, + {21, One}, + {11, Many}, + {52, Few}, + {101, One}, + {"0.1", Other}, + {"0.7", Other}, + {"1.5", Other}, + {"1.0", One}, + {onePlusEpsilon, Other}, + {"2.0", Few}, + {"10.0", Many}, + } + runTests(t, "be", tests) +} + +func TestBurmese(t *testing.T) { + tests := appendIntTests(nil, 0, 10, Other) + tests = appendFloatTests(tests, 0, 10, Other) + runTests(t, "my", tests) +} + +func TestCatalan(t *testing.T) { + tests := []pluralTest{ + {0, Other}, + {"0", Other}, + {1, One}, + {"1", One}, + {"1.0", Other}, + {onePlusEpsilon, Other}, + {2, Other}, + {"2", Other}, + } + tests = appendIntTests(tests, 2, 10, Other) + tests = appendFloatTests(tests, 0, 10, Other) + runTests(t, "ca", tests) +} + +func TestChinese(t *testing.T) { + tests := appendIntTests(nil, 0, 10, Other) + tests = appendFloatTests(tests, 0, 10, Other) + runTests(t, "zh", tests) +} + +func TestCzech(t *testing.T) { + tests := []pluralTest{ + {0, Other}, + {"0", Other}, + {1, One}, + {"1", One}, + {onePlusEpsilon, Many}, + {2, Few}, + {"2", Few}, + {3, Few}, + {"3", Few}, + {4, Few}, + {"4", Few}, + {5, Other}, + {"5", Other}, + } + tests = appendFloatTests(tests, 0, 10, Many) + runTests(t, "cs", tests) +} + +func TestDanish(t *testing.T) { + tests := []pluralTest{ + {0, Other}, + {1, One}, + {onePlusEpsilon, One}, + {2, Other}, + } + tests = appendFloatTests(tests, 0.1, 1.9, One) + tests = appendFloatTests(tests, 2.0, 10.0, Other) + runTests(t, "da", tests) +} + +func TestDutch(t *testing.T) { + tests := []pluralTest{ + {0, Other}, + {1, One}, + {onePlusEpsilon, Other}, + {2, Other}, + } + tests = appendFloatTests(tests, 0.0, 10.0, Other) + runTests(t, "nl", tests) +} + +func TestEnglish(t *testing.T) { + tests := []pluralTest{ + {0, Other}, + {1, One}, + {onePlusEpsilon, Other}, + {2, Other}, + } + tests = appendFloatTests(tests, 0.0, 10.0, Other) + runTests(t, "en", tests) +} + +func TestFrench(t *testing.T) { + tests := []pluralTest{ + {0, One}, + {1, One}, + {onePlusEpsilon, One}, + {2, Other}, + } + tests = appendFloatTests(tests, 0.0, 1.9, One) + tests = appendFloatTests(tests, 2.0, 10.0, Other) + runTests(t, "fr", tests) +} + +func TestGerman(t *testing.T) { + tests := []pluralTest{ + {0, Other}, + {1, One}, + {onePlusEpsilon, Other}, + {2, Other}, + } + tests = appendFloatTests(tests, 0.0, 10.0, Other) + runTests(t, "de", tests) +} + +func TestIcelandic(t *testing.T) { + tests := []pluralTest{ + {0, Other}, + {1, One}, + {2, Other}, + {11, Other}, + {21, One}, + {111, Other}, + {"0.0", Other}, + {"0.1", One}, + {"2.0", Other}, + } + runTests(t, "is", tests) +} + +func TestIndonesian(t *testing.T) { + tests := appendIntTests(nil, 0, 10, Other) + tests = appendFloatTests(tests, 0, 10, Other) + runTests(t, "id", tests) +} + +func TestItalian(t *testing.T) { + tests := []pluralTest{ + {0, Other}, + {1, One}, + {onePlusEpsilon, Other}, + {2, Other}, + } + tests = appendFloatTests(tests, 0.0, 10.0, Other) + runTests(t, "it", tests) +} + +func TestKorean(t *testing.T) { + tests := appendIntTests(nil, 0, 10, Other) + tests = appendFloatTests(tests, 0, 10, Other) + runTests(t, "ko", tests) +} + +func TestLatvian(t *testing.T) { + tests := []pluralTest{ + {0, Zero}, + {"0", Zero}, + {"0.1", One}, + {1, One}, + {"1", One}, + {onePlusEpsilon, One}, + {"10.0", Zero}, + {"10.1", One}, + {"10.2", Other}, + {21, One}, + } + tests = appendFloatTests(tests, 0.2, 0.9, Other) + tests = appendFloatTests(tests, 1.2, 1.9, Other) + tests = appendIntTests(tests, 2, 9, Other) + tests = appendIntTests(tests, 10, 20, Zero) + tests = appendIntTests(tests, 22, 29, Other) + runTests(t, "lv", tests) +} + +func TestJapanese(t *testing.T) { + tests := appendIntTests(nil, 0, 10, Other) + tests = appendFloatTests(tests, 0, 10, Other) + runTests(t, "ja", tests) +} + +func TestLithuanian(t *testing.T) { + tests := []pluralTest{ + {0, Other}, + {1, One}, + {2, Few}, + {3, Few}, + {9, Few}, + {10, Other}, + {11, Other}, + {"0.1", Many}, + {"0.7", Many}, + {"1.0", One}, + {onePlusEpsilon, Many}, + {"2.0", Few}, + {"10.0", Other}, + } + runTests(t, "lt", tests) +} + +func TestMalay(t *testing.T) { + tests := appendIntTests(nil, 0, 10, Other) + tests = appendFloatTests(tests, 0, 10, Other) + runTests(t, "ms", tests) +} + +func TestPolish(t *testing.T) { + tests := []pluralTest{ + {0, Many}, + {1, One}, + {2, Few}, + {3, Few}, + {4, Few}, + {5, Many}, + {19, Many}, + {20, Many}, + {10, Many}, + {11, Many}, + {52, Few}, + {"0.1", Other}, + {"0.7", Other}, + {"1.5", Other}, + {"1.0", Other}, + {onePlusEpsilon, Other}, + {"2.0", Other}, + {"10.0", Other}, + } + runTests(t, "pl", tests) +} + +func TestPortuguese(t *testing.T) { + tests := []pluralTest{ + {0, One}, + {"0.0", One}, + {1, One}, + {"1.0", One}, + {onePlusEpsilon, One}, + {2, Other}, + } + tests = appendFloatTests(tests, 0, 1.5, One) + tests = appendFloatTests(tests, 2, 10.0, Other) + runTests(t, "pt", tests) +} + +func TestMacedonian(t *testing.T) { + tests := []pluralTest{ + {0, Other}, + {1, One}, + {"1.1", One}, + {"2.1", One}, + {onePlusEpsilon, One}, + {2, Other}, + {"2.2", Other}, + {11, One}, + } + runTests(t, "mk", tests) +} + +func TestRussian(t *testing.T) { + tests := []pluralTest{ + {0, Many}, + {1, One}, + {2, Few}, + {3, Few}, + {4, Few}, + {5, Many}, + {19, Many}, + {20, Many}, + {21, One}, + {11, Many}, + {52, Few}, + {101, One}, + {"0.1", Other}, + {"0.7", Other}, + {"1.5", Other}, + {"1.0", Other}, + {onePlusEpsilon, Other}, + {"2.0", Other}, + {"10.0", Other}, + } + runTests(t, "ru", tests) +} + +func TestSpanish(t *testing.T) { + tests := []pluralTest{ + {0, Other}, + {1, One}, + {"1", One}, + {"1.0", One}, + {"1.00", One}, + {onePlusEpsilon, Other}, + {2, Other}, + } + tests = appendFloatTests(tests, 0.0, 0.9, Other) + tests = appendFloatTests(tests, 1.1, 10.0, Other) + runTests(t, "es", tests) +} + +func TestNorweigan(t *testing.T) { + tests := []pluralTest{ + {0, Other}, + {1, One}, + {"1", One}, + {"1.0", One}, + {"1.00", One}, + {onePlusEpsilon, Other}, + {2, Other}, + } + tests = appendFloatTests(tests, 0.0, 0.9, Other) + tests = appendFloatTests(tests, 1.1, 10.0, Other) + runTests(t, "no", tests) +} + +func TestBulgarian(t *testing.T) { + tests := []pluralTest{ + {0, Other}, + {1, One}, + {2, Other}, + {3, Other}, + {9, Other}, + {10, Other}, + {11, Other}, + {"0.1", Other}, + {"0.7", Other}, + {"1.0", One}, + {"1.001", Other}, + {onePlusEpsilon, Other}, + {"1.1", Other}, + {"2.0", Other}, + {"10.0", Other}, + } + runTests(t, "bg", tests) +} + +func TestSwedish(t *testing.T) { + tests := []pluralTest{ + {0, Other}, + {1, One}, + {onePlusEpsilon, Other}, + {2, Other}, + } + tests = appendFloatTests(tests, 0.0, 10.0, Other) + runTests(t, "sv", tests) +} + +func TestThai(t *testing.T) { + tests := appendIntTests(nil, 0, 10, Other) + tests = appendFloatTests(tests, 0, 10, Other) + runTests(t, "th", tests) +} + +func TestVietnamese(t *testing.T) { + tests := appendIntTests(nil, 0, 10, Other) + tests = appendFloatTests(tests, 0, 10, Other) + runTests(t, "vi", tests) +} + +func TestTurkish(t *testing.T) { + tests := []pluralTest{ + {0, Other}, + {1, One}, + {"1", One}, + {"1.0", One}, + {"1.00", One}, + {"1.001", Other}, + {"1.100", Other}, + {"1.101", Other}, + {onePlusEpsilon, Other}, + {2, Other}, + {"0.7", Other}, + {"2.0", Other}, + } + runTests(t, "tr", tests) +} + +func TestUkrainian(t *testing.T) { + tests := []pluralTest{ + {0, Many}, + {1, One}, + {2, Few}, + {3, Few}, + {4, Few}, + {5, Many}, + {19, Many}, + {20, Many}, + {21, One}, + {11, Many}, + {52, Few}, + {101, One}, + {"0.1", Other}, + {"0.7", Other}, + {"1.5", Other}, + {"1.0", Other}, + {onePlusEpsilon, Other}, + {"2.0", Other}, + {"10.0", Other}, + } + runTests(t, "uk", tests) +} + +func TestCroatian(t *testing.T) { + tests := makeCroatianBosnianSerbianTests() + runTests(t, "hr", tests) +} + +func TestBosnian(t *testing.T) { + tests := makeCroatianBosnianSerbianTests() + runTests(t, "bs", tests) +} + +func TestSerbian(t *testing.T) { + tests := makeCroatianBosnianSerbianTests() + runTests(t, "sr", tests) +} + +func makeCroatianBosnianSerbianTests() []pluralTest { + return []pluralTest{ + {1, One}, + {"0.1", One}, + {21, One}, + {101, One}, + {1001, One}, + {51, One}, + {"1.1", One}, + {"5.1", One}, + {"100.1", One}, + {"1000.1", One}, + {2, Few}, + {"0.2", Few}, + {22, Few}, + {"1.2", Few}, + {24, Few}, + {"2.4", Few}, + {102, Few}, + {"100.2", Few}, + {1002, Few}, + {"1000.2", Few}, + {5, Other}, + {"0.5", Other}, + {0, Other}, + {100, Other}, + {19, Other}, + {"0.0", Other}, + {"100.0", Other}, + {"1000.0", Other}, + } +} + +func TestTigrinya(t *testing.T) { + tests := []pluralTest{ + {0, One}, + {1, One}, + } + tests = appendIntTests(tests, 2, 10, Other) + tests = appendFloatTests(tests, 1.1, 10.0, Other) + runTests(t, "ti", tests) +} + +func appendIntTests(tests []pluralTest, from, to int, p Plural) []pluralTest { + for i := from; i <= to; i++ { + tests = append(tests, pluralTest{i, p}) + } + return tests +} + +func appendFloatTests(tests []pluralTest, from, to float64, p Plural) []pluralTest { + stride := 0.1 + format := "%.1f" + for f := from; f < to; f += stride { + tests = append(tests, pluralTest{fmt.Sprintf(format, f), p}) + } + tests = append(tests, pluralTest{fmt.Sprintf(format, to), p}) + return tests +} + +func runTests(t *testing.T, pluralSpecID string, tests []pluralTest) { + pluralSpecID = normalizePluralSpecID(pluralSpecID) + if spec := pluralSpecs[pluralSpecID]; spec != nil { + for _, test := range tests { + if plural, err := spec.Plural(test.num); plural != test.plural { + t.Errorf("%s: PluralCategory(%#v) returned %s, %v; expected %s", pluralSpecID, test.num, plural, err, test.plural) + } + } + } else { + t.Errorf("could not find plural spec for locale %s", pluralSpecID) + } + +} diff --git a/internal/go-i18n/i18n/translation/plural_translation.go b/internal/go-i18n/i18n/translation/plural_translation.go new file mode 100644 index 0000000..93223c6 --- /dev/null +++ b/internal/go-i18n/i18n/translation/plural_translation.go @@ -0,0 +1,82 @@ +package translation + +import ( + "github.com/gobuffalo/mw-i18n/internal/go-i18n/i18n/language" +) + +type pluralTranslation struct { + id string + templates map[language.Plural]*template +} + +func (pt *pluralTranslation) MarshalInterface() interface{} { + return map[string]interface{}{ + "id": pt.id, + "translation": pt.templates, + } +} + +func (pt *pluralTranslation) MarshalFlatInterface() interface{} { + return pt.templates +} + +func (pt *pluralTranslation) ID() string { + return pt.id +} + +func (pt *pluralTranslation) Template(pc language.Plural) *template { + return pt.templates[pc] +} + +func (pt *pluralTranslation) UntranslatedCopy() Translation { + return &pluralTranslation{pt.id, make(map[language.Plural]*template)} +} + +func (pt *pluralTranslation) Normalize(l *language.Language) Translation { + // Delete plural categories that don't belong to this language. + for pc := range pt.templates { + if _, ok := l.Plurals[pc]; !ok { + delete(pt.templates, pc) + } + } + // Create map entries for missing valid categories. + for pc := range l.Plurals { + if _, ok := pt.templates[pc]; !ok { + pt.templates[pc] = mustNewTemplate("") + } + } + return pt +} + +func (pt *pluralTranslation) Backfill(src Translation) Translation { + for pc, t := range pt.templates { + if (t == nil || t.src == "") && src != nil { + pt.templates[pc] = src.Template(language.Other) + } + } + return pt +} + +func (pt *pluralTranslation) Merge(t Translation) Translation { + other, ok := t.(*pluralTranslation) + if !ok || pt.ID() != t.ID() { + return t + } + for pluralCategory, template := range other.templates { + if template != nil && template.src != "" { + pt.templates[pluralCategory] = template + } + } + return pt +} + +func (pt *pluralTranslation) Incomplete(l *language.Language) bool { + for pc := range l.Plurals { + if t := pt.templates[pc]; t == nil || t.src == "" { + return true + } + } + return false +} + +var _ = Translation(&pluralTranslation{}) diff --git a/internal/go-i18n/i18n/translation/plural_translation_test.go b/internal/go-i18n/i18n/translation/plural_translation_test.go new file mode 100644 index 0000000..6346396 --- /dev/null +++ b/internal/go-i18n/i18n/translation/plural_translation_test.go @@ -0,0 +1,308 @@ +package translation + +import ( + "reflect" + "testing" + + "github.com/gobuffalo/mw-i18n/internal/go-i18n/i18n/language" +) + +func mustTemplate(t *testing.T, src string) *template { + tmpl, err := newTemplate(src) + if err != nil { + t.Fatal(err) + } + return tmpl +} + +func pluralTranslationFixture(t *testing.T, id string, pluralCategories ...language.Plural) *pluralTranslation { + templates := make(map[language.Plural]*template, len(pluralCategories)) + for _, pc := range pluralCategories { + templates[pc] = mustTemplate(t, string(pc)) + } + return &pluralTranslation{id, templates} +} + +func verifyDeepEqual(t *testing.T, actual, expected interface{}) { + if !reflect.DeepEqual(actual, expected) { + t.Fatalf("\n%#v\nnot equal to expected value\n%#v", actual, expected) + } +} + +func TestPluralTranslationMerge(t *testing.T) { + pt := pluralTranslationFixture(t, "id", language.One, language.Other) + oneTemplate, otherTemplate := pt.templates[language.One], pt.templates[language.Other] + + pt.Merge(pluralTranslationFixture(t, "id")) + verifyDeepEqual(t, pt.templates, map[language.Plural]*template{ + language.One: oneTemplate, + language.Other: otherTemplate, + }) + + pt2 := pluralTranslationFixture(t, "id", language.One, language.Two) + pt.Merge(pt2) + verifyDeepEqual(t, pt.templates, map[language.Plural]*template{ + language.One: pt2.templates[language.One], + language.Two: pt2.templates[language.Two], + language.Other: otherTemplate, + }) +} + +/* Test implementations from old idea + +func TestCopy(t *testing.T) { + ls := &LocalizedString{ + ID: "id", + Translation: testingTemplate(t, "translation {{.Hello}}"), + Translations: map[language.Plural]*template{ + language.One: testingTemplate(t, "plural {{.One}}"), + language.Other: testingTemplate(t, "plural {{.Other}}"), + }, + } + + c := ls.Copy() + delete(c.Translations, language.One) + if _, ok := ls.Translations[language.One]; !ok { + t.Errorf("deleting plural translation from copy deleted it from the original") + } + c.Translations[language.Two] = testingTemplate(t, "plural {{.Two}}") + if _, ok := ls.Translations[language.Two]; ok { + t.Errorf("adding plural translation to copy added it to the original") + } +} + +func TestNormalize(t *testing.T) { + oneTemplate := testingTemplate(t, "one {{.One}}") + ls := &LocalizedString{ + Translation: testingTemplate(t, "single {{.Single}}"), + Translations: map[language.Plural]*template{ + language.One: oneTemplate, + language.Two: testingTemplate(t, "two {{.Two}}"), + }, + } + ls.Normalize(LanguageWithCode("en")) + if ls.Translation != nil { + t.Errorf("ls.Translation is %#v; expected nil", ls.Translation) + } + if actual := ls.Translations[language.Two]; actual != nil { + t.Errorf("ls.Translation[language.Two] is %#v; expected nil", actual) + } + if actual := ls.Translations[language.One]; actual != oneTemplate { + t.Errorf("ls.Translations[language.One] is %#v; expected %#v", actual, oneTemplate) + } + if _, ok := ls.Translations[language.Other]; !ok { + t.Errorf("ls.Translations[language.Other] shouldn't be empty") + } +} + +func TestMergeTranslation(t *testing.T) { + ls := &LocalizedString{} + + translation := testingTemplate(t, "one {{.Hello}}") + ls.Merge(&LocalizedString{ + Translation: translation, + }) + if ls.Translation != translation { + t.Errorf("expected %#v; got %#v", translation, ls.Translation) + } + + ls.Merge(&LocalizedString{}) + if ls.Translation != translation { + t.Errorf("expected %#v; got %#v", translation, ls.Translation) + } + + translation = testingTemplate(t, "two {{.Hello}}") + ls.Merge(&LocalizedString{ + Translation: translation, + }) + if ls.Translation != translation { + t.Errorf("expected %#v; got %#v", translation, ls.Translation) + } +} + +func TestMergeTranslations(t *testing.T) { + ls := &LocalizedString{} + + oneTemplate := testingTemplate(t, "one {{.One}}") + otherTemplate := testingTemplate(t, "other {{.Other}}") + ls.Merge(&LocalizedString{ + Translations: map[language.Plural]*template{ + language.One: oneTemplate, + language.Other: otherTemplate, + }, + }) + if actual := ls.Translations[language.One]; actual != oneTemplate { + t.Errorf("ls.Translations[language.One] expected %#v; got %#v", oneTemplate, actual) + } + if actual := ls.Translations[language.Other]; actual != otherTemplate { + t.Errorf("ls.Translations[language.Other] expected %#v; got %#v", otherTemplate, actual) + } + + ls.Merge(&LocalizedString{ + Translations: map[language.Plural]*template{}, + }) + if actual := ls.Translations[language.One]; actual != oneTemplate { + t.Errorf("ls.Translations[language.One] expected %#v; got %#v", oneTemplate, actual) + } + if actual := ls.Translations[language.Other]; actual != otherTemplate { + t.Errorf("ls.Translations[language.Other] expected %#v; got %#v", otherTemplate, actual) + } + + twoTemplate := testingTemplate(t, "two {{.Two}}") + otherTemplate = testingTemplate(t, "second other {{.Other}}") + ls.Merge(&LocalizedString{ + Translations: map[language.Plural]*template{ + language.Two: twoTemplate, + language.Other: otherTemplate, + }, + }) + if actual := ls.Translations[language.One]; actual != oneTemplate { + t.Errorf("ls.Translations[language.One] expected %#v; got %#v", oneTemplate, actual) + } + if actual := ls.Translations[language.Two]; actual != twoTemplate { + t.Errorf("ls.Translations[language.Two] expected %#v; got %#v", twoTemplate, actual) + } + if actual := ls.Translations[language.Other]; actual != otherTemplate { + t.Errorf("ls.Translations[language.Other] expected %#v; got %#v", otherTemplate, actual) + } +} + +func TestMissingTranslations(t *testing.T) { + en := LanguageWithCode("en") + + tests := []struct { + localizedString *LocalizedString + language *Language + expected bool + }{ + { + &LocalizedString{}, + en, + true, + }, + { + &LocalizedString{Translation: testingTemplate(t, "single {{.Single}}")}, + en, + false, + }, + { + &LocalizedString{ + Translation: testingTemplate(t, "single {{.Single}}"), + Translations: map[language.Plural]*template{ + language.One: testingTemplate(t, "one {{.One}}"), + }}, + en, + true, + }, + { + &LocalizedString{Translations: map[language.Plural]*template{ + language.One: testingTemplate(t, "one {{.One}}"), + }}, + en, + true, + }, + { + &LocalizedString{Translations: map[language.Plural]*template{ + language.One: nil, + language.Other: nil, + }}, + en, + true, + }, + { + &LocalizedString{Translations: map[language.Plural]*template{ + language.One: testingTemplate(t, ""), + language.Other: testingTemplate(t, ""), + }}, + en, + true, + }, + { + &LocalizedString{Translations: map[language.Plural]*template{ + language.One: testingTemplate(t, "one {{.One}}"), + language.Other: testingTemplate(t, "other {{.Other}}"), + }}, + en, + false, + }, + } + + for _, tt := range tests { + if actual := tt.localizedString.MissingTranslations(tt.language); actual != tt.expected { + t.Errorf("expected %t got %t for %s, %#v", + tt.expected, actual, tt.language.code, tt.localizedString) + } + } +} + +func TestHasTranslations(t *testing.T) { + en := LanguageWithCode("en") + + tests := []struct { + localizedString *LocalizedString + language *Language + expected bool + }{ + { + &LocalizedString{}, + en, + false, + }, + { + &LocalizedString{Translation: testingTemplate(t, "single {{.Single}}")}, + en, + true, + }, + { + &LocalizedString{ + Translation: testingTemplate(t, "single {{.Single}}"), + Translations: map[language.Plural]*template{}}, + en, + false, + }, + { + &LocalizedString{Translations: map[language.Plural]*template{ + language.One: testingTemplate(t, "one {{.One}}"), + }}, + en, + true, + }, + { + &LocalizedString{Translations: map[language.Plural]*template{ + language.Two: testingTemplate(t, "two {{.Two}}"), + }}, + en, + false, + }, + { + &LocalizedString{Translations: map[language.Plural]*template{ + language.One: nil, + }}, + en, + false, + }, + { + &LocalizedString{Translations: map[language.Plural]*template{ + language.One: testingTemplate(t, ""), + }}, + en, + false, + }, + } + + for _, tt := range tests { + if actual := tt.localizedString.HasTranslations(tt.language); actual != tt.expected { + t.Errorf("expected %t got %t for %s, %#v", + tt.expected, actual, tt.language.code, tt.localizedString) + } + } +} + +func testingTemplate(t *testing.T, src string) *template { + tmpl, err := newTemplate(src) + if err != nil { + t.Fatal(err) + } + return tmpl +} +*/ diff --git a/internal/go-i18n/i18n/translation/single_translation.go b/internal/go-i18n/i18n/translation/single_translation.go new file mode 100644 index 0000000..2a80264 --- /dev/null +++ b/internal/go-i18n/i18n/translation/single_translation.go @@ -0,0 +1,61 @@ +package translation + +import ( + "github.com/gobuffalo/mw-i18n/internal/go-i18n/i18n/language" +) + +type singleTranslation struct { + id string + template *template +} + +func (st *singleTranslation) MarshalInterface() interface{} { + return map[string]interface{}{ + "id": st.id, + "translation": st.template, + } +} + +func (st *singleTranslation) MarshalFlatInterface() interface{} { + return map[string]interface{}{"other": st.template} +} + +func (st *singleTranslation) ID() string { + return st.id +} + +func (st *singleTranslation) Template(pc language.Plural) *template { + return st.template +} + +func (st *singleTranslation) UntranslatedCopy() Translation { + return &singleTranslation{st.id, mustNewTemplate("")} +} + +func (st *singleTranslation) Normalize(language *language.Language) Translation { + return st +} + +func (st *singleTranslation) Backfill(src Translation) Translation { + if (st.template == nil || st.template.src == "") && src != nil { + st.template = src.Template(language.Other) + } + return st +} + +func (st *singleTranslation) Merge(t Translation) Translation { + other, ok := t.(*singleTranslation) + if !ok || st.ID() != t.ID() { + return t + } + if other.template != nil && other.template.src != "" { + st.template = other.template + } + return st +} + +func (st *singleTranslation) Incomplete(l *language.Language) bool { + return st.template == nil || st.template.src == "" +} + +var _ = Translation(&singleTranslation{}) diff --git a/internal/go-i18n/i18n/translation/template.go b/internal/go-i18n/i18n/translation/template.go new file mode 100644 index 0000000..3310150 --- /dev/null +++ b/internal/go-i18n/i18n/translation/template.go @@ -0,0 +1,65 @@ +package translation + +import ( + "bytes" + "encoding" + "strings" + gotemplate "text/template" +) + +type template struct { + tmpl *gotemplate.Template + src string +} + +func newTemplate(src string) (*template, error) { + if src == "" { + return new(template), nil + } + + var tmpl template + err := tmpl.parseTemplate(src) + return &tmpl, err +} + +func mustNewTemplate(src string) *template { + t, err := newTemplate(src) + if err != nil { + panic(err) + } + return t +} + +func (t *template) String() string { + return t.src +} + +func (t *template) Execute(args interface{}) string { + if t.tmpl == nil { + return t.src + } + var buf bytes.Buffer + if err := t.tmpl.Execute(&buf, args); err != nil { + return err.Error() + } + return buf.String() +} + +func (t *template) MarshalText() ([]byte, error) { + return []byte(t.src), nil +} + +func (t *template) UnmarshalText(src []byte) error { + return t.parseTemplate(string(src)) +} + +func (t *template) parseTemplate(src string) (err error) { + t.src = src + if strings.Contains(src, "{{") { + t.tmpl, err = gotemplate.New(src).Parse(src) + } + return +} + +var _ = encoding.TextMarshaler(&template{}) +var _ = encoding.TextUnmarshaler(&template{}) diff --git a/internal/go-i18n/i18n/translation/template_test.go b/internal/go-i18n/i18n/translation/template_test.go new file mode 100644 index 0000000..73a9234 --- /dev/null +++ b/internal/go-i18n/i18n/translation/template_test.go @@ -0,0 +1,146 @@ +package translation + +import ( + "bytes" + "fmt" + //"launchpad.net/goyaml" + "testing" + gotemplate "text/template" +) + +func TestNilTemplate(t *testing.T) { + expected := "hello" + tmpl := &template{ + tmpl: nil, + src: expected, + } + if actual := tmpl.Execute(nil); actual != expected { + t.Errorf("Execute(nil) returned %s; expected %s", actual, expected) + } +} + +func TestMarshalText(t *testing.T) { + tmpl := &template{ + tmpl: gotemplate.Must(gotemplate.New("id").Parse("this is a {{.foo}} template")), + src: "boom", + } + expectedBuf := []byte(tmpl.src) + if buf, err := tmpl.MarshalText(); !bytes.Equal(buf, expectedBuf) || err != nil { + t.Errorf("MarshalText() returned %#v, %#v; expected %#v, nil", buf, err, expectedBuf) + } +} + +func TestUnmarshalText(t *testing.T) { + tmpl := &template{} + tmpl.UnmarshalText([]byte("hello {{.World}}")) + result := tmpl.Execute(map[string]string{ + "World": "world!", + }) + expected := "hello world!" + if result != expected { + t.Errorf("expected %#v; got %#v", expected, result) + } +} + +/* +func TestYAMLMarshal(t *testing.T) { + src := "hello {{.World}}" + tmpl, err := newTemplate(src) + if err != nil { + t.Fatal(err) + } + buf, err := goyaml.Marshal(tmpl) + if err != nil { + t.Fatal(err) + } + if !bytes.Equal(buf, []byte(src)) { + t.Fatalf(`expected "%s"; got "%s"`, src, buf) + } +} + +func TestYAMLUnmarshal(t *testing.T) { + buf := []byte(`Tmpl: "hello"`) + + var out struct { + Tmpl *template + } + var foo map[string]string + if err := goyaml.Unmarshal(buf, &foo); err != nil { + t.Fatal(err) + } + if out.Tmpl == nil { + t.Fatalf("out.Tmpl was nil") + } + if out.Tmpl.tmpl == nil { + t.Fatalf("out.Tmpl.tmpl was nil") + } + if expected := "hello {{.World}}"; out.Tmpl.src != expected { + t.Fatalf("expected %s; got %s", expected, out.Tmpl.src) + } +} + +func TestGetYAML(t *testing.T) { + src := "hello" + tmpl := &template{ + tmpl: nil, + src: src, + } + if tag, value := tmpl.GetYAML(); tag != "" || value != src { + t.Errorf("GetYAML() returned (%#v, %#v); expected (%#v, %#v)", tag, value, "", src) + } +} + +func TestSetYAML(t *testing.T) { + tmpl := &template{} + tmpl.SetYAML("tagDoesntMatter", "hello {{.World}}") + result := tmpl.Execute(map[string]string{ + "World": "world!", + }) + expected := "hello world!" + if result != expected { + t.Errorf("expected %#v; got %#v", expected, result) + } +} +*/ + +func BenchmarkExecuteNilTemplate(b *testing.B) { + template := &template{src: "hello world"} + b.ResetTimer() + for i := 0; i < b.N; i++ { + template.Execute(nil) + } +} + +func BenchmarkExecuteHelloWorldTemplate(b *testing.B) { + template, err := newTemplate("hello world") + if err != nil { + b.Fatal(err) + } + b.ResetTimer() + for i := 0; i < b.N; i++ { + template.Execute(nil) + } +} + +// Executing a simple template like this is ~6x slower than Sprintf +// but it is still only a few microseconds which should be sufficiently fast. +// The benefit is that we have nice semantic tags in the translation. +func BenchmarkExecuteHelloNameTemplate(b *testing.B) { + template, err := newTemplate("hello {{.Name}}") + if err != nil { + b.Fatal(err) + } + b.ResetTimer() + for i := 0; i < b.N; i++ { + template.Execute(map[string]string{ + "Name": "Nick", + }) + } +} + +func BenchmarkSprintf(b *testing.B) { + b.ResetTimer() + for i := 0; i < b.N; i++ { + fmt.Sprintf("hello %s", "nick") + } +} diff --git a/internal/go-i18n/i18n/translation/translation.go b/internal/go-i18n/i18n/translation/translation.go new file mode 100644 index 0000000..78ace5e --- /dev/null +++ b/internal/go-i18n/i18n/translation/translation.go @@ -0,0 +1,84 @@ +// Package translation defines the interface for a translation. +package translation + +import ( + "fmt" + + "github.com/gobuffalo/mw-i18n/internal/go-i18n/i18n/language" +) + +// Translation is the interface that represents a translated string. +type Translation interface { + // MarshalInterface returns the object that should be used + // to serialize the translation. + MarshalInterface() interface{} + MarshalFlatInterface() interface{} + ID() string + Template(language.Plural) *template + UntranslatedCopy() Translation + Normalize(language *language.Language) Translation + Backfill(src Translation) Translation + Merge(Translation) Translation + Incomplete(l *language.Language) bool +} + +// SortableByID implements sort.Interface for a slice of translations. +type SortableByID []Translation + +func (a SortableByID) Len() int { return len(a) } +func (a SortableByID) Swap(i, j int) { a[i], a[j] = a[j], a[i] } +func (a SortableByID) Less(i, j int) bool { return a[i].ID() < a[j].ID() } + +// NewTranslation reflects on data to create a new Translation. +// +// data["id"] must be a string and data["translation"] must be either a string +// for a non-plural translation or a map[string]interface{} for a plural translation. +func NewTranslation(data map[string]interface{}) (Translation, error) { + id, ok := data["id"].(string) + if !ok { + return nil, fmt.Errorf(`missing "id" key`) + } + var pluralObject map[string]interface{} + switch translation := data["translation"].(type) { + case string: + tmpl, err := newTemplate(translation) + if err != nil { + return nil, err + } + return &singleTranslation{id, tmpl}, nil + case map[interface{}]interface{}: + // The YAML parser uses interface{} keys so we first convert them to string keys. + pluralObject = make(map[string]interface{}) + for k, v := range translation { + kStr, ok := k.(string) + if !ok { + return nil, fmt.Errorf(`invalid plural category type %T; expected string`, k) + } + pluralObject[kStr] = v + } + case map[string]interface{}: + pluralObject = translation + case nil: + return nil, fmt.Errorf(`missing "translation" key`) + default: + return nil, fmt.Errorf(`unsupported type for "translation" key %T`, translation) + } + + templates := make(map[language.Plural]*template, len(pluralObject)) + for k, v := range pluralObject { + pc, err := language.NewPlural(k) + if err != nil { + return nil, err + } + str, ok := v.(string) + if !ok { + return nil, fmt.Errorf(`plural category "%s" has value of type %T; expected string`, pc, v) + } + tmpl, err := newTemplate(str) + if err != nil { + return nil, err + } + templates[pc] = tmpl + } + return &pluralTranslation{id, templates}, nil +} diff --git a/internal/go-i18n/i18n/translation/translation_test.go b/internal/go-i18n/i18n/translation/translation_test.go new file mode 100644 index 0000000..7380d5a --- /dev/null +++ b/internal/go-i18n/i18n/translation/translation_test.go @@ -0,0 +1,17 @@ +package translation + +import ( + "sort" + "testing" +) + +// Check this here to avoid unnecessary import of sort package. +var _ = sort.Interface(make(SortableByID, 0, 0)) + +func TestNewSingleTranslation(t *testing.T) { + t.Skipf("not implemented") +} + +func TestNewPluralTranslation(t *testing.T) { + t.Skipf("not implemented") +} diff --git a/internal/go-i18n/i18n/translations_test.go b/internal/go-i18n/i18n/translations_test.go new file mode 100644 index 0000000..153be45 --- /dev/null +++ b/internal/go-i18n/i18n/translations_test.go @@ -0,0 +1,89 @@ +package i18n + +import ( + "testing" + + "github.com/gobuffalo/mw-i18n/internal/go-i18n/i18n/bundle" +) + +var bobMap = map[string]interface{}{"Person": "Bob"} +var bobStruct = struct{ Person string }{Person: "Bob"} + +var testCases = []struct { + id string + arg interface{} + want string +}{ + {"program_greeting", nil, "Hello world"}, + {"person_greeting", bobMap, "Hello Bob"}, + {"person_greeting", bobStruct, "Hello Bob"}, + + {"your_unread_email_count", 0, "You have 0 unread emails."}, + {"your_unread_email_count", 1, "You have 1 unread email."}, + {"your_unread_email_count", 2, "You have 2 unread emails."}, + {"my_height_in_meters", "1.7", "I am 1.7 meters tall."}, + + {"person_unread_email_count", []interface{}{0, bobMap}, "Bob has 0 unread emails."}, + {"person_unread_email_count", []interface{}{1, bobMap}, "Bob has 1 unread email."}, + {"person_unread_email_count", []interface{}{2, bobMap}, "Bob has 2 unread emails."}, + {"person_unread_email_count", []interface{}{0, bobStruct}, "Bob has 0 unread emails."}, + {"person_unread_email_count", []interface{}{1, bobStruct}, "Bob has 1 unread email."}, + {"person_unread_email_count", []interface{}{2, bobStruct}, "Bob has 2 unread emails."}, + + {"person_unread_email_count_timeframe", []interface{}{3, map[string]interface{}{ + "Person": "Bob", + "Timeframe": "0 days", + }}, "Bob has 3 unread emails in the past 0 days."}, + {"person_unread_email_count_timeframe", []interface{}{3, map[string]interface{}{ + "Person": "Bob", + "Timeframe": "1 day", + }}, "Bob has 3 unread emails in the past 1 day."}, + {"person_unread_email_count_timeframe", []interface{}{3, map[string]interface{}{ + "Person": "Bob", + "Timeframe": "2 days", + }}, "Bob has 3 unread emails in the past 2 days."}, +} + +func testFile(t *testing.T, path string) { + b := bundle.New() + b.MustLoadTranslationFile(path) + + T, err := b.Tfunc("en-US") + if err != nil { + t.Fatal(err) + } + + for _, tc := range testCases { + var args []interface{} + if _, ok := tc.arg.([]interface{}); ok { + args = tc.arg.([]interface{}) + } else { + args = []interface{}{tc.arg} + } + + got := T(tc.id, args...) + if got != tc.want { + t.Errorf("got: %s; want: %s", got, tc.want) + } + } +} + +func TestJSONParse(t *testing.T) { + testFile(t, "../goi18n/testdata/expected/en-us.all.json") +} + +func TestYAMLParse(t *testing.T) { + testFile(t, "../goi18n/testdata/en-us.yaml") +} + +func TestJSONFlatParse(t *testing.T) { + testFile(t, "../goi18n/testdata/en-us.flat.json") +} + +func TestYAMLFlatParse(t *testing.T) { + testFile(t, "../goi18n/testdata/en-us.flat.yaml") +} + +func TestTOMLFlatParse(t *testing.T) { + testFile(t, "../goi18n/testdata/en-us.flat.toml") +}