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
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Foxtail
|
|
4
|
+
module Function
|
|
5
|
+
# Wraps a datetime value with formatting options
|
|
6
|
+
# The raw value is preserved for selector matching
|
|
7
|
+
class DateTime < Value
|
|
8
|
+
# Convert FTL/JS style datetime options to ICU4X options
|
|
9
|
+
# @param options [Hash] FTL/JS style options (camelCase)
|
|
10
|
+
# @return [Hash] ICU4X style options (snake_case with symbols)
|
|
11
|
+
def self.convert_options(options)
|
|
12
|
+
result = {}
|
|
13
|
+
|
|
14
|
+
options.each do |key, value|
|
|
15
|
+
case key
|
|
16
|
+
when :dateStyle
|
|
17
|
+
result[:date_style] = value.to_sym
|
|
18
|
+
when :timeStyle
|
|
19
|
+
result[:time_style] = value.to_sym
|
|
20
|
+
when :timeZone
|
|
21
|
+
result[:time_zone] = value.to_s
|
|
22
|
+
else
|
|
23
|
+
warn "Unknown DATETIME option: #{key}"
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
result
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
# Format the datetime using ICU4X
|
|
31
|
+
# @param bundle [Foxtail::Bundle] The bundle providing locale and context
|
|
32
|
+
# @return [String] The formatted datetime
|
|
33
|
+
def format(bundle:)
|
|
34
|
+
icu_options = self.class.convert_options(options)
|
|
35
|
+
ICU4XCache.instance.datetime_formatter(bundle.locale, **icu_options).format(value)
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Foxtail
|
|
4
|
+
module Function
|
|
5
|
+
# Wraps a numeric value with formatting options
|
|
6
|
+
# The raw value is preserved for selector matching (plural rules)
|
|
7
|
+
class Number < Value
|
|
8
|
+
# Convert FTL/JS style number options to ICU4X options
|
|
9
|
+
# @param options [Hash] FTL/JS style options (camelCase)
|
|
10
|
+
# @return [Hash] ICU4X style options (snake_case with symbols)
|
|
11
|
+
def self.convert_options(options)
|
|
12
|
+
result = {}
|
|
13
|
+
|
|
14
|
+
options.each do |key, value|
|
|
15
|
+
case key
|
|
16
|
+
when :style
|
|
17
|
+
result[:style] = value.to_sym
|
|
18
|
+
when :currency
|
|
19
|
+
result[:currency] = value.to_s
|
|
20
|
+
when :minimumIntegerDigits
|
|
21
|
+
result[:minimum_integer_digits] = Integer(value)
|
|
22
|
+
when :minimumFractionDigits
|
|
23
|
+
result[:minimum_fraction_digits] = Integer(value)
|
|
24
|
+
when :maximumFractionDigits
|
|
25
|
+
result[:maximum_fraction_digits] = Integer(value)
|
|
26
|
+
when :useGrouping
|
|
27
|
+
result[:use_grouping] = value
|
|
28
|
+
else
|
|
29
|
+
warn "Unknown NUMBER option: #{key}"
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
result
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
# Format the number using ICU4X
|
|
37
|
+
# @param bundle [Foxtail::Bundle] The bundle providing locale and context
|
|
38
|
+
# @return [String] The formatted number
|
|
39
|
+
def format(bundle:)
|
|
40
|
+
icu_options = self.class.convert_options(options)
|
|
41
|
+
ICU4XCache.instance.number_formatter(bundle.locale, **icu_options).format(value)
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
end
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Foxtail
|
|
4
|
+
module Function
|
|
5
|
+
Value = Data.define(:value, :options)
|
|
6
|
+
|
|
7
|
+
# Base class for deferred-formatting values
|
|
8
|
+
# Wraps a value with formatting options, deferring locale-specific formatting until display time
|
|
9
|
+
#
|
|
10
|
+
# @!attribute [r] value
|
|
11
|
+
# @return [Object] The wrapped raw value
|
|
12
|
+
# @!attribute [r] options
|
|
13
|
+
# @return [Hash] Formatting options
|
|
14
|
+
class Value
|
|
15
|
+
# Format the value for display
|
|
16
|
+
# Subclasses may override for locale-specific formatting
|
|
17
|
+
# @param bundle [Foxtail::Bundle] The bundle providing locale and context (unused in base implementation)
|
|
18
|
+
# @return [String] The formatted value
|
|
19
|
+
def format(**) = value.to_s
|
|
20
|
+
|
|
21
|
+
# String representation for interpolation
|
|
22
|
+
# @return [String] The string representation of the wrapped value
|
|
23
|
+
def to_s = value.to_s
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Foxtail
|
|
4
|
+
# Built-in formatting functions for FTL
|
|
5
|
+
# Uses ICU4X for number and datetime formatting
|
|
6
|
+
module Function
|
|
7
|
+
# Default functions available to all bundles
|
|
8
|
+
# @return [Hash{String => #call}] Function name to callable object mapping
|
|
9
|
+
def self.defaults
|
|
10
|
+
{
|
|
11
|
+
"NUMBER" => ->(value, **options) {
|
|
12
|
+
# Unwrap value and merge options from nested function calls (like fluent.js)
|
|
13
|
+
raw_value, existing_options = unwrap_value(value)
|
|
14
|
+
unwrapped_options = unwrap_options(options)
|
|
15
|
+
Number[raw_value, existing_options.merge(unwrapped_options)]
|
|
16
|
+
},
|
|
17
|
+
"DATETIME" => ->(value, **options) {
|
|
18
|
+
# Unwrap value and merge options from nested function calls (like fluent.js)
|
|
19
|
+
raw_value, existing_options = unwrap_value(value)
|
|
20
|
+
unwrapped_options = unwrap_options(options)
|
|
21
|
+
DateTime[raw_value, existing_options.merge(unwrapped_options)]
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
# Unwrap a Function::Value to get raw value and options
|
|
27
|
+
# @param value [Object] the value to unwrap
|
|
28
|
+
# @return [Array(Object, Hash)] the raw value and options
|
|
29
|
+
def self.unwrap_value(value)
|
|
30
|
+
if value.is_a?(Value)
|
|
31
|
+
[value.value, value.options]
|
|
32
|
+
else
|
|
33
|
+
[value, {}]
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
# Unwrap option values that may be Function::Value instances
|
|
38
|
+
# @param options [Hash] the options hash
|
|
39
|
+
# @return [Hash] options with unwrapped values
|
|
40
|
+
def self.unwrap_options(options)
|
|
41
|
+
options.transform_values do |v|
|
|
42
|
+
v.is_a?(Value) ? v.value : v
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
end
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "dry/core/cache"
|
|
4
|
+
require "singleton"
|
|
5
|
+
|
|
6
|
+
module Foxtail
|
|
7
|
+
# Singleton cache for ICU4X formatter and rules instances.
|
|
8
|
+
#
|
|
9
|
+
# ICU4X formatters and rules internally load and parse locale data,
|
|
10
|
+
# making instance creation non-trivial. This cache stores instances
|
|
11
|
+
# keyed by locale and options to avoid repeated instantiation.
|
|
12
|
+
#
|
|
13
|
+
# Thread safety is provided by Dry::Core::Cache, which uses
|
|
14
|
+
# Concurrent::Map internally.
|
|
15
|
+
#
|
|
16
|
+
# @example
|
|
17
|
+
# cache = Foxtail::ICU4XCache.instance
|
|
18
|
+
# formatter = cache.number_formatter(locale)
|
|
19
|
+
# formatter.format(1234) #=> "1,234"
|
|
20
|
+
class ICU4XCache
|
|
21
|
+
extend Dry::Core::Cache
|
|
22
|
+
include Singleton
|
|
23
|
+
|
|
24
|
+
# Returns a cached ICU4X::NumberFormat instance.
|
|
25
|
+
#
|
|
26
|
+
# @param locale [ICU4X::Locale] The locale for formatting
|
|
27
|
+
# @param options [Hash] Formatting options passed to ICU4X::NumberFormat.new
|
|
28
|
+
# @return [ICU4X::NumberFormat] Cached formatter instance
|
|
29
|
+
def number_formatter(locale, **options)
|
|
30
|
+
self.class.fetch_or_store(:number_formatter, locale, options) do
|
|
31
|
+
ICU4X::NumberFormat.new(locale, **options)
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
# Returns a cached ICU4X::DateTimeFormat instance.
|
|
36
|
+
#
|
|
37
|
+
# @param locale [ICU4X::Locale] The locale for formatting
|
|
38
|
+
# @param options [Hash] Formatting options passed to ICU4X::DateTimeFormat.new
|
|
39
|
+
# @return [ICU4X::DateTimeFormat] Cached formatter instance
|
|
40
|
+
def datetime_formatter(locale, **options)
|
|
41
|
+
self.class.fetch_or_store(:datetime_formatter, locale, options) do
|
|
42
|
+
ICU4X::DateTimeFormat.new(locale, **options)
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
# Returns a cached ICU4X::PluralRules instance.
|
|
47
|
+
#
|
|
48
|
+
# @param locale [ICU4X::Locale] The locale for plural rules
|
|
49
|
+
# @param type [Symbol] Plural rule type (:cardinal or :ordinal)
|
|
50
|
+
# @return [ICU4X::PluralRules] Cached rules instance
|
|
51
|
+
def plural_rules(locale, type: :cardinal)
|
|
52
|
+
self.class.fetch_or_store(:plural_rules, locale, type) do
|
|
53
|
+
ICU4X::PluralRules.new(locale, type:)
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
end
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Foxtail
|
|
4
|
+
# Container for parsed FTL entries (messages and terms).
|
|
5
|
+
#
|
|
6
|
+
# Created via {.from_string} or {.from_file}, which use the runtime parser
|
|
7
|
+
# ({Bundle::Parser}) optimized for performance with error recovery.
|
|
8
|
+
# Invalid entries are silently skipped; comments are not preserved.
|
|
9
|
+
#
|
|
10
|
+
# For full AST with source positions, comments, and error details,
|
|
11
|
+
# use {Syntax::Parser} instead.
|
|
12
|
+
class Resource
|
|
13
|
+
include Enumerable
|
|
14
|
+
|
|
15
|
+
# @return [Array<Bundle::Parser::AST::Message, Bundle::Parser::AST::Term>] Parsed FTL entries (messages and terms)
|
|
16
|
+
attr_reader :entries
|
|
17
|
+
|
|
18
|
+
# Parse FTL source string into a Resource
|
|
19
|
+
#
|
|
20
|
+
# @param source [String] FTL source text to parse
|
|
21
|
+
# @return [Foxtail::Resource] New resource with parsed entries
|
|
22
|
+
#
|
|
23
|
+
# @example Parse FTL content
|
|
24
|
+
# source = <<~FTL
|
|
25
|
+
# hello = Hello, {$name}!
|
|
26
|
+
# goodbye = Goodbye!
|
|
27
|
+
# FTL
|
|
28
|
+
# resource = Foxtail::Resource.from_string(source)
|
|
29
|
+
# @raise [ArgumentError] if source is not a String
|
|
30
|
+
def self.from_string(source)
|
|
31
|
+
raise ArgumentError, "source must be a String, got #{source.class}" unless source.is_a?(String)
|
|
32
|
+
|
|
33
|
+
parser = Bundle::Parser.new
|
|
34
|
+
entries = parser.parse(source)
|
|
35
|
+
|
|
36
|
+
new(entries)
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
# Parse FTL file into a Resource
|
|
40
|
+
#
|
|
41
|
+
# @param path [Pathname] Path to FTL file
|
|
42
|
+
# @return [Foxtail::Resource] New resource with parsed entries
|
|
43
|
+
def self.from_file(path)
|
|
44
|
+
source = path.read
|
|
45
|
+
from_string(source)
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def initialize(entries)
|
|
49
|
+
@entries = entries
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
private_class_method :new
|
|
53
|
+
|
|
54
|
+
# Check if resource has any entries
|
|
55
|
+
# @return [Boolean]
|
|
56
|
+
def empty? = @entries.empty?
|
|
57
|
+
|
|
58
|
+
# Get the number of entries
|
|
59
|
+
# @return [Integer]
|
|
60
|
+
def size = @entries.size
|
|
61
|
+
|
|
62
|
+
# Iterate over entries
|
|
63
|
+
# @return [self]
|
|
64
|
+
def each(&)
|
|
65
|
+
@entries.each(&)
|
|
66
|
+
self
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
# Get message entries (IDs not starting with "-")
|
|
70
|
+
# @return [Array<Bundle::Parser::AST::Message>]
|
|
71
|
+
def messages = @entries.select {|entry| entry.id && !entry.id.start_with?("-") }
|
|
72
|
+
|
|
73
|
+
# Get term entries (IDs starting with "-")
|
|
74
|
+
# @return [Array<Bundle::Parser::AST::Term>]
|
|
75
|
+
def terms = @entries.select {|entry| entry.id&.start_with?("-") }
|
|
76
|
+
|
|
77
|
+
# Find entry by ID
|
|
78
|
+
# @return [Bundle::Parser::AST::Message, Bundle::Parser::AST::Term, nil]
|
|
79
|
+
def find(id) = @entries.find {|entry| entry.id == id }
|
|
80
|
+
end
|
|
81
|
+
end
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Foxtail
|
|
4
|
+
# Manages ordered sequences of Bundles for language fallback.
|
|
5
|
+
#
|
|
6
|
+
# @example Basic usage
|
|
7
|
+
# sequence = Foxtail::Sequence.new(bundle_en_us, bundle_en, bundle_default)
|
|
8
|
+
# sequence.format("hello", name: "World")
|
|
9
|
+
#
|
|
10
|
+
# @example Finding the bundle that contains a message
|
|
11
|
+
# bundle = sequence.find("hello")
|
|
12
|
+
# puts "Using locale: #{bundle.locale}" if bundle
|
|
13
|
+
#
|
|
14
|
+
# @see https://projectfluent.org/fluent.js/sequence/
|
|
15
|
+
class Sequence
|
|
16
|
+
# Creates a new Sequence with the given bundles.
|
|
17
|
+
#
|
|
18
|
+
# @param bundles [Array<Bundle>] Bundles in priority order (first = highest priority)
|
|
19
|
+
def initialize(*bundles)
|
|
20
|
+
@bundles = bundles.flatten.freeze
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
# Finds the first bundle that contains a message with the given ID(s).
|
|
24
|
+
#
|
|
25
|
+
# @param ids [Array<String>] One or more message IDs to find
|
|
26
|
+
# @return [Bundle, nil] The first bundle containing the message (single ID)
|
|
27
|
+
# @return [Array<Bundle, nil>] Array of bundles for each ID (multiple IDs)
|
|
28
|
+
def find(*ids)
|
|
29
|
+
if ids.size == 1
|
|
30
|
+
find_bundle(ids.first)
|
|
31
|
+
else
|
|
32
|
+
ids.map {|id| find_bundle(id) }
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
# Formats a message using the first bundle that contains it.
|
|
37
|
+
# Keyword arguments are passed through to the bundle's format method.
|
|
38
|
+
#
|
|
39
|
+
# @param id [String] The message ID
|
|
40
|
+
# @param errors [Array, nil] If provided, errors are collected into this array instead of being ignored.
|
|
41
|
+
# @return [String] The formatted message, or the ID if not found
|
|
42
|
+
def format(id, errors=nil, **)
|
|
43
|
+
bundle = find_bundle(id)
|
|
44
|
+
bundle ? bundle.format(id, errors, **) : id.to_s
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
private def find_bundle(id) = @bundles.find {|bundle| bundle.message?(id) }
|
|
48
|
+
end
|
|
49
|
+
end
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "icu4x"
|
|
4
|
+
require "zeitwerk"
|
|
5
|
+
require_relative "foxtail/runtime/version"
|
|
6
|
+
|
|
7
|
+
# Ruby implementation of Project Fluent localization system
|
|
8
|
+
module Foxtail
|
|
9
|
+
# Configure Zeitwerk loader for this gem
|
|
10
|
+
loader = Zeitwerk::Loader.new
|
|
11
|
+
loader.push_dir(__dir__ + "/foxtail", namespace: Foxtail)
|
|
12
|
+
|
|
13
|
+
# Ignore version.rb since it's required by gemspec before Zeitwerk loads
|
|
14
|
+
loader.ignore(__dir__ + "/foxtail/runtime/version.rb")
|
|
15
|
+
# Ignore gem entrypoint file (no constant defined)
|
|
16
|
+
loader.ignore(__dir__ + "/foxtail.rb")
|
|
17
|
+
loader.ignore(__dir__ + "/foxtail-runtime.rb")
|
|
18
|
+
|
|
19
|
+
# Configure inflections for acronyms
|
|
20
|
+
loader.inflector.inflect(
|
|
21
|
+
"ast" => "AST",
|
|
22
|
+
"datetime" => "DateTime",
|
|
23
|
+
"icu4x_cache" => "ICU4XCache"
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
loader.setup
|
|
27
|
+
end
|
data/lib/foxtail.rb
ADDED
metadata
ADDED
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: foxtail-runtime
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.5.0
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- OZAWA Sakuro
|
|
8
|
+
autorequire:
|
|
9
|
+
bindir: bin
|
|
10
|
+
cert_chain: []
|
|
11
|
+
date: 2026-05-08 00:00:00.000000000 Z
|
|
12
|
+
dependencies:
|
|
13
|
+
- !ruby/object:Gem::Dependency
|
|
14
|
+
name: bigdecimal
|
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
|
16
|
+
requirements:
|
|
17
|
+
- - ">="
|
|
18
|
+
- !ruby/object:Gem::Version
|
|
19
|
+
version: '3.1'
|
|
20
|
+
type: :runtime
|
|
21
|
+
prerelease: false
|
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
23
|
+
requirements:
|
|
24
|
+
- - ">="
|
|
25
|
+
- !ruby/object:Gem::Version
|
|
26
|
+
version: '3.1'
|
|
27
|
+
- !ruby/object:Gem::Dependency
|
|
28
|
+
name: dry-core
|
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
|
30
|
+
requirements:
|
|
31
|
+
- - "~>"
|
|
32
|
+
- !ruby/object:Gem::Version
|
|
33
|
+
version: '1.1'
|
|
34
|
+
type: :runtime
|
|
35
|
+
prerelease: false
|
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
37
|
+
requirements:
|
|
38
|
+
- - "~>"
|
|
39
|
+
- !ruby/object:Gem::Version
|
|
40
|
+
version: '1.1'
|
|
41
|
+
- !ruby/object:Gem::Dependency
|
|
42
|
+
name: icu4x
|
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
|
44
|
+
requirements:
|
|
45
|
+
- - "~>"
|
|
46
|
+
- !ruby/object:Gem::Version
|
|
47
|
+
version: '0.9'
|
|
48
|
+
type: :runtime
|
|
49
|
+
prerelease: false
|
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
51
|
+
requirements:
|
|
52
|
+
- - "~>"
|
|
53
|
+
- !ruby/object:Gem::Version
|
|
54
|
+
version: '0.9'
|
|
55
|
+
- !ruby/object:Gem::Dependency
|
|
56
|
+
name: zeitwerk
|
|
57
|
+
requirement: !ruby/object:Gem::Requirement
|
|
58
|
+
requirements:
|
|
59
|
+
- - "~>"
|
|
60
|
+
- !ruby/object:Gem::Version
|
|
61
|
+
version: '2.7'
|
|
62
|
+
type: :runtime
|
|
63
|
+
prerelease: false
|
|
64
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
65
|
+
requirements:
|
|
66
|
+
- - "~>"
|
|
67
|
+
- !ruby/object:Gem::Version
|
|
68
|
+
version: '2.7'
|
|
69
|
+
description: 'Runtime components for Foxtail: bundle parsing, message formatting,
|
|
70
|
+
and ICU4X integration.
|
|
71
|
+
|
|
72
|
+
'
|
|
73
|
+
email:
|
|
74
|
+
- 10973+sakuro@users.noreply.github.com
|
|
75
|
+
executables: []
|
|
76
|
+
extensions: []
|
|
77
|
+
extra_rdoc_files: []
|
|
78
|
+
files:
|
|
79
|
+
- CHANGELOG.md
|
|
80
|
+
- LICENSE.txt
|
|
81
|
+
- README.md
|
|
82
|
+
- lib/foxtail-runtime.rb
|
|
83
|
+
- lib/foxtail.rb
|
|
84
|
+
- lib/foxtail/bundle.rb
|
|
85
|
+
- lib/foxtail/bundle/parser.rb
|
|
86
|
+
- lib/foxtail/bundle/parser/ast.rb
|
|
87
|
+
- lib/foxtail/bundle/resolver.rb
|
|
88
|
+
- lib/foxtail/bundle/scope.rb
|
|
89
|
+
- lib/foxtail/error.rb
|
|
90
|
+
- lib/foxtail/function.rb
|
|
91
|
+
- lib/foxtail/function/datetime.rb
|
|
92
|
+
- lib/foxtail/function/number.rb
|
|
93
|
+
- lib/foxtail/function/value.rb
|
|
94
|
+
- lib/foxtail/icu4x_cache.rb
|
|
95
|
+
- lib/foxtail/resource.rb
|
|
96
|
+
- lib/foxtail/runtime/version.rb
|
|
97
|
+
- lib/foxtail/sequence.rb
|
|
98
|
+
homepage: https://github.com/sakuro/foxtail
|
|
99
|
+
licenses:
|
|
100
|
+
- MIT
|
|
101
|
+
metadata:
|
|
102
|
+
homepage_uri: https://github.com/sakuro/foxtail
|
|
103
|
+
source_code_uri: https://github.com/sakuro/foxtail.git
|
|
104
|
+
changelog_uri: https://github.com/sakuro/foxtail/blob/main/foxtail-runtime/CHANGELOG.md
|
|
105
|
+
rubygems_mfa_required: 'true'
|
|
106
|
+
post_install_message:
|
|
107
|
+
rdoc_options: []
|
|
108
|
+
require_paths:
|
|
109
|
+
- lib
|
|
110
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
111
|
+
requirements:
|
|
112
|
+
- - ">="
|
|
113
|
+
- !ruby/object:Gem::Version
|
|
114
|
+
version: '3.3'
|
|
115
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
116
|
+
requirements:
|
|
117
|
+
- - ">="
|
|
118
|
+
- !ruby/object:Gem::Version
|
|
119
|
+
version: '0'
|
|
120
|
+
requirements: []
|
|
121
|
+
rubygems_version: 3.5.22
|
|
122
|
+
signing_key:
|
|
123
|
+
specification_version: 4
|
|
124
|
+
summary: Foxtail runtime for Project Fluent localization
|
|
125
|
+
test_files: []
|