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 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