Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
104 changes: 104 additions & 0 deletions vlib/i18n/i18n.v
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
// Copyright (c) 2019-2026 Alexander Medvednikov. All rights reserved.
// Use of this source code is governed by an MIT license
// that can be found in the LICENSE file.
module i18n

import os

pub const default_translations_dir = 'translations'

const default_tr_map = load_tr_map()

// load_tr_map loads all .tr files from the default translations directory.
pub fn load_tr_map() map[string]map[string]string {
return load_tr_map_from_dir(default_translations_dir)
}

// load_tr_map_from_dir loads all .tr files from dir into a lang -> key -> text map.
pub fn load_tr_map_from_dir(dir string) map[string]map[string]string {
files := os.walk_ext(dir, '.tr')
mut res := map[string]map[string]string{}
for tr_path in files {
lang := fetch_lang_from_tr_path(tr_path)
if lang.len == 0 {
continue
}
text := os.read_file(tr_path) or {
eprintln('translation file "${tr_path}" failed to load')
return {}
}
for key, val in parse_tr_text(text) {
res[lang][key] = val
}
}
return res
}

fn parse_tr_text(text string) map[string]string {
mut res := map[string]string{}
normalized := text.replace('\r\n', '\n')
for section in normalized.split('-----\n') {
nl_pos := section.index('\n') or { continue }
key := section[..nl_pos].trim_space()
if key.len == 0 {
continue
}
res[key] = section[nl_pos + 1..].trim_right('\n')
}
return res
}

fn fetch_lang_from_tr_path(path string) string {
return os.file_name(path).all_before_last('.tr')
}

// tr returns the translation for key from the default translations directory.
pub fn tr(lang string, key string) string {
return tr_from_map(default_tr_map, lang, key)
}

// tr_from_map returns the translation for key from translations.
pub fn tr_from_map(translations map[string]map[string]string, lang string, key string) string {
res := translations[lang][key]
if res == '' {
eprintln('NO TRANSLATION FOR KEY "${key}"')
return key
}
return res
}

// tr_plural returns the pluralized translation for key from the default translations directory.
pub fn tr_plural(lang string, key string, amount int) string {
return tr_plural_from_map(default_tr_map, lang, key, amount)
}

// tr_plural_from_map returns the pluralized translation for key from translations.
pub fn tr_plural_from_map(translations map[string]map[string]string, lang string, key string, amount int) string {
Comment on lines +71 to +76

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Add required doc comments to plural APIs

These two new public functions are exported without V doc comments, while the repo guide requires a doc comment immediately before each new or modified public function and says it should start with the function name (AGENTS.md lines 286-289). This leaves the new i18n public API undocumented in generated module docs; please add tr_plural... comments for both helpers.

Useful? React with 👍 / 👎.

s := translations[lang][key]
if s == '' {
eprintln('NO TRANSLATION FOR KEY "${key}"')
return key
}
if !s.contains('|') {
return s
}
// goods
// товар|а|ов
vals := s.split('|')
if vals.len != 3 {
return s
}
amount_str := amount.str()
ending := if amount % 10 == 1 && !amount_str.ends_with('11') {
''
} else if amount % 10 == 2 && !amount_str.ends_with('12') {
vals[1]
} else if amount % 10 == 3 && !amount_str.ends_with('13') {
vals[1]
} else if amount % 10 == 4 && !amount_str.ends_with('14') {
vals[1]
} else {
vals[2]
}
return vals[0] + ending
}
30 changes: 30 additions & 0 deletions vlib/i18n/i18n_test.v
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
module i18n

import os

fn test_load_tr_map_from_dir() {
translations := load_tr_map_from_dir(os.join_path(os.dir(@FILE), 'testdata', 'translations'))

assert 'en' in translations
assert 'pt-br' in translations
assert translations['en']['msg_hello'] == 'Hello'
assert translations['pt-br']['msg_hello'] == 'Ola'
}

fn test_tr_from_map_returns_key_for_missing_translation() {
translations := load_tr_map_from_dir(os.join_path(os.dir(@FILE), 'testdata', 'translations'))

assert tr_from_map(translations, 'en', 'missing_key') == 'missing_key'
}

fn test_tr_plural_from_map() {
translations := {
'ru': {
'goods': 'товар|а|ов'
}
}

assert tr_plural_from_map(translations, 'ru', 'goods', 1) == 'товар'
assert tr_plural_from_map(translations, 'ru', 'goods', 2) == 'товара'
assert tr_plural_from_map(translations, 'ru', 'goods', 5) == 'товаров'
}
2 changes: 2 additions & 0 deletions vlib/i18n/testdata/translations/en.tr
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
msg_hello
Hello
2 changes: 2 additions & 0 deletions vlib/i18n/testdata/translations/pt-br.tr
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
msg_hello
Ola
2 changes: 1 addition & 1 deletion vlib/v/builder/cc.v
Original file line number Diff line number Diff line change
Expand Up @@ -1213,7 +1213,7 @@ fn (v &Builder) only_linker_args(ccoptions CcompilerOptions) []string {
mut all := []string{}
// in `build-mode` or when producing a .o file, we do not need -lxyz flags,
// since we are building an (.o) object file, that will be linked later.
if v.pref.build_mode != .build_module && !v.pref.is_o {
if (v.pref.build_mode != .build_module || v.pref.is_shared) && !v.pref.is_o {
all << ccoptions.linker_flags
all << ccoptions.env_ldflags
all << ccoptions.ldflags
Expand Down
8 changes: 8 additions & 0 deletions vlib/v/builder/cc_test.v
Original file line number Diff line number Diff line change
Expand Up @@ -540,6 +540,14 @@ fn test_shared_tcc_compile_args_skip_bt25_after_late_compiler_resolution() {
assert !builder.get_compile_args().contains('-bt25')
}

fn test_shared_build_module_keeps_shared_linker_flag() {
mut builder := new_test_builder(['-shared', hello_world_example()])
builder.pref.build_mode = .build_module
linker_args := builder.get_linker_args()

assert linker_args.contains('-shared')
}

fn test_linux_shared_boehm_linker_args_hide_static_gc_archive_symbols() {
linker_args := builder_linker_args_with_cc([
'-os',
Expand Down
2 changes: 1 addition & 1 deletion vlib/v3/gen/c/cleanc.v
Original file line number Diff line number Diff line change
Expand Up @@ -8164,7 +8164,7 @@ fn (mut g FlatGen) builtin_abi_decls() {
g.writeln('static inline string v3_int_zpad(int n, int width) { string s = int__str(n); if (n < 0) return s; while (s.len < width) s = string__plus(v3_c_lit("0", 1), s); return s; }')
g.writeln('static inline string v3_i64_zpad(i64 n, int width) { string s = i64__str(n); if (n < 0) return s; while (s.len < width) s = string__plus(v3_c_lit("0", 1), s); return s; }')
g.writeln('static inline string v3_u64_zpad(u64 n, int width) { string s = u64__str(n); while (s.len < width) s = string__plus(v3_c_lit("0", 1), s); return s; }')
g.writeln('static inline string v3_string_zpad(string s, int width) { if (s.len >= width) return s; int sign = s.len > 0 && s.str[0] == \'-\'; int pad = width - s.len; u8* out = malloc_noscan((ptrdiff_t)width + 1); int pos = 0; if (sign) out[pos++] = \'-\'; memset(out + pos, \'0\', (size_t)pad); pos += pad; memcpy(out + pos, s.str + sign, (size_t)(s.len - sign)); out[width] = 0; return (string){.str = out, .len = width, .is_lit = 0}; }')
g.writeln("static inline string v3_string_zpad(string s, int width) { if (s.len >= width) return s; int sign = s.len > 0 && s.str[0] == '-'; int pad = width - s.len; u8* out = malloc_noscan((ptrdiff_t)width + 1); int pos = 0; if (sign) out[pos++] = '-'; memset(out + pos, '0', (size_t)pad); pos += pad; memcpy(out + pos, s.str + sign, (size_t)(s.len - sign)); out[width] = 0; return (string){.str = out, .len = width, .is_lit = 0}; }")
g.writeln('static inline i64 v3_map_signed(void* p, int bytes) { if (bytes == 1) return *(signed char*)p; if (bytes == 2) return *(short*)p; if (bytes == 8) return *(long long*)p; return *(int*)p; }')
g.writeln('static inline u64 v3_map_unsigned(void* p, int bytes) { if (bytes == 1) return *(unsigned char*)p; if (bytes == 2) return *(unsigned short*)p; if (bytes == 8) return *(unsigned long long*)p; return *(unsigned int*)p; }')
g.writeln('static inline string v3_f32_array_str(float* vals, int n) { string out = v3_c_lit("[", 1); for (int i = 0; i < n; ++i) { if (i > 0) out = string__plus(out, v3_c_lit(", ", 2)); out = string__plus(out, f64__str((double)vals[i])); } return string__plus(out, v3_c_lit("]", 1)); }')
Expand Down
8 changes: 4 additions & 4 deletions vlib/v3/gen/c/fn.v
Original file line number Diff line number Diff line change
Expand Up @@ -4014,8 +4014,8 @@ fn (mut g FlatGen) gen_call(id flat.NodeId, node flat.Node) {
receiver_wants_shared := g.fn_param_is_shared(actual_fn, 0)
|| g.fn_param_is_shared(method_name, 0) || g.fn_param_is_shared(fn_name, 0)
|| g.fn_param_is_shared(emitted_callee_name, 0)
if receiver_wants_shared
&& (g.gen_shared_local_receiver_arg(base_id) || g.gen_shared_storage_expr(base_id)) {
if receiver_wants_shared && (g.gen_shared_local_receiver_arg(base_id)
|| g.gen_shared_storage_expr(base_id)) {
arg_start = 1
} else if param_types.len > 0
&& g.gen_embedded_method_receiver(base_id, base_type, param_types[0], receiver_wants_ptr) {
Expand Down Expand Up @@ -4132,8 +4132,8 @@ fn (mut g FlatGen) gen_call(id flat.NodeId, node flat.Node) {
|| g.fn_param_is_shared(g.direct_call_name(target_name), arg_idx)
|| g.fn_param_is_shared(g.direct_call_name(fn_name), arg_idx)
|| g.fn_param_is_shared(g.direct_call_name(emitted_callee_name), arg_idx)
if arg_param_is_shared
&& (g.gen_shared_local_receiver_arg(arg_id) || g.gen_shared_storage_expr(arg_id)) {
if arg_param_is_shared && (g.gen_shared_local_receiver_arg(arg_id)
|| g.gen_shared_storage_expr(arg_id)) {
continue
}
}
Expand Down
10 changes: 5 additions & 5 deletions vlib/v3/gen/c/fn_parallel_notd_v3_no_parallel.v
Original file line number Diff line number Diff line change
Expand Up @@ -449,11 +449,11 @@ fn (g &FlatGen) new_parallel_worker(worker_id int) &FlatGen {
const_modules: g.const_modules
const_init_order: g.const_init_order
fixed_storage_consts: g.fixed_storage_consts
global_modules: g.global_modules
global_inits: g.global_inits
global_init_order: g.global_init_order
enum_backing_infos: g.enum_backing_infos
iface_impls: g.iface_impls
global_modules: g.global_modules
global_inits: g.global_inits
global_init_order: g.global_init_order
enum_backing_infos: g.enum_backing_infos
iface_impls: g.iface_impls
iface_type_ids: g.iface_type_ids
ierror_method_emit_names: g.ierror_method_emit_names
ierror_stack_pointer_aliases: []map[string]bool{}
Expand Down
26 changes: 15 additions & 11 deletions vlib/v3/gen/c/types.v
Original file line number Diff line number Diff line change
Expand Up @@ -471,7 +471,11 @@ fn (mut g FlatGen) enum_decls() {
f := g.a.child_node(&node, i)
mut value := next_value
mut value_known := next_value_known
mut value_expr := if next_value_known { value.str() } else { next_value_expr }
mut value_expr := if next_value_known {
value.str()
} else {
next_value_expr
}
if f.children_count > 0 {
expr_id := g.a.child(f, 0)
mut resolving := map[string]bool{}
Expand Down Expand Up @@ -799,15 +803,15 @@ fn (g &FlatGen) enum_field_expr_value_with_enum(id flat.NodeId, enum_module stri
if node.children_count == 0 {
return none
}
return g.enum_field_expr_value_with_enum(g.a.child(&node, 0), enum_module, enum_name,
mut field_values, field_exprs, mut resolving)
return g.enum_field_expr_value_with_enum(g.a.child(&node, 0), enum_module, enum_name, mut
field_values, field_exprs, mut resolving)
}
.prefix {
if node.children_count == 0 {
return none
}
value := g.enum_field_expr_value_with_enum(g.a.child(&node, 0), enum_module, enum_name,
mut field_values, field_exprs, mut resolving)?
value := g.enum_field_expr_value_with_enum(g.a.child(&node, 0), enum_module, enum_name, mut
field_values, field_exprs, mut resolving)?
return match node.op {
.plus { value }
.minus { -value }
Expand All @@ -819,10 +823,10 @@ fn (g &FlatGen) enum_field_expr_value_with_enum(id flat.NodeId, enum_module stri
if node.children_count < 2 {
return none
}
left := g.enum_field_expr_value_with_enum(g.a.child(&node, 0), enum_module, enum_name,
mut field_values, field_exprs, mut resolving)?
right := g.enum_field_expr_value_with_enum(g.a.child(&node, 1), enum_module, enum_name,
mut field_values, field_exprs, mut resolving)?
left := g.enum_field_expr_value_with_enum(g.a.child(&node, 0), enum_module, enum_name, mut
field_values, field_exprs, mut resolving)?
right := g.enum_field_expr_value_with_enum(g.a.child(&node, 1), enum_module, enum_name, mut
field_values, field_exprs, mut resolving)?
if (node.op == .div || node.op == .mod) && right == 0 {
return none
}
Expand All @@ -847,8 +851,8 @@ fn (g &FlatGen) enum_field_expr_value_with_enum(id flat.NodeId, enum_module stri
}
.selector {
if field := g.enum_decl_selector_ref_field(id, enum_module, enum_name) {
return g.enum_decl_field_ref_value(field, enum_module, enum_name, mut
field_values, field_exprs, mut resolving)
return g.enum_decl_field_ref_value(field, enum_module, enum_name, mut field_values,
field_exprs, mut resolving)
}
return none
}
Expand Down
6 changes: 5 additions & 1 deletion vlib/v3/parser/parser.v
Original file line number Diff line number Diff line change
Expand Up @@ -960,7 +960,11 @@ fn (mut p Parser) fn_decl_body(name string, receiver_name string, receiver_type
prev_struct := p.cur_struct
p.cur_fn = name
// `@STRUCT` inside a method expands to the receiver's (dereferenced) type name.
p.cur_struct = if is_method { method_receiver_type_name(receiver_type).all_after_last('.') } else { '' }
p.cur_struct = if is_method {
method_receiver_type_name(receiver_type).all_after_last('.')
} else {
''
}
p.push_local_type_scope(name)
// A disabled `@[if flag ?]` function keeps its signature but gets an empty body
// (a no-op stub), so callers still resolve while the body is compiled out.
Expand Down
Loading
Loading