foxtail-runtime 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/CHANGELOG.md +20 -0
- data/LICENSE.txt +21 -0
- data/README.md +61 -0
- data/lib/foxtail/bundle/parser/ast.rb +166 -0
- data/lib/foxtail/bundle/parser.rb +543 -0
- data/lib/foxtail/bundle/resolver.rb +444 -0
- data/lib/foxtail/bundle/scope.rb +63 -0
- data/lib/foxtail/bundle.rb +162 -0
- data/lib/foxtail/error.rb +6 -0
- data/lib/foxtail/function/datetime.rb +39 -0
- data/lib/foxtail/function/number.rb +45 -0
- data/lib/foxtail/function/value.rb +26 -0
- data/lib/foxtail/function.rb +46 -0
- data/lib/foxtail/icu4x_cache.rb +57 -0
- data/lib/foxtail/resource.rb +81 -0
- data/lib/foxtail/runtime/version.rb +9 -0
- data/lib/foxtail/sequence.rb +49 -0
- data/lib/foxtail-runtime.rb +27 -0
- data/lib/foxtail.rb +3 -0
- metadata +125 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: d7c03fa078365be8892be86558075ca205acdc8cce593a18f594d85332239a87
|
|
4
|
+
data.tar.gz: df182477a72b683c9055f579d88a83d896c97a2937f506e28b4df2dc31f4d90b
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: 5b884a63ba415f43fc3062644d2447090ed29fb49be70c68cb807c5f9cc34861e24b96652cb7826a311b3c74e29a8cdb8066cf269a4fb9632df4d3558f570b7f
|
|
7
|
+
data.tar.gz: 15632c63f8abae557b11958be5ccd512839b3e357e9170b44afe06efd57949acedb9a26bf9bde1d06c336bd7a059f9c20c386dacea7691ebda5bb1636395b918
|
data/CHANGELOG.md
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
## [Unreleased]
|
|
2
|
+
|
|
3
|
+
## [0.5.0] - 2026-05-08
|
|
4
|
+
|
|
5
|
+
### Added
|
|
6
|
+
- **Core System**
|
|
7
|
+
- Ruby implementation of Project Fluent localization system
|
|
8
|
+
- Runtime message formatting with Bundle system
|
|
9
|
+
- Pattern selection with pluralization support
|
|
10
|
+
- `Sequence` class for language fallback chains
|
|
11
|
+
- Bidi isolation support (`use_isolating` option)
|
|
12
|
+
- **Formatting Features**
|
|
13
|
+
- `icu4x`-based number and date formatting
|
|
14
|
+
- Built-in functions: NUMBER() and DATETIME()
|
|
15
|
+
- Implicit NUMBER/DATETIME function calling for numeric and time variables
|
|
16
|
+
- **Examples**
|
|
17
|
+
- Executable usage demonstrations in `examples/`
|
|
18
|
+
### Compatibility
|
|
19
|
+
- fluent.js bundle parser: 62/62 (100%)
|
|
20
|
+
- Ruby: 3.3+ supported
|
data/LICENSE.txt
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
The MIT License (MIT)
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 OZAWA Sakuro
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in
|
|
13
|
+
all copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
|
21
|
+
THE SOFTWARE.
|
data/README.md
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
# 🦊 Foxtail Runtime 🌐
|
|
2
|
+
|
|
3
|
+
Runtime components for [Project Fluent](https://projectfluent.org/) localization in Ruby.
|
|
4
|
+
|
|
5
|
+
## Features
|
|
6
|
+
|
|
7
|
+
- Bundle parsing and runtime message formatting
|
|
8
|
+
- ICU4X-based number, date/time, and plural rules formatting
|
|
9
|
+
- Fluent.js-compatible runtime AST
|
|
10
|
+
|
|
11
|
+
## Installation
|
|
12
|
+
|
|
13
|
+
Add this line to your application's Gemfile:
|
|
14
|
+
|
|
15
|
+
```ruby
|
|
16
|
+
gem "foxtail-runtime"
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
Then execute:
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
$ bundle install
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
Or install it yourself as:
|
|
26
|
+
|
|
27
|
+
```bash
|
|
28
|
+
$ gem install foxtail-runtime
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
## Basic Usage
|
|
32
|
+
|
|
33
|
+
```ruby
|
|
34
|
+
require "foxtail-runtime"
|
|
35
|
+
require "icu4x-data-recommended" # for locale-aware NUMBER formatting
|
|
36
|
+
|
|
37
|
+
resource = Foxtail::Resource.from_string(<<~FTL)
|
|
38
|
+
hello = Hello, { $name }!
|
|
39
|
+
price = { NUMBER($amount, style: "currency", currency: "USD") }
|
|
40
|
+
FTL
|
|
41
|
+
|
|
42
|
+
bundle = Foxtail::Bundle.new(ICU4X::Locale.parse("en-US"))
|
|
43
|
+
bundle.add_resource(resource)
|
|
44
|
+
|
|
45
|
+
bundle.format("hello", name: "Alice")
|
|
46
|
+
# => "Hello, Alice!"
|
|
47
|
+
|
|
48
|
+
bundle.format("price", amount: 1234.5)
|
|
49
|
+
# => "$1,234.5"
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
## Documentation
|
|
53
|
+
|
|
54
|
+
- [Architecture](doc/architecture.md)
|
|
55
|
+
- [Bundle System](doc/bundle-system.md)
|
|
56
|
+
- [ICU4X Integration](doc/icu4x-integration.md)
|
|
57
|
+
- [Sequence](doc/sequence.md)
|
|
58
|
+
|
|
59
|
+
## License
|
|
60
|
+
|
|
61
|
+
The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Foxtail
|
|
4
|
+
class Bundle
|
|
5
|
+
class Parser
|
|
6
|
+
# Ruby port of fluent-bundle/src/ast.ts type system
|
|
7
|
+
# Data class-based implementation for immutability and type safety
|
|
8
|
+
module AST
|
|
9
|
+
StringLiteral = Data.define(:value)
|
|
10
|
+
|
|
11
|
+
# String literal expression in Fluent patterns
|
|
12
|
+
# @!attribute value [r] [String] The string value
|
|
13
|
+
class StringLiteral
|
|
14
|
+
# @param value [#to_s] The string value (will be converted to String)
|
|
15
|
+
def initialize(value:) = super(value: value.to_s)
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
NumberLiteral = Data.define(:value, :precision)
|
|
19
|
+
|
|
20
|
+
# Number literal expression in Fluent patterns
|
|
21
|
+
# @!attribute value [r] [Float] The numeric value
|
|
22
|
+
# @!attribute precision [r] [Integer] Number of decimal places
|
|
23
|
+
class NumberLiteral
|
|
24
|
+
# @param value [Numeric, String] The numeric value (will be converted to Float)
|
|
25
|
+
# @param precision [Integer] Number of decimal places (default: 0)
|
|
26
|
+
def initialize(value:, precision: 0) = super(value: Float(value), precision: Integer(precision))
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
VariableReference = Data.define(:name)
|
|
30
|
+
|
|
31
|
+
# Variable reference expression ($variable) in Fluent patterns
|
|
32
|
+
# @!attribute name [r] [String] The variable name (without $ prefix)
|
|
33
|
+
class VariableReference
|
|
34
|
+
# @param name [#to_s] The variable name (will be converted to String)
|
|
35
|
+
def initialize(name:) = super(name: name.to_s)
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
TermReference = Data.define(:name, :attr, :args)
|
|
39
|
+
|
|
40
|
+
# Term reference expression (-term) in Fluent patterns
|
|
41
|
+
# @!attribute name [r] [String] The term name (without - prefix)
|
|
42
|
+
# @!attribute attr [r] [String, nil] The attribute name if accessing an attribute
|
|
43
|
+
# @!attribute args [r] [Array] Arguments passed to the term
|
|
44
|
+
class TermReference
|
|
45
|
+
# @param name [#to_s] The term name (will be converted to String)
|
|
46
|
+
# @param attr [#to_s, nil] The attribute name (default: nil)
|
|
47
|
+
# @param args [Array] Arguments passed to the term (default: [])
|
|
48
|
+
def initialize(name:, attr: nil, args: []) = super(name: name.to_s, attr: attr&.to_s, args:)
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
MessageReference = Data.define(:name, :attr)
|
|
52
|
+
|
|
53
|
+
# Message reference expression (message) in Fluent patterns
|
|
54
|
+
# @!attribute name [r] [String] The message identifier
|
|
55
|
+
# @!attribute attr [r] [String, nil] The attribute name if accessing an attribute
|
|
56
|
+
class MessageReference
|
|
57
|
+
# @param name [#to_s] The message identifier (will be converted to String)
|
|
58
|
+
# @param attr [#to_s, nil] The attribute name (default: nil)
|
|
59
|
+
def initialize(name:, attr: nil) = super(name: name.to_s, attr: attr&.to_s)
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
FunctionReference = Data.define(:name, :args)
|
|
63
|
+
|
|
64
|
+
# Function call expression (FUNCTION()) in Fluent patterns
|
|
65
|
+
# @!attribute name [r] [String] The function name (uppercase by convention)
|
|
66
|
+
# @!attribute args [r] [Array] Function arguments (positional and named)
|
|
67
|
+
class FunctionReference
|
|
68
|
+
# @param name [#to_s] The function name (will be converted to String)
|
|
69
|
+
# @param args [Array] Function arguments (default: [])
|
|
70
|
+
def initialize(name:, args: []) = super(name: name.to_s, args:)
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
NamedArgument = Data.define(:name, :value)
|
|
74
|
+
|
|
75
|
+
# Named argument in function calls (key: value)
|
|
76
|
+
# @!attribute name [r] [String] The argument name
|
|
77
|
+
# @!attribute value [r] The argument value expression
|
|
78
|
+
class NamedArgument
|
|
79
|
+
# @param name [#to_s] The argument name (will be converted to String)
|
|
80
|
+
# @param value The argument value expression
|
|
81
|
+
def initialize(name:, value:) = super(name: name.to_s, value:)
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
SelectExpression = Data.define(:selector, :variants, :star)
|
|
85
|
+
|
|
86
|
+
# Select expression for pluralization and variants
|
|
87
|
+
# @!attribute selector [r] The expression to match against
|
|
88
|
+
# @!attribute variants [r] [Array<Variant>] The variant branches
|
|
89
|
+
# @!attribute star [r] [Integer] Index of the default variant
|
|
90
|
+
class SelectExpression
|
|
91
|
+
# @param selector The expression to match against
|
|
92
|
+
# @param variants [Array<Variant>] The variant branches
|
|
93
|
+
# @param star [Integer] Index of the default variant (default: 0)
|
|
94
|
+
def initialize(selector:, variants:, star: 0) = super
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
# Variant for select expressions (no type field, no special initialization)
|
|
98
|
+
Variant = Data.define(:key, :value)
|
|
99
|
+
|
|
100
|
+
Message = Data.define(:id, :value, :attributes)
|
|
101
|
+
|
|
102
|
+
# Message entry in fluent-bundle compatible format
|
|
103
|
+
# @!attribute id [r] [String] The message identifier
|
|
104
|
+
# @!attribute value [r] The message pattern (String or Array)
|
|
105
|
+
# @!attribute attributes [r] [Hash, nil] Message attributes
|
|
106
|
+
class Message
|
|
107
|
+
# @param id [#to_s] The message identifier (will be converted to String)
|
|
108
|
+
# @param value The message pattern (default: nil)
|
|
109
|
+
# @param attributes [Hash, nil] Message attributes (default: nil)
|
|
110
|
+
def initialize(id:, value: nil, attributes: nil) = super(id: id.to_s, value:, attributes:)
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
Term = Data.define(:id, :value, :attributes)
|
|
114
|
+
|
|
115
|
+
# Term entry in fluent-bundle compatible format
|
|
116
|
+
# @!attribute id [r] [String] The term identifier (with - prefix)
|
|
117
|
+
# @!attribute value [r] The term pattern (String or Array)
|
|
118
|
+
# @!attribute attributes [r] [Hash, nil] Term attributes
|
|
119
|
+
class Term
|
|
120
|
+
# @param id [#to_s] The term identifier (- prefix will be added if missing)
|
|
121
|
+
# @param value The term pattern
|
|
122
|
+
# @param attributes [Hash, nil] Term attributes (default: nil)
|
|
123
|
+
def initialize(id:, value:, attributes: nil)
|
|
124
|
+
term_id = id.to_s
|
|
125
|
+
term_id = "-#{term_id}" unless term_id.start_with?("-")
|
|
126
|
+
super(id: term_id, value:, attributes:)
|
|
127
|
+
end
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
# Junk entry for unparseable content
|
|
131
|
+
# @!attribute content [r] [String] The raw unparseable content
|
|
132
|
+
# @!attribute annotations [r] [Array<Hash>] Parser annotations/errors
|
|
133
|
+
Junk = Data.define(:content, :annotations)
|
|
134
|
+
|
|
135
|
+
# Comment entry for FTL comments
|
|
136
|
+
# @!attribute content [r] [String] The comment text
|
|
137
|
+
Comment = Data.define(:content)
|
|
138
|
+
|
|
139
|
+
# Type checking helpers (following TypeScript union types)
|
|
140
|
+
|
|
141
|
+
# Check if node is a literal (string or number)
|
|
142
|
+
def self.literal?(node) = node.is_a?(StringLiteral) || node.is_a?(NumberLiteral)
|
|
143
|
+
|
|
144
|
+
# Check if node is an expression
|
|
145
|
+
def self.expression?(node)
|
|
146
|
+
return true if literal?(node)
|
|
147
|
+
|
|
148
|
+
node.is_a?(VariableReference) ||
|
|
149
|
+
node.is_a?(TermReference) ||
|
|
150
|
+
node.is_a?(MessageReference) ||
|
|
151
|
+
node.is_a?(FunctionReference) ||
|
|
152
|
+
node.is_a?(SelectExpression)
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
# Check if node can be a pattern element
|
|
156
|
+
def self.pattern_element?(node) = node.is_a?(String) || expression?(node)
|
|
157
|
+
|
|
158
|
+
# Check if node is a complex pattern (array of pattern elements)
|
|
159
|
+
def self.complex_pattern?(node) = node.is_a?(Array) && node.all? {|el| pattern_element?(el) }
|
|
160
|
+
|
|
161
|
+
# Check if node is any valid pattern
|
|
162
|
+
def self.pattern?(node) = node.is_a?(String) || complex_pattern?(node)
|
|
163
|
+
end
|
|
164
|
+
end
|
|
165
|
+
end
|
|
166
|
+
end
|