├── .busted ├── .editorconfig ├── .github └── workflows │ ├── busted.yml │ ├── deploy.yml │ └── luacheck.yml ├── .gitignore ├── .luacheckrc ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── rockspecs ├── say-1.3-1.rockspec ├── say-1.4.0-1.rockspec ├── say-1.4.1-1.rockspec ├── say-1.4.1-2.rockspec └── say-1.4.1-3.rockspec ├── say-scm-1.rockspec ├── spec └── say_spec.lua └── src └── say └── init.lua /.busted: -------------------------------------------------------------------------------- 1 | return { 2 | default = { 3 | verbose = true, 4 | output = "gtest", 5 | }, 6 | } 7 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | end_of_line = lf 5 | insert_final_newline = true 6 | trim_trailing_whitespace = true 7 | charset = utf-8 8 | 9 | [*.lua] 10 | indent_style = space 11 | indent_size = 2 12 | 13 | [Makefile] 14 | indent_style = tab 15 | 16 | [*.bat] 17 | end_of_line = crlf 18 | -------------------------------------------------------------------------------- /.github/workflows/busted.yml: -------------------------------------------------------------------------------- 1 | name: Busted 2 | 3 | on: [ push, pull_request ] 4 | 5 | jobs: 6 | 7 | busted: 8 | strategy: 9 | fail-fast: false 10 | matrix: 11 | luaVersion: [ "5.4", "5.3", "5.2", "5.1", "luajit" ] # , "luajit-openresty" ] 12 | runs-on: ubuntu-22.04 13 | steps: 14 | - name: Checkout 15 | uses: actions/checkout@v3 16 | 17 | - name: Setup ‘lua’ 18 | uses: leafo/gh-actions-lua@v9 19 | with: 20 | luaVersion: ${{ matrix.luaVersion }} 21 | 22 | - name: Setup ‘luarocks’ 23 | uses: leafo/gh-actions-luarocks@v4 24 | 25 | - name: Setup dependencies 26 | run: | 27 | luarocks install busted 28 | luarocks install luacov-coveralls 29 | 30 | - name: Replace system say with self 31 | run: | 32 | luarocks remove say --force 33 | luarocks make 34 | 35 | - name: Run regression tests 36 | # disable project-local path prefixes to force use of system installation 37 | run: busted --coverage --lpath="" --cpath="" -Xoutput --color 38 | 39 | - name: Report test coverage 40 | if: ${{ success() && github.repository == 'lunarmodules/say' }} 41 | continue-on-error: true 42 | run: luacov-coveralls -i src -e .luarocks 43 | env: 44 | COVERALLS_REPO_TOKEN: ${{ github.token }} 45 | -------------------------------------------------------------------------------- /.github/workflows/deploy.yml: -------------------------------------------------------------------------------- 1 | name: Deploy 2 | 3 | on: [ push, workflow_dispatch ] 4 | 5 | jobs: 6 | 7 | affected: 8 | uses: lunarmodules/.github/.github/workflows/list_affected_rockspecs.yml@main 9 | 10 | build: 11 | needs: affected 12 | if: ${{ needs.affected.outputs.rockspecs }} 13 | uses: lunarmodules/.github/.github/workflows/test_build_rock.yml@main 14 | with: 15 | rockspecs: ${{ needs.affected.outputs.rockspecs }} 16 | 17 | upload: 18 | needs: [ affected, build ] 19 | # Only run upload if: 20 | # 1. We are on the canonical repository (no uploads from forks) 21 | # 2. The current commit is either tagged or on the default branch (the workflow will upload dev/scm rockspecs any 22 | # time they are touched, tagged ones whenever the edited rockspec and tag match) 23 | # 3. Some rockspecs were changed — this implies the commit changing the rockspec is the same one that gets tagged 24 | if: >- 25 | ${{ 26 | github.repository == 'lunarmodules/say' && 27 | ( github.ref_name == 'master' || startsWith(github.ref, 'refs/tags/') ) && 28 | needs.affected.outputs.rockspecs 29 | }} 30 | uses: lunarmodules/.github/.github/workflows/upload_to_luarocks.yml@main 31 | with: 32 | rockspecs: ${{ needs.affected.outputs.rockspecs }} 33 | secrets: 34 | apikey: ${{ secrets.LUAROCKS_APIKEY }} 35 | -------------------------------------------------------------------------------- /.github/workflows/luacheck.yml: -------------------------------------------------------------------------------- 1 | name: Luacheck 2 | 3 | on: [push, pull_request] 4 | 5 | jobs: 6 | 7 | luacheck: 8 | runs-on: ubuntu-20.04 9 | steps: 10 | - name: Checkout 11 | uses: actions/checkout@v3 12 | - name: Luacheck 13 | uses: lunarmodules/luacheck@v0 14 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.swp 2 | *.rock 3 | luacov.* 4 | -------------------------------------------------------------------------------- /.luacheckrc: -------------------------------------------------------------------------------- 1 | std = "max+busted" 2 | unused_args = false 3 | redefined = false 4 | max_line_length = false 5 | 6 | 7 | globals = { 8 | "randomize", 9 | "match", 10 | "async", 11 | "done", 12 | "busted", 13 | --"ngx.IS_CLI", 14 | } 15 | 16 | 17 | not_globals = { 18 | "string.len", 19 | "table.getn", 20 | } 21 | 22 | 23 | ignore = { 24 | --"6.", -- ignore whitespace warnings 25 | } 26 | 27 | 28 | exclude_files = { 29 | ".install", 30 | ".lua", 31 | ".luarocks", 32 | "spec/insulate-expose_spec.lua", 33 | "spec/cl_compile_fail.lua", 34 | } 35 | 36 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | Contributing to Say 2 | =================== 3 | 4 | So you want to contribute to luassert? Fantastic! Here's a brief overview on 5 | how best to do so. 6 | 7 | ## What to change 8 | 9 | Here's some examples of things you might want to make a pull request for: 10 | 11 | * New features 12 | * Bugfixes 13 | * Inefficient blocks of code 14 | 15 | If you have a more deeply-rooted problem with how the program is built or some 16 | of the stylistic decisions made in the code, it's best to 17 | [create an issue](https://github.com/lunarmodules/say/issues) before putting 18 | the effort into a pull request. The same goes for new features - it might be 19 | best to check the project's direction, existing pull requests, and currently open 20 | and closed issues first. 21 | 22 | ## Style 23 | 24 | * Two spaces, not tabs 25 | * Variables have_underscores, classes are Uppercase 26 | * Wrap everything in `local`, expose blocks of code using the module pattern 27 | 28 | Look at existing code to get a good feel for the patterns we use. 29 | 30 | ## Using Git appropriately 31 | 32 | 1. [Fork the repository](https://github.com/lunarmodules/say/fork_select) to 33 | your Github account. 34 | 2. Create a *topical branch* - a branch whose name is succint but explains what 35 | you're doing, such as "klingon-translations" 36 | 3. Make your changes, committing at logical breaks. 37 | 4. Push your branch to your personal account 38 | 5. [Create a pull request](https://help.github.com/articles/using-pull-requests) 39 | 6. Watch for comments or acceptance 40 | 41 | Please note - if you want to change multiple things that don't depend on each 42 | other, make sure you check the master branch back out before making more 43 | changes - that way we can take in each change separately. 44 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License Terms 2 | ================= 3 | 4 | Copyright (c) 2012 Olivine Labs, LLC. 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy of 7 | this software and associated documentation files (the "Software"), to deal in 8 | the Software without restriction, including without limitation the rights to 9 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies 10 | of the Software, and to permit persons to whom the Software is furnished to do 11 | so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in all 14 | copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | SOFTWARE. 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Say 2 | 3 | [![Busted](https://img.shields.io/github/workflow/status/lunarmodules/say/Busted?label=Busted&logo=Lua)](https://github.com/lunarmodules/say/actions?workflow=Busted) 4 | [![Luacheck](https://img.shields.io/github/workflow/status/lunarmodules/say/Luacheck?label=Luacheck&logo=Lua)](https://github.com/lunarmodules/say/actions?workflow=Luacheck) 5 | [![GitHub tag (latest SemVer)](https://img.shields.io/github/v/tag/lunarmodules/say?label=Tag&logo=GitHub)](https://github.com/lunarmodules/say/releases) 6 | [![Luarocks](https://img.shields.io/luarocks/v/lunarmodules/say?label=Luarocks&logo=Lua)](https://luarocks.org/modules/lunarmodules/say) 7 | 8 | say is a simple string key/value store for i18n or any other case where you want namespaced strings. 9 | 10 | Check out [busted](https://lunarmodules.github.io/busted/) for extended examples. 11 | 12 | ```lua 13 | s = require("say") 14 | 15 | s:set_namespace("en") 16 | 17 | s:set('money', 'I have %s dollars') 18 | s:set('wow', 'So much money!') 19 | 20 | print(s('money', {1000})) -- I have 1000 dollars 21 | 22 | s:set_namespace("fr") -- switch to french! 23 | s:set('wow', "Tant d'argent!") 24 | 25 | print(s('wow')) -- Tant d'argent! 26 | s:set_namespace("en") -- switch back to english! 27 | print(s('wow')) -- So much money! 28 | ``` 29 | 30 | NOTE: the parameters table can have `nil` values, but in that case it must have an `n` field to indicate table size. 31 | 32 | ```lua 33 | s = require("say") 34 | 35 | s:set('money', 'I have %s %s') 36 | 37 | print(s('money', {1000, "dollars"})) -- I have 1000 dollars 38 | print(s('money', {nil, "euros", n = 2})) -- I have nil euros 39 | ``` 40 | -------------------------------------------------------------------------------- /rockspecs/say-1.3-1.rockspec: -------------------------------------------------------------------------------- 1 | package = "say" 2 | version = "1.3-1" 3 | source = { 4 | url = "https://github.com/Olivine-Labs/say/archive/v1.3-1.tar.gz", 5 | dir = "say-1.3-1" 6 | } 7 | description = { 8 | summary = "Lua String Hashing/Indexing Library", 9 | detailed = [[ 10 | Useful for internationalization. 11 | ]], 12 | homepage = "http://olivinelabs.com/busted/", 13 | license = "MIT " 14 | } 15 | dependencies = { 16 | "lua >= 5.1" 17 | } 18 | build = { 19 | type = "builtin", 20 | modules = { 21 | ["say.init"] = "src/init.lua" 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /rockspecs/say-1.4.0-1.rockspec: -------------------------------------------------------------------------------- 1 | local package_name = "say" 2 | local package_version = "1.4.0" 3 | local rockspec_revision = "1" 4 | local github_account_name = "lunarmodules" 5 | local github_repo_name = package_name 6 | 7 | package = package_name 8 | version = package_version .. "-" .. rockspec_revision 9 | 10 | source = { 11 | url = "git+https://github.com/"..github_account_name.."/"..github_repo_name..".git" 12 | } 13 | 14 | if package_version == "scm" then source.branch = "master" else source.tag = "v" .. package_version end 15 | 16 | description = { 17 | summary = "Lua string hashing/indexing library", 18 | detailed = [[ 19 | Useful for internationalization. 20 | ]], 21 | homepage = "https://lunarmodules.github.io/busted/", 22 | license = "MIT " 23 | } 24 | 25 | dependencies = { 26 | "lua >= 5.1" 27 | } 28 | build = { 29 | type = "builtin", 30 | modules = { 31 | ["say.init"] = "src/say/init.lua" 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /rockspecs/say-1.4.1-1.rockspec: -------------------------------------------------------------------------------- 1 | package = "say" 2 | local rock_version = "1.4.1" 3 | local rock_release = "1" 4 | local namespace = "lunarmodules" 5 | local repository = package 6 | 7 | version = ("%s-%s"):format(rock_version, rock_release) 8 | 9 | source = { 10 | url = ("git+https://github.com/%s/%s.git"):format(namespace, repository), 11 | branch = rock_version == "scm" and "master" or nil, 12 | tag = rock_version ~= "scm" and rock_version or nil, 13 | } 14 | 15 | description = { 16 | summary = "Lua string hashing/indexing library", 17 | detailed = [[ 18 | Useful for internationalization. 19 | ]], 20 | homepage = ("https://%s.github.io/%s"):format(namespace, repository), 21 | issues_url = ("https://github.com/%s/%s/issues"):format(namespace, repository), 22 | maintainer = "Caleb Maclennan ", 23 | } 24 | 25 | dependencies = { 26 | "lua >= 5.1", 27 | } 28 | build = { 29 | type = "builtin", 30 | modules = { 31 | say = "src/say/init.lua" 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /rockspecs/say-1.4.1-2.rockspec: -------------------------------------------------------------------------------- 1 | package = "say" 2 | local rock_version = "1.4.1" 3 | local rock_release = "2" 4 | local namespace = "lunarmodules" 5 | local repository = package 6 | 7 | version = ("%s-%s"):format(rock_version, rock_release) 8 | 9 | source = { 10 | url = ("git+https://github.com/%s/%s.git"):format(namespace, repository), 11 | branch = rock_version == "scm" and "master" or nil, 12 | tag = rock_version ~= "scm" and rock_version or nil, 13 | } 14 | 15 | description = { 16 | summary = "Lua string hashing/indexing library", 17 | detailed = [[ 18 | Useful for internationalization. 19 | ]], 20 | license = "MIT", 21 | homepage = ("https://%s.github.io/%s"):format(namespace, repository), 22 | maintainer = "Caleb Maclennan ", 23 | } 24 | 25 | dependencies = { 26 | "lua >= 5.1", 27 | } 28 | build = { 29 | type = "builtin", 30 | modules = { 31 | say = "src/say/init.lua" 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /rockspecs/say-1.4.1-3.rockspec: -------------------------------------------------------------------------------- 1 | package = "say" 2 | local rock_version = "1.4.1" 3 | local rock_release = "3" 4 | local namespace = "lunarmodules" 5 | local repository = package 6 | 7 | version = ("%s-%s"):format(rock_version, rock_release) 8 | 9 | source = { 10 | url = ("git+https://github.com/%s/%s.git"):format(namespace, repository), 11 | branch = rock_version == "scm" and "master" or nil, 12 | tag = rock_version ~= "scm" and "v"..rock_version or nil, 13 | } 14 | 15 | description = { 16 | summary = "Lua string hashing/indexing library", 17 | detailed = [[ 18 | Useful for internationalization. 19 | ]], 20 | license = "MIT", 21 | homepage = ("https://%s.github.io/%s"):format(namespace, repository), 22 | maintainer = "Caleb Maclennan ", 23 | } 24 | 25 | dependencies = { 26 | "lua >= 5.1", 27 | } 28 | build = { 29 | type = "builtin", 30 | modules = { 31 | say = "src/say/init.lua" 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /say-scm-1.rockspec: -------------------------------------------------------------------------------- 1 | package = "say" 2 | local rock_version = "scm" 3 | local rock_release = "1" 4 | local namespace = "lunarmodules" 5 | local repository = package 6 | 7 | rockspec_format = "3.0" 8 | version = ("%s-%s"):format(rock_version, rock_release) 9 | 10 | source = { 11 | url = ("git+https://github.com/%s/%s.git"):format(namespace, repository), 12 | branch = rock_version == "scm" and "master" or nil, 13 | tag = rock_version ~= "scm" and "v"..rock_version or nil, 14 | } 15 | 16 | description = { 17 | summary = "Lua string hashing/indexing library", 18 | detailed = [[ 19 | Useful for internationalization. 20 | ]], 21 | license = "MIT", 22 | homepage = ("https://%s.github.io/%s"):format(namespace, repository), 23 | issues_url = ("https://github.com/%s/%s/issues"):format(namespace, repository), 24 | maintainer = "Caleb Maclennan ", 25 | } 26 | 27 | dependencies = { 28 | "lua >= 5.1", 29 | } 30 | 31 | test_dependencies = { 32 | "busted", 33 | } 34 | 35 | test = { 36 | type = "busted", 37 | } 38 | 39 | build = { 40 | type = "builtin", 41 | modules = { 42 | say = "src/say/init.lua" 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /spec/say_spec.lua: -------------------------------------------------------------------------------- 1 | local s 2 | 3 | describe("Tests to make sure the say library is functional", function() 4 | 5 | setup(function() 6 | -- busted loads say internally, force reload in case the sources we're 7 | -- expected to test (via LUA_PATH) aren't the ones busted found earlier 8 | package.loaded['say'] = false 9 | s = require('say') 10 | end) 11 | 12 | it("tests the set function metamethod", function() 13 | s:set('herp', 'derp') 14 | assert(s._registry.en.herp == 'derp') 15 | end) 16 | 17 | it("tests the __call metamethod", function() 18 | s:set('herp', 'derp') 19 | assert(s('herp') == 'derp') 20 | 21 | s:set('herp', '%s') 22 | assert(s('herp', {'test'}) == 'test') 23 | 24 | s:set('herp', '%s%s') 25 | assert(s('herp', {'test', 'test'}) == 'testtest') 26 | end) 27 | 28 | it("tests the substitution of variable types; boolean, number, string and table", function() 29 | s:set('substitute_test', 'boolean = %s, number = %s, string = "%s", table = %s') 30 | local atable = {} 31 | assert(s('substitute_test', {true, 100, 'some text', atable}) == 'boolean = true, number = 100, string = "some text", table = ' .. tostring(atable)) 32 | end) 33 | 34 | it("tests the substitution of variable types; nil", function() 35 | s:set('substitute_test', 'boolean = %s, nil = %s, number = %s, string = "%s", table = %s') 36 | local atable = {} 37 | assert(s('substitute_test', {true, nil, 100, 'some text', atable, n = 5}) == 'boolean = true, nil = nil, number = 100, string = "some text", table = ' .. tostring(atable)) 38 | end) 39 | 40 | it("tests the set_fallback method", function() 41 | s:set_namespace('en') 42 | s:set('herp', 'derp') 43 | s:set_namespace('not-en') 44 | s:set('not-herp', 'not-derp') 45 | 46 | assert(s('not-herp') == 'not-derp') 47 | assert(s('herp') == 'derp') 48 | end) 49 | 50 | it("errors on bad type of param table", function() 51 | s:set_namespace('en') 52 | s:set('herp', 'derp %s') 53 | assert.has.error(function() s('herp', 1000) end, "expected parameter table to be a table, got 'number'") 54 | end) 55 | 56 | it("tests missing elements returns nil", function() 57 | assert(s('this does not exist') == nil) 58 | end) 59 | 60 | it("tests the wrong arg count", function() 61 | -- backward compatibility after the nil-safe fix, in which the 'n' field got precendence over # 62 | s:set('substitute_test', '1 = %s, 2 = %s') 63 | -- two arguments, but "n = 1" 64 | assert(s('substitute_test', {"one", "two", n = 1}) == '1 = one, 2 = two') 65 | end) 66 | 67 | end) 68 | -------------------------------------------------------------------------------- /src/say/init.lua: -------------------------------------------------------------------------------- 1 | local unpack = table.unpack or unpack 2 | 3 | local registry = { } 4 | local current_namespace 5 | local fallback_namespace 6 | 7 | local s = { 8 | 9 | _COPYRIGHT = "Copyright (c) 2012 Olivine Labs, LLC.", 10 | _DESCRIPTION = "A simple string key/value store for i18n or any other case where you want namespaced strings.", 11 | _VERSION = "Say 1.3", 12 | 13 | set_namespace = function(self, namespace) 14 | current_namespace = namespace 15 | if not registry[current_namespace] then 16 | registry[current_namespace] = {} 17 | end 18 | end, 19 | 20 | set_fallback = function(self, namespace) 21 | fallback_namespace = namespace 22 | if not registry[fallback_namespace] then 23 | registry[fallback_namespace] = {} 24 | end 25 | end, 26 | 27 | set = function(self, key, value) 28 | registry[current_namespace][key] = value 29 | end 30 | } 31 | 32 | local __meta = { 33 | __call = function(self, key, vars) 34 | if vars ~= nil and type(vars) ~= "table" then 35 | error(("expected parameter table to be a table, got '%s'"):format(type(vars)), 2) 36 | end 37 | vars = vars or {} 38 | vars.n = math.max((vars.n or 0), #vars) 39 | 40 | local str = registry[current_namespace][key] or registry[fallback_namespace][key] 41 | 42 | if str == nil then 43 | return nil 44 | end 45 | str = tostring(str) 46 | local strings = {} 47 | 48 | for i = 1, vars.n or #vars do 49 | table.insert(strings, tostring(vars[i])) 50 | end 51 | 52 | return #strings > 0 and str:format(unpack(strings)) or str 53 | end, 54 | 55 | __index = function(self, key) 56 | return registry[key] 57 | end 58 | } 59 | 60 | s:set_fallback('en') 61 | s:set_namespace('en') 62 | 63 | s._registry = registry 64 | 65 | return setmetatable(s, __meta) 66 | --------------------------------------------------------------------------------